Compare commits

..

140 Commits

Author SHA1 Message Date
izzy a7045b3bd7 chore: sql query was renamed 2026-02-25 12:10:13 +00:00
izzy 50eea116cc chore: open api
Signed-off-by: izzy <me@insrt.uk>
2026-02-25 12:07:34 +00:00
izzy 6f35ab969c fix: use @immich/sql-tools
Signed-off-by: izzy <me@insrt.uk>
2026-02-25 11:55:49 +00:00
izzy 18122482e7 Merge remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-02-25 11:53:18 +00:00
izzy 891a3c8123 refactor: stricter repository typing
Signed-off-by: izzy <me@insrt.uk>
2026-02-25 11:36:26 +00:00
izzy 0a358090cb refactor: use medium tests for integrity service
Signed-off-by: izzy <me@insrt.uk>
2026-02-25 11:16:34 +00:00
izzy e32ed6cb4d chore: change variable name casing 2026-02-24 17:16:13 +00:00
izzy df51bea585 chore: remove uppercase from UI 2026-02-24 17:15:26 +00:00
izzy f4ccf18d67 chore: openapi 2026-02-24 17:08:45 +00:00
izzy a44a886121 Merge remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-02-24 17:08:35 +00:00
izzy f6d88ffa96 refactor: use makeStream for 2nd case; update makeStream type
Signed-off-by: izzy <me@insrt.uk>
2026-02-12 12:09:58 +00:00
izzy 6b7ac81dcc refactor: split asset checksum check into private func.
Signed-off-by: izzy <me@insrt.uk>
2026-02-12 12:04:01 +00:00
izzy c5fb13e0d5 refactor: split refresh into private functions
Signed-off-by: izzy <me@insrt.uk>
2026-02-12 12:00:01 +00:00
izzy bf17d8cbd1 refactor: arrow functions in svelte components
fix: should provide arrow function to , oops!

Signed-off-by: izzy <me@insrt.uk>
2026-02-12 11:43:17 +00:00
izzy 4908289879 refactor: pass only userId to deleteIntegrityReport
Signed-off-by: izzy <me@insrt.uk>
2026-02-12 11:02:22 +00:00
izzy fbdeb0409e refactor: use GET/Query for report fetch
Signed-off-by: izzy <me@insrt.uk>
2026-02-12 10:54:19 +00:00
izzy 5ed0ff41e3 chore: update icon for integrity checks queue
Signed-off-by: izzy <me@insrt.uk>
2026-02-12 10:32:41 +00:00
izzy 8cb63ebb51 refactor: review suggestions for web/../integrity.service.ts
Signed-off-by: izzy <me@insrt.uk>
2026-02-12 10:30:47 +00:00
izzy 477d43290b chore: update readme/sql
Signed-off-by: izzy <me@insrt.uk>
2026-02-11 16:19:13 +00:00
izzy a1812e98d0 Merge remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-02-11 16:00:32 +00:00
izzy cc7d8f2335 refactor: prefer type over interface
Signed-off-by: izzy <me@insrt.uk>
2026-02-11 15:54:06 +00:00
izzy 6c38417ebd refactor: move code out of try {} block
Signed-off-by: izzy <me@insrt.uk>
2026-02-06 17:32:32 +00:00
izzy bbcb6b8247 refactor: use makeStream instead of inline generator
Signed-off-by: izzy <me@insrt.uk>
2026-02-06 17:28:24 +00:00
izzy 08fb19b5b7 refactor: rewrite countAll SQL to use GROUP BY count
Signed-off-by: izzy <me@insrt.uk>
2026-02-06 17:20:27 +00:00
izzy 3d5db67656 refactor: move /report/:id route definitions together
Signed-off-by: izzy <me@insrt.uk>
2026-02-06 16:30:57 +00:00
izzy 7e5ab25418 refactor: merge duplicate beforeAll blocks
Signed-off-by: izzy <me@insrt.uk>
2026-02-06 16:29:17 +00:00
izzy 1b033c03c0 Merge remote-tracking branch 'origin/main' into feat/integrity-checks-izzy
Signed-off-by: izzy <me@insrt.uk>
2026-02-06 16:28:53 +00:00
izzy 48ca31f4af merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-01-22 16:44:23 +00:00
izzy e22beb0223 test: probably stuff that got lost in merge
Signed-off-by: izzy <me@insrt.uk>
2026-01-22 16:44:21 +00:00
Paul Makles ec8628f5ca Merge branch 'main' into feat/integrity-checks-izzy 2026-01-21 17:03:08 +00:00
izzy bffbf02f3a chore: merge fixes 2026-01-21 17:02:30 +00:00
izzy ffdc9012d7 merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-01-21 17:02:22 +00:00
izzy e1ba8e68f8 fix: incorrect merge on admin page layout 2026-01-16 14:08:42 +00:00
izzy b68c75855a merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-01-16 10:10:57 +00:00
izzy e9dafefb02 merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-01-16 09:43:30 +00:00
izzy 28443a090d merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-01-15 17:08:06 +00:00
izzy a20458f7ba chore: lint fixes 2026-01-15 15:34:01 +00:00
izzy bf835077d5 refactor: use handleCreateJob 2026-01-14 12:43:50 +00:00
izzy ef6e31bf28 merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-01-14 12:25:28 +00:00
izzy 1c73f7f433 refactor: split delete event into two 2026-01-13 14:56:44 +00:00
izzy 69b2e36a38 refactor: use new web service architecture (1/2) 2026-01-13 13:18:58 +00:00
izzy 67cc937bb0 chore: pick a version in controller 2026-01-13 10:56:30 +00:00
izzy 812419deab test: update concurrency 2026-01-13 10:43:29 +00:00
izzy 85f6490ea1 refactor: use new ui Table component 2026-01-13 10:36:09 +00:00
izzy bfd0ac2247 chore: lint 2026-01-13 10:35:49 +00:00
izzy 259e93d2c6 chore: i18n pass on table 2026-01-13 09:53:51 +00:00
izzy 26d2b41e05 chore: add index to asset.createdAt 2026-01-13 09:46:52 +00:00
izzy aebde65825 chore: open api 2026-01-13 09:25:34 +00:00
izzy 66e7517bc7 merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy 2026-01-13 09:21:09 +00:00
Paul Makles e0624ad5f9 Merge branch 'main' into feat/integrity-checks-izzy 2026-01-08 10:58:00 +00:00
izzy 1bab670e44 test: ensure directory we write to exists first 2026-01-08 10:50:01 +00:00
izzy 728e018fa8 chore: e2e lint 2026-01-08 10:38:16 +00:00
izzy 300d40917d chore: format e2e test 2026-01-07 15:37:57 +00:00
izzy 1b15e4f01c test: wait for files to be detected before deleting them 2026-01-07 15:34:50 +00:00
izzy 0ef02ba515 test: e2e web test 2026-01-07 15:34:24 +00:00
izzy a9867c3f94 test: update e2e tests with refactoring changes 2026-01-07 13:01:56 +00:00
izzy 7f8b0772a9 fix: accept UUIDv7 in URL param 2026-01-07 13:01:46 +00:00
izzy 5f35abbaa5 test: correct service spec 2026-01-07 12:40:29 +00:00
izzy 7e0e4bd0c6 chore: sync sql 2026-01-07 12:33:54 +00:00
izzy aaec6db27b chore: i18n sort 2026-01-07 12:32:48 +00:00
izzy ed33f79e2a refactor: orphan -> untracked 2026-01-07 12:17:28 +00:00
izzy d189722bbf refactor: use cursors instead of pages 2026-01-06 15:49:36 +00:00
izzy 06ee275202 chore: fmt 2026-01-06 14:25:38 +00:00
izzy c4c5358ef7 merge: remote-tracking branch 'immich/main' into feat/integrity-checks-izzy 2026-01-06 14:19:34 +00:00
izzy 7e1e283710 chore: remove debug code 2025-12-19 18:03:49 +00:00
izzy b3ba8806db chore: remove stray comments 2025-12-19 10:50:48 +00:00
izzy d4161a25f3 feat: run all jobs button 2025-12-19 10:48:42 +00:00
izzy d35c4f88dd test: e2e for delete all jobs
test: less flake
2025-12-19 10:17:25 +00:00
izzy 66da3c15da chore: summary text 2025-12-19 09:18:32 +00:00
izzy 2d48d05943 chore: open api / sql 2025-12-18 18:39:35 +00:00
izzy a6cac7db1d test: add tests for csv/download file 2025-12-18 18:37:56 +00:00
izzy a81074fff8 fix: filter assets that are trashed 2025-12-18 18:27:07 +00:00
izzy bb4893d0d7 test: e2e tests for get report and delete entries 2025-12-18 18:26:27 +00:00
izzy 042335f3cd fix: wrap and don't disappear UI on smaller screens 2025-12-18 17:35:33 +00:00
izzy 82351f4fb9 chore: add descriptions to routes 2025-12-18 17:33:11 +00:00
izzy ff7453e46a test: service tests for deleting reports 2025-12-18 17:27:33 +00:00
izzy 06f81f4b14 chore: reload tables if job queue empties 2025-12-18 17:16:55 +00:00
izzy 4ed3386f07 fix: use correct queue type in web 2025-12-18 17:13:52 +00:00
izzy 2962c54ee2 refactor: queue separate jobs for deleting integrity reports 2025-12-18 17:08:45 +00:00
izzy 8b1e29998e chore: i18n pass 2025-12-18 16:27:26 +00:00
izzy 748ba6780d chore: update sql 2025-12-18 14:57:26 +00:00
izzy ff07b4ff16 test: service tests for checksum 2025-12-18 14:55:48 +00:00
izzy b1f3c7579d test: add mock for plain read 2025-12-18 14:47:33 +00:00
izzy 92d23ce955 fix: use plain read stream instead of real 2025-12-18 14:40:59 +00:00
izzy c045fa27af test: service tests for missing files 2025-12-18 14:36:51 +00:00
izzy b73066268f test: service tests for orphaned files 2025-12-18 14:24:34 +00:00
izzy 098563ef4e refactor: use storage repository stat instead of real stat 2025-12-18 14:14:24 +00:00
izzy 5028c56ad8 feat: paginate integrity report results 2025-12-18 14:08:06 +00:00
izzy 31ac88f158 test: cont. integrity service tests 2025-12-18 12:56:14 +00:00
izzy b2053503bb chore: type issues 2025-12-17 15:53:24 +00:00
izzy f1c7f13d20 test: remove un-used variables 2025-12-17 15:46:37 +00:00
izzy 16c2082721 chore: use new buttons for admin pages 2025-12-17 15:46:06 +00:00
izzy 05acf74626 test: add new queue name 2025-12-17 15:41:23 +00:00
izzy b8feaecf86 merge: remote-tracking branch 'immich/main' into feat/integrity-checks-izzy 2025-12-17 15:11:45 +00:00
izzy 0e75f38e4a merge: remote-tracking branch 'immich/main' into feat/integrity-checks-izzy 2025-12-17 15:09:39 +00:00
izzy 08e532170f refactor: split maintenance dto for integrity checks 2025-12-17 15:04:45 +00:00
izzy 21c26dd65f refactor: split integrity out of maintenance controller/service 2025-12-17 14:55:38 +00:00
izzy 7d71f99783 test: split integrity out of maintenance 2025-12-17 14:42:07 +00:00
izzy 8fdec465c5 refactor: use separate queue for integrity checks 2025-12-17 14:37:43 +00:00
izzy 6e7854b5bb chore: sync SQL 2025-12-03 16:46:08 +00:00
izzy 5d5d421201 fix: path -> reportId as reportId 2025-12-03 15:24:30 +00:00
izzy 7a215c16ab fix: flip deletedAt filter 2025-12-02 14:23:56 +00:00
izzy ae653f9bf5 chore: lint 2025-12-02 14:07:14 +00:00
izzy 73a17bb58e chore: generate SQL 2025-12-02 13:54:55 +00:00
izzy e1a1662225 chore: more compliant csv 2025-12-02 13:33:13 +00:00
izzy 6e752bed77 fix: don't process trashed/deleted assets for integrity 2025-12-02 13:19:37 +00:00
izzy 64cc64dd56 refactor: move all new queries into integrity repository 2025-12-02 13:15:48 +00:00
izzy 6cfd1994c4 feat: ability to delete all reports (and corresponding objects) 2025-12-02 11:59:23 +00:00
izzy 806a2880ca feat: assetId, fileAssetId columns on integrity reports 2025-12-01 15:49:03 +00:00
izzy 042af30bef chore: use checksum configuration 2025-12-01 14:27:04 +00:00
izzy 06fcd54b9f feat: download csv report, download file, delete file 2025-12-01 14:20:38 +00:00
izzy fec8923431 test: increase timeouts 2025-12-01 12:07:24 +00:00
izzy db690bcf63 chore: generate SQL 2025-12-01 11:56:01 +00:00
izzy 1daf1b471f chore: lint 2025-12-01 11:51:49 +00:00
izzy 01f96de3e5 test: serialise the buffer over events 2025-12-01 11:20:34 +00:00
izzy c4ac8d9f63 stash: incomplete checksum outdated test 2025-11-28 18:01:24 +00:00
izzy 0362d21945 test: take baseline, check for each issue, check refreshes work 2025-11-28 17:44:48 +00:00
izzy 4d7f7b80da feat: refresh missing & checksum 2025-11-28 17:44:37 +00:00
izzy e447ba87c6 chore: sort i18n 2025-11-28 15:28:50 +00:00
izzy 2779fce7d0 feat: manually trigger integrity jobs
feat: update summary after job runs
2025-11-28 15:27:12 +00:00
izzy 13e9cf0ed9 stash: moving computers because pnpm is cooked 2025-11-28 12:50:30 +00:00
izzy c50118e535 chore: remove old table comment 2025-11-28 12:10:41 +00:00
izzy ca358f4dae feat: sub-pages for integrity reports 2025-11-28 11:40:53 +00:00
izzy d3abed3414 feat: view integrity report in maintenance page (cherry picked) 2025-11-27 17:53:20 +00:00
izzy 0fdc7b4448 feat: draft controller entry
chore: lint & format
2025-11-27 17:23:54 +00:00
izzy 8db6132669 fix: add mock for asset repo. 2025-11-27 16:42:46 +00:00
izzy 03276de6b2 fix: add integrity report repository to service depends. 2025-11-27 16:34:28 +00:00
izzy 4462683739 chore: generate SQL queries 2025-11-27 16:19:34 +00:00
izzy 919eb839ef revert: override migration db url 2025-11-27 16:14:03 +00:00
izzy 251631948b fix: mock the new repository 2025-11-27 16:11:19 +00:00
izzy 93860238af feat: add config options & cron entries for checks 2025-11-27 16:05:26 +00:00
izzy 1744237aeb chore: open api 2025-11-27 15:40:44 +00:00
izzy ef7d8e94fa feat: check orphaned file reports are not out of date 2025-11-27 15:40:14 +00:00
izzy cc31b9c7f1 feat: clean up old reports of checksum or missing files
refactor: combine the stream query
2025-11-27 15:13:19 +00:00
izzy 929ad529f4 feat: add createdAt to integrity report table
refactor: rename checksum_fail to checksum_mismatched
2025-11-27 15:13:00 +00:00
izzy 1e941f3f88 feat: write integrity report to database 2025-11-27 12:53:04 +00:00
izzy 15503b150a chore: open api 2025-11-27 12:01:26 +00:00
izzy 3414210450 feat: checksum job 2025-11-27 12:00:35 +00:00
izzy 4a7120cdeb refactor: batched integrity checks 2025-11-26 17:36:28 +00:00
izzy f77f43a83d stash: integrity checks 2025-11-26 15:45:58 +00:00
278 changed files with 7925 additions and 14227 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ Immich has three main clients:
3. CLI - Command-line utility for bulk upload
:::info
All three clients use [OpenAPI](/api.md) to auto-generate rest clients for easy integration. For more information about this process, see [OpenAPI](/api.md).
All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for easy integration. For more information about this process, see [OpenAPI](./open-api.md).
:::
### Mobile App
@@ -71,7 +71,7 @@ An incoming HTTP request is mapped to a controller (`src/controllers`). Controll
### Domain Transfer Objects (DTOs)
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](/api.md) schemas and control the generated code used by each client.
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client.
### Background Jobs
@@ -1,4 +1,4 @@
# API
# OpenAPI
Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generate API documentation. To view the published docs see [here](https://api.immich.app/).
+1 -1
View File
@@ -53,7 +53,7 @@ You can use `dart fix --apply` and `dcm fix lib` to potentially correct some iss
## OpenAPI
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/api.md) for more details.
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/developer/open-api.md) for more details.
## Database Migrations
@@ -50,7 +50,6 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
- MIGraphX is a new backend for AMD cards, which compiles models at runtime. As such, the first few inferences will be slow.
#### OpenVINO
+48 -70
View File
@@ -6,7 +6,7 @@ const prism = require('prism-react-renderer');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Immich',
tagline: 'Self-hosted photo and video management solution',
tagline: 'High performance self-hosted photo and video backup solution directly from your mobile phone',
url: 'https://docs.immich.app',
baseUrl: '/',
onBrokenLinks: 'throw',
@@ -93,15 +93,35 @@ const config = {
position: 'right',
},
{
href: 'https://immich.app/',
to: '/overview/quick-start',
position: 'right',
label: 'Home',
label: 'Docs',
},
{
href: 'https://immich.app/roadmap',
position: 'right',
label: 'Roadmap',
},
{
href: 'https://api.immich.app/',
position: 'right',
label: 'API',
},
{
href: 'https://immich.store',
position: 'right',
label: 'Merch',
},
{
href: 'https://github.com/immich-app/immich',
label: 'GitHub',
position: 'right',
},
{
href: 'https://discord.immich.app',
label: 'Discord',
position: 'right',
},
{
type: 'html',
position: 'right',
@@ -114,78 +134,19 @@ const config = {
style: 'light',
links: [
{
title: 'Download',
title: 'Overview',
items: [
{
label: 'Android',
href: 'https://get.immich.app/android',
label: 'Quick start',
to: '/overview/quick-start',
},
{
label: 'iOS',
href: 'https://get.immich.app/ios',
label: 'Installation',
to: '/install/requirements',
},
{
label: 'Server',
href: 'https://immich.app/download',
},
],
},
{
title: 'Company',
items: [
{
label: 'FUTO',
href: 'https://futo.tech/',
},
{
label: 'Purchase',
href: 'https://buy.immich.app/',
},
{
label: 'Merch',
href: 'https://immich.store/',
},
],
},
{
title: 'Sites',
items: [
{
label: 'Home',
href: 'https://immich.app',
},
{
label: 'My Immich',
href: 'https://my.immich.app/',
},
{
label: 'Awesome Immich',
href: 'https://awesome.immich.app/',
},
{
label: 'Immich API',
href: 'https://api.immich.app/',
},
{
label: 'Immich Data',
href: 'https://data.immich.app/',
},
{
label: 'Immich Datasets',
href: 'https://datasets.immich.app/',
},
],
},
{
title: 'Miscellaneous',
items: [
{
label: 'Roadmap',
href: 'https://immich.app/roadmap',
},
{
label: 'Cursed Knowledge',
href: 'https://immich.app/cursed-knowledge',
label: 'Contributing',
to: '/overview/support-the-project',
},
{
label: 'Privacy Policy',
@@ -194,7 +155,24 @@ const config = {
],
},
{
title: 'Social',
title: 'Documentation',
items: [
{
label: 'Roadmap',
href: 'https://immich.app/roadmap',
},
{
label: 'API',
href: 'https://api.immich.app/',
},
{
label: 'Cursed Knowledge',
href: 'https://immich.app/cursed-knowledge',
},
],
},
{
title: 'Links',
items: [
{
label: 'GitHub',
-1
View File
@@ -23,7 +23,6 @@
/features/storage-template /administration/storage-template 307
/features/user-management /administration/user-management 307
/developer/contributing /developer/pr-checklist 307
/developer/open-api /api 307
/guides/machine-learning /guides/remote-machine-learning 307
/administration/password-login /administration/system-settings 307
/features/search /features/searching 307
@@ -99,7 +99,7 @@ describe('/admin/maintenance', () => {
},
{
interval: 500,
timeout: 10_000,
timeout: 60_000,
},
)
.toBeTruthy();
@@ -190,7 +190,7 @@ describe('/admin/maintenance', () => {
},
{
interval: 500,
timeout: 10_000,
timeout: 60_000,
},
)
.toBeFalsy();
@@ -0,0 +1,669 @@
import {
AssetMediaResponseDto,
IntegrityReportResponseDto,
LoginResponseDto,
ManualJobName,
QueueCommand,
QueueName,
} from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { app, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
const assetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const asset1Filepath = `${testAssetDir}/albums/nature/el_torcal_rocks.jpg`;
const asset2Filepath = `${testAssetDir}/albums/nature/wood_anemones.jpg`;
describe('/admin/integrity', () => {
let admin: LoginResponseDto;
let asset: AssetMediaResponseDto;
let user1: LoginResponseDto;
let asset1: AssetMediaResponseDto;
let user2: LoginResponseDto;
let asset2: AssetMediaResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
user1 = await utils.userSetup(admin.accessToken, {
email: '1@example.com',
name: '1',
password: '1',
});
user2 = await utils.userSetup(admin.accessToken, {
email: '2@example.com',
name: '2',
password: '2',
});
for (const queue of Object.values(QueueName)) {
if (queue === QueueName.IntegrityCheck) {
continue;
}
await utils.queueCommand(admin.accessToken, queue, {
command: QueueCommand.Pause,
});
}
asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'asset.jpg',
bytes: await readFile(assetFilepath),
},
});
asset1 = await utils.createAsset(user1.accessToken, {
assetData: {
filename: 'asset.jpg',
bytes: await readFile(asset1Filepath),
},
});
asset2 = await utils.createAsset(user2.accessToken, {
assetData: {
filename: 'asset.jpg',
bytes: await readFile(asset2Filepath),
},
});
await utils.mkFolder('/data/bak');
await utils.copyFolder(`/data/upload/${admin.userId}`, `/data/bak/${admin.userId}`);
for (const queue of Object.values(QueueName)) {
if (queue === QueueName.IntegrityCheck) {
continue;
}
await utils.queueCommand(admin.accessToken, queue, {
command: QueueCommand.Empty,
});
await utils.queueCommand(admin.accessToken, queue, {
command: QueueCommand.Resume,
});
}
});
afterEach(async () => {
await utils.deleteFolder(`/data/upload/${admin.userId}`);
await utils.copyFolder(`/data/bak/${admin.userId}`, `/data/upload/${admin.userId}`);
});
describe('POST /summary (& jobs)', async () => {
it.sequential('reports no issues', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFiles,
});
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFilesDeleteAll,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual({
missing_file: 0,
untracked_file: 0,
checksum_mismatch: 0,
});
});
it.sequential('should detect an untracked file (job: check untracked files)', async () => {
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
untracked_file: 1,
}),
);
});
it.sequential('should detect outdated untracked file reports (job: refresh untracked files)', async () => {
// these should not be detected:
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked2.png`);
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked3.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFilesRefresh,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
untracked_file: 0,
}),
);
});
it.sequential('should delete untracked files (job: delete all untracked file reports)', async () => {
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFilesDeleteAll,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
untracked_file: 0,
}),
);
});
it.sequential('should detect a missing file and not a checksum mismatch (job: check missing files)', async () => {
await utils.deleteFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
missing_file: 1,
checksum_mismatch: 0,
}),
);
});
it.sequential('should detect outdated missing file reports (job: refresh missing files)', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFilesRefresh,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
missing_file: 0,
checksum_mismatch: 0,
}),
);
});
it.sequential('should delete assets with missing files (job: delete all missing file reports)', async () => {
await utils.deleteFolder(`/data/upload/${user1.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status: listStatus, body: listBody } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(listStatus).toBe(200);
expect(listBody).toEqual(
expect.objectContaining({
missing_file: 1,
}),
);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFilesDeleteAll,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
missing_file: 0,
}),
);
await expect(utils.getAssetInfo(user1.accessToken, asset1.id)).resolves.toEqual(
expect.objectContaining({
isTrashed: true,
}),
);
});
it.sequential('should detect a checksum mismatch (job: check file checksums)', async () => {
await utils.truncateFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
checksum_mismatch: 1,
}),
);
});
it.sequential('should detect outdated checksum mismatch reports (job: refresh file checksums)', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatchRefresh,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
checksum_mismatch: 0,
}),
);
});
it.sequential(
'should delete assets with mismatched checksum (job: delete all checksum mismatch reports)',
async () => {
await utils.truncateFolder(`/data/upload/${user2.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status: listStatus, body: listBody } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(listStatus).toBe(200);
expect(listBody).toEqual(
expect.objectContaining({
checksum_mismatch: 1,
}),
);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatchDeleteAll,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
checksum_mismatch: 0,
}),
);
await expect(utils.getAssetInfo(user2.accessToken, asset2.id)).resolves.toEqual(
expect.objectContaining({
isTrashed: true,
}),
);
},
);
});
describe('POST /report', async () => {
it.sequential('reports untracked files', async () => {
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/report?type=untracked_file')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual({
nextCursor: undefined,
items: expect.arrayContaining([
{
id: expect.any(String),
type: 'untracked_file',
path: `/data/upload/${admin.userId}/untracked1.png`,
assetId: null,
fileAssetId: null,
createdAt: expect.any(String),
},
]),
});
});
it.sequential('reports missing files', async () => {
await utils.deleteFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/report?type=missing_file')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual({
nextCursor: undefined,
items: expect.arrayContaining([
{
id: expect.any(String),
type: 'missing_file',
path: expect.any(String),
assetId: asset.id,
fileAssetId: null,
createdAt: expect.any(String),
},
]),
});
});
it.sequential('reports checksum mismatched files', async () => {
await utils.truncateFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/integrity/report?type=checksum_mismatch')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual({
nextCursor: undefined,
items: expect.arrayContaining([
{
id: expect.any(String),
type: 'checksum_mismatch',
path: expect.any(String),
assetId: asset.id,
fileAssetId: null,
createdAt: expect.any(String),
},
]),
});
});
});
describe('DELETE /report/:id', async () => {
it.sequential('delete untracked files', async () => {
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status: listStatus, body: listBody } = await request(app)
.get('/admin/integrity/report?type=untracked_file')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(listStatus).toBe(200);
const report = (listBody as IntegrityReportResponseDto).items.find(
(item) => item.path === `/data/upload/${admin.userId}/untracked1.png`,
)!;
const { status } = await request(app)
.delete(`/admin/integrity/report/${report.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status: listStatus2, body: listBody2 } = await request(app)
.get('/admin/integrity/report?type=untracked_file')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(listStatus2).toBe(200);
expect(listBody2).not.toBe(
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
id: report.id,
}),
]),
}),
);
});
it.sequential('delete assets missing files', async () => {
await utils.deleteFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status: listStatus, body: listBody } = await request(app)
.get('/admin/integrity/report?type=missing_file')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(listStatus).toBe(200);
expect(listBody.items.length).toBe(1);
const report = (listBody as IntegrityReportResponseDto).items[0];
const { status } = await request(app)
.delete(`/admin/integrity/report/${report.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status: listStatus2, body: listBody2 } = await request(app)
.get('/admin/integrity/report?type=missing_file')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(listStatus2).toBe(200);
expect(listBody2.items.length).toBe(0);
});
it.sequential('delete assets with failing checksum', async () => {
await utils.truncateFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status: listStatus, body: listBody } = await request(app)
.get('/admin/integrity/report?type=checksum_mismatch')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(listStatus).toBe(200);
expect(listBody.items.length).toBe(1);
const report = (listBody as IntegrityReportResponseDto).items[0];
const { status } = await request(app)
.delete(`/admin/integrity/report/${report.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status: listStatus2, body: listBody2 } = await request(app)
.get('/admin/integrity/report?type=checksum_mismatch')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(listStatus2).toBe(200);
expect(listBody2.items.length).toBe(0);
});
});
describe('GET /report/:type/csv', () => {
it.sequential('exports untracked files as csv', async () => {
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, headers, text } = await request(app)
.get('/admin/integrity/report/untracked_file/csv')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(headers['content-type']).toContain('text/csv');
expect(headers['content-disposition']).toContain('.csv');
expect(text).toContain('id,type,assetId,fileAssetId,path');
expect(text).toContain(`untracked_file`);
expect(text).toContain(`/data/upload/${admin.userId}/untracked1.png`);
});
});
describe('GET /report/:id/file', () => {
it.sequential('downloads untracked file', async () => {
await utils.putTextFile('untracked-content', `/data/upload/${admin.userId}/untracked1.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { body: listBody } = await request(app)
.get('/admin/integrity/report?type=untracked_file')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
const report = (listBody as IntegrityReportResponseDto).items.find(
(item) => item.path === `/data/upload/${admin.userId}/untracked1.png`,
)!;
const { status, headers, body } = await request(app)
.get(`/admin/integrity/report/${report.id}/file`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.buffer(true)
.send();
expect(status).toBe(200);
expect(headers['content-type']).toContain('application/octet-stream');
expect(body.toString()).toBe('untracked-content');
});
});
});
+41
View File
@@ -0,0 +1,41 @@
import { LoginResponseDto, ManualJobName, QueueName } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe.configure({ mode: 'serial' });
test.describe('Integrity', () => {
let admin: LoginResponseDto;
test.beforeAll(async () => {
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
});
test('run integrity jobs to update stats', async ({ context, page }) => {
await utils.setAuthCookies(context, admin.accessToken);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityUntrackedFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
await page.goto('/admin/maintenance');
const count = page.getByText('Untracked Files').locator('..').locator('..').locator('div').nth(1);
const previousCount = Number.parseInt((await count.textContent()) ?? '');
await utils.mkFolder(`/data/upload/${admin.userId}`);
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
const checkButton = page.getByText('Integrity Report').locator('..').getByRole('button', { name: 'Check All' });
await checkButton.click();
await expect(checkButton).toBeEnabled();
await expect(count).toContainText((previousCount + 1).toString());
});
});
+46 -3
View File
@@ -195,6 +195,7 @@ export const utils = {
'user',
'system_metadata',
'tag',
'integrity_report',
];
const sql: string[] = [];
@@ -586,10 +587,54 @@ export const utils = {
mkdirSync(`${testAssetDir}/temp`, { recursive: true });
},
putFile(source: string, dest: string) {
return executeCommand('docker', ['cp', source, `immich-e2e-server:${dest}`]).promise;
},
async putTextFile(contents: string, dest: string) {
const dir = await mkdtemp(join(tmpdir(), 'test-'));
const fn = join(dir, 'file');
await pipeline(Readable.from(contents), createWriteStream(fn));
return executeCommand('docker', ['cp', fn, `immich-e2e-server:${dest}`]).promise;
},
async move(source: string, dest: string) {
return executeCommand('docker', ['exec', 'immich-e2e-server', 'mv', source, dest]).promise;
},
async copyFolder(source: string, dest: string) {
return executeCommand('docker', ['exec', 'immich-e2e-server', 'cp', '-r', source, dest]).promise;
},
async deleteFile(path: string) {
return executeCommand('docker', ['exec', 'immich-e2e-server', 'rm', path]).promise;
},
async deleteFolder(path: string) {
return executeCommand('docker', ['exec', 'immich-e2e-server', 'rm', '-r', path]).promise;
},
async truncateFolder(path: string) {
return executeCommand('docker', [
'exec',
'immich-e2e-server',
'find',
path,
'-type',
'f',
'-exec',
'truncate',
'-s',
'1',
'{}',
';',
]).promise;
},
async mkFolder(path: string) {
return executeCommand('docker', ['exec', 'immich-e2e-server', 'mkdir', '-p', path]).promise;
},
createBackup: async (accessToken: string) => {
await utils.createJob(accessToken, {
name: ManualJobName.BackupDatabase,
@@ -604,10 +649,8 @@ export const utils = {
resetBackups: async (accessToken: string) => {
const { backups } = await listDatabaseBackups({ headers: asBearerAuth(accessToken) });
const backupFiles = backups.map((b) => b.filename);
await deleteDatabaseBackup(
{ databaseBackupDeleteDto: { backups: backupFiles } },
{ databaseBackupDeleteDto: { backups: backups.map((dto) => dto.filename) } },
{ headers: asBearerAuth(accessToken) },
);
},
+18 -7
View File
@@ -81,6 +81,7 @@
"cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
"cron_expression_presets": "Cron expression presets",
"disable_login": "Disable login",
"download_csv": "Download CSV",
"duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search",
"exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
"export_config_as_json_description": "Download the current system config as a JSON file",
@@ -193,6 +194,17 @@
"maintenance_delete_backup": "Delete Backup",
"maintenance_delete_backup_description": "This file will be irrevocably deleted.",
"maintenance_delete_error": "Failed to delete backup.",
"maintenance_integrity_check_all": "Check All",
"maintenance_integrity_checksum_mismatch": "Checksum Mismatch",
"maintenance_integrity_checksum_mismatch_job": "Check for checksum mismatches",
"maintenance_integrity_checksum_mismatch_refresh_job": "Refresh checksum mismatch reports",
"maintenance_integrity_missing_file": "Missing Files",
"maintenance_integrity_missing_file_job": "Check for missing files",
"maintenance_integrity_missing_file_refresh_job": "Refresh missing file reports",
"maintenance_integrity_report": "Integrity Report",
"maintenance_integrity_untracked_file": "Untracked Files",
"maintenance_integrity_untracked_file_job": "Check for untracked files",
"maintenance_integrity_untracked_file_refresh_job": "Refresh untracked file reports",
"maintenance_restore_backup": "Restore Backup",
"maintenance_restore_backup_description": "Immich will be wiped and restored from the chosen backup. A backup will be created before continuing.",
"maintenance_restore_backup_different_version": "This backup was created with a different version of Immich!",
@@ -1074,7 +1086,6 @@
"failed_to_update_notification_status": "Failed to update notification status",
"incorrect_email_or_password": "Incorrect email or password",
"library_folder_already_exists": "This import path already exists.",
"page_not_found": "Page not found :/",
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
"profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.",
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
@@ -1198,6 +1209,7 @@
"failed": "Failed",
"failed_count": "Failed: {count}",
"failed_to_authenticate": "Failed to authenticate",
"failed_to_delete_file": "Failed to delete file",
"failed_to_load_assets": "Failed to load assets",
"failed_to_load_folder": "Failed to load folder",
"favorite": "Favorite",
@@ -1327,6 +1339,7 @@
"individual_share": "Individual share",
"individual_shares": "Individual shares",
"info": "Info",
"integrity_checks": "Integrity Checks",
"interval": {
"day_at_onepm": "Every day at 1pm",
"hours": "Every {hours, plural, one {hour} other {{hours, number} hours}}",
@@ -1393,6 +1406,7 @@
"link_to_oauth": "Link to OAuth",
"linked_oauth_account": "Linked OAuth account",
"list": "List",
"load_more": "Load More",
"loading": "Loading",
"loading_search_results_failed": "Loading search results failed",
"local": "Local",
@@ -1810,8 +1824,9 @@
"rate_asset": "Rate Asset",
"rating": "Star rating",
"rating_clear": "Clear rating",
"rating_count": "{count, plural, =0 {Unrated} one {# star} other {# stars}}",
"rating_count": "{count, plural, one {# star} other {# stars}}",
"rating_description": "Display the EXIF rating in the info panel",
"rating_set": "Rating set to {rating, plural, one {# star} other {# stars}}",
"reaction_options": "Reaction options",
"read_changelog": "Read Changelog",
"readonly_mode_disabled": "Read-only mode disabled",
@@ -1883,10 +1898,7 @@
"reset_pin_code_success": "Successfully reset PIN code",
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
"reset_sqlite": "Reset SQLite Database",
"reset_sqlite_clear_app_data": "Clear Data",
"reset_sqlite_confirmation": "Are you sure you want to clear the app data? This will remove all settings and sign you out.",
"reset_sqlite_confirmation_note": "Note: You will need to restart the app after clearing.",
"reset_sqlite_done": "App data has been cleared. Please restart Immich and log in again.",
"reset_sqlite_confirmation": "Are you sure you want to reset the SQLite database? You will need to log out and log in again to resync the data",
"reset_sqlite_success": "Successfully reset the SQLite database",
"reset_to_default": "Reset to default",
"resolution": "Resolution",
@@ -1914,7 +1926,6 @@
"saved_settings": "Saved settings",
"say_something": "Say something",
"scaffold_body_error_occurred": "Error occurred",
"scaffold_body_error_unrecoverable": "An unrecoverable error has occurred. Please share the error and stack trace on Discord or GitHub so we can help. If advised, you can clear the app data below.",
"scan": "Scan",
"scan_all_libraries": "Scan All Libraries",
"scan_library": "Scan",
-1
View File
@@ -1 +0,0 @@
3.13
+50 -10
View File
@@ -22,7 +22,48 @@ FROM builder-cpu AS builder-rknn
# Warning: 25GiB+ disk space required to pull this image
# TODO: find a way to reduce the image size
FROM rocm/dev-ubuntu-24.04:7.2-complete@sha256:86e11093b4a7ec2a79b1b6701d10e840a6994f21c7e05929b51eb9be361c683a AS builder-rocm
FROM rocm/dev-ubuntu-24.04:6.4.4-complete@sha256:31418ac10a3769a71eaef330c07280d1d999d7074621339b8f93c484c35f6078 AS builder-rocm
# renovate: datasource=github-releases depName=Microsoft/onnxruntime
ARG ONNXRUNTIME_VERSION="v1.22.1"
WORKDIR /code
RUN apt-get update && apt-get install -y --no-install-recommends wget git
RUN wget -nv https://github.com/Kitware/CMake/releases/download/v3.31.9/cmake-3.31.9-linux-x86_64.sh && \
chmod +x cmake-3.31.9-linux-x86_64.sh && \
mkdir -p /code/cmake-3.31.9-linux-x86_64 && \
./cmake-3.31.9-linux-x86_64.sh --skip-license --prefix=/code/cmake-3.31.9-linux-x86_64 && \
rm cmake-3.31.9-linux-x86_64.sh
RUN git clone --single-branch --branch "${ONNXRUNTIME_VERSION}" --recursive "https://github.com/Microsoft/onnxruntime" onnxruntime
WORKDIR /code/onnxruntime
# Fix for multi-threading based on comments in https://github.com/microsoft/onnxruntime/pull/19567
# TODO: find a way to fix this without disabling algo caching
COPY ./patches/* /tmp/
RUN git apply /tmp/*.patch
RUN /bin/sh ./dockerfiles/scripts/install_common_deps.sh
ENV PATH=/opt/rocm-venv/bin:/code/cmake-3.31.9-linux-x86_64/bin:${PATH}
ENV CCACHE_DIR="/ccache"
# Note: the `parallel` setting uses a substantial amount of RAM
RUN --mount=type=cache,target=/ccache \
./build.sh \
--allow_running_as_root \
--config Release \
--build_wheel \
--update \
--build \
--parallel 48 \
--cmake_extra_defines \
ONNXRUNTIME_VERSION="${ONNXRUNTIME_VERSION}" \
CMAKE_HIP_ARCHITECTURES="gfx900;gfx906;gfx908;gfx90a;gfx940;gfx941;gfx942;gfx1030;gfx1100;gfx1101;gfx1102;gfx1200;gfx1201" \
--skip_tests \
--use_rocm \
--rocm_home=/opt/rocm \
--use_cache \
--compile_no_warning_as_error
RUN mv /code/onnxruntime/build/Linux/Release/dist/*.whl /opt/
FROM builder-${DEVICE} AS builder
@@ -38,6 +79,9 @@ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
RUN if [ "$DEVICE" = "rocm" ]; then \
uv pip install /opt/onnxruntime_rocm-*.whl; \
fi
FROM python:3.11-slim-bookworm@sha256:04cd27899595a99dfe77709d96f08876bf2ee99139ee2f0fe9ac948005034e5b AS prod-cpu
@@ -48,14 +92,14 @@ FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c3
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.27.10/intel-igc-core-2_2.27.10+20617_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-opencl-2_2.27.10+20617_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/intel-opencl-icd_26.01.36711.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.01.36711.4/libigdgmm12_22.9.0_amd64.deb && \
dpkg -i *.deb && \
rm *.deb && \
apt-get remove wget -yqq && \
@@ -76,11 +120,7 @@ COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11
COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so
FROM rocm/dev-ubuntu-24.04:7.2-complete@sha256:86e11093b4a7ec2a79b1b6701d10e840a6994f21c7e05929b51eb9be361c683a AS prod-rocm
RUN apt-get update && apt-get install --no-install-recommends -yqq migraphx miopen-hip && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
FROM rocm/dev-ubuntu-24.04:6.4.4-complete@sha256:31418ac10a3769a71eaef330c07280d1d999d7074621339b8f93c484c35f6078 AS prod-rocm
FROM prod-cpu AS prod-armnn
-1
View File
@@ -79,7 +79,6 @@ class Settings(BaseSettings):
preload: PreloadModelData | None = None
max_batch_size: MaxBatchSize | None = None
openvino_precision: ModelPrecision = ModelPrecision.FP32
rocm_precision: ModelPrecision = ModelPrecision.FP32
@property
def device_id(self) -> str:
@@ -90,7 +90,7 @@ _PADDLE_MODELS = {
SUPPORTED_PROVIDERS = [
"CUDAExecutionProvider",
"MIGraphXExecutionProvider",
"ROCMExecutionProvider",
"OpenVINOExecutionProvider",
"CoreMLExecutionProvider",
"CPUExecutionProvider",
+2 -11
View File
@@ -8,7 +8,7 @@ import onnxruntime as ort
from numpy.typing import NDArray
from immich_ml.models.constants import SUPPORTED_PROVIDERS
from immich_ml.schemas import ModelPrecision, SessionNode
from immich_ml.schemas import SessionNode
from ..config import log, settings
@@ -90,17 +90,8 @@ class OrtSession:
match provider:
case "CPUExecutionProvider":
options = {"arena_extend_strategy": "kSameAsRequested"}
case "CUDAExecutionProvider":
case "CUDAExecutionProvider" | "ROCMExecutionProvider":
options = {"arena_extend_strategy": "kSameAsRequested", "device_id": settings.device_id}
case "MIGraphXExecutionProvider":
migraphx_dir = self.model_path.parent / "migraphx"
# MIGraphX does not create the underlying folder and will crash if it does not exist
migraphx_dir.mkdir(parents=True, exist_ok=True)
options = {
"device_id": settings.device_id,
"migraphx_model_cache_dir": migraphx_dir.as_posix(),
"migraphx_fp16_enable": "1" if settings.rocm_precision == ModelPrecision.FP16 else "0",
}
case "OpenVINOExecutionProvider":
openvino_dir = self.model_path.parent / "openvino"
device = f"GPU.{settings.device_id}"
@@ -0,0 +1,179 @@
commit 16839b58d9b3c3162a67ce5d776b36d4d24e801f
Author: mertalev <101130780+mertalev@users.noreply.github.com>
Date: Wed Mar 5 11:25:38 2025 -0500
disable algo caching (attributed to @dmnieto in https://github.com/microsoft/onnxruntime/pull/19567)
diff --git a/onnxruntime/core/providers/rocm/nn/conv.cc b/onnxruntime/core/providers/rocm/nn/conv.cc
index d7f47d07a8..4060a2af52 100644
--- a/onnxruntime/core/providers/rocm/nn/conv.cc
+++ b/onnxruntime/core/providers/rocm/nn/conv.cc
@@ -127,7 +127,6 @@ Status Conv<T, NHWC>::UpdateState(OpKernelContext* context, bool bias_expected)
if (w_dims_changed) {
s_.last_w_dims = gsl::make_span(w_dims);
- s_.cached_benchmark_fwd_results.clear();
}
ORT_RETURN_IF_ERROR(conv_attrs_.ValidateInputShape(X->Shape(), W->Shape(), channels_last, channels_last));
@@ -277,35 +276,6 @@ Status Conv<T, NHWC>::UpdateState(OpKernelContext* context, bool bias_expected)
HIP_CALL_THROW(hipMalloc(&s_.b_zero, malloc_size));
HIP_CALL_THROW(hipMemsetAsync(s_.b_zero, 0, malloc_size, Stream(context)));
}
-
- if (!s_.cached_benchmark_fwd_results.contains(x_dims_miopen)) {
- miopenConvAlgoPerf_t perf;
- int algo_count = 1;
- const ROCMExecutionProvider* rocm_ep = static_cast<const ROCMExecutionProvider*>(this->Info().GetExecutionProvider());
- static constexpr int num_algos = MIOPEN_CONVOLUTION_FWD_ALGO_COUNT;
- size_t max_ws_size = rocm_ep->GetMiopenConvUseMaxWorkspace() ? GetMaxWorkspaceSize(GetMiopenHandle(context), s_, kAllAlgos, num_algos, rocm_ep->GetDeviceId())
- : AlgoSearchWorkspaceSize;
- IAllocatorUniquePtr<void> algo_search_workspace = GetTransientScratchBuffer<void>(max_ws_size);
- MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionForwardAlgorithm(
- GetMiopenHandle(context),
- s_.x_tensor,
- s_.x_data,
- s_.w_desc,
- s_.w_data,
- s_.conv_desc,
- s_.y_tensor,
- s_.y_data,
- 1, // requestedAlgoCount
- &algo_count, // returnedAlgoCount
- &perf,
- algo_search_workspace.get(),
- max_ws_size,
- false)); // Do not do exhaustive algo search.
- s_.cached_benchmark_fwd_results.insert(x_dims_miopen, {perf.fwd_algo, perf.memory});
- }
- const auto& perf = s_.cached_benchmark_fwd_results.at(x_dims_miopen);
- s_.fwd_algo = perf.fwd_algo;
- s_.workspace_bytes = perf.memory;
} else {
// set Y
s_.Y = context->Output(0, TensorShape(s_.y_dims));
@@ -319,6 +289,31 @@ Status Conv<T, NHWC>::UpdateState(OpKernelContext* context, bool bias_expected)
s_.y_data = reinterpret_cast<HipT*>(s_.Y->MutableData<T>());
}
}
+
+ miopenConvAlgoPerf_t perf;
+ int algo_count = 1;
+ const ROCMExecutionProvider* rocm_ep = static_cast<const ROCMExecutionProvider*>(this->Info().GetExecutionProvider());
+ static constexpr int num_algos = MIOPEN_CONVOLUTION_FWD_ALGO_COUNT;
+ size_t max_ws_size = rocm_ep->GetMiopenConvUseMaxWorkspace() ? GetMaxWorkspaceSize(GetMiopenHandle(context), s_, kAllAlgos, num_algos, rocm_ep->GetDeviceId())
+ : AlgoSearchWorkspaceSize;
+ IAllocatorUniquePtr<void> algo_search_workspace = GetTransientScratchBuffer<void>(max_ws_size);
+ MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionForwardAlgorithm(
+ GetMiopenHandle(context),
+ s_.x_tensor,
+ s_.x_data,
+ s_.w_desc,
+ s_.w_data,
+ s_.conv_desc,
+ s_.y_tensor,
+ s_.y_data,
+ 1, // requestedAlgoCount
+ &algo_count, // returnedAlgoCount
+ &perf,
+ algo_search_workspace.get(),
+ max_ws_size,
+ false)); // Do not do exhaustive algo search.
+ s_.fwd_algo = perf.fwd_algo;
+ s_.workspace_bytes = perf.memory;
return Status::OK();
}
diff --git a/onnxruntime/core/providers/rocm/nn/conv.h b/onnxruntime/core/providers/rocm/nn/conv.h
index bc9846203e..d54218f258 100644
--- a/onnxruntime/core/providers/rocm/nn/conv.h
+++ b/onnxruntime/core/providers/rocm/nn/conv.h
@@ -108,9 +108,6 @@ class lru_unordered_map {
list_type lru_list_;
};
-// cached miopen descriptors
-constexpr size_t MAX_CACHED_ALGO_PERF_RESULTS = 10000;
-
template <typename AlgoPerfType>
struct MiopenConvState {
// if x/w dims changed, update algo and miopenTensors
@@ -148,9 +145,6 @@ struct MiopenConvState {
decltype(AlgoPerfType().memory) memory;
};
- lru_unordered_map<TensorShapeVector, PerfFwdResultParams, vector_hash> cached_benchmark_fwd_results{MAX_CACHED_ALGO_PERF_RESULTS};
- lru_unordered_map<TensorShapeVector, PerfBwdResultParams, vector_hash> cached_benchmark_bwd_results{MAX_CACHED_ALGO_PERF_RESULTS};
-
// Some properties needed to support asymmetric padded Conv nodes
bool post_slicing_required;
TensorShapeVector slice_starts;
diff --git a/onnxruntime/core/providers/rocm/nn/conv_transpose.cc b/onnxruntime/core/providers/rocm/nn/conv_transpose.cc
index 7447113fdf..a662e35b2e 100644
--- a/onnxruntime/core/providers/rocm/nn/conv_transpose.cc
+++ b/onnxruntime/core/providers/rocm/nn/conv_transpose.cc
@@ -76,7 +76,6 @@ Status ConvTranspose<T, NHWC>::DoConvTranspose(OpKernelContext* context, bool dy
if (w_dims_changed) {
s_.last_w_dims = gsl::make_span(w_dims);
- s_.cached_benchmark_bwd_results.clear();
}
ConvTransposeAttributes::Prepare p;
@@ -126,35 +125,29 @@ Status ConvTranspose<T, NHWC>::DoConvTranspose(OpKernelContext* context, bool dy
}
y_data = reinterpret_cast<HipT*>(p.Y->MutableData<T>());
-
- if (!s_.cached_benchmark_bwd_results.contains(x_dims)) {
- IAllocatorUniquePtr<void> algo_search_workspace = GetScratchBuffer<void>(AlgoSearchWorkspaceSize, context->GetComputeStream());
-
- miopenConvAlgoPerf_t perf;
- int algo_count = 1;
- MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionBackwardDataAlgorithm(
- GetMiopenHandle(context),
- s_.x_tensor,
- x_data,
- s_.w_desc,
- w_data,
- s_.conv_desc,
- s_.y_tensor,
- y_data,
- 1,
- &algo_count,
- &perf,
- algo_search_workspace.get(),
- AlgoSearchWorkspaceSize,
- false));
- s_.cached_benchmark_bwd_results.insert(x_dims, {perf.bwd_data_algo, perf.memory});
- }
-
- const auto& perf = s_.cached_benchmark_bwd_results.at(x_dims);
- s_.bwd_data_algo = perf.bwd_data_algo;
- s_.workspace_bytes = perf.memory;
}
+ IAllocatorUniquePtr<void> algo_search_workspace = GetScratchBuffer<void>(AlgoSearchWorkspaceSize, context->GetComputeStream());
+ miopenConvAlgoPerf_t perf;
+ int algo_count = 1;
+ MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionBackwardDataAlgorithm(
+ GetMiopenHandle(context),
+ s_.x_tensor,
+ x_data,
+ s_.w_desc,
+ w_data,
+ s_.conv_desc,
+ s_.y_tensor,
+ y_data,
+ 1,
+ &algo_count,
+ &perf,
+ algo_search_workspace.get(),
+ AlgoSearchWorkspaceSize,
+ false));
+ s_.bwd_data_algo = perf.bwd_data_algo;
+ s_.workspace_bytes = perf.memory;
+
// The following block will be executed in case there has been no change in the shapes of the
// input and the filter compared to the previous run
if (!y_data) {
@@ -0,0 +1,33 @@
diff --git a/dockerfiles/scripts/install_common_deps.sh b/dockerfiles/scripts/install_common_deps.sh
index bbb672a99e..0dc652fbda 100644
--- a/dockerfiles/scripts/install_common_deps.sh
+++ b/dockerfiles/scripts/install_common_deps.sh
@@ -8,16 +8,23 @@ apt-get update && apt-get install -y --no-install-recommends \
curl \
libcurl4-openssl-dev \
libssl-dev \
- python3-dev
+ python3-dev \
+ ccache
# Dependencies: conda
-wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-4.5.11-Linux-x86_64.sh -O ~/miniconda.sh --no-check-certificate && /bin/bash ~/miniconda.sh -b -p /opt/miniconda
+wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-py312_25.9.1-1-Linux-x86_64.sh -O ~/miniconda.sh && /bin/bash ~/miniconda.sh -b -p /opt/miniconda
rm ~/miniconda.sh
/opt/miniconda/bin/conda clean -ya
-pip install numpy
-pip install packaging
-pip install "wheel>=0.35.1"
+# Dependencies: venv and packages
+/opt/miniconda/bin/python3 -m venv /opt/rocm-venv
+/opt/rocm-venv/bin/pip install --no-cache-dir --upgrade pip
+/opt/rocm-venv/bin/pip install --no-cache-dir \
+ "numpy==2.3.4" \
+ "packaging==25.0" \
+ "wheel==0.45.1" \
+ "setuptools==80.9.0"
+
rm -rf /opt/miniconda/pkgs
# Dependencies: cmake
+2 -2
View File
@@ -49,10 +49,10 @@ dev = ["locust>=2.15.1", { include-group = "test" }, { include-group = "lint" }]
[project.optional-dependencies]
cpu = ["onnxruntime>=1.23.2,<2"]
cuda = ["onnxruntime-gpu>=1.23.2,<2"]
openvino = ["onnxruntime-openvino>=1.24.1,<2"]
openvino = ["onnxruntime-openvino>=1.23.0,<2"]
armnn = ["onnxruntime>=1.23.2,<2"]
rknn = ["onnxruntime>=1.23.2,<2", "rknn-toolkit-lite2>=2.3.0,<3"]
rocm = ["onnxruntime-migraphx>=1.23.2,<2"]
rocm = []
[tool.uv]
compile-bytecode = true
+4 -30
View File
@@ -179,7 +179,7 @@ class TestOrtSession:
OV_EP = ["OpenVINOExecutionProvider", "CPUExecutionProvider"]
CUDA_EP_OUT_OF_ORDER = ["CPUExecutionProvider", "CUDAExecutionProvider"]
TRT_EP = ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"]
ROCM_EP = ["MIGraphXExecutionProvider", "CPUExecutionProvider"]
ROCM_EP = ["ROCMExecutionProvider", "CPUExecutionProvider"]
COREML_EP = ["CoreMLExecutionProvider", "CPUExecutionProvider"]
@pytest.mark.providers(CPU_EP)
@@ -289,38 +289,12 @@ class TestOrtSession:
assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}]
def test_sets_provider_options_for_rocm(self, mocker: MockerFixture) -> None:
model_path = "/cache/ViT-B-32__openai/textual/model.onnx"
def test_sets_provider_options_for_rocm(self) -> None:
os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
mkdir = mocker.patch("immich_ml.sessions.ort.Path.mkdir")
session = OrtSession(model_path, providers=["MIGraphXExecutionProvider"])
session = OrtSession("ViT-B-32__openai", providers=["ROCMExecutionProvider"])
assert session.provider_options == [
{
"device_id": "1",
"migraphx_model_cache_dir": "/cache/ViT-B-32__openai/textual/migraphx",
"migraphx_fp16_enable": "0",
}
]
mkdir.assert_called_once_with(parents=True, exist_ok=True)
def test_sets_rocm_to_fp16_if_enabled(self, path: mock.Mock, mocker: MockerFixture) -> None:
model_path = "/cache/ViT-B-32__openai/textual/model.onnx"
os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
mocker.patch.object(settings, "rocm_precision", ModelPrecision.FP16)
mkdir = mocker.patch("immich_ml.sessions.ort.Path.mkdir")
session = OrtSession(model_path, providers=["MIGraphXExecutionProvider"])
assert session.provider_options == [
{
"device_id": "1",
"migraphx_model_cache_dir": "/cache/ViT-B-32__openai/textual/migraphx",
"migraphx_fp16_enable": "1",
}
]
mkdir.assert_called_once_with(parents=True, exist_ok=True)
assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}]
def test_sets_provider_options_kwarg(self) -> None:
session = OrtSession(
+66 -77
View File
@@ -262,6 +262,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coloredlogs"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "humanfriendly" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
]
[[package]]
name = "colorlog"
version = "6.9.0"
@@ -874,6 +886,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
]
[[package]]
name = "humanfriendly"
version = "10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
]
[[package]]
name = "idna"
version = "3.11"
@@ -936,9 +960,6 @@ rknn = [
{ name = "onnxruntime" },
{ name = "rknn-toolkit-lite2" },
]
rocm = [
{ name = "onnxruntime-migraphx" },
]
[package.dev-dependencies]
dev = [
@@ -992,8 +1013,7 @@ requires-dist = [
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime-migraphx", marker = "extra == 'rocm'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" },
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.23.0,<2" },
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
{ name = "orjson", specifier = ">=3.9.5" },
{ name = "pillow", specifier = ">=12.1.1,<12.2" },
@@ -1409,55 +1429,32 @@ wheels = [
[[package]]
name = "msgpack"
version = "1.1.2"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/d5/5662032db1571110b5b51647aed4b56dfbd01bfae789fa566a2be1f385d1/msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", size = 166311, upload-time = "2023-09-28T13:20:36.726Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
{ url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
{ url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
{ url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
{ url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
{ url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
{ url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
{ url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
{ url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
{ url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
{ url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
{ url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
{ url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
{ url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
{ url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
{ url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
{ url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
{ url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
{ url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
{ url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
{ url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
{ url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
{ url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
{ url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
{ url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
{ url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
{ url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
{ url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
{ url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
{ url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
{ url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
{ url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
{ url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
{ url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
{ url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
{ url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
{ url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
{ url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
{ url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
{ url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
{ url = "https://files.pythonhosted.org/packages/f9/b3/309de40dc7406b7f3492332c5ee2b492a593c2a9bb97ea48ebf2f5279999/msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", size = 305096, upload-time = "2023-09-28T13:18:49.678Z" },
{ url = "https://files.pythonhosted.org/packages/15/56/a677cd761a2cefb2e3ffe7e684633294dccb161d78e8ea6da9277e45b4a2/msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", size = 235210, upload-time = "2023-09-28T13:18:51.039Z" },
{ url = "https://files.pythonhosted.org/packages/f5/4e/1ab4a982cbd90f988e49f849fc1212f2c04a59870c59daabf8950617e2aa/msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", size = 231952, upload-time = "2023-09-28T13:18:52.871Z" },
{ url = "https://files.pythonhosted.org/packages/6d/74/bd02044eb628c7361ad2bd8c1a6147af5c6c2bbceb77b3b1da20f4a8a9c5/msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", size = 549511, upload-time = "2023-09-28T13:18:54.422Z" },
{ url = "https://files.pythonhosted.org/packages/df/09/dee50913ba5cc047f7fd7162f09453a676e7935c84b3bf3a398e12108677/msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", size = 557980, upload-time = "2023-09-28T13:18:56.058Z" },
{ url = "https://files.pythonhosted.org/packages/26/a5/78a7d87f5f8ffe4c32167afa15d4957db649bab4822f909d8d765339bbab/msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", size = 545547, upload-time = "2023-09-28T13:18:57.396Z" },
{ url = "https://files.pythonhosted.org/packages/d4/53/698c10913947f97f6fe7faad86a34e6aa1b66cea2df6f99105856bd346d9/msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", size = 554669, upload-time = "2023-09-28T13:18:58.957Z" },
{ url = "https://files.pythonhosted.org/packages/f5/3f/9730c6cb574b15d349b80cd8523a7df4b82058528339f952ea1c32ac8a10/msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", size = 583353, upload-time = "2023-09-28T13:19:01.186Z" },
{ url = "https://files.pythonhosted.org/packages/4c/bc/dc184d943692671149848438fb3bed3a3de288ce7998cb91bc98f40f201b/msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", size = 557455, upload-time = "2023-09-28T13:19:03.201Z" },
{ url = "https://files.pythonhosted.org/packages/cf/7b/1bc69d4a56c8d2f4f2dfbe4722d40344af9a85b6fb3b09cfb350ba6a42f6/msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", size = 216367, upload-time = "2023-09-28T13:19:04.554Z" },
{ url = "https://files.pythonhosted.org/packages/b4/3d/c8dd23050eefa3d9b9c5b8329ed3308c2f2f80f65825e9ea4b7fa621cdab/msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", size = 222860, upload-time = "2023-09-28T13:19:06.397Z" },
{ url = "https://files.pythonhosted.org/packages/d7/47/20dff6b4512cf3575550c8801bc53fe7d540f4efef9c5c37af51760fcdcf/msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", size = 305759, upload-time = "2023-09-28T13:19:08.148Z" },
{ url = "https://files.pythonhosted.org/packages/6f/8a/34f1726d2c9feccec3d946776e9bce8f20ae09d8b91899fc20b296c942af/msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", size = 235330, upload-time = "2023-09-28T13:19:09.417Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f6/e64c72577d6953789c3cb051b059a4b56317056b3c65013952338ed8a34e/msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", size = 232537, upload-time = "2023-09-28T13:19:10.898Z" },
{ url = "https://files.pythonhosted.org/packages/89/75/1ed3a96e12941873fd957e016cc40c0c178861a872bd45e75b9a188eb422/msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", size = 546561, upload-time = "2023-09-28T13:19:12.779Z" },
{ url = "https://files.pythonhosted.org/packages/e5/0a/c6a1390f9c6a31da0fecbbfdb86b1cb39ad302d9e24f9cca3d9e14c364f0/msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", size = 559009, upload-time = "2023-09-28T13:19:14.373Z" },
{ url = "https://files.pythonhosted.org/packages/a5/74/99f6077754665613ea1f37b3d91c10129f6976b7721ab4d0973023808e5a/msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", size = 543882, upload-time = "2023-09-28T13:19:16.277Z" },
{ url = "https://files.pythonhosted.org/packages/9c/7e/dc0dc8de2bf27743b31691149258f9b1bd4bf3c44c105df3df9b97081cd1/msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", size = 546949, upload-time = "2023-09-28T13:19:18.114Z" },
{ url = "https://files.pythonhosted.org/packages/78/61/91bae9474def032f6c333d62889bbeda9e1554c6b123375ceeb1767efd78/msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", size = 579836, upload-time = "2023-09-28T13:19:19.729Z" },
{ url = "https://files.pythonhosted.org/packages/5d/4d/d98592099d4f18945f89cf3e634dc0cb128bb33b1b93f85a84173d35e181/msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", size = 556587, upload-time = "2023-09-28T13:19:21.666Z" },
{ url = "https://files.pythonhosted.org/packages/5e/44/6556ffe169bf2c0e974e2ea25fb82a7e55ebcf52a81b03a5e01820de5f84/msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", size = 216509, upload-time = "2023-09-28T13:19:23.161Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c1/63903f30d51d165e132e5221a2a4a1bbfab7508b68131c871d70bffac78a/msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", size = 223287, upload-time = "2023-09-28T13:19:25.097Z" },
]
[[package]]
@@ -1704,29 +1701,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/94/a3b20276261f5e64dbd72bda656af988282cff01f18c2685953600e2f810/onnxruntime_gpu-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2cee7e12b0f4813c62f9a48df83fd01d066cc970400c832252cf3c155a6957", size = 252633096, upload-time = "2026-02-05T17:24:53.248Z" },
]
[[package]]
name = "onnxruntime-migraphx"
version = "1.24.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flatbuffers" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "sympy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/da/ca7ebc1a8d1193c97ceb9a05fad50f675eb955dc51beb7eb9ba89c8e7db0/onnxruntime_migraphx-1.24.2-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:a2b434fb8880cac2b268950bdf279f33741d29c1f1c5461d27af835e8e288043", size = 20339710, upload-time = "2026-02-21T07:25:13.17Z" },
{ url = "https://files.pythonhosted.org/packages/fa/2e/8c83ec45a9365b4256495ca55eea30da7f03b02177b6da423c7da1ff5f6a/onnxruntime_migraphx-1.24.2-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ec814818da952bda3062e26f56c88bb713c00491ef91f86716c8d7346f9bc31b", size = 20341883, upload-time = "2026-02-21T07:25:17.86Z" },
{ url = "https://files.pythonhosted.org/packages/9f/52/4776ac68dbc46ca02c9a14cc9e5c496017f47a18cedf606cc38f4911b96a/onnxruntime_migraphx-1.24.2-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:20e497538362170af639b03a40249d7ed61b873ac354f20d732b90252206e320", size = 20342422, upload-time = "2026-02-21T07:25:22.526Z" },
{ url = "https://files.pythonhosted.org/packages/76/44/db9035204a3363f9c0a4822c68e9a7520c13ef8d261f96b89b1375106dab/onnxruntime_migraphx-1.24.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:9d7f1b1a2b9651143a2080b4f42ee99eead02023de1855d1b8a02199a9c179aa", size = 20343783, upload-time = "2026-02-21T07:25:29.155Z" },
]
[[package]]
name = "onnxruntime-openvino"
version = "1.24.1"
version = "1.23.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coloredlogs" },
{ name = "flatbuffers" },
{ name = "numpy" },
{ name = "packaging" },
@@ -1734,12 +1714,12 @@ dependencies = [
{ name = "sympy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/16/69ca742f0b65c40d4de3ff44bb6abc23c47b23e932bc901116176ae69922/onnxruntime_openvino-1.24.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3007c803634cc69c6d52af1dea7ce729d9bb62b9a11070fd2f959119199007a8", size = 84430935, upload-time = "2026-02-26T13:44:32.193Z" },
{ url = "https://files.pythonhosted.org/packages/aa/73/619bb416bbfc40aebdd493fd6800d2637359294fe683d8a6bae3ff8d869a/onnxruntime_openvino-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:8042698232bf67f1f6b219c2b07728d7ae7ddff17d8524588de3675480609aef", size = 13655357, upload-time = "2026-02-26T13:44:35.555Z" },
{ url = "https://files.pythonhosted.org/packages/50/cf/17ba72de2df0fcba349937d2788f154397bbc2d1a2d67772a97e26f6bc5f/onnxruntime_openvino-1.24.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d617fac2f59a6ab5ea59a788c3e1592240a129642519aaeaa774761dfe35150e", size = 84433207, upload-time = "2026-02-26T13:44:41.395Z" },
{ url = "https://files.pythonhosted.org/packages/59/37/d301f2c68b19a9485ed5db3047e0fb52478f3e73eb08c7d2a7c61be7cc1c/onnxruntime_openvino-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:f186335a9c9b255633275290da7521d3d4d14c7773fee3127bfa040234d3fa5a", size = 13658075, upload-time = "2026-02-26T13:44:44.905Z" },
{ url = "https://files.pythonhosted.org/packages/08/07/f225999919f56506b603aaa3ff837ad563ab26f86906ed7fa7e5abcd849e/onnxruntime_openvino-1.24.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2c3bb73e68ac27f4891af8a595c1faf574ec68b772e6583c90a0b997a1822782", size = 84433183, upload-time = "2026-02-26T13:44:50.254Z" },
{ url = "https://files.pythonhosted.org/packages/3e/92/46ae2cd565961a89189900f385bb2f13a9fa731ea4674001d23720fbb1e0/onnxruntime_openvino-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:434bf49aa71393c577a456c9d76c98e6d6958a833fa0876793e3d5437b5a511a", size = 13658485, upload-time = "2026-02-26T13:44:53.889Z" },
{ url = "https://files.pythonhosted.org/packages/5a/10/adcd4ac68ffc8dee003553125ef5c091be822e2d7c1077d0bb85690baa9c/onnxruntime_openvino-1.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:91938837e6e92e30c63d12fad68a8a4959c40d2eade2bd60f38bdd5b6392f8d3", size = 70481480, upload-time = "2025-10-14T15:19:45.882Z" },
{ url = "https://files.pythonhosted.org/packages/97/95/25f28d6fecf300aa0af393e96af9e00cc676e5dab650ab84f2122610df50/onnxruntime_openvino-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f05d2d6a804fb70d3f4329d777ac62439773dcc2df827dd5f42644b10bf1fea", size = 13117353, upload-time = "2025-10-14T15:19:49.014Z" },
{ url = "https://files.pythonhosted.org/packages/42/0c/8d97419dfeedf419c5fe5293f3dbc59284855a63ad22e71f46c0010c9dc4/onnxruntime_openvino-1.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b963ea19bf9856f3d6b2f719d451f2eeae482a8f69c729906465aa4f27f4d39c", size = 70483359, upload-time = "2025-10-14T15:19:52.88Z" },
{ url = "https://files.pythonhosted.org/packages/29/30/ff6111b16ffb4187c462824aa4e95acc20fdd90f856d44a339d56c6dacd6/onnxruntime_openvino-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:937e52657f94c56990a6e5bd4c3705bd6e970834c7c94e23d300dde6848f2889", size = 13117933, upload-time = "2025-10-14T15:19:58.319Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/e42f618a8ec5fcf825fed4fdc8125f7105256cc6020b84567ecb88d5e2b7/onnxruntime_openvino-1.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2e93b9a8323e196b7433866054a59260f2206ab6fb0e7223dda91da71f1db8c5", size = 70483088, upload-time = "2025-10-14T15:20:02.425Z" },
{ url = "https://files.pythonhosted.org/packages/4a/f9/a531dc497dc113dc14df9a9de5aacb1676cadebc3ec6cc7cd3ca65cb3db0/onnxruntime_openvino-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:0ebbf70929de4ce269371cb255536bbedef588932d744da0b40e66c38a620f35", size = 13118206, upload-time = "2025-10-14T15:20:05.587Z" },
]
[[package]]
@@ -2179,6 +2159,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/92/8486ede85fcc088f1b3dba4ce92dd29d126fd96b0008ea213167940a2475/pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", size = 103139, upload-time = "2023-07-30T15:06:59.829Z" },
]
[[package]]
name = "pyreadline3"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/86/3d61a61f36a0067874a00cb4dceb9028d34b6060e47828f7fc86fb9f7ee9/pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae", size = 86465, upload-time = "2022-01-24T20:05:11.66Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/fc/a3c13ded7b3057680c8ae95a9b6cc83e63657c38e0005c400a5d018a33a7/pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb", size = 95203, upload-time = "2022-01-24T20:05:10.442Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
@@ -59,7 +59,7 @@ private open class LocalImagesPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface LocalImageApi {
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, preferEncoded: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
fun cancelRequest(requestId: Long)
fun getThumbhash(thumbhash: String, callback: (Result<Map<String, Long>>) -> Unit)
@@ -82,8 +82,7 @@ interface LocalImageApi {
val widthArg = args[2] as Long
val heightArg = args[3] as Long
val isVideoArg = args[4] as Boolean
val preferEncodedArg = args[5] as Boolean
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg, preferEncodedArg) { result: Result<Map<String, Long>?> ->
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg) { result: Result<Map<String, Long>?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(LocalImagesPigeonUtils.wrapError(error))
@@ -14,7 +14,6 @@ import android.util.Size
import androidx.annotation.RequiresApi
import app.alextran.immich.NativeBuffer
import kotlin.math.*
import java.io.IOException
import java.util.concurrent.Executors
import com.bumptech.glide.Glide
import com.bumptech.glide.Priority
@@ -100,17 +99,12 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
width: Long,
height: Long,
isVideo: Boolean,
preferEncoded: Boolean,
callback: (Result<Map<String, Long>?>) -> Unit
) {
val signal = CancellationSignal()
val task = threadPool.submit {
try {
if (preferEncoded) {
getEncodedImageInternal(assetId, callback, signal)
} else {
getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal)
}
getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal)
} catch (e: Exception) {
when (e) {
is OperationCanceledException -> callback(CANCELLED)
@@ -139,35 +133,6 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
}
}
private fun getEncodedImageInternal(
assetId: String,
callback: (Result<Map<String, Long>?>) -> Unit,
signal: CancellationSignal
) {
signal.throwIfCanceled()
val id = assetId.toLong()
val uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id)
signal.throwIfCanceled()
val bytes = resolver.openInputStream(uri)?.use { it.readBytes() }
?: throw IOException("Could not read image data for $assetId")
signal.throwIfCanceled()
val pointer = NativeBuffer.allocate(bytes.size)
try {
val buffer = NativeBuffer.wrap(pointer, bytes.size)
buffer.put(bytes)
signal.throwIfCanceled()
callback(Result.success(mapOf(
"pointer" to pointer,
"length" to bytes.size.toLong()
)))
} catch (e: Exception) {
NativeBuffer.free(pointer)
throw e
}
}
private fun getThumbnailBufferInternal(
assetId: String,
width: Long,
@@ -47,7 +47,7 @@ private open class RemoteImagesPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface RemoteImageApi {
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, preferEncoded: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>?>) -> Unit)
fun cancelRequest(requestId: Long)
fun clearCache(callback: (Result<Long>) -> Unit)
@@ -68,8 +68,7 @@ interface RemoteImageApi {
val urlArg = args[0] as String
val headersArg = args[1] as Map<String, String>
val requestIdArg = args[2] as Long
val preferEncodedArg = args[3] as Boolean
api.requestImage(urlArg, headersArg, requestIdArg, preferEncodedArg) { result: Result<Map<String, Long>?> ->
api.requestImage(urlArg, headersArg, requestIdArg) { result: Result<Map<String, Long>?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(RemoteImagesPigeonUtils.wrapError(error))
@@ -51,7 +51,6 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
url: String,
headers: Map<String, String>,
requestId: Long,
@Suppress("UNUSED_PARAMETER") preferEncoded: Boolean, // always returns encoded; setting has no effect on Android
callback: (Result<Map<String, Long>?>) -> Unit
) {
val signal = CancellationSignal()
@@ -78,21 +78,6 @@ class FlutterError (
val details: Any? = null
) : Throwable()
enum class PlatformAssetPlaybackStyle(val raw: Int) {
UNKNOWN(0),
IMAGE(1),
VIDEO(2),
IMAGE_ANIMATED(3),
LIVE_PHOTO(4),
VIDEO_LOOPING(5);
companion object {
fun ofRaw(raw: Int): PlatformAssetPlaybackStyle? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class PlatformAsset (
val id: String,
@@ -107,8 +92,7 @@ data class PlatformAsset (
val isFavorite: Boolean,
val adjustmentTime: Long? = null,
val latitude: Double? = null,
val longitude: Double? = null,
val playbackStyle: PlatformAssetPlaybackStyle
val longitude: Double? = null
)
{
companion object {
@@ -126,8 +110,7 @@ data class PlatformAsset (
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, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude)
}
}
fun toList(): List<Any?> {
@@ -145,7 +128,6 @@ data class PlatformAsset (
adjustmentTime,
latitude,
longitude,
playbackStyle,
)
}
override fun equals(other: Any?): Boolean {
@@ -308,31 +290,26 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
129.toByte() -> {
return (readValue(buffer) as Long?)?.let {
PlatformAssetPlaybackStyle.ofRaw(it.toInt())
}
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PlatformAsset.fromList(it)
}
}
131.toByte() -> {
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PlatformAlbum.fromList(it)
}
}
132.toByte() -> {
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
SyncDelta.fromList(it)
}
}
133.toByte() -> {
132.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HashResult.fromList(it)
}
}
134.toByte() -> {
133.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CloudIdResult.fromList(it)
}
@@ -342,28 +319,24 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is PlatformAssetPlaybackStyle -> {
stream.write(129)
writeValue(stream, value.raw)
}
is PlatformAsset -> {
stream.write(130)
stream.write(129)
writeValue(stream, value.toList())
}
is PlatformAlbum -> {
stream.write(131)
stream.write(130)
writeValue(stream, value.toList())
}
is SyncDelta -> {
stream.write(132)
stream.write(131)
writeValue(stream, value.toList())
}
is HashResult -> {
stream.write(133)
stream.write(132)
writeValue(stream, value.toList())
}
is CloudIdResult -> {
stream.write(134)
stream.write(133)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
@@ -4,17 +4,11 @@ import android.annotation.SuppressLint
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import androidx.exifinterface.media.ExifInterface
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Base64
import android.util.Log
import androidx.core.database.getStringOrNull
import app.alextran.immich.core.ImmichPlugin
import com.bumptech.glide.Glide
import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.ImageHeaderParserUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -34,8 +28,6 @@ sealed class AssetResult {
data class InvalidAsset(val assetId: String) : AssetResult()
}
private const val TAG = "NativeSyncApiImplBase"
@SuppressLint("InlinedApi")
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
private val ctx: Context = context.applicationContext
@@ -47,13 +39,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED"
// MediaStore.Files.FileColumns.SPECIAL_FORMAT — S Extensions 21+
// https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT
private const val SPECIAL_FORMAT_COLUMN = "_special_format"
private const val SPECIAL_FORMAT_GIF = 1
private const val SPECIAL_FORMAT_MOTION_PHOTO = 2
private const val SPECIAL_FORMAT_ANIMATED_WEBP = 3
const val MEDIA_SELECTION =
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
val MEDIA_SELECTION_ARGS = arrayOf(
@@ -75,15 +60,9 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
add(MediaStore.MediaColumns.DURATION)
add(MediaStore.MediaColumns.ORIENTATION)
// IS_FAVORITE is only available on Android 11 and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
add(MediaStore.MediaColumns.IS_FAVORITE)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add(SPECIAL_FORMAT_COLUMN)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Fallback: read XMP from MediaStore to detect Motion Photos
add(MediaStore.MediaColumns.XMP)
}
}.toTypedArray()
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
@@ -130,12 +109,9 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
val orientationColumn =
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
val specialFormatColumn = c.getColumnIndex(SPECIAL_FORMAT_COLUMN)
val xmpColumn = c.getColumnIndex(MediaStore.MediaColumns.XMP)
while (c.moveToNext()) {
val numericId = c.getLong(idColumn)
val id = numericId.toString()
val id = c.getLong(idColumn).toString()
val name = c.getStringOrNull(nameColumn)
val bucketId = c.getStringOrNull(bucketIdColumn)
val path = c.getStringOrNull(dataColumn)
@@ -149,11 +125,10 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
continue
}
val rawMediaType = c.getInt(mediaTypeColumn)
val assetType: Long = when (rawMediaType) {
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1L
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2L
else -> 0L
val mediaType = when (c.getInt(mediaTypeColumn)) {
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2
else -> 0
}
// Date taken is milliseconds since epoch, Date added is seconds since epoch
val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
@@ -163,19 +138,15 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
val width = c.getInt(widthColumn).toLong()
val height = c.getInt(heightColumn).toLong()
// Duration is milliseconds
val duration = if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0L
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
else c.getLong(durationColumn) / 1000
val orientation = c.getInt(orientationColumn)
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
val playbackStyle = detectPlaybackStyle(
numericId, rawMediaType, specialFormatColumn, xmpColumn, c
)
val asset = PlatformAsset(
id,
name,
assetType,
mediaType.toLong(),
createdAt,
modifiedAt,
width,
@@ -183,7 +154,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
duration,
orientation.toLong(),
isFavorite,
playbackStyle = playbackStyle,
)
yield(AssetResult.ValidAsset(asset, bucketId))
}
@@ -191,81 +161,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
}
}
/**
* Detects the playback style for an asset using _special_format (API 33+)
* or XMP / MIME / RIFF header fallbacks (pre-33).
*/
@SuppressLint("NewApi")
private fun detectPlaybackStyle(
assetId: Long,
rawMediaType: Int,
specialFormatColumn: Int,
xmpColumn: Int,
cursor: Cursor
): PlatformAssetPlaybackStyle {
// video currently has no special formats, so we can short circuit and avoid unnecessary work
if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) {
return PlatformAssetPlaybackStyle.VIDEO
}
// API 33+: use _special_format from cursor
if (specialFormatColumn != -1) {
val specialFormat = cursor.getInt(specialFormatColumn)
return when {
specialFormat == SPECIAL_FORMAT_MOTION_PHOTO -> PlatformAssetPlaybackStyle.LIVE_PHOTO
specialFormat == SPECIAL_FORMAT_GIF || specialFormat == SPECIAL_FORMAT_ANIMATED_WEBP -> PlatformAssetPlaybackStyle.IMAGE_ANIMATED
rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> PlatformAssetPlaybackStyle.IMAGE
else -> PlatformAssetPlaybackStyle.UNKNOWN
}
}
if (rawMediaType != MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) {
return PlatformAssetPlaybackStyle.UNKNOWN
}
// Pre-API 33 fallback
val uri = ContentUris.withAppendedId(
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
assetId
)
// Read XMP from cursor (API 30+) or ExifInterface stream (pre-30)
val xmp: String? = if (xmpColumn != -1) {
cursor.getBlob(xmpColumn)?.toString(Charsets.UTF_8)
} else {
try {
ctx.contentResolver.openInputStream(uri)?.use { stream ->
ExifInterface(stream).getAttribute(ExifInterface.TAG_XMP)
}
} catch (e: Exception) {
Log.w(TAG, "Failed to read XMP for asset $assetId", e)
null
}
}
if (xmp != null && "Camera:MotionPhoto" in xmp) {
return PlatformAssetPlaybackStyle.LIVE_PHOTO
}
try {
ctx.contentResolver.openInputStream(uri)?.use { stream ->
val glide = Glide.get(ctx)
val type = ImageHeaderParserUtils.getType(
glide.registry.imageHeaderParsers,
stream,
glide.arrayPool
)
if (type == ImageHeaderParser.ImageType.GIF || type == ImageHeaderParser.ImageType.ANIMATED_WEBP) {
return PlatformAssetPlaybackStyle.IMAGE_ANIMATED
}
}
} catch (e: Exception) {
Log.w(TAG, "Failed to parse image header for asset $assetId", e)
}
return PlatformAssetPlaybackStyle.IMAGE
}
fun getAlbums(): List<PlatformAlbum> {
val albums = mutableListOf<PlatformAlbum>()
val albumsCount = mutableMapOf<String, Int>()
File diff suppressed because one or more lines are too long
+2 -3
View File
@@ -70,7 +70,7 @@ class LocalImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol LocalImageApi {
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
func cancelRequest(requestId: Int64) throws
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String: Int64], Error>) -> Void)
}
@@ -90,8 +90,7 @@ class LocalImageApiSetup {
let widthArg = args[2] as! Int64
let heightArg = args[3] as! Int64
let isVideoArg = args[4] as! Bool
let preferEncodedArg = args[5] as! Bool
api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg, preferEncoded: preferEncodedArg) { result in
api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
+25 -68
View File
@@ -7,7 +7,7 @@ class LocalImageRequest {
weak var workItem: DispatchWorkItem?
var isCancelled = false
let callback: (Result<[String: Int64]?, any Error>) -> Void
init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
self.callback = callback
}
@@ -30,11 +30,11 @@ class LocalImageApiImpl: LocalImageApi {
requestOptions.version = .current
return requestOptions
}()
private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated)
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
private static var rgbaFormat = vImage_CGImageFormat(
bitsPerComponent: 8,
bitsPerPixel: 32,
@@ -48,12 +48,12 @@ class LocalImageApiImpl: LocalImageApi {
assetCache.countLimit = 10000
return assetCache
}()
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
ImageProcessing.queue.async {
guard let data = Data(base64Encoded: thumbhash)
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
let (width, height, pointer) = thumbHashToRGBA(hash: data)
completion(.success([
"pointer": Int64(Int(bitPattern: pointer.baseAddress)),
@@ -63,77 +63,34 @@ class LocalImageApiImpl: LocalImageApi {
]))
}
}
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
let request = LocalImageRequest(callback: completion)
let item = DispatchWorkItem {
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
}
ImageProcessing.semaphore.wait()
defer {
ImageProcessing.semaphore.signal()
}
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
}
guard let asset = Self.requestAsset(assetId: assetId)
else {
Self.remove(requestId: requestId)
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
return
}
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
}
if preferEncoded {
let dataOptions = PHImageRequestOptions()
dataOptions.isNetworkAccessAllowed = true
dataOptions.isSynchronous = true
dataOptions.version = .current
var imageData: Data?
Self.imageManager.requestImageDataAndOrientation(
for: asset,
options: dataOptions,
resultHandler: { (data, _, _, _) in
imageData = data
}
)
if request.isCancelled {
Self.remove(requestId: requestId)
return completion(ImageProcessing.cancelledResult)
}
guard let data = imageData else {
Self.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil)))
}
let length = data.count
let pointer = malloc(length)!
data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)
if request.isCancelled {
free(pointer)
Self.remove(requestId: requestId)
return completion(ImageProcessing.cancelledResult)
}
request.callback(.success([
"pointer": Int64(Int(bitPattern: pointer)),
"length": Int64(length),
]))
Self.remove(requestId: requestId)
return
}
var image: UIImage?
Self.imageManager.requestImage(
for: asset,
@@ -144,29 +101,29 @@ class LocalImageApiImpl: LocalImageApi {
image = _image
}
)
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
}
guard let image = image,
let cgImage = image.cgImage else {
Self.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
}
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
}
do {
let buffer = try vImage_Buffer(cgImage: cgImage, format: Self.rgbaFormat)
if request.isCancelled {
buffer.free()
return completion(ImageProcessing.cancelledResult)
}
request.callback(.success([
"pointer": Int64(Int(bitPattern: buffer.data)),
"width": Int64(buffer.width),
@@ -179,24 +136,24 @@ class LocalImageApiImpl: LocalImageApi {
return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil)))
}
}
request.workItem = item
Self.add(requestId: requestId, request: request)
ImageProcessing.queue.async(execute: item)
}
func cancelRequest(requestId: Int64) {
Self.cancel(requestId: requestId)
}
private static func add(requestId: Int64, request: LocalImageRequest) -> Void {
requestQueue.sync { requests[requestId] = request }
}
private static func remove(requestId: Int64) -> Void {
requestQueue.sync { requests[requestId] = nil }
}
private static func cancel(requestId: Int64) -> Void {
requestQueue.async {
guard let request = requests.removeValue(forKey: requestId) else { return }
@@ -207,12 +164,12 @@ class LocalImageApiImpl: LocalImageApi {
}
}
}
private static func requestAsset(assetId: String) -> PHAsset? {
var asset: PHAsset?
assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) }
if asset != nil { return asset }
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
else { return nil }
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
@@ -70,7 +70,7 @@ class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol RemoteImageApi {
func requestImage(url: String, headers: [String: String], requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
func cancelRequest(requestId: Int64) throws
func clearCache(completion: @escaping (Result<Int64, Error>) -> Void)
}
@@ -88,8 +88,7 @@ class RemoteImageApiSetup {
let urlArg = args[0] as! String
let headersArg = args[1] as! [String: String]
let requestIdArg = args[2] as! Int64
let preferEncodedArg = args[3] as! Bool
api.requestImage(url: urlArg, headers: headersArg, requestId: requestIdArg, preferEncoded: preferEncodedArg) { result in
api.requestImage(url: urlArg, headers: headersArg, requestId: requestIdArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
+23 -41
View File
@@ -8,7 +8,7 @@ class RemoteImageRequest {
let id: Int64
var isCancelled = false
let completion: (Result<[String: Int64]?, any Error>) -> Void
init(id: Int64, task: URLSessionDataTask, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
self.id = id
self.task = task
@@ -32,93 +32,75 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceCreateThumbnailFromImageAlways: true
] as CFDictionary
func requestImage(url: String, headers: [String : String], requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
func requestImage(url: String, headers: [String : String], requestId: Int64, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
var urlRequest = URLRequest(url: URL(string: url)!)
urlRequest.cachePolicy = .returnCacheDataElseLoad
for (key, value) in headers {
urlRequest.setValue(value, forHTTPHeaderField: key)
}
let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in
Self.handleCompletion(requestId: requestId, encoded: preferEncoded, data: data, response: response, error: error)
Self.handleCompletion(requestId: requestId, data: data, response: response, error: error)
}
let request = RemoteImageRequest(id: requestId, task: task, completion: completion)
os_unfair_lock_lock(&Self.lock)
Self.requests[requestId] = request
os_unfair_lock_unlock(&Self.lock)
task.resume()
}
private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) {
private static func handleCompletion(requestId: Int64, data: Data?, response: URLResponse?, error: Error?) {
os_unfair_lock_lock(&Self.lock)
guard let request = requests[requestId] else {
return os_unfair_lock_unlock(&Self.lock)
}
requests[requestId] = nil
os_unfair_lock_unlock(&Self.lock)
if let error = error {
if request.isCancelled || (error as NSError).code == NSURLErrorCancelled {
return request.completion(ImageProcessing.cancelledResult)
}
return request.completion(.failure(error))
}
if request.isCancelled {
return request.completion(ImageProcessing.cancelledResult)
}
guard let data = data else {
return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil)))
}
ImageProcessing.queue.async {
ImageProcessing.semaphore.wait()
defer { ImageProcessing.semaphore.signal() }
if request.isCancelled {
return request.completion(ImageProcessing.cancelledResult)
}
// Return raw encoded bytes when requested (for animated images)
if encoded {
let length = data.count
let pointer = malloc(length)!
data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)
if request.isCancelled {
free(pointer)
return request.completion(ImageProcessing.cancelledResult)
}
return request.completion(
.success([
"pointer": Int64(Int(bitPattern: pointer)),
"length": Int64(length),
]))
}
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else {
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil)))
}
if request.isCancelled {
return request.completion(ImageProcessing.cancelledResult)
}
do {
let buffer = try vImage_Buffer(cgImage: cgImage, format: rgbaFormat)
if request.isCancelled {
buffer.free()
return request.completion(ImageProcessing.cancelledResult)
}
request.completion(
.success([
"pointer": Int64(Int(bitPattern: buffer.data)),
@@ -131,17 +113,17 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
}
}
}
func cancelRequest(requestId: Int64) {
os_unfair_lock_lock(&Self.lock)
let request = Self.requests[requestId]
os_unfair_lock_unlock(&Self.lock)
guard let request = request else { return }
request.isCancelled = true
request.task?.cancel()
}
func clearCache(completion: @escaping (Result<Int64, any Error>) -> Void) {
Task {
let cache = URLSessionManager.shared.session.configuration.urlCache!
+10 -32
View File
@@ -128,15 +128,6 @@ func deepHashMessages(value: Any?, hasher: inout Hasher) {
enum PlatformAssetPlaybackStyle: Int {
case unknown = 0
case image = 1
case video = 2
case imageAnimated = 3
case livePhoto = 4
case videoLooping = 5
}
/// Generated class from Pigeon that represents data sent in messages.
struct PlatformAsset: Hashable {
var id: String
@@ -152,7 +143,6 @@ struct PlatformAsset: Hashable {
var adjustmentTime: Int64? = nil
var latitude: Double? = nil
var longitude: Double? = nil
var playbackStyle: PlatformAssetPlaybackStyle
// swift-format-ignore: AlwaysUseLowerCamelCase
@@ -170,7 +160,6 @@ struct PlatformAsset: Hashable {
let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10])
let latitude: Double? = nilOrValue(pigeonVar_list[11])
let longitude: Double? = nilOrValue(pigeonVar_list[12])
let playbackStyle = pigeonVar_list[13] as! PlatformAssetPlaybackStyle
return PlatformAsset(
id: id,
@@ -185,8 +174,7 @@ struct PlatformAsset: Hashable {
isFavorite: isFavorite,
adjustmentTime: adjustmentTime,
latitude: latitude,
longitude: longitude,
playbackStyle: playbackStyle
longitude: longitude
)
}
func toList() -> [Any?] {
@@ -204,7 +192,6 @@ struct PlatformAsset: Hashable {
adjustmentTime,
latitude,
longitude,
playbackStyle,
]
}
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
@@ -362,20 +349,14 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
case 129:
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
if let enumResultAsInt = enumResultAsInt {
return PlatformAssetPlaybackStyle(rawValue: enumResultAsInt)
}
return nil
case 130:
return PlatformAsset.fromList(self.readValue() as! [Any?])
case 131:
case 130:
return PlatformAlbum.fromList(self.readValue() as! [Any?])
case 132:
case 131:
return SyncDelta.fromList(self.readValue() as! [Any?])
case 133:
case 132:
return HashResult.fromList(self.readValue() as! [Any?])
case 134:
case 133:
return CloudIdResult.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
@@ -385,23 +366,20 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
private class MessagesPigeonCodecWriter: FlutterStandardWriter {
override func writeValue(_ value: Any) {
if let value = value as? PlatformAssetPlaybackStyle {
if let value = value as? PlatformAsset {
super.writeByte(129)
super.writeValue(value.rawValue)
} else if let value = value as? PlatformAsset {
super.writeByte(130)
super.writeValue(value.toList())
} else if let value = value as? PlatformAlbum {
super.writeByte(131)
super.writeByte(130)
super.writeValue(value.toList())
} else if let value = value as? SyncDelta {
super.writeByte(132)
super.writeByte(131)
super.writeValue(value.toList())
} else if let value = value as? HashResult {
super.writeByte(133)
super.writeByte(132)
super.writeValue(value.toList())
} else if let value = value as? CloudIdResult {
super.writeByte(134)
super.writeByte(133)
super.writeValue(value.toList())
} else {
super.writeValue(value)
+1 -2
View File
@@ -173,8 +173,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
type: 0,
durationInSeconds: 0,
orientation: 0,
isFavorite: false,
playbackStyle: .unknown
isFavorite: false
)
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
continue
+2 -14
View File
@@ -1,17 +1,6 @@
import Photos
extension PHAsset {
var platformPlaybackStyle: PlatformAssetPlaybackStyle {
switch playbackStyle {
case .image: return .image
case .imageAnimated: return .imageAnimated
case .livePhoto: return .livePhoto
case .video: return .video
case .videoLooping: return .videoLooping
@unknown default: return .unknown
}
}
func toPlatformAsset() -> PlatformAsset {
return PlatformAsset(
id: localIdentifier,
@@ -26,8 +15,7 @@ extension PHAsset {
isFavorite: isFavorite,
adjustmentTime: adjustmentTimestamp,
latitude: location?.coordinate.latitude,
longitude: location?.coordinate.longitude,
playbackStyle: platformPlaybackStyle
longitude: location?.coordinate.longitude
)
}
@@ -38,7 +26,7 @@ extension PHAsset {
var filename: String? {
return value(forKey: "filename") as? String
}
var adjustmentTimestamp: Int64? {
if let date = value(forKey: "adjustmentTimestamp") as? Date {
return Int64(date.timeIntervalSince1970)
@@ -11,10 +11,6 @@ enum AssetType {
enum AssetState { local, remote, merged }
// do not change!
// keep in sync with PlatformAssetPlaybackStyle
enum AssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping }
sealed class BaseAsset {
final String name;
final String? checksum;
@@ -47,14 +43,6 @@ sealed class BaseAsset {
bool get isMotionPhoto => livePhotoVideoId != null;
AssetPlaybackStyle get playbackStyle {
if (isVideo) return AssetPlaybackStyle.video;
if (isMotionPhoto) return AssetPlaybackStyle.livePhoto;
if (isImage && durationInSeconds != null && durationInSeconds! > 0) return AssetPlaybackStyle.imageAnimated;
if (isImage) return AssetPlaybackStyle.image;
return AssetPlaybackStyle.unknown;
}
Duration get duration {
final durationInSeconds = this.durationInSeconds;
if (durationInSeconds != null) {
@@ -5,8 +5,6 @@ class LocalAsset extends BaseAsset {
final String? remoteAssetId;
final String? cloudId;
final int orientation;
@override
final AssetPlaybackStyle playbackStyle;
final DateTime? adjustmentTime;
final double? latitude;
@@ -27,7 +25,6 @@ class LocalAsset extends BaseAsset {
super.isFavorite = false,
super.livePhotoVideoId,
this.orientation = 0,
required this.playbackStyle,
this.adjustmentTime,
this.latitude,
this.longitude,
@@ -59,7 +56,6 @@ class LocalAsset extends BaseAsset {
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
playbackStyle: $playbackStyle,
remoteId: ${remoteId ?? "<NA>"},
cloudId: ${cloudId ?? "<NA>"},
checksum: ${checksum ?? "<NA>"},
@@ -80,7 +76,6 @@ class LocalAsset extends BaseAsset {
id == other.id &&
cloudId == other.cloudId &&
orientation == other.orientation &&
playbackStyle == other.playbackStyle &&
adjustmentTime == other.adjustmentTime &&
latitude == other.latitude &&
longitude == other.longitude;
@@ -92,7 +87,6 @@ class LocalAsset extends BaseAsset {
id.hashCode ^
remoteId.hashCode ^
orientation.hashCode ^
playbackStyle.hashCode ^
adjustmentTime.hashCode ^
latitude.hashCode ^
longitude.hashCode;
@@ -111,7 +105,6 @@ class LocalAsset extends BaseAsset {
int? durationInSeconds,
bool? isFavorite,
int? orientation,
AssetPlaybackStyle? playbackStyle,
DateTime? adjustmentTime,
double? latitude,
double? longitude,
@@ -131,7 +124,6 @@ class LocalAsset extends BaseAsset {
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
playbackStyle: playbackStyle ?? this.playbackStyle,
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
@@ -435,19 +435,9 @@ extension PlatformToLocalAsset on PlatformAsset {
durationInSeconds: durationInSeconds,
isFavorite: isFavorite,
orientation: orientation,
playbackStyle: _toPlaybackStyle(playbackStyle),
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
latitude: latitude,
longitude: longitude,
isEdited: false,
);
}
AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) {
PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown,
PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image,
PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video,
PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated,
PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto,
PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping,
};
@@ -25,8 +25,6 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
RealColumn get longitude => real().nullable()();
IntColumn get playbackStyle => intEnum<AssetPlaybackStyle>().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {id};
}
@@ -45,7 +43,6 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
width: width,
remoteId: remoteId,
orientation: orientation,
playbackStyle: playbackStyle,
adjustmentTime: adjustmentTime,
latitude: latitude,
longitude: longitude,
@@ -25,7 +25,6 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder =
i0.Value<DateTime?> adjustmentTime,
i0.Value<double?> latitude,
i0.Value<double?> longitude,
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
});
typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i1.LocalAssetEntityCompanion Function({
@@ -44,7 +43,6 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i0.Value<DateTime?> adjustmentTime,
i0.Value<double?> latitude,
i0.Value<double?> longitude,
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
});
class $$LocalAssetEntityTableFilterComposer
@@ -131,16 +129,6 @@ class $$LocalAssetEntityTableFilterComposer
column: $table.longitude,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnWithTypeConverterFilters<
i2.AssetPlaybackStyle,
i2.AssetPlaybackStyle,
int
>
get playbackStyle => $composableBuilder(
column: $table.playbackStyle,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
}
class $$LocalAssetEntityTableOrderingComposer
@@ -226,11 +214,6 @@ class $$LocalAssetEntityTableOrderingComposer
column: $table.longitude,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get playbackStyle => $composableBuilder(
column: $table.playbackStyle,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$LocalAssetEntityTableAnnotationComposer
@@ -294,12 +277,6 @@ class $$LocalAssetEntityTableAnnotationComposer
i0.GeneratedColumn<double> get longitude =>
$composableBuilder(column: $table.longitude, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.AssetPlaybackStyle, int>
get playbackStyle => $composableBuilder(
column: $table.playbackStyle,
builder: (column) => column,
);
}
class $$LocalAssetEntityTableTableManager
@@ -357,8 +334,6 @@ class $$LocalAssetEntityTableTableManager
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion(
name: name,
type: type,
@@ -375,7 +350,6 @@ class $$LocalAssetEntityTableTableManager
adjustmentTime: adjustmentTime,
latitude: latitude,
longitude: longitude,
playbackStyle: playbackStyle,
),
createCompanionCallback:
({
@@ -394,8 +368,6 @@ class $$LocalAssetEntityTableTableManager
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion.insert(
name: name,
type: type,
@@ -412,7 +384,6 @@ class $$LocalAssetEntityTableTableManager
adjustmentTime: adjustmentTime,
latitude: latitude,
longitude: longitude,
playbackStyle: playbackStyle,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
@@ -625,19 +596,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
requiredDuringInsert: false,
);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.AssetPlaybackStyle, int>
playbackStyle =
i0.GeneratedColumn<int>(
'playback_style',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
).withConverter<i2.AssetPlaybackStyle>(
i1.$LocalAssetEntityTable.$converterplaybackStyle,
);
@override
List<i0.GeneratedColumn> get $columns => [
name,
type,
@@ -654,7 +612,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
adjustmentTime,
latitude,
longitude,
playbackStyle,
];
@override
String get aliasedName => _alias ?? actualTableName;
@@ -836,12 +793,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
i0.DriftSqlType.double,
data['${effectivePrefix}longitude'],
),
playbackStyle: i1.$LocalAssetEntityTable.$converterplaybackStyle.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}playback_style'],
)!,
),
);
}
@@ -852,10 +803,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
static i0.JsonTypeConverter2<i2.AssetType, int, int> $convertertype =
const i0.EnumIndexConverter<i2.AssetType>(i2.AssetType.values);
static i0.JsonTypeConverter2<i2.AssetPlaybackStyle, int, int>
$converterplaybackStyle = const i0.EnumIndexConverter<i2.AssetPlaybackStyle>(
i2.AssetPlaybackStyle.values,
);
@override
bool get withoutRowId => true;
@override
@@ -879,7 +826,6 @@ class LocalAssetEntityData extends i0.DataClass
final DateTime? adjustmentTime;
final double? latitude;
final double? longitude;
final i2.AssetPlaybackStyle playbackStyle;
const LocalAssetEntityData({
required this.name,
required this.type,
@@ -896,7 +842,6 @@ class LocalAssetEntityData extends i0.DataClass
this.adjustmentTime,
this.latitude,
this.longitude,
required this.playbackStyle,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -936,11 +881,6 @@ class LocalAssetEntityData extends i0.DataClass
if (!nullToAbsent || longitude != null) {
map['longitude'] = i0.Variable<double>(longitude);
}
{
map['playback_style'] = i0.Variable<int>(
i1.$LocalAssetEntityTable.$converterplaybackStyle.toSql(playbackStyle),
);
}
return map;
}
@@ -967,9 +907,6 @@ class LocalAssetEntityData extends i0.DataClass
adjustmentTime: serializer.fromJson<DateTime?>(json['adjustmentTime']),
latitude: serializer.fromJson<double?>(json['latitude']),
longitude: serializer.fromJson<double?>(json['longitude']),
playbackStyle: i1.$LocalAssetEntityTable.$converterplaybackStyle.fromJson(
serializer.fromJson<int>(json['playbackStyle']),
),
);
}
@override
@@ -993,9 +930,6 @@ class LocalAssetEntityData extends i0.DataClass
'adjustmentTime': serializer.toJson<DateTime?>(adjustmentTime),
'latitude': serializer.toJson<double?>(latitude),
'longitude': serializer.toJson<double?>(longitude),
'playbackStyle': serializer.toJson<int>(
i1.$LocalAssetEntityTable.$converterplaybackStyle.toJson(playbackStyle),
),
};
}
@@ -1015,7 +949,6 @@ class LocalAssetEntityData extends i0.DataClass
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
i2.AssetPlaybackStyle? playbackStyle,
}) => i1.LocalAssetEntityData(
name: name ?? this.name,
type: type ?? this.type,
@@ -1036,7 +969,6 @@ class LocalAssetEntityData extends i0.DataClass
: this.adjustmentTime,
latitude: latitude.present ? latitude.value : this.latitude,
longitude: longitude.present ? longitude.value : this.longitude,
playbackStyle: playbackStyle ?? this.playbackStyle,
);
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
return LocalAssetEntityData(
@@ -1063,9 +995,6 @@ class LocalAssetEntityData extends i0.DataClass
: this.adjustmentTime,
latitude: data.latitude.present ? data.latitude.value : this.latitude,
longitude: data.longitude.present ? data.longitude.value : this.longitude,
playbackStyle: data.playbackStyle.present
? data.playbackStyle.value
: this.playbackStyle,
);
}
@@ -1086,8 +1015,7 @@ class LocalAssetEntityData extends i0.DataClass
..write('iCloudId: $iCloudId, ')
..write('adjustmentTime: $adjustmentTime, ')
..write('latitude: $latitude, ')
..write('longitude: $longitude, ')
..write('playbackStyle: $playbackStyle')
..write('longitude: $longitude')
..write(')'))
.toString();
}
@@ -1109,7 +1037,6 @@ class LocalAssetEntityData extends i0.DataClass
adjustmentTime,
latitude,
longitude,
playbackStyle,
);
@override
bool operator ==(Object other) =>
@@ -1129,8 +1056,7 @@ class LocalAssetEntityData extends i0.DataClass
other.iCloudId == this.iCloudId &&
other.adjustmentTime == this.adjustmentTime &&
other.latitude == this.latitude &&
other.longitude == this.longitude &&
other.playbackStyle == this.playbackStyle);
other.longitude == this.longitude);
}
class LocalAssetEntityCompanion
@@ -1150,7 +1076,6 @@ class LocalAssetEntityCompanion
final i0.Value<DateTime?> adjustmentTime;
final i0.Value<double?> latitude;
final i0.Value<double?> longitude;
final i0.Value<i2.AssetPlaybackStyle> playbackStyle;
const LocalAssetEntityCompanion({
this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(),
@@ -1167,7 +1092,6 @@ class LocalAssetEntityCompanion
this.adjustmentTime = const i0.Value.absent(),
this.latitude = const i0.Value.absent(),
this.longitude = const i0.Value.absent(),
this.playbackStyle = const i0.Value.absent(),
});
LocalAssetEntityCompanion.insert({
required String name,
@@ -1185,7 +1109,6 @@ class LocalAssetEntityCompanion
this.adjustmentTime = const i0.Value.absent(),
this.latitude = const i0.Value.absent(),
this.longitude = const i0.Value.absent(),
this.playbackStyle = const i0.Value.absent(),
}) : name = i0.Value(name),
type = i0.Value(type),
id = i0.Value(id);
@@ -1205,7 +1128,6 @@ class LocalAssetEntityCompanion
i0.Expression<DateTime>? adjustmentTime,
i0.Expression<double>? latitude,
i0.Expression<double>? longitude,
i0.Expression<int>? playbackStyle,
}) {
return i0.RawValuesInsertable({
if (name != null) 'name': name,
@@ -1223,7 +1145,6 @@ class LocalAssetEntityCompanion
if (adjustmentTime != null) 'adjustment_time': adjustmentTime,
if (latitude != null) 'latitude': latitude,
if (longitude != null) 'longitude': longitude,
if (playbackStyle != null) 'playback_style': playbackStyle,
});
}
@@ -1243,7 +1164,6 @@ class LocalAssetEntityCompanion
i0.Value<DateTime?>? adjustmentTime,
i0.Value<double?>? latitude,
i0.Value<double?>? longitude,
i0.Value<i2.AssetPlaybackStyle>? playbackStyle,
}) {
return i1.LocalAssetEntityCompanion(
name: name ?? this.name,
@@ -1261,7 +1181,6 @@ class LocalAssetEntityCompanion
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
playbackStyle: playbackStyle ?? this.playbackStyle,
);
}
@@ -1315,13 +1234,6 @@ class LocalAssetEntityCompanion
if (longitude.present) {
map['longitude'] = i0.Variable<double>(longitude.value);
}
if (playbackStyle.present) {
map['playback_style'] = i0.Variable<int>(
i1.$LocalAssetEntityTable.$converterplaybackStyle.toSql(
playbackStyle.value,
),
);
}
return map;
}
@@ -1342,8 +1254,7 @@ class LocalAssetEntityCompanion
..write('iCloudId: $iCloudId, ')
..write('adjustmentTime: $adjustmentTime, ')
..write('latitude: $latitude, ')
..write('longitude: $longitude, ')
..write('playbackStyle: $playbackStyle')
..write('longitude: $longitude')
..write(')'))
.toString();
}
@@ -26,8 +26,7 @@ SELECT
NULL as latitude,
NULL as longitude,
NULL as adjustmentTime,
rae.is_edited,
0 as playback_style
rae.is_edited
FROM
remote_asset_entity rae
LEFT JOIN
@@ -64,8 +63,7 @@ SELECT
lae.latitude,
lae.longitude,
lae.adjustment_time,
0 as is_edited,
lae.playback_style
0 as is_edited
FROM
local_asset_entity lae
WHERE NOT EXISTS (
+1 -4
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_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 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 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,
@@ -67,7 +67,6 @@ class MergedAssetDrift extends i1.ModularAccessor {
longitude: row.readNullable<double>('longitude'),
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
isEdited: row.read<bool>('is_edited'),
playbackStyle: row.read<int>('playback_style'),
),
);
}
@@ -140,7 +139,6 @@ class MergedAssetResult {
final double? longitude;
final DateTime? adjustmentTime;
final bool isEdited;
final int playbackStyle;
MergedAssetResult({
this.remoteId,
this.localId,
@@ -163,7 +161,6 @@ class MergedAssetResult {
this.longitude,
this.adjustmentTime,
required this.isEdited,
required this.playbackStyle,
});
}
@@ -28,8 +28,6 @@ class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntity
IntColumn get source => intEnum<TrashOrigin>()();
IntColumn get playbackStyle => intEnum<AssetPlaybackStyle>().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {id, albumId};
}
@@ -47,7 +45,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD
height: height,
width: width,
orientation: orientation,
playbackStyle: playbackStyle,
isEdited: false,
);
}
@@ -23,7 +23,6 @@ typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
i0.Value<bool> isFavorite,
i0.Value<int> orientation,
required i3.TrashOrigin source,
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
});
typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
i1.TrashedLocalAssetEntityCompanion Function({
@@ -40,7 +39,6 @@ typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
i0.Value<bool> isFavorite,
i0.Value<int> orientation,
i0.Value<i3.TrashOrigin> source,
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
});
class $$TrashedLocalAssetEntityTableFilterComposer
@@ -119,16 +117,6 @@ class $$TrashedLocalAssetEntityTableFilterComposer
column: $table.source,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnWithTypeConverterFilters<
i2.AssetPlaybackStyle,
i2.AssetPlaybackStyle,
int
>
get playbackStyle => $composableBuilder(
column: $table.playbackStyle,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
}
class $$TrashedLocalAssetEntityTableOrderingComposer
@@ -205,11 +193,6 @@ class $$TrashedLocalAssetEntityTableOrderingComposer
column: $table.source,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get playbackStyle => $composableBuilder(
column: $table.playbackStyle,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$TrashedLocalAssetEntityTableAnnotationComposer
@@ -266,12 +249,6 @@ class $$TrashedLocalAssetEntityTableAnnotationComposer
i0.GeneratedColumnWithTypeConverter<i3.TrashOrigin, int> get source =>
$composableBuilder(column: $table.source, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.AssetPlaybackStyle, int>
get playbackStyle => $composableBuilder(
column: $table.playbackStyle,
builder: (column) => column,
);
}
class $$TrashedLocalAssetEntityTableTableManager
@@ -333,8 +310,6 @@ class $$TrashedLocalAssetEntityTableTableManager
i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(),
i0.Value<i3.TrashOrigin> source = const i0.Value.absent(),
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
const i0.Value.absent(),
}) => i1.TrashedLocalAssetEntityCompanion(
name: name,
type: type,
@@ -349,7 +324,6 @@ class $$TrashedLocalAssetEntityTableTableManager
isFavorite: isFavorite,
orientation: orientation,
source: source,
playbackStyle: playbackStyle,
),
createCompanionCallback:
({
@@ -366,8 +340,6 @@ class $$TrashedLocalAssetEntityTableTableManager
i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(),
required i3.TrashOrigin source,
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
const i0.Value.absent(),
}) => i1.TrashedLocalAssetEntityCompanion.insert(
name: name,
type: type,
@@ -382,7 +354,6 @@ class $$TrashedLocalAssetEntityTableTableManager
isFavorite: isFavorite,
orientation: orientation,
source: source,
playbackStyle: playbackStyle,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
@@ -579,19 +550,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
i1.$TrashedLocalAssetEntityTable.$convertersource,
);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.AssetPlaybackStyle, int>
playbackStyle =
i0.GeneratedColumn<int>(
'playback_style',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
).withConverter<i2.AssetPlaybackStyle>(
i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle,
);
@override
List<i0.GeneratedColumn> get $columns => [
name,
type,
@@ -606,7 +564,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
isFavorite,
orientation,
source,
playbackStyle,
];
@override
String get aliasedName => _alias ?? actualTableName;
@@ -763,13 +720,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
data['${effectivePrefix}source'],
)!,
),
playbackStyle: i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle
.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}playback_style'],
)!,
),
);
}
@@ -782,10 +732,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
const i0.EnumIndexConverter<i2.AssetType>(i2.AssetType.values);
static i0.JsonTypeConverter2<i3.TrashOrigin, int, int> $convertersource =
const i0.EnumIndexConverter<i3.TrashOrigin>(i3.TrashOrigin.values);
static i0.JsonTypeConverter2<i2.AssetPlaybackStyle, int, int>
$converterplaybackStyle = const i0.EnumIndexConverter<i2.AssetPlaybackStyle>(
i2.AssetPlaybackStyle.values,
);
@override
bool get withoutRowId => true;
@override
@@ -807,7 +753,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
final bool isFavorite;
final int orientation;
final i3.TrashOrigin source;
final i2.AssetPlaybackStyle playbackStyle;
const TrashedLocalAssetEntityData({
required this.name,
required this.type,
@@ -822,7 +767,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
required this.isFavorite,
required this.orientation,
required this.source,
required this.playbackStyle,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -856,13 +800,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
i1.$TrashedLocalAssetEntityTable.$convertersource.toSql(source),
);
}
{
map['playback_style'] = i0.Variable<int>(
i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle.toSql(
playbackStyle,
),
);
}
return map;
}
@@ -889,8 +826,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
source: i1.$TrashedLocalAssetEntityTable.$convertersource.fromJson(
serializer.fromJson<int>(json['source']),
),
playbackStyle: i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle
.fromJson(serializer.fromJson<int>(json['playbackStyle'])),
);
}
@override
@@ -914,11 +849,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
'source': serializer.toJson<int>(
i1.$TrashedLocalAssetEntityTable.$convertersource.toJson(source),
),
'playbackStyle': serializer.toJson<int>(
i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle.toJson(
playbackStyle,
),
),
};
}
@@ -936,7 +866,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
bool? isFavorite,
int? orientation,
i3.TrashOrigin? source,
i2.AssetPlaybackStyle? playbackStyle,
}) => i1.TrashedLocalAssetEntityData(
name: name ?? this.name,
type: type ?? this.type,
@@ -953,7 +882,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
source: source ?? this.source,
playbackStyle: playbackStyle ?? this.playbackStyle,
);
TrashedLocalAssetEntityData copyWithCompanion(
i1.TrashedLocalAssetEntityCompanion data,
@@ -978,9 +906,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
? data.orientation.value
: this.orientation,
source: data.source.present ? data.source.value : this.source,
playbackStyle: data.playbackStyle.present
? data.playbackStyle.value
: this.playbackStyle,
);
}
@@ -999,8 +924,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation, ')
..write('source: $source, ')
..write('playbackStyle: $playbackStyle')
..write('source: $source')
..write(')'))
.toString();
}
@@ -1020,7 +944,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
isFavorite,
orientation,
source,
playbackStyle,
);
@override
bool operator ==(Object other) =>
@@ -1038,8 +961,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
other.checksum == this.checksum &&
other.isFavorite == this.isFavorite &&
other.orientation == this.orientation &&
other.source == this.source &&
other.playbackStyle == this.playbackStyle);
other.source == this.source);
}
class TrashedLocalAssetEntityCompanion
@@ -1057,7 +979,6 @@ class TrashedLocalAssetEntityCompanion
final i0.Value<bool> isFavorite;
final i0.Value<int> orientation;
final i0.Value<i3.TrashOrigin> source;
final i0.Value<i2.AssetPlaybackStyle> playbackStyle;
const TrashedLocalAssetEntityCompanion({
this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(),
@@ -1072,7 +993,6 @@ class TrashedLocalAssetEntityCompanion
this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
this.source = const i0.Value.absent(),
this.playbackStyle = const i0.Value.absent(),
});
TrashedLocalAssetEntityCompanion.insert({
required String name,
@@ -1088,7 +1008,6 @@ class TrashedLocalAssetEntityCompanion
this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
required i3.TrashOrigin source,
this.playbackStyle = const i0.Value.absent(),
}) : name = i0.Value(name),
type = i0.Value(type),
id = i0.Value(id),
@@ -1108,7 +1027,6 @@ class TrashedLocalAssetEntityCompanion
i0.Expression<bool>? isFavorite,
i0.Expression<int>? orientation,
i0.Expression<int>? source,
i0.Expression<int>? playbackStyle,
}) {
return i0.RawValuesInsertable({
if (name != null) 'name': name,
@@ -1124,7 +1042,6 @@ class TrashedLocalAssetEntityCompanion
if (isFavorite != null) 'is_favorite': isFavorite,
if (orientation != null) 'orientation': orientation,
if (source != null) 'source': source,
if (playbackStyle != null) 'playback_style': playbackStyle,
});
}
@@ -1142,7 +1059,6 @@ class TrashedLocalAssetEntityCompanion
i0.Value<bool>? isFavorite,
i0.Value<int>? orientation,
i0.Value<i3.TrashOrigin>? source,
i0.Value<i2.AssetPlaybackStyle>? playbackStyle,
}) {
return i1.TrashedLocalAssetEntityCompanion(
name: name ?? this.name,
@@ -1158,7 +1074,6 @@ class TrashedLocalAssetEntityCompanion
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
source: source ?? this.source,
playbackStyle: playbackStyle ?? this.playbackStyle,
);
}
@@ -1208,13 +1123,6 @@ class TrashedLocalAssetEntityCompanion
i1.$TrashedLocalAssetEntityTable.$convertersource.toSql(source.value),
);
}
if (playbackStyle.present) {
map['playback_style'] = i0.Variable<int>(
i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle.toSql(
playbackStyle.value,
),
);
}
return map;
}
@@ -1233,8 +1141,7 @@ class TrashedLocalAssetEntityCompanion
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation, ')
..write('source: $source, ')
..write('playbackStyle: $playbackStyle')
..write('source: $source')
..write(')'))
.toString();
}
@@ -24,8 +24,6 @@ abstract class ImageRequest {
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0});
Future<ui.Codec?> loadCodec();
void cancel() {
if (_isCancelled) {
return;
@@ -36,7 +34,7 @@ abstract class ImageRequest {
void _onCancelled();
Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromEncodedPlatformImage(int address, int length) async {
Future<ui.FrameInfo?> _fromEncodedPlatformImage(int address, int length) async {
final pointer = Pointer<Uint8>.fromAddress(address);
if (_isCancelled) {
malloc.free(pointer);
@@ -69,20 +67,6 @@ abstract class ImageRequest {
return null;
}
return (codec, descriptor);
}
Future<ui.FrameInfo?> _fromEncodedPlatformImage(int address, int length) async {
final result = await _codecFromEncodedPlatformImage(address, length);
if (result == null) return null;
final (codec, descriptor) = result;
if (_isCancelled) {
descriptor.dispose();
codec.dispose();
return null;
}
final frame = await codec.getNextFrame();
descriptor.dispose();
codec.dispose();
@@ -22,7 +22,6 @@ class LocalImageRequest extends ImageRequest {
width: width,
height: height,
isVideo: assetType == AssetType.video,
preferEncoded: false,
);
if (info == null) {
return null;
@@ -32,26 +31,6 @@ class LocalImageRequest extends ImageRequest {
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
}
@override
Future<ui.Codec?> loadCodec() async {
if (_isCancelled) {
return null;
}
final info = await localImageApi.requestImage(
localId,
requestId: requestId,
width: width,
height: height,
isVideo: assetType == AssetType.video,
preferEncoded: true,
);
if (info == null) return null;
final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null);
return codec;
}
@override
Future<void> _onCancelled() {
return localImageApi.cancelRequest(requestId);
@@ -12,8 +12,7 @@ class RemoteImageRequest extends ImageRequest {
return null;
}
final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId, preferEncoded: false);
// Android always returns encoded data, so we need to check for both shapes of the response.
final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId);
final frame = switch (info) {
{'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length),
{'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} =>
@@ -23,19 +22,6 @@ class RemoteImageRequest extends ImageRequest {
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
}
@override
Future<ui.Codec?> loadCodec() async {
if (_isCancelled) {
return null;
}
final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId, preferEncoded: true);
if (info == null) return null;
final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null);
return codec;
}
@override
Future<void> _onCancelled() {
return remoteImageApi.cancelRequest(requestId);
@@ -16,9 +16,6 @@ class ThumbhashImageRequest extends ImageRequest {
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
}
@override
Future<ui.Codec?> loadCodec() => throw UnsupportedError('Thumbhash does not support codec loading');
@override
void _onCancelled() {}
}
@@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository {
}
@override
int get schemaVersion => 21;
int get schemaVersion => 20;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -230,10 +230,6 @@ class Drift extends $Drift implements IDatabaseRepository {
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
},
from20To21: (m, v21) async {
await m.addColumn(v21.localAssetEntity, v21.localAssetEntity.playbackStyle);
await m.addColumn(v21.trashedLocalAssetEntity, v21.trashedLocalAssetEntity.playbackStyle);
},
),
);
@@ -8904,591 +8904,6 @@ i1.GeneratedColumn<bool> _column_102(String aliasedName) =>
),
defaultValue: const CustomExpression('1'),
);
final class Schema21 extends i0.VersionedSchema {
Schema21({required super.database}) : super(version: 21);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAlbumAssetAlbumAsset,
idxRemoteAlbumOwnerId,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxStackPrimaryAssetId,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetLocalDateTimeDay,
idxRemoteAssetLocalDateTimeMonth,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
];
late final Shape20 userEntity = Shape20(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_84,
_column_85,
_column_91,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape28 remoteAssetEntity = Shape28(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
_column_101,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape30 localAssetEntity = Shape30(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
_column_98,
_column_96,
_column_46,
_column_47,
_column_103,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape19 localAlbumEntity = Shape19(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_90,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape22 localAlbumAssetEntity = Shape22(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35, _column_33],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
'idx_local_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAlbumOwnerId = i1.Index(
'idx_remote_album_owner_id',
'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)',
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxStackPrimaryAssetId = i1.Index(
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetStackId = i1.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
'idx_remote_asset_local_date_time_day',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
);
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
'idx_remote_asset_local_date_time_month',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
);
late final Shape21 authUserEntity = Shape21(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_2,
_column_84,
_column_85,
_column_92,
_column_93,
_column_7,
_column_94,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape27 remoteAssetCloudIdEntity = Shape27(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_99,
_column_100,
_column_96,
_column_46,
_column_47,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape29 assetFaceEntity = Shape29(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
_column_102,
_column_18,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
late final Shape31 trashedLocalAssetEntity = Shape31(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_95,
_column_22,
_column_14,
_column_23,
_column_97,
_column_103,
],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxPartnerSharedWithId = i1.Index(
'idx_partner_shared_with_id',
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAssetCloudId = i1.Index(
'idx_remote_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
);
final i1.Index idxPersonOwnerId = i1.Index(
'idx_person_owner_id',
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
);
final i1.Index idxAssetFacePersonId = i1.Index(
'idx_asset_face_person_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
);
final i1.Index idxAssetFaceAssetId = i1.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
}
class Shape30 extends i0.VersionedTable {
Shape30({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get durationInSeconds =>
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<int> get orientation =>
columnsByName['orientation']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get iCloudId =>
columnsByName['i_cloud_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get adjustmentTime =>
columnsByName['adjustment_time']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<double> get latitude =>
columnsByName['latitude']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<double> get longitude =>
columnsByName['longitude']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<int> get playbackStyle =>
columnsByName['playback_style']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_103(String aliasedName) =>
i1.GeneratedColumn<int>(
'playback_style',
aliasedName,
false,
type: i1.DriftSqlType.int,
defaultValue: const CustomExpression('0'),
);
class Shape31 extends i0.VersionedTable {
Shape31({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get durationInSeconds =>
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get albumId =>
columnsByName['album_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<int> get orientation =>
columnsByName['orientation']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get source =>
columnsByName['source']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get playbackStyle =>
columnsByName['playback_style']! as i1.GeneratedColumn<int>;
}
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -9509,7 +8924,6 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -9608,11 +9022,6 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from19To20(migrator, schema);
return 20;
case 20:
final schema = Schema21(database: database);
final migrator = i1.Migrator(database, schema);
await from20To21(migrator, schema);
return 21;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -9639,7 +9048,6 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -9661,6 +9069,5 @@ i1.OnUpgrade stepByStep({
from17To18: from17To18,
from18To19: from18To19,
from19To20: from19To20,
from20To21: from20To21,
),
);
@@ -301,7 +301,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
id: asset.id,
orientation: Value(asset.orientation),
isFavorite: Value(asset.isFavorite),
playbackStyle: Value(asset.playbackStyle),
latitude: Value(asset.latitude),
longitude: Value(asset.longitude),
adjustmentTime: Value(asset.adjustmentTime),
@@ -334,7 +333,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
checksum: const Value(null),
orientation: Value(asset.orientation),
isFavorite: Value(asset.isFavorite),
playbackStyle: Value(asset.playbackStyle),
);
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
_db.localAssetEntity,
@@ -101,7 +101,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
isFavorite: row.isFavorite,
durationInSeconds: row.durationInSeconds,
orientation: row.orientation,
playbackStyle: AssetPlaybackStyle.values[row.playbackStyle],
cloudId: row.iCloudId,
latitude: row.latitude,
longitude: row.longitude,
@@ -85,7 +85,6 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
durationInSeconds: Value(item.asset.durationInSeconds),
isFavorite: Value(item.asset.isFavorite),
orientation: Value(item.asset.orientation),
playbackStyle: Value(item.asset.playbackStyle),
source: TrashOrigin.localSync,
);
@@ -148,7 +147,6 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
durationInSeconds: Value(asset.durationInSeconds),
isFavorite: Value(asset.isFavorite),
orientation: Value(asset.orientation),
playbackStyle: Value(asset.playbackStyle),
createdAt: Value(asset.createdAt),
updatedAt: Value(asset.updatedAt),
source: const Value(TrashOrigin.remoteSync),
@@ -197,7 +195,6 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
checksum: Value(e.checksum),
isFavorite: Value(e.isFavorite),
orientation: Value(e.orientation),
playbackStyle: Value(e.playbackStyle),
);
});
@@ -248,7 +245,6 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
checksum: Value(e.asset.checksum),
isFavorite: Value(e.asset.isFavorite),
orientation: Value(e.asset.orientation),
playbackStyle: Value(e.asset.playbackStyle),
source: TrashOrigin.localUser,
albumId: e.albumId,
);
+20 -25
View File
@@ -20,7 +20,6 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
@@ -50,34 +49,30 @@ import 'package:logging/logging.dart';
import 'package:timezone/data/latest.dart';
void main() async {
try {
ImmichWidgetsBinding();
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
await EasyLocalization.ensureInitialized();
final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb);
await initApp();
// Warm-up isolate pool for worker manager
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
await migrateDatabaseIfNeeded(isar, drift);
HttpSSLOptions.apply();
ImmichWidgetsBinding();
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb);
await initApp();
// Warm-up isolate pool for worker manager
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
await migrateDatabaseIfNeeded(isar, drift);
HttpSSLOptions.apply();
runApp(
ProviderScope(
overrides: [
dbProvider.overrideWithValue(isar),
isarProvider.overrideWithValue(isar),
driftProvider.overrideWith(driftOverride(drift)),
],
child: const MainWidget(),
),
);
} catch (error, stack) {
runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString()));
}
runApp(
ProviderScope(
overrides: [
dbProvider.overrideWithValue(isar),
isarProvider.overrideWithValue(isar),
driftProvider.overrideWith(driftOverride(drift)),
],
child: const MainWidget(),
),
);
}
Future<void> initApp() async {
await EasyLocalization.ensureInitialized();
await initializeDateFormatting();
if (Platform.isAndroid) {
@@ -1,19 +1,10 @@
import 'dart:async';
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
@@ -22,259 +13,7 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/theme/color_scheme.dart';
import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/widgets/common/immich_logo.dart';
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
import 'package:logging/logging.dart';
import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode;
class BootstrapErrorWidget extends StatelessWidget {
final String error;
final String stack;
const BootstrapErrorWidget({super.key, required this.error, required this.stack});
@override
Widget build(BuildContext _) {
final immichTheme = defaultColorPreset.themeOfPreset;
return EasyLocalization(
supportedLocales: locales.values.toList(),
path: translationsPath,
useFallbackTranslations: true,
fallbackLocale: locales.values.first,
assetLoader: const CodegenLoader(),
child: Builder(
builder: (lCtx) => MaterialApp(
title: 'Immich',
debugShowCheckedModeBanner: true,
localizationsDelegates: lCtx.localizationDelegates,
supportedLocales: lCtx.supportedLocales,
locale: lCtx.locale,
themeMode: ThemeMode.system,
darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: lCtx.locale),
theme: getThemeData(colorScheme: immichTheme.light, locale: lCtx.locale),
home: Builder(
builder: (ctx) => Scaffold(
body: Column(
children: [
const SafeArea(
bottom: false,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ImmichLogo(size: 48), SizedBox(width: 12), ImmichTitleText(fontSize: 24)],
),
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: _ErrorCard(error: error, stack: stack),
),
),
const Divider(height: 1),
const SafeArea(
top: false,
child: Padding(padding: EdgeInsets.fromLTRB(24, 16, 24, 16), child: _BottomPanel()),
),
],
),
),
),
),
),
);
}
}
class _BottomPanel extends StatefulWidget {
const _BottomPanel();
@override
State<_BottomPanel> createState() => _BottomPanelState();
}
class _BottomPanelState extends State<_BottomPanel> {
bool _cleared = false;
Future<void> _clearDatabase() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogCtx) => AlertDialog(
title: Text(context.t.reset_sqlite_clear_app_data),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.t.reset_sqlite_confirmation),
const SizedBox(height: 12),
Text(
context.t.reset_sqlite_confirmation_note,
style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.of(dialogCtx).pop(false), child: Text(context.t.cancel)),
TextButton(
onPressed: () => Navigator.of(dialogCtx).pop(true),
child: Text(context.t.confirm, style: TextStyle(color: Theme.of(context).colorScheme.error)),
),
],
),
);
if (confirmed != true || !mounted) {
return;
}
try {
final dir = await getApplicationDocumentsDirectory();
for (final suffix in ['', '-wal', '-shm']) {
final file = File(path.join(dir.path, 'immich.sqlite$suffix'));
if (await file.exists()) {
await file.delete();
}
}
} catch (_) {
return;
}
if (mounted) {
setState(() => _cleared = true);
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
children: [
Text(
_cleared ? context.t.reset_sqlite_done : context.t.scaffold_body_error_unrecoverable,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_ActionLink(
icon: Icons.chat_bubble_outline,
label: context.t.discord,
onTap: () => launchUrl(Uri.parse('https://discord.immich.app/'), mode: LaunchMode.externalApplication),
),
_ActionLink(
icon: Icons.bug_report_outlined,
label: context.t.profile_drawer_github,
onTap: () => launchUrl(
Uri.parse('https://github.com/immich-app/immich/issues'),
mode: LaunchMode.externalApplication,
),
),
if (!_cleared)
_ActionLink(
icon: Icons.delete_outline,
label: context.t.reset_sqlite_clear_app_data,
onTap: _clearDatabase,
),
],
),
],
);
}
}
class _ActionLink extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
const _ActionLink({required this.icon, required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 24),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 12)),
],
),
),
);
}
}
class _ErrorCard extends StatelessWidget {
final String error;
final String stack;
const _ErrorCard({required this.error, required this.stack});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ColoredBox(
color: scheme.error,
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 8, 8),
child: Row(
children: [
Expanded(
child: Text(
context.t.scaffold_body_error_occurred,
style: textTheme.titleSmall?.copyWith(color: scheme.onError),
),
),
IconButton(
tooltip: context.t.copy_error,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: Icon(Icons.copy_outlined, size: 16, color: scheme.onError),
onPressed: () => Clipboard.setData(ClipboardData(text: '$error\n\n$stack')),
),
],
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Text(error, style: textTheme.bodyMedium),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.t.stacktrace, style: textTheme.labelMedium),
const SizedBox(height: 4),
SelectableText(stack, style: textTheme.bodySmall?.copyWith(fontFamily: 'GoogleSansCode')),
],
),
),
],
),
);
}
}
@RoutePage()
class SplashScreenPage extends StatefulHookConsumerWidget {
-2
View File
@@ -55,7 +55,6 @@ class LocalImageApi {
required int width,
required int height,
required bool isVideo,
required bool preferEncoded,
}) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$pigeonVar_messageChannelSuffix';
@@ -70,7 +69,6 @@ class LocalImageApi {
width,
height,
isVideo,
preferEncoded,
]);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
+9 -22
View File
@@ -29,8 +29,6 @@ bool _deepEquals(Object? a, Object? b) {
return a == b;
}
enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping }
class PlatformAsset {
PlatformAsset({
required this.id,
@@ -46,7 +44,6 @@ class PlatformAsset {
this.adjustmentTime,
this.latitude,
this.longitude,
required this.playbackStyle,
});
String id;
@@ -75,8 +72,6 @@ class PlatformAsset {
double? longitude;
PlatformAssetPlaybackStyle playbackStyle;
List<Object?> _toList() {
return <Object?>[
id,
@@ -92,7 +87,6 @@ class PlatformAsset {
adjustmentTime,
latitude,
longitude,
playbackStyle,
];
}
@@ -116,7 +110,6 @@ class PlatformAsset {
adjustmentTime: result[10] as int?,
latitude: result[11] as double?,
longitude: result[12] as double?,
playbackStyle: result[13]! as PlatformAssetPlaybackStyle,
);
}
@@ -323,23 +316,20 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is PlatformAssetPlaybackStyle) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is PlatformAsset) {
buffer.putUint8(130);
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is PlatformAlbum) {
buffer.putUint8(131);
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is SyncDelta) {
buffer.putUint8(132);
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is HashResult) {
buffer.putUint8(133);
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else if (value is CloudIdResult) {
buffer.putUint8(134);
buffer.putUint8(133);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
@@ -350,17 +340,14 @@ class _PigeonCodec extends StandardMessageCodec {
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
final int? value = readValue(buffer) as int?;
return value == null ? null : PlatformAssetPlaybackStyle.values[value];
case 130:
return PlatformAsset.decode(readValue(buffer)!);
case 131:
case 130:
return PlatformAlbum.decode(readValue(buffer)!);
case 132:
case 131:
return SyncDelta.decode(readValue(buffer)!);
case 133:
case 132:
return HashResult.decode(readValue(buffer)!);
case 134:
case 133:
return CloudIdResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
+1 -7
View File
@@ -53,7 +53,6 @@ class RemoteImageApi {
String url, {
required Map<String, String> headers,
required int requestId,
required bool preferEncoded,
}) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$pigeonVar_messageChannelSuffix';
@@ -62,12 +61,7 @@ class RemoteImageApi {
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[
url,
headers,
requestId,
preferEncoded,
]);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[url, headers, requestId]);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
@@ -8,7 +8,6 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This delete action has the following behavior:
@@ -26,15 +25,6 @@ class DeletePermanentActionButton extends ConsumerWidget {
return;
}
final count = source == ActionSource.viewer ? 1 : ref.read(multiSelectProvider).selectedAssets.length;
final confirm =
await showDialog<bool>(
context: context,
builder: (context) => PermanentDeleteDialog(count: count),
) ??
false;
if (!confirm) return;
final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source);
ref.read(multiSelectProvider.notifier).reset();
@@ -5,7 +5,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart';
import 'package:immich_mobile/widgets/asset_grid/trash_delete_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This delete action has the following behavior:
@@ -28,7 +28,7 @@ class DeleteTrashActionButton extends ConsumerWidget {
final confirmDelete =
await showDialog<bool>(
context: context,
builder: (context) => PermanentDeleteDialog(count: selectCount),
builder: (context) => TrashDeleteDialog(count: selectCount),
) ??
false;
if (!confirmDelete) {
@@ -64,6 +64,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
@override
void initState() {
super.initState();
_proxyScrollController.addListener(_onScroll);
_eventSubscription = EventStream.shared.listen(_onEvent);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !_proxyScrollController.hasClients) return;
@@ -93,7 +94,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
void _showDetails() {
if (!_proxyScrollController.hasClients || _snapOffset <= 0) return;
_viewer.setShowingDetails(true);
_proxyScrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic);
}
@@ -105,7 +105,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
SnapScrollPhysics.target(position, scrollVelocity, _snapOffset) < SnapScrollPhysics.minSnapDistance;
}
void _syncShowingDetails() {
void _onScroll() {
final offset = _proxyScrollController.offset;
if (offset > SnapScrollPhysics.minSnapDistance) {
_viewer.setShowingDetails(true);
@@ -149,8 +149,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
case _DragIntent.scroll:
if (_drag == null) _startProxyDrag();
_drag?.update(details);
_syncShowingDetails();
case _DragIntent.dismiss:
_handleDragDown(context, details.localPosition - _dragStart!.localPosition);
}
@@ -169,8 +167,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
case _DragIntent.none:
case _DragIntent.scroll:
final scrollVelocity = -(details.primaryVelocity ?? 0.0);
_viewer.setShowingDetails(!_willClose(scrollVelocity));
if (_willClose(scrollVelocity)) {
_viewer.setShowingDetails(false);
}
_drag?.end(details);
_drag = null;
case _DragIntent.dismiss:
@@ -307,7 +306,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
if (displayAsset.isImage && !isPlayingMotionVideo) {
final size = context.sizeData;
return PhotoView(
key: Key(displayAsset.heroTag),
key: ValueKey(displayAsset.heroTag),
index: widget.index,
imageProvider: getFullImageProvider(displayAsset, size: size),
heroAttributes: heroAttributes,
@@ -335,7 +334,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
}
return PhotoView.customChild(
key: Key(displayAsset.heroTag),
key: ValueKey(displayAsset),
onDragStart: _onDragStart,
onDragUpdate: _onDragUpdate,
onDragEnd: _onDragEnd,
@@ -351,11 +350,12 @@ class _AssetPageState extends ConsumerState<AssetPage> {
enablePanAlways: true,
backgroundDecoration: backgroundDecoration,
child: NativeVideoViewer(
key: _NativeVideoViewerKey(displayAsset.heroTag),
key: ValueKey(displayAsset),
asset: displayAsset,
scaleStateNotifier: _videoScaleStateNotifier,
disableScaleGestures: showingDetails,
image: Image(
key: ValueKey(displayAsset.heroTag),
image: getFullImageProvider(displayAsset, size: context.sizeData),
height: context.height,
width: context.width,
@@ -459,25 +459,3 @@ class _AssetPageState extends ConsumerState<AssetPage> {
);
}
}
// A global key is used for video viewers to prevent them from being
// unnecessarily recreated. They're quite expensive, and maintain internal
// state. This can cause videos to restart multiple times during normal usage,
// like a hero animation.
//
// A plain ValueKey is insufficient, as it does not allow widgets to reparent. A
// GlobalObjectKey is fragile, as it checks if the given objects are identical,
// rather than equal. Hero tags are created with string interpolation, which
// prevents Dart from interning them. As such, hero tags are not identical, even
// if they are equal.
class _NativeVideoViewerKey extends GlobalKey {
final String value;
const _NativeVideoViewerKey(this.value) : super.constructor();
@override
bool operator ==(Object other) => other is _NativeVideoViewerKey && other.value == value;
@override
int get hashCode => value.hashCode;
}
@@ -117,9 +117,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
_reloadSubscription = EventStream.shared.listen(_onEvent);
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
final assetViewer = ref.read(assetViewerProvider);
_setSystemUIMode(assetViewer.showingControls, assetViewer.showingDetails);
}
@override
@@ -229,13 +226,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
_onAssetChanged(index);
}
void _setSystemUIMode(bool controls, bool details) {
final mode = !controls || (CurrentPlatform.isIOS && details)
? SystemUiMode.immersiveSticky
: SystemUiMode.edgeToEdge;
unawaited(SystemChrome.setEnabledSystemUIMode(mode));
}
@override
Widget build(BuildContext context) {
final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
@@ -255,7 +245,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
ref.listen(assetViewerProvider.select((value) => (value.showingControls, value.showingDetails)), (_, state) {
final (controls, details) = state;
_setSystemUIMode(controls, details);
final mode = !controls || (CurrentPlatform.isIOS && details)
? SystemUiMode.immersiveSticky
: SystemUiMode.edgeToEdge;
unawaited(SystemChrome.setEnabledSystemUIMode(mode));
});
return PopScope(
@@ -420,18 +420,20 @@ class NativeVideoViewer extends HookConsumerWidget {
child: Stack(
children: [
// Hide thumbnail once video is visible to avoid it showing in background when zooming out on video.
if (!isVisible.value || controller.value == null) Center(child: image),
if (!isVisible.value || controller.value == null) Center(key: ValueKey(asset.heroTag), child: image),
if (aspectRatio.value != null && !isCasting && isCurrent)
Visibility.maintain(
key: ValueKey(asset),
visible: isVisible.value,
child: PhotoView.customChild(
key: ValueKey(asset),
enableRotation: false,
disableScaleGestures: disableScaleGestures,
// Transparent to avoid a black flash when viewer becomes visible but video isn't loaded yet.
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
scaleStateChangedCallback: (state) => scaleStateNotifier?.value = state,
childSize: videoContextSize(aspectRatio.value, context),
child: NativeVideoPlayerView(onViewReady: initController),
child: NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController),
),
),
if (showControls) const Center(child: VideoViewerControls()),
@@ -36,7 +36,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
const ShareLinkActionButton(source: ActionSource.timeline),
const UnArchiveActionButton(source: ActionSource.timeline),
const FavoriteActionButton(source: ActionSource.timeline),
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(source: ActionSource.timeline),
@@ -75,7 +75,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
const ShareLinkActionButton(source: ActionSource.timeline),
const UnFavoriteActionButton(source: ActionSource.timeline),
const ArchiveActionButton(source: ActionSource.timeline),
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(source: ActionSource.timeline),
@@ -108,7 +108,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
const ShareActionButton(source: ActionSource.timeline),
if (multiselect.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.timeline),
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(source: ActionSource.timeline),
@@ -119,11 +119,10 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline),
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline),
],
if (multiselect.onlyLocal || multiselect.hasMerged)
const DeleteLocalActionButton(source: ActionSource.timeline),
if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline),
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
if (multiselect.hasLocal) const UploadActionButton(source: ActionSource.timeline),
],
slivers: multiselect.hasRemote
? [
@@ -1,5 +1,3 @@
import 'dart:ui' as ui;
import 'package:async/async.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -50,7 +48,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
return null;
}
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {bool evictOnError = true}) async* {
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode) async* {
if (isCancelled) {
this.request = null;
PaintingBinding.instance.imageCache.evict(this);
@@ -59,39 +57,11 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
try {
final image = await request.load(decode);
if ((image == null && evictOnError) || isCancelled) {
if (image == null || isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return;
} else if (image == null) {
return;
}
yield image;
} catch (e, stack) {
if (evictOnError) {
PaintingBinding.instance.imageCache.evict(this);
rethrow;
}
_log.warning('Non-fatal image load error', e, stack);
} finally {
this.request = null;
}
}
Future<ui.Codec?> loadCodecRequest(ImageRequest request) async {
if (isCancelled) {
this.request = null;
PaintingBinding.instance.imageCache.evict(this);
return null;
}
try {
final codec = await request.loadCodec();
if (codec == null || isCancelled) {
codec?.dispose();
PaintingBinding.instance.imageCache.evict(this);
return null;
}
return codec;
} catch (e) {
PaintingBinding.instance.imageCache.evict(this);
rethrow;
@@ -94,6 +94,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType,
);
yield* loadRequest(request, decode);
if (!Store.get(StoreKey.loadOriginal, false)) {
@@ -93,10 +93,9 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
headers: headers,
);
final loadOriginal = assetType == AssetType.image && AppSetting.get(Setting.loadOriginal);
yield* loadRequest(previewRequest, decode, evictOnError: !loadOriginal);
yield* loadRequest(previewRequest, decode);
if (!loadOriginal) {
if (assetType != AssetType.image || !AppSetting.get(Setting.loadOriginal)) {
return;
}
@@ -24,12 +24,10 @@ class MultiSelectState {
bool get hasStacked => selectedAssets.any((asset) => asset is RemoteAsset && asset.stackId != null);
bool get hasLocal => selectedAssets.any((asset) => asset.storage == AssetState.local);
bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged);
bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local);
bool get onlyRemote => selectedAssets.any((asset) => asset.storage == AssetState.remote);
MultiSelectState copyWith({
Set<BaseAsset>? selectedAssets,
Set<BaseAsset>? lockedSelectionAssets,
@@ -25,7 +25,6 @@ class FileMediaRepository {
type: AssetType.image,
createdAt: entity.createDateTime,
updatedAt: entity.modifiedDateTime,
playbackStyle: AssetPlaybackStyle.image,
isEdited: false,
);
}
+1 -53
View File
@@ -5,7 +5,6 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
@@ -18,7 +17,6 @@ import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
@@ -35,7 +33,7 @@ import 'package:isar/isar.dart';
// ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 23;
const int targetVersion = 22;
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
final hasVersion = Store.tryGet(StoreKey.version) != null;
@@ -101,10 +99,6 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
}
}
if (version < 23 && Store.isBetaTimelineEnabled) {
await _populateLocalAssetPlaybackStyle(drift);
}
if (version < 22 && !Store.isBetaTimelineEnabled) {
await Store.put(StoreKey.needBetaMigration, true);
}
@@ -398,52 +392,6 @@ Future<void> migrateStoreToIsar(Isar db, Drift drift) async {
}
}
Future<void> _populateLocalAssetPlaybackStyle(Drift db) async {
try {
final nativeApi = NativeSyncApi();
final albums = await nativeApi.getAlbums();
for (final album in albums) {
final assets = await nativeApi.getAssetsForAlbum(album.id);
await db.batch((batch) {
for (final asset in assets) {
batch.update(
db.localAssetEntity,
LocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))),
where: (t) => t.id.equals(asset.id),
);
}
});
}
final trashedAssetMap = await nativeApi.getTrashedAssets();
for (final assets in trashedAssetMap.values) {
await db.batch((batch) {
for (final asset in assets) {
batch.update(
db.trashedLocalAssetEntity,
TrashedLocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))),
where: (t) => t.id.equals(asset.id),
);
}
});
}
dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local and trashed assets");
} catch (error) {
dPrint(() => "[MIGRATION] Error while populating playbackStyle: $error");
}
}
AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) {
PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown,
PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image,
PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video,
PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated,
PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto,
PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping,
};
class _DeviceAsset {
final String assetId;
final List<int>? hash;
@@ -3,8 +3,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_ui/immich_ui.dart';
class PermanentDeleteDialog extends StatelessWidget {
const PermanentDeleteDialog({super.key, required this.count});
class TrashDeleteDialog extends StatelessWidget {
const TrashDeleteDialog({super.key, required this.count});
final int count;
@@ -62,7 +62,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
pinned: pinned,
snap: snap,
expandedHeight: expandedHeight,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(5))),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
automaticallyImplyLeading: false,
centerTitle: false,
title: title ?? const _ImmichLogoWithText(),
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
@@ -6,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
@@ -89,27 +87,25 @@ class SyncStatusAndActions extends HookConsumerWidget {
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.t.reset_sqlite),
content: Text(context.t.reset_sqlite_confirmation),
title: Text("reset_sqlite".t(context: context)),
content: Text("reset_sqlite_confirmation".t(context: context)),
actions: [
TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel)),
TextButton(
onPressed: () => context.pop(),
child: Text("cancel".t(context: context)),
),
TextButton(
onPressed: () async {
await ref.read(driftProvider).reset();
context.pop();
unawaited(
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: Text(context.t.reset_sqlite_success),
content: Text(context.t.reset_sqlite_done),
actions: [TextButton(onPressed: () => ctx.pop(), child: Text(context.t.ok))],
),
),
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("reset_sqlite_success".t(context: context))),
);
},
child: Text(context.t.confirm, style: TextStyle(color: context.colorScheme.error)),
child: Text(
"confirm".t(context: context),
style: TextStyle(color: context.colorScheme.error),
),
),
],
);
+12 -3
View File
@@ -121,7 +121,6 @@ Class | Method | HTTP request | Description
*AssetsApi* | [**updateBulkAssetMetadata**](doc//AssetsApi.md#updatebulkassetmetadata) | **PUT** /assets/metadata | Upsert asset metadata
*AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets | Upload asset
*AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail | View asset thumbnail
*AssetsApi* | [**viewAssetTile**](doc//AssetsApi.md#viewassettile) | **GET** /assets/{id}/tiles/{level}/{col}/{row} | View an asset tile
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | Change password
*AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code | Change pin code
*AuthenticationApi* | [**finishOAuth**](doc//AuthenticationApi.md#finishoauth) | **POST** /oauth/callback | Finish OAuth
@@ -172,7 +171,12 @@ Class | Method | HTTP request | Description
*LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings
*MaintenanceAdminApi* | [**deleteIntegrityReport**](doc//MaintenanceAdminApi.md#deleteintegrityreport) | **DELETE** /admin/integrity/report/{id} | Delete integrity report item
*MaintenanceAdminApi* | [**detectPriorInstall**](doc//MaintenanceAdminApi.md#detectpriorinstall) | **GET** /admin/maintenance/detect-install | Detect existing install
*MaintenanceAdminApi* | [**getIntegrityReport**](doc//MaintenanceAdminApi.md#getintegrityreport) | **GET** /admin/integrity/report | Get integrity report by type
*MaintenanceAdminApi* | [**getIntegrityReportCsv**](doc//MaintenanceAdminApi.md#getintegrityreportcsv) | **GET** /admin/integrity/report/{type}/csv | Export integrity report by type as CSV
*MaintenanceAdminApi* | [**getIntegrityReportFile**](doc//MaintenanceAdminApi.md#getintegrityreportfile) | **GET** /admin/integrity/report/{id}/file | Download flagged file
*MaintenanceAdminApi* | [**getIntegrityReportSummary**](doc//MaintenanceAdminApi.md#getintegrityreportsummary) | **GET** /admin/integrity/summary | Get integrity report summary
*MaintenanceAdminApi* | [**getMaintenanceStatus**](doc//MaintenanceAdminApi.md#getmaintenancestatus) | **GET** /admin/maintenance/status | Get maintenance mode status
*MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode
*MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode
@@ -432,6 +436,10 @@ Class | Method | HTTP request | Description
- [FoldersResponse](doc//FoldersResponse.md)
- [FoldersUpdate](doc//FoldersUpdate.md)
- [ImageFormat](doc//ImageFormat.md)
- [IntegrityReportDto](doc//IntegrityReportDto.md)
- [IntegrityReportResponseDto](doc//IntegrityReportResponseDto.md)
- [IntegrityReportSummaryResponseDto](doc//IntegrityReportSummaryResponseDto.md)
- [IntegrityReportType](doc//IntegrityReportType.md)
- [JobCreateDto](doc//JobCreateDto.md)
- [JobName](doc//JobName.md)
- [JobSettingsDto](doc//JobSettingsDto.md)
@@ -577,8 +585,6 @@ Class | Method | HTTP request | Description
- [SyncAlbumUserV1](doc//SyncAlbumUserV1.md)
- [SyncAlbumV1](doc//SyncAlbumV1.md)
- [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md)
- [SyncAssetEditDeleteV1](doc//SyncAssetEditDeleteV1.md)
- [SyncAssetEditV1](doc//SyncAssetEditV1.md)
- [SyncAssetExifV1](doc//SyncAssetExifV1.md)
- [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md)
- [SyncAssetFaceV1](doc//SyncAssetFaceV1.md)
@@ -611,6 +617,9 @@ Class | Method | HTTP request | Description
- [SystemConfigGeneratedFullsizeImageDto](doc//SystemConfigGeneratedFullsizeImageDto.md)
- [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md)
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
- [SystemConfigIntegrityChecks](doc//SystemConfigIntegrityChecks.md)
- [SystemConfigIntegrityChecksumJob](doc//SystemConfigIntegrityChecksumJob.md)
- [SystemConfigIntegrityJob](doc//SystemConfigIntegrityJob.md)
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
- [SystemConfigLibraryScanDto](doc//SystemConfigLibraryScanDto.md)
+7 -2
View File
@@ -170,6 +170,10 @@ part 'model/facial_recognition_config.dart';
part 'model/folders_response.dart';
part 'model/folders_update.dart';
part 'model/image_format.dart';
part 'model/integrity_report_dto.dart';
part 'model/integrity_report_response_dto.dart';
part 'model/integrity_report_summary_response_dto.dart';
part 'model/integrity_report_type.dart';
part 'model/job_create_dto.dart';
part 'model/job_name.dart';
part 'model/job_settings_dto.dart';
@@ -315,8 +319,6 @@ part 'model/sync_album_user_delete_v1.dart';
part 'model/sync_album_user_v1.dart';
part 'model/sync_album_v1.dart';
part 'model/sync_asset_delete_v1.dart';
part 'model/sync_asset_edit_delete_v1.dart';
part 'model/sync_asset_edit_v1.dart';
part 'model/sync_asset_exif_v1.dart';
part 'model/sync_asset_face_delete_v1.dart';
part 'model/sync_asset_face_v1.dart';
@@ -349,6 +351,9 @@ part 'model/system_config_faces_dto.dart';
part 'model/system_config_generated_fullsize_image_dto.dart';
part 'model/system_config_generated_image_dto.dart';
part 'model/system_config_image_dto.dart';
part 'model/system_config_integrity_checks.dart';
part 'model/system_config_integrity_checksum_job.dart';
part 'model/system_config_integrity_job.dart';
part 'model/system_config_job_dto.dart';
part 'model/system_config_library_dto.dart';
part 'model/system_config_library_scan_dto.dart';
-87
View File
@@ -1836,91 +1836,4 @@ class AssetsApi {
}
return null;
}
/// View an asset tile
///
/// Download a specific tile from an image at the specified level - must currently be 0 - and position
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [num] col (required):
///
/// * [String] id (required):
///
/// * [num] level (required):
///
/// * [num] row (required):
///
/// * [String] key:
///
/// * [String] slug:
Future<Response> viewAssetTileWithHttpInfo(num col, String id, num level, num row, { String? key, String? slug, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/{id}/tiles/{level}/{col}/{row}'
.replaceAll('{col}', col.toString())
.replaceAll('{id}', id)
.replaceAll('{level}', level.toString())
.replaceAll('{row}', row.toString());
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
if (slug != null) {
queryParams.addAll(_queryParams('', 'slug', slug));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// View an asset tile
///
/// Download a specific tile from an image at the specified level - must currently be 0 - and position
///
/// Parameters:
///
/// * [num] col (required):
///
/// * [String] id (required):
///
/// * [num] level (required):
///
/// * [num] row (required):
///
/// * [String] key:
///
/// * [String] slug:
Future<MultipartFile?> viewAssetTile(num col, String id, num level, num row, { String? key, String? slug, }) async {
final response = await viewAssetTileWithHttpInfo(col, id, level, row, key: key, slug: slug, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile;
}
return null;
}
}
+287
View File
@@ -16,6 +16,55 @@ class MaintenanceAdminApi {
final ApiClient apiClient;
/// Delete integrity report item
///
/// Delete a given report item and perform corresponding deletion (e.g. trash asset, delete file)
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> deleteIntegrityReportWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/integrity/report/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Delete integrity report item
///
/// Delete a given report item and perform corresponding deletion (e.g. trash asset, delete file)
///
/// Parameters:
///
/// * [String] id (required):
Future<void> deleteIntegrityReport(String id,) async {
final response = await deleteIntegrityReportWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Detect existing install
///
/// Collect integrity checks and other heuristics about local data.
@@ -64,6 +113,244 @@ class MaintenanceAdminApi {
return null;
}
/// Get integrity report by type
///
/// Get all flagged items by integrity report type
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [IntegrityReportType] type (required):
///
/// * [String] cursor:
/// Cursor for pagination
///
/// * [num] limit:
/// Number of items per page
Future<Response> getIntegrityReportWithHttpInfo(IntegrityReportType type, { String? cursor, num? limit, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/integrity/report';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (cursor != null) {
queryParams.addAll(_queryParams('', 'cursor', cursor));
}
if (limit != null) {
queryParams.addAll(_queryParams('', 'limit', limit));
}
queryParams.addAll(_queryParams('', 'type', type));
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Get integrity report by type
///
/// Get all flagged items by integrity report type
///
/// Parameters:
///
/// * [IntegrityReportType] type (required):
///
/// * [String] cursor:
/// Cursor for pagination
///
/// * [num] limit:
/// Number of items per page
Future<IntegrityReportResponseDto?> getIntegrityReport(IntegrityReportType type, { String? cursor, num? limit, }) async {
final response = await getIntegrityReportWithHttpInfo(type, cursor: cursor, limit: limit, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'IntegrityReportResponseDto',) as IntegrityReportResponseDto;
}
return null;
}
/// Export integrity report by type as CSV
///
/// Get all integrity report entries for a given type as a CSV
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [IntegrityReportType] type (required):
Future<Response> getIntegrityReportCsvWithHttpInfo(IntegrityReportType type,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/integrity/report/{type}/csv'
.replaceAll('{type}', type.toString());
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Export integrity report by type as CSV
///
/// Get all integrity report entries for a given type as a CSV
///
/// Parameters:
///
/// * [IntegrityReportType] type (required):
Future<MultipartFile?> getIntegrityReportCsv(IntegrityReportType type,) async {
final response = await getIntegrityReportCsvWithHttpInfo(type,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile;
}
return null;
}
/// Download flagged file
///
/// Download the untracked/broken file if one exists
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getIntegrityReportFileWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/integrity/report/{id}/file'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Download flagged file
///
/// Download the untracked/broken file if one exists
///
/// Parameters:
///
/// * [String] id (required):
Future<MultipartFile?> getIntegrityReportFile(String id,) async {
final response = await getIntegrityReportFileWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile;
}
return null;
}
/// Get integrity report summary
///
/// Get a count of the items flagged in each integrity report
///
/// Note: This method returns the HTTP [Response].
Future<Response> getIntegrityReportSummaryWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/integrity/summary';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Get integrity report summary
///
/// Get a count of the items flagged in each integrity report
Future<IntegrityReportSummaryResponseDto?> getIntegrityReportSummary() async {
final response = await getIntegrityReportSummaryWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'IntegrityReportSummaryResponseDto',) as IntegrityReportSummaryResponseDto;
}
return null;
}
/// Get maintenance mode status
///
/// Fetch information about the currently running maintenance action.
+2 -2
View File
@@ -410,7 +410,7 @@ class SearchApi {
/// Filter by person IDs
///
/// * [num] rating:
/// Filter by rating [1-5], or null for unrated
/// Filter by rating
///
/// * [num] size:
/// Number of results to return
@@ -633,7 +633,7 @@ class SearchApi {
/// Filter by person IDs
///
/// * [num] rating:
/// Filter by rating [1-5], or null for unrated
/// Filter by rating
///
/// * [num] size:
/// Number of results to return
+6 -24
View File
@@ -30,9 +30,6 @@ class TimelineApi {
/// * [String] albumId:
/// Filter assets belonging to a specific album
///
/// * [String] bbox:
/// Bounding box coordinates as west,south,east,north (WGS84)
///
/// * [bool] isFavorite:
/// Filter by favorite status (true for favorites only, false for non-favorites only)
///
@@ -66,7 +63,7 @@ class TimelineApi {
///
/// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/timeline/bucket';
@@ -80,9 +77,6 @@ class TimelineApi {
if (albumId != null) {
queryParams.addAll(_queryParams('', 'albumId', albumId));
}
if (bbox != null) {
queryParams.addAll(_queryParams('', 'bbox', bbox));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
@@ -147,9 +141,6 @@ class TimelineApi {
/// * [String] albumId:
/// Filter assets belonging to a specific album
///
/// * [String] bbox:
/// Bounding box coordinates as west,south,east,north (WGS84)
///
/// * [bool] isFavorite:
/// Filter by favorite status (true for favorites only, false for non-favorites only)
///
@@ -183,8 +174,8 @@ class TimelineApi {
///
/// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -209,9 +200,6 @@ class TimelineApi {
/// * [String] albumId:
/// Filter assets belonging to a specific album
///
/// * [String] bbox:
/// Bounding box coordinates as west,south,east,north (WGS84)
///
/// * [bool] isFavorite:
/// Filter by favorite status (true for favorites only, false for non-favorites only)
///
@@ -245,7 +233,7 @@ class TimelineApi {
///
/// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/timeline/buckets';
@@ -259,9 +247,6 @@ class TimelineApi {
if (albumId != null) {
queryParams.addAll(_queryParams('', 'albumId', albumId));
}
if (bbox != null) {
queryParams.addAll(_queryParams('', 'bbox', bbox));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
@@ -322,9 +307,6 @@ class TimelineApi {
/// * [String] albumId:
/// Filter assets belonging to a specific album
///
/// * [String] bbox:
/// Bounding box coordinates as west,south,east,north (WGS84)
///
/// * [bool] isFavorite:
/// Filter by favorite status (true for favorites only, false for non-favorites only)
///
@@ -358,8 +340,8 @@ class TimelineApi {
///
/// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
+14 -4
View File
@@ -386,6 +386,14 @@ class ApiClient {
return FoldersUpdate.fromJson(value);
case 'ImageFormat':
return ImageFormatTypeTransformer().decode(value);
case 'IntegrityReportDto':
return IntegrityReportDto.fromJson(value);
case 'IntegrityReportResponseDto':
return IntegrityReportResponseDto.fromJson(value);
case 'IntegrityReportSummaryResponseDto':
return IntegrityReportSummaryResponseDto.fromJson(value);
case 'IntegrityReportType':
return IntegrityReportTypeTypeTransformer().decode(value);
case 'JobCreateDto':
return JobCreateDto.fromJson(value);
case 'JobName':
@@ -676,10 +684,6 @@ class ApiClient {
return SyncAlbumV1.fromJson(value);
case 'SyncAssetDeleteV1':
return SyncAssetDeleteV1.fromJson(value);
case 'SyncAssetEditDeleteV1':
return SyncAssetEditDeleteV1.fromJson(value);
case 'SyncAssetEditV1':
return SyncAssetEditV1.fromJson(value);
case 'SyncAssetExifV1':
return SyncAssetExifV1.fromJson(value);
case 'SyncAssetFaceDeleteV1':
@@ -744,6 +748,12 @@ class ApiClient {
return SystemConfigGeneratedImageDto.fromJson(value);
case 'SystemConfigImageDto':
return SystemConfigImageDto.fromJson(value);
case 'SystemConfigIntegrityChecks':
return SystemConfigIntegrityChecks.fromJson(value);
case 'SystemConfigIntegrityChecksumJob':
return SystemConfigIntegrityChecksumJob.fromJson(value);
case 'SystemConfigIntegrityJob':
return SystemConfigIntegrityJob.fromJson(value);
case 'SystemConfigJobDto':
return SystemConfigJobDto.fromJson(value);
case 'SystemConfigLibraryDto':
+3
View File
@@ -94,6 +94,9 @@ String parameterToString(dynamic value) {
if (value is ImageFormat) {
return ImageFormatTypeTransformer().encode(value).toString();
}
if (value is IntegrityReportType) {
return IntegrityReportTypeTypeTransformer().encode(value).toString();
}
if (value is JobName) {
return JobNameTypeTransformer().encode(value).toString();
}
+8 -4
View File
@@ -86,10 +86,16 @@ class AssetBulkUpdateDto {
///
num? longitude;
/// Rating in range [1-5], or null for unrated
/// Rating
///
/// Minimum value: -1
/// Maximum value: 5
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
/// Time zone (IANA timezone)
@@ -217,9 +223,7 @@ class AssetBulkUpdateDto {
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: num.parse('${json[r'latitude']}'),
longitude: num.parse('${json[r'longitude']}'),
rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
rating: num.parse('${json[r'rating']}'),
timeZone: mapValueOfType<String>(json, r'timeZone'),
visibility: AssetVisibility.fromJson(json[r'visibility']),
);
+115
View File
@@ -0,0 +1,115 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class IntegrityReportDto {
/// Returns a new [IntegrityReportDto] instance.
IntegrityReportDto({
required this.id,
required this.path,
required this.type,
});
String id;
String path;
IntegrityReportType type;
@override
bool operator ==(Object other) => identical(this, other) || other is IntegrityReportDto &&
other.id == id &&
other.path == path &&
other.type == type;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(id.hashCode) +
(path.hashCode) +
(type.hashCode);
@override
String toString() => 'IntegrityReportDto[id=$id, path=$path, type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'id'] = this.id;
json[r'path'] = this.path;
json[r'type'] = this.type;
return json;
}
/// Returns a new [IntegrityReportDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static IntegrityReportDto? fromJson(dynamic value) {
upgradeDto(value, "IntegrityReportDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return IntegrityReportDto(
id: mapValueOfType<String>(json, r'id')!,
path: mapValueOfType<String>(json, r'path')!,
type: IntegrityReportType.fromJson(json[r'type'])!,
);
}
return null;
}
static List<IntegrityReportDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <IntegrityReportDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = IntegrityReportDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, IntegrityReportDto> mapFromJson(dynamic json) {
final map = <String, IntegrityReportDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = IntegrityReportDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of IntegrityReportDto-objects as value to a dart map
static Map<String, List<IntegrityReportDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<IntegrityReportDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = IntegrityReportDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'id',
'path',
'type',
};
}
@@ -0,0 +1,116 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class IntegrityReportResponseDto {
/// Returns a new [IntegrityReportResponseDto] instance.
IntegrityReportResponseDto({
this.items = const [],
this.nextCursor,
});
List<IntegrityReportDto> items;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? nextCursor;
@override
bool operator ==(Object other) => identical(this, other) || other is IntegrityReportResponseDto &&
_deepEquality.equals(other.items, items) &&
other.nextCursor == nextCursor;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(items.hashCode) +
(nextCursor == null ? 0 : nextCursor!.hashCode);
@override
String toString() => 'IntegrityReportResponseDto[items=$items, nextCursor=$nextCursor]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'items'] = this.items;
if (this.nextCursor != null) {
json[r'nextCursor'] = this.nextCursor;
} else {
// json[r'nextCursor'] = null;
}
return json;
}
/// Returns a new [IntegrityReportResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static IntegrityReportResponseDto? fromJson(dynamic value) {
upgradeDto(value, "IntegrityReportResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return IntegrityReportResponseDto(
items: IntegrityReportDto.listFromJson(json[r'items']),
nextCursor: mapValueOfType<String>(json, r'nextCursor'),
);
}
return null;
}
static List<IntegrityReportResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <IntegrityReportResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = IntegrityReportResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, IntegrityReportResponseDto> mapFromJson(dynamic json) {
final map = <String, IntegrityReportResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = IntegrityReportResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of IntegrityReportResponseDto-objects as value to a dart map
static Map<String, List<IntegrityReportResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<IntegrityReportResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = IntegrityReportResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'items',
};
}
@@ -0,0 +1,115 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class IntegrityReportSummaryResponseDto {
/// Returns a new [IntegrityReportSummaryResponseDto] instance.
IntegrityReportSummaryResponseDto({
required this.checksumMismatch,
required this.missingFile,
required this.untrackedFile,
});
int checksumMismatch;
int missingFile;
int untrackedFile;
@override
bool operator ==(Object other) => identical(this, other) || other is IntegrityReportSummaryResponseDto &&
other.checksumMismatch == checksumMismatch &&
other.missingFile == missingFile &&
other.untrackedFile == untrackedFile;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(checksumMismatch.hashCode) +
(missingFile.hashCode) +
(untrackedFile.hashCode);
@override
String toString() => 'IntegrityReportSummaryResponseDto[checksumMismatch=$checksumMismatch, missingFile=$missingFile, untrackedFile=$untrackedFile]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'checksum_mismatch'] = this.checksumMismatch;
json[r'missing_file'] = this.missingFile;
json[r'untracked_file'] = this.untrackedFile;
return json;
}
/// Returns a new [IntegrityReportSummaryResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static IntegrityReportSummaryResponseDto? fromJson(dynamic value) {
upgradeDto(value, "IntegrityReportSummaryResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return IntegrityReportSummaryResponseDto(
checksumMismatch: mapValueOfType<int>(json, r'checksum_mismatch')!,
missingFile: mapValueOfType<int>(json, r'missing_file')!,
untrackedFile: mapValueOfType<int>(json, r'untracked_file')!,
);
}
return null;
}
static List<IntegrityReportSummaryResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <IntegrityReportSummaryResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = IntegrityReportSummaryResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, IntegrityReportSummaryResponseDto> mapFromJson(dynamic json) {
final map = <String, IntegrityReportSummaryResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = IntegrityReportSummaryResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of IntegrityReportSummaryResponseDto-objects as value to a dart map
static Map<String, List<IntegrityReportSummaryResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<IntegrityReportSummaryResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = IntegrityReportSummaryResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'checksum_mismatch',
'missing_file',
'untracked_file',
};
}
+88
View File
@@ -0,0 +1,88 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class IntegrityReportType {
/// Instantiate a new enum with the provided [value].
const IntegrityReportType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const untrackedFile = IntegrityReportType._(r'untracked_file');
static const missingFile = IntegrityReportType._(r'missing_file');
static const checksumMismatch = IntegrityReportType._(r'checksum_mismatch');
/// List of all possible values in this [enum][IntegrityReportType].
static const values = <IntegrityReportType>[
untrackedFile,
missingFile,
checksumMismatch,
];
static IntegrityReportType? fromJson(dynamic value) => IntegrityReportTypeTypeTransformer().decode(value);
static List<IntegrityReportType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <IntegrityReportType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = IntegrityReportType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [IntegrityReportType] to String,
/// and [decode] dynamic data back to [IntegrityReportType].
class IntegrityReportTypeTypeTransformer {
factory IntegrityReportTypeTypeTransformer() => _instance ??= const IntegrityReportTypeTypeTransformer._();
const IntegrityReportTypeTypeTransformer._();
String encode(IntegrityReportType data) => data.value;
/// Decodes a [dynamic value][data] to a IntegrityReportType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
IntegrityReportType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'untracked_file': return IntegrityReportType.untrackedFile;
case r'missing_file': return IntegrityReportType.missingFile;
case r'checksum_mismatch': return IntegrityReportType.checksumMismatch;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [IntegrityReportTypeTypeTransformer] instance.
static IntegrityReportTypeTypeTransformer? _instance;
}
+30
View File
@@ -79,6 +79,16 @@ class JobName {
static const ocrQueueAll = JobName._(r'OcrQueueAll');
static const ocr = JobName._(r'Ocr');
static const workflowRun = JobName._(r'WorkflowRun');
static const integrityUntrackedFilesQueueAll = JobName._(r'IntegrityUntrackedFilesQueueAll');
static const integrityUntrackedFiles = JobName._(r'IntegrityUntrackedFiles');
static const integrityUntrackedRefresh = JobName._(r'IntegrityUntrackedRefresh');
static const integrityMissingFilesQueueAll = JobName._(r'IntegrityMissingFilesQueueAll');
static const integrityMissingFiles = JobName._(r'IntegrityMissingFiles');
static const integrityMissingFilesRefresh = JobName._(r'IntegrityMissingFilesRefresh');
static const integrityChecksumFiles = JobName._(r'IntegrityChecksumFiles');
static const integrityChecksumFilesRefresh = JobName._(r'IntegrityChecksumFilesRefresh');
static const integrityDeleteReportType = JobName._(r'IntegrityDeleteReportType');
static const integrityDeleteReports = JobName._(r'IntegrityDeleteReports');
/// List of all possible values in this [enum][JobName].
static const values = <JobName>[
@@ -138,6 +148,16 @@ class JobName {
ocrQueueAll,
ocr,
workflowRun,
integrityUntrackedFilesQueueAll,
integrityUntrackedFiles,
integrityUntrackedRefresh,
integrityMissingFilesQueueAll,
integrityMissingFiles,
integrityMissingFilesRefresh,
integrityChecksumFiles,
integrityChecksumFilesRefresh,
integrityDeleteReportType,
integrityDeleteReports,
];
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
@@ -232,6 +252,16 @@ class JobNameTypeTransformer {
case r'OcrQueueAll': return JobName.ocrQueueAll;
case r'Ocr': return JobName.ocr;
case r'WorkflowRun': return JobName.workflowRun;
case r'IntegrityUntrackedFilesQueueAll': return JobName.integrityUntrackedFilesQueueAll;
case r'IntegrityUntrackedFiles': return JobName.integrityUntrackedFiles;
case r'IntegrityUntrackedRefresh': return JobName.integrityUntrackedRefresh;
case r'IntegrityMissingFilesQueueAll': return JobName.integrityMissingFilesQueueAll;
case r'IntegrityMissingFiles': return JobName.integrityMissingFiles;
case r'IntegrityMissingFilesRefresh': return JobName.integrityMissingFilesRefresh;
case r'IntegrityChecksumFiles': return JobName.integrityChecksumFiles;
case r'IntegrityChecksumFilesRefresh': return JobName.integrityChecksumFilesRefresh;
case r'IntegrityDeleteReportType': return JobName.integrityDeleteReportType;
case r'IntegrityDeleteReports': return JobName.integrityDeleteReports;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
+27
View File
@@ -29,6 +29,15 @@ class ManualJobName {
static const memoryCleanup = ManualJobName._(r'memory-cleanup');
static const memoryCreate = ManualJobName._(r'memory-create');
static const backupDatabase = ManualJobName._(r'backup-database');
static const integrityMissingFiles = ManualJobName._(r'integrity-missing-files');
static const integrityUntrackedFiles = ManualJobName._(r'integrity-untracked-files');
static const integrityChecksumMismatch = ManualJobName._(r'integrity-checksum-mismatch');
static const integrityMissingFilesRefresh = ManualJobName._(r'integrity-missing-files-refresh');
static const integrityUntrackedFilesRefresh = ManualJobName._(r'integrity-untracked-files-refresh');
static const integrityChecksumMismatchRefresh = ManualJobName._(r'integrity-checksum-mismatch-refresh');
static const integrityMissingFilesDeleteAll = ManualJobName._(r'integrity-missing-files-delete-all');
static const integrityUntrackedFilesDeleteAll = ManualJobName._(r'integrity-untracked-files-delete-all');
static const integrityChecksumMismatchDeleteAll = ManualJobName._(r'integrity-checksum-mismatch-delete-all');
/// List of all possible values in this [enum][ManualJobName].
static const values = <ManualJobName>[
@@ -38,6 +47,15 @@ class ManualJobName {
memoryCleanup,
memoryCreate,
backupDatabase,
integrityMissingFiles,
integrityUntrackedFiles,
integrityChecksumMismatch,
integrityMissingFilesRefresh,
integrityUntrackedFilesRefresh,
integrityChecksumMismatchRefresh,
integrityMissingFilesDeleteAll,
integrityUntrackedFilesDeleteAll,
integrityChecksumMismatchDeleteAll,
];
static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value);
@@ -82,6 +100,15 @@ class ManualJobNameTypeTransformer {
case r'memory-cleanup': return ManualJobName.memoryCleanup;
case r'memory-create': return ManualJobName.memoryCreate;
case r'backup-database': return ManualJobName.backupDatabase;
case r'integrity-missing-files': return ManualJobName.integrityMissingFiles;
case r'integrity-untracked-files': return ManualJobName.integrityUntrackedFiles;
case r'integrity-checksum-mismatch': return ManualJobName.integrityChecksumMismatch;
case r'integrity-missing-files-refresh': return ManualJobName.integrityMissingFilesRefresh;
case r'integrity-untracked-files-refresh': return ManualJobName.integrityUntrackedFilesRefresh;
case r'integrity-checksum-mismatch-refresh': return ManualJobName.integrityChecksumMismatchRefresh;
case r'integrity-missing-files-delete-all': return ManualJobName.integrityMissingFilesDeleteAll;
case r'integrity-untracked-files-delete-all': return ManualJobName.integrityUntrackedFilesDeleteAll;
case r'integrity-checksum-mismatch-delete-all': return ManualJobName.integrityChecksumMismatchDeleteAll;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
+8 -4
View File
@@ -256,10 +256,16 @@ class MetadataSearchDto {
///
String? previewPath;
/// Filter by rating [1-5], or null for unrated
/// Filter by rating
///
/// Minimum value: -1
/// Maximum value: 5
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
/// Number of results to return
@@ -748,9 +754,7 @@ class MetadataSearchDto {
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
previewPath: mapValueOfType<String>(json, r'previewPath'),
rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
rating: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable
+3
View File
@@ -40,6 +40,7 @@ class QueueName {
static const backupDatabase = QueueName._(r'backupDatabase');
static const ocr = QueueName._(r'ocr');
static const workflow = QueueName._(r'workflow');
static const integrityCheck = QueueName._(r'integrityCheck');
static const editor = QueueName._(r'editor');
/// List of all possible values in this [enum][QueueName].
@@ -61,6 +62,7 @@ class QueueName {
backupDatabase,
ocr,
workflow,
integrityCheck,
editor,
];
@@ -117,6 +119,7 @@ class QueueNameTypeTransformer {
case r'backupDatabase': return QueueName.backupDatabase;
case r'ocr': return QueueName.ocr;
case r'workflow': return QueueName.workflow;
case r'integrityCheck': return QueueName.integrityCheck;
case r'editor': return QueueName.editor;
default:
if (!allowNull) {
+9 -1
View File
@@ -19,6 +19,7 @@ class QueuesResponseLegacyDto {
required this.editor,
required this.faceDetection,
required this.facialRecognition,
required this.integrityCheck,
required this.library_,
required this.metadataExtraction,
required this.migration,
@@ -45,6 +46,8 @@ class QueuesResponseLegacyDto {
QueueResponseLegacyDto facialRecognition;
QueueResponseLegacyDto integrityCheck;
QueueResponseLegacyDto library_;
QueueResponseLegacyDto metadataExtraction;
@@ -77,6 +80,7 @@ class QueuesResponseLegacyDto {
other.editor == editor &&
other.faceDetection == faceDetection &&
other.facialRecognition == facialRecognition &&
other.integrityCheck == integrityCheck &&
other.library_ == library_ &&
other.metadataExtraction == metadataExtraction &&
other.migration == migration &&
@@ -99,6 +103,7 @@ class QueuesResponseLegacyDto {
(editor.hashCode) +
(faceDetection.hashCode) +
(facialRecognition.hashCode) +
(integrityCheck.hashCode) +
(library_.hashCode) +
(metadataExtraction.hashCode) +
(migration.hashCode) +
@@ -113,7 +118,7 @@ class QueuesResponseLegacyDto {
(workflow.hashCode);
@override
String toString() => 'QueuesResponseLegacyDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, editor=$editor, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]';
String toString() => 'QueuesResponseLegacyDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, editor=$editor, faceDetection=$faceDetection, facialRecognition=$facialRecognition, integrityCheck=$integrityCheck, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -123,6 +128,7 @@ class QueuesResponseLegacyDto {
json[r'editor'] = this.editor;
json[r'faceDetection'] = this.faceDetection;
json[r'facialRecognition'] = this.facialRecognition;
json[r'integrityCheck'] = this.integrityCheck;
json[r'library'] = this.library_;
json[r'metadataExtraction'] = this.metadataExtraction;
json[r'migration'] = this.migration;
@@ -153,6 +159,7 @@ class QueuesResponseLegacyDto {
editor: QueueResponseLegacyDto.fromJson(json[r'editor'])!,
faceDetection: QueueResponseLegacyDto.fromJson(json[r'faceDetection'])!,
facialRecognition: QueueResponseLegacyDto.fromJson(json[r'facialRecognition'])!,
integrityCheck: QueueResponseLegacyDto.fromJson(json[r'integrityCheck'])!,
library_: QueueResponseLegacyDto.fromJson(json[r'library'])!,
metadataExtraction: QueueResponseLegacyDto.fromJson(json[r'metadataExtraction'])!,
migration: QueueResponseLegacyDto.fromJson(json[r'migration'])!,
@@ -218,6 +225,7 @@ class QueuesResponseLegacyDto {
'editor',
'faceDetection',
'facialRecognition',
'integrityCheck',
'library',
'metadataExtraction',
'migration',
+8 -4
View File
@@ -159,10 +159,16 @@ class RandomSearchDto {
/// Filter by person IDs
List<String> personIds;
/// Filter by rating [1-5], or null for unrated
/// Filter by rating
///
/// Minimum value: -1
/// Maximum value: 5
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
/// Number of results to return
@@ -559,9 +565,7 @@ class RandomSearchDto {
personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
rating: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable
+8 -4
View File
@@ -199,10 +199,16 @@ class SmartSearchDto {
///
String? queryAssetId;
/// Filter by rating [1-5], or null for unrated
/// Filter by rating
///
/// Minimum value: -1
/// Maximum value: 5
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
/// Number of results to return
@@ -599,9 +605,7 @@ class SmartSearchDto {
: const [],
query: mapValueOfType<String>(json, r'query'),
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
rating: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable
+8 -4
View File
@@ -164,10 +164,16 @@ class StatisticsSearchDto {
/// Filter by person IDs
List<String> personIds;
/// Filter by rating [1-5], or null for unrated
/// Filter by rating
///
/// Minimum value: -1
/// Maximum value: 5
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
/// Filter by state/province name
@@ -489,9 +495,7 @@ class StatisticsSearchDto {
personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
rating: num.parse('${json[r'rating']}'),
state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
-99
View File
@@ -1,99 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncAssetEditDeleteV1 {
/// Returns a new [SyncAssetEditDeleteV1] instance.
SyncAssetEditDeleteV1({
required this.editId,
});
String editId;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncAssetEditDeleteV1 &&
other.editId == editId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(editId.hashCode);
@override
String toString() => 'SyncAssetEditDeleteV1[editId=$editId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'editId'] = this.editId;
return json;
}
/// Returns a new [SyncAssetEditDeleteV1] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncAssetEditDeleteV1? fromJson(dynamic value) {
upgradeDto(value, "SyncAssetEditDeleteV1");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncAssetEditDeleteV1(
editId: mapValueOfType<String>(json, r'editId')!,
);
}
return null;
}
static List<SyncAssetEditDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAssetEditDeleteV1>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAssetEditDeleteV1.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncAssetEditDeleteV1> mapFromJson(dynamic json) {
final map = <String, SyncAssetEditDeleteV1>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncAssetEditDeleteV1.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncAssetEditDeleteV1-objects as value to a dart map
static Map<String, List<SyncAssetEditDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncAssetEditDeleteV1>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncAssetEditDeleteV1.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'editId',
};
}
-131
View File
@@ -1,131 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncAssetEditV1 {
/// Returns a new [SyncAssetEditV1] instance.
SyncAssetEditV1({
required this.action,
required this.assetId,
required this.id,
required this.parameters,
required this.sequence,
});
AssetEditAction action;
String assetId;
String id;
Object parameters;
int sequence;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncAssetEditV1 &&
other.action == action &&
other.assetId == assetId &&
other.id == id &&
other.parameters == parameters &&
other.sequence == sequence;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(assetId.hashCode) +
(id.hashCode) +
(parameters.hashCode) +
(sequence.hashCode);
@override
String toString() => 'SyncAssetEditV1[action=$action, assetId=$assetId, id=$id, parameters=$parameters, sequence=$sequence]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'assetId'] = this.assetId;
json[r'id'] = this.id;
json[r'parameters'] = this.parameters;
json[r'sequence'] = this.sequence;
return json;
}
/// Returns a new [SyncAssetEditV1] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncAssetEditV1? fromJson(dynamic value) {
upgradeDto(value, "SyncAssetEditV1");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncAssetEditV1(
action: AssetEditAction.fromJson(json[r'action'])!,
assetId: mapValueOfType<String>(json, r'assetId')!,
id: mapValueOfType<String>(json, r'id')!,
parameters: mapValueOfType<Object>(json, r'parameters')!,
sequence: mapValueOfType<int>(json, r'sequence')!,
);
}
return null;
}
static List<SyncAssetEditV1> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAssetEditV1>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAssetEditV1.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncAssetEditV1> mapFromJson(dynamic json) {
final map = <String, SyncAssetEditV1>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncAssetEditV1.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncAssetEditV1-objects as value to a dart map
static Map<String, List<SyncAssetEditV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncAssetEditV1>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncAssetEditV1.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'assetId',
'id',
'parameters',
'sequence',
};
}
-6
View File
@@ -29,8 +29,6 @@ class SyncEntityType {
static const assetV1 = SyncEntityType._(r'AssetV1');
static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1');
static const assetExifV1 = SyncEntityType._(r'AssetExifV1');
static const assetEditV1 = SyncEntityType._(r'AssetEditV1');
static const assetEditDeleteV1 = SyncEntityType._(r'AssetEditDeleteV1');
static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1');
static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1');
static const partnerV1 = SyncEntityType._(r'PartnerV1');
@@ -82,8 +80,6 @@ class SyncEntityType {
assetV1,
assetDeleteV1,
assetExifV1,
assetEditV1,
assetEditDeleteV1,
assetMetadataV1,
assetMetadataDeleteV1,
partnerV1,
@@ -170,8 +166,6 @@ class SyncEntityTypeTypeTransformer {
case r'AssetV1': return SyncEntityType.assetV1;
case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1;
case r'AssetExifV1': return SyncEntityType.assetExifV1;
case r'AssetEditV1': return SyncEntityType.assetEditV1;
case r'AssetEditDeleteV1': return SyncEntityType.assetEditDeleteV1;
case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1;
case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1;
case r'PartnerV1': return SyncEntityType.partnerV1;
-3
View File
@@ -30,7 +30,6 @@ class SyncRequestType {
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
static const assetsV1 = SyncRequestType._(r'AssetsV1');
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1');
static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1');
static const authUsersV1 = SyncRequestType._(r'AuthUsersV1');
static const memoriesV1 = SyncRequestType._(r'MemoriesV1');
@@ -55,7 +54,6 @@ class SyncRequestType {
albumAssetExifsV1,
assetsV1,
assetExifsV1,
assetEditsV1,
assetMetadataV1,
authUsersV1,
memoriesV1,
@@ -115,7 +113,6 @@ class SyncRequestTypeTypeTransformer {
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
case r'AssetsV1': return SyncRequestType.assetsV1;
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
case r'AssetEditsV1': return SyncRequestType.assetEditsV1;
case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1;
case r'AuthUsersV1': return SyncRequestType.authUsersV1;
case r'MemoriesV1': return SyncRequestType.memoriesV1;

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