Compare commits

...

76 Commits

Author SHA1 Message Date
Alex The Bot
219f99e516 Version v1.82.0 2023-10-17 01:24:08 +00:00
Jason Rasmussen
1890c0ab6b fix(server): time buckets (#4498) 2023-10-16 13:11:50 -05:00
shenlong
a78e08bac1 fix(mobile): handle asset trash, restore and delete ws events (#4482)
* server: add ASSET_RESTORE ws event

* mobile: handle ASSET_TRASH, ASSET_RESTORE and ASSET_DELETE ws events

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-10-16 13:01:38 -05:00
shenlong
634169235a fix(mobile): reset trashed state for local assets on remote asset deletion (#4501) 2023-10-16 12:26:35 -05:00
shenlong
45ffa65173 feat(web): show trash days info in trash page (#4484) 2023-10-16 11:04:22 -05:00
shenlong
62cb14e4b6 fix(server): include trashed assets in existing assets list (#4483) 2023-10-16 08:47:17 -05:00
cfitzw
3d7e9b7184 chore: ignore default UPLOAD_LOCATION (#4486) 2023-10-15 09:44:20 -04:00
Jason Rasmussen
d2807b8d6a feat(web,server): offline/untracked files admin tool (#4447)
* feat: admin repair orphans tool

* chore: open api

* fix: include upload folder

* fix: bugs

* feat: empty placeholder

* fix: checks

* feat: move buttons to top of page

* feat: styling and clipboard

* styling

* better clicking hitbox

* fix: show title on hover

* feat: download report

* restrict file access to immich related files

* Add description

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2023-10-14 19:12:59 +02:00
GenericGuy
ed386dd12a fix(server): always show people with name, ignore count (#4414) 2023-10-13 20:50:18 -05:00
Jonathan Jogenfors
dadcf49eca fix(server,web): correctly remove metadata from shared links (#4464)
* wip: strip metadata

* fix: authenticate time buckets

* hide detail panel

* fix tests

* fix lint

* add e2e tests

* chore: open api

* fix web compilation error

* feat: test with asset with gps position

* fix: only import fs.promises.cp

* fix: cleanup mapasset

* fix: format

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-14 01:46:30 +00:00
Jason Rasmussen
4a9f58bf9b fix(web): empty placeholders (#4470) 2023-10-13 14:47:31 -04:00
Jason Rasmussen
9d225d3d06 refactor(web): admin layout (#4461) 2023-10-13 15:02:28 +00:00
Jason Rasmussen
268a9c4803 chore(web): move trash to alphabetical order (#4462) 2023-10-13 09:40:53 -05:00
Jason Rasmussen
bddeb03fd5 docs: immich title (#4463) 2023-10-13 09:15:29 -05:00
Jonathan Jogenfors
f0bb50b61a fix(server,cli): don't float promises (#4433)
* fix: don't allow floating promises

* fix: await all promises

* fix: download archives

* fix cli tests

* fix: skip web
2023-10-13 01:22:40 -04:00
Alex
7e9fc4aa97 fix(mobile): remove debug description text 2023-10-12 13:23:41 -05:00
Alexander Groß
e57c926676 feat(mobile): offer the same album sorting options on mobile as on web (#3804)
* Add translations for new album sort options

* Support additional album sort options like on web

* Update generated code

* Fix lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-10-12 13:18:54 -05:00
shenlong
f3b17d8f73 fix(server): include trashed asset during face re-index and cleanup (#4450) 2023-10-12 11:35:49 -05:00
martin
41af76bbe2 fix(web): previous previous route when hiding person (#4452) 2023-10-12 10:31:34 -05:00
Alex
9af5e7838f fix(mobile): description not render on first opening (#4451) 2023-10-12 10:30:56 -05:00
shenlong
5dacea6f74 fix(mobile): asset state change not updated in gallery app bar (#4441) 2023-10-11 21:10:59 -05:00
shenlong
18fcca2884 style(mobile): update video indicator (#4443) 2023-10-11 14:03:04 -05:00
martin
8222327299 fix: people initialization on the merge face selector (#4435) 2023-10-11 06:21:02 -04:00
Alex
eebe9bcd5f fix(docs): production build (#4431) 2023-10-10 21:49:24 -05:00
Jonathan Jogenfors
41befc0948 fix(server): don't publicly reveal user count (#4409)
* fix: don't reveal user count publicly

* fix: mobile and user controller

* fix: update other frontend endpoints

* fix: revert openapi change

* chore: open api

* fix: initialize

* openapi

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-11 02:37:13 +00:00
Daniel Dietzler
09bf1c9175 feat(server): harden move file (#4361)
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-10-10 21:14:44 -05:00
Alex
332a8d80f2 chore: update flutter build version (#4429) 2023-10-10 20:12:01 -05:00
Jonathan Jogenfors
56eb7bf0fc fix(server): improve library scan queuing performance (#4418)
* fix: inline mark asset as offline

* fix: improve log message

* chore: lint

* fix: offline asset algorithm

* fix: use set comparison to check what to import

* fix: only mark new offline files as offline

* fix: compare the correct array

* fix: set default library concurrency to 5

* fix: remove one db call when scanning new files

* chore: remove unused import

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-10-10 18:59:13 -04:00
Jason Rasmussen
99e9c2ada6 fix(server): schema generation (#4424) 2023-10-10 13:16:57 -05:00
Mert
d8ecefaea5 chore(ml): removed vit-b check and st warning (#4422) 2023-10-10 12:26:30 -05:00
martin
b8d6cc1e09 feat(server,web): improve performances in person page (1) (#4387)
* feat: improve performances in people page

* feat: add loadingspinner when searching

* fix: reset people on error

* fix: case insensitive

* feat: better sql query

* fix: reset people list before api request

* fix: format
2023-10-10 09:34:25 -05:00
Nassos Kat
f36c40bc6b feat(web) add hover background to image toolbar icons (#4402)
* feat: add hover background to image toolbar icons

* not use background color when not set

---------

Co-authored-by: katsadim <athanasios.katsadimas@refurbed.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-10 14:34:17 +00:00
Jonathan Jogenfors
83b63ca12e fix: only check external path once (#4419) 2023-10-10 13:44:43 +00:00
Jonathan Jogenfors
f57acc0802 fix(server): add original path and library id index to asset (#4410)
* fix: add original path index

* fix: use libraryId for index, too

* fix: revert openapi change
2023-10-10 09:38:26 -04:00
Alex
29981b1088 chore(mobile): pump photo_manager version (#4412) 2023-10-10 08:20:52 -05:00
debricked[bot]
43f4dac3ad chore(server,web,docs) bulk bump of dependencies with vulnerabilities (#4312)
* chore: bump node version to 20.8

* chore(server,web) upgrade vulnerable dependencies

* fix: revert node change

* fix: commander version

* fix: set country to null if undefined

* fix: set docs module resolution

* fix(web): correct return type of interval

* fix(docs): set node in tsconfig

---------

Co-authored-by: Jonathan Jogenfors <jonathan@jogenfors.se>
Co-authored-by: debricked[bot] <47180885+debricked[bot]@users.noreply.github.com>
2023-10-10 08:31:21 -04:00
Jonas Mayer
2370c9ef41 feat(web): save album sort direction (#4401) 2023-10-09 21:29:04 -05:00
Jason Rasmussen
ebb50476ac fix(server): timeline bucket access for shared links (#4404) 2023-10-09 15:57:36 +00:00
Jason Rasmussen
2ea080cacd refactor: domain repositories (#4403) 2023-10-09 14:25:03 +00:00
Jonathan Jogenfors
b56f22aac3 docs: add troubleshooting info to libraries + minor docs tweaks (#4377)
* docs: add troubleshooting info to libraries

* fix: revert docker-compose
2023-10-09 09:13:47 -05:00
Jason Rasmussen
9033e7f179 refactor(server): filesystem crawl (#4395)
Co-authored-by: Jonathan Jogenfors <jonathan@jogenfors.se>
2023-10-09 08:25:26 -04:00
Jason Rasmussen
9070a361bc chore: organize library imports (#4396) 2023-10-08 22:52:12 -05:00
Jason Rasmussen
d8e66acd02 chore: use force instead of forceRefresh (#4394) 2023-10-08 23:16:13 -04:00
Jason Rasmussen
687d896c63 fix(server): deletable motion assets (#4393) 2023-10-08 20:36:02 -05:00
Daniel Dietzler
0243570c0b fix(server): make system config core singleton (#4392)
* make system config core singleton

* refactor

* fix tests

* chore: fix tests

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-10-09 00:51:03 +00:00
Daniel Dietzler
66ccf298ba fix library deletion actually deleting assets (#4386) 2023-10-07 16:42:08 -04:00
Jason Rasmussen
982dcd7b8d refactor(server): library asset deletion (#4366) 2023-10-07 13:44:10 -04:00
martin
c68702c0a7 fix(web): merge faces (#4383)
* fix(web): merge faces

* pr feedback
2023-10-07 11:04:08 +00:00
Mert
98a7412855 fix(web): dev mode repeatedly optimizing dependencies (#4379)
* added glob for checking imports

* prettier
2023-10-07 05:49:34 -05:00
shenlong
104880a729 feat(web): ws - on_config_update (#4378) 2023-10-07 05:21:05 -05:00
shenlong
c48d4f01dc Fix/mobile remove upload soft limit (#4380)
* fix(mobile): remove soft limit for manual upload

* chore(mobile): remove translation text
2023-10-07 05:17:50 -05:00
Jason Rasmussen
8d5bf93360 test(server): full backend end-to-end testing with microservices (#4225)
* feat: asset e2e with job option

* feat: checkout test assets

* feat: library e2e tests

* fix: use node 21 in e2e

* fix: tests

* fix: use normalized external path

* feat: more external path tests

* chore: use parametrized tests

* chore: remove unused test code

* chore: refactor test asset path

* feat: centralize test app creation

* fix: correct error message for missing assets

* feat: test file formats

* fix: don't compare checksum

* feat: build libvips

* fix: install meson

* fix: use immich test asset repo

* feat: test nikon raw files

* fix: set Z timezone

* feat: test offline library files

* feat: richer metadata tests

* feat: e2e tests in docker

* feat: e2e test with arm64 docker

* fix: manual docker compose run

* fix: remove metadata processor import

* fix: run e2e tests in test.yml

* fix: checkout e2e assets

* fix: typo

* fix: checkout files in app directory

* fix: increase e2e memory

* fix: rm submodules

* fix: revert action name

* test: mark file offline when external path changes

* feat: rename env var to TEST_ENV

* docs: new test procedures

* feat: can run docker e2e tests manually if needed

* chore: use new node 20.8 for e2e

* chore: bump exiftool-vendored

* feat: simplify test launching

* fix: rename env vars to use immich_ prefix

* feat: asset folder is submodule

* chore: cleanup after 20.8 upgrade

* fix: don't log postgres in e2e

* fix: better warning about not running all tests

---------

Co-authored-by: Jonathan Jogenfors <jonathan@jogenfors.se>
2023-10-06 23:32:28 +02:00
Jason Rasmussen
2f9d0a2404 feat: jpe extension (#4374) 2023-10-06 15:55:20 -05:00
Alex
36b21948bf feat(web): enable websocket (#3765)
* send store event to page

* fix format

* add new asset to existing bucket

* format

* debouncing

* format

* load bucket

* feedback

* feat: listen to deletes and auto-subscribe on all asset grid pages

* feat: auto refresh on person thumbnail

* chore: skip upload event for now

* fix: person thumbnail event

* fix merge

* update handleAssetDeletion with websocket communication

* update info box on mount

* fix test

* fix test

* feat: event for trash asset

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-10-06 15:48:11 -05:00
Jonathan Jogenfors
4dffae3f39 fix(server): normalize external path (#4239)
* fix: use normalized external path

* fix: move normalization to user core
2023-10-06 20:47:38 +00:00
Jason Rasmussen
35fa6397ea fix: time buckets (#4358)
* fix: time buckets

* chore: update entity metadata

* fix: set correct localDateTime

* fix: display without timezone shifting

* fix: handle non-utc databases

* fix: scrollbar

* docs: comment how buckets are sorted

* chore: remove test/log

* chore: lint

---------

Co-authored-by: Jonathan Jogenfors <jonathan@jogenfors.se>
2023-10-06 12:12:09 +00:00
shenlong
4a8887f37b feat(server): trash asset (#4015)
* refactor(server): delete assets endpoint

* fix: formatting

* chore: cleanup

* chore: open api

* chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs

* feat: trash an asset

* chore(server): formatting

* chore: open api

* chore: wording

* chore: open-api

* feat(server): add withDeleted to getAssets queries

* WIP: mobile-recycle-bin

* feat(server): recycle-bin to system config

* feat(web): use recycle-bin system config

* chore(server): domain assetcore removed

* chore(server): rename recycle-bin to trash

* chore(web): rename recycle-bin to trash

* chore(server): always send soft deleted assets for getAllByUserId

* chore(web): formatting

* feat(server): permanent delete assets older than trashed period

* feat(web): trash empty placeholder image

* feat(server): empty trash

* feat(web): empty trash

* WIP: mobile-recycle-bin

* refactor(server): empty / restore trash to separate endpoint

* test(server): handle failures

* test(server): fix e2e server-info test

* test(server): deletion test refactor

* feat(mobile): use map settings from server-config to enable / disable map

* feat(mobile): trash asset

* fix(server): operations on assets in trash

* feat(web): show trash statistics

* fix(web): handle trash enabled

* fix(mobile): restore updates from trash

* fix(server): ignore trashed assets for person

* fix(server): add / remove search index when trashed / restored

* chore(web): format

* fix(server): asset service test

* fix(server): include trashed assts for duplicates from uploads

* feat(mobile): no dialog for trash, always dialog for permanent delete

* refactor(mobile): use isar where instead of dart filter

* refactor(mobile): asset provide - handle deletes in single db txn

* chore(mobile): review changes

* feat(web): confirmation before empty trash

* server: review changes

* fix(server): handle library changes

* fix: filter external assets from getting trashed / deleted

* fix(server): empty-bin

* feat: broadcast config update events through ws

* change order of trash button on mobile

* styling

* fix(mobile): do not show trashed toast for local only assets

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 02:01:14 -05:00
Jason Rasmussen
fc93762230 fix: migration efficiency (#4359)
* fix: migration efficiency

* fix: workflow
2023-10-05 12:35:58 -04:00
Jason Rasmussen
ebd3f7f125 fix: dev compose (#4357) 2023-10-05 09:16:23 -05:00
Jonathan Jogenfors
81009c17bf fix(server): use fileCreatedAt in buckets (#4354) 2023-10-05 08:26:42 -05:00
Daniele Ricci
beb92e8ffb fix: open external link in new tab (#4353) 2023-10-05 12:22:14 +02:00
Oliver Wipfli
7b4e36e990 Update nginx config docs (#4346) 2023-10-04 21:06:20 -04:00
Jason Rasmussen
192e950567 fix: use local time for time buckets and improve memories (#4072)
* fix: timezone bucket timezones

* chore: open api

* fix: interpret local time in utc

* fix: tests

* fix: refactor memory lane

* fix(web): use local date in memory viewer

* chore: set localDateTime non-null

* fix: filter out memories from the current year

* wip: move localDateTime to asset

* fix: correct sorting from db

* fix: migration

* fix: web typo

* fix: formatting

* fix: e2e

* chore: localDateTime is non-null

* chore: more non-nulliness

* fix: asset stub

* fix: tests

* fix: use extract and index for day of year

* fix: don't show memories before today

* fix: cleanup

* fix: tests

* fix: only use localtime for tz

* fix: display memories in client timezone

* fix: tests

* fix: svelte tests

* fix: bugs

* chore: open api

---------

Co-authored-by: Jonathan Jogenfors <jonathan@jogenfors.se>
2023-10-04 22:11:11 +00:00
Alex The Bot
126dd45751 Version v1.81.1 2023-10-04 17:53:42 +00:00
Daniel Dietzler
ff331ffad9 fix(server): Offset of random endpoint could be higher than user's asset count (#4342)
* fix offset of all assets with correct ownerId

* (e2e): test if user does not have all assets
2023-10-04 12:51:44 -05:00
Daniel Dietzler
e571880c16 feat(web, mobile): Options to show archived assets in map (#4293)
* Add include archive setting to map on web

* open api

* better naming for web isArchived variable

* add withArchived setting to mobile

* (e2e): tests for mapMarker endpoint and isArchived

* isArchived to mobile

* chore: cleanup test

* chore: optimize e2e

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-10-04 09:51:07 -04:00
markeeisner
e5b4d09827 Remove /etc/timezone volume mount from compose (#4336) 2023-10-04 04:01:00 -05:00
Alex The Bot
81d51fbd7e Version v1.81.0 2023-10-03 20:48:23 +00:00
Alex
02f9b40d67 fix(server): library control doesn't apply to new library from the third row (#4331) 2023-10-03 14:05:14 -05:00
Jason Rasmussen
260a600bbc chore(server): dev compose changes (#4316) 2023-10-03 13:06:08 -05:00
Jason Rasmussen
818005fcb5 fix(server): fallback to local timezone when rendering storage template (#4317) 2023-10-03 13:05:44 -05:00
Daniel Dietzler
e5f704cf3b fix asset upload permissions for shared links (#4325) 2023-10-03 12:36:51 -04:00
Jonathan Jogenfors
e2f1e38472 chore(server,web): bump node version to 20.8 (#4311)
* chore: bump node version to 20.8

* fix: remove node hash
2023-10-03 09:34:35 -05:00
Alex
b3c82d5ba2 fix(server): incorrect video creation date EXIF extraction (#4309)
* fix(server): incorrect video creation date EXIF extraction

* update dependency

* update dependency

* revert

* remove unused code
2023-10-03 08:51:40 -05:00
Jonathan Jogenfors
6d1868a6e0 feat: server containers use host timezone (#4313) 2023-10-02 20:50:27 -05:00
Daniel Dietzler
98db9331d8 fix(server): delete face thumbnails when merging people (#4310)
* new job for person deletion, including face thumbnail deletion

* fix tests, delete files directly instead queueing jobs
2023-10-02 21:15:11 -04:00
390 changed files with 27948 additions and 16793 deletions

View File

@@ -45,7 +45,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.13.3"
flutter-version: "3.13.6"
cache: true
- name: Create the Keystore

View File

@@ -23,7 +23,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.13.3"
flutter-version: "3.13.6"
- name: Install dependencies
run: dart pub get

View File

@@ -13,20 +13,15 @@ jobs:
e2e-tests:
name: Run end-to-end test suites
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./server
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
with:
submodules: "recursive"
- name: Run e2e tests
run: npm run test:e2e
if: ${{ !cancelled() }}
run: docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
doc-tests:
name: Run documentation checks
@@ -149,7 +144,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.13.3"
flutter-version: "3.13.6"
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1
@@ -223,15 +218,27 @@ jobs:
--health-retries 5
ports:
- 5432:5432
defaults:
run:
working-directory: ./server
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
- name: Install server dependencies
run: npm --prefix server ci
run: npm ci
- name: Build the
run: npm run build
- name: Run existing migrations
run: npm --prefix server run typeorm:migrations:run
run: npm run typeorm:migrations:run
- name: Generate new migrations
continue-on-error: true
run: npm --prefix server run typeorm:migrations:generate ./src/infra/migrations/TestMigration
run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
- name: Find file changes
uses: tj-actions/verify-changed-files@v13.1
id: verify-changed-files

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
.idea
docker/upload
docker/library
uploads
coverage

3
.gitmodules vendored
View File

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

View File

@@ -20,7 +20,7 @@ pull-stage:
docker-compose -f ./docker/docker-compose.staging.yml pull
test-e2e:
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
prod:
docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans

View File

@@ -18,6 +18,7 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'prettier/prettier': 0,
},
};

6808
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.82.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.82.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.82.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.82.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -19,9 +19,9 @@ program
)
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
.argument('[paths...]', 'One or more paths to assets to be uploaded')
.action((paths, options) => {
.action(async (paths, options) => {
options.excludePatterns = options.ignore;
new Upload().run(paths, options);
await new Upload().run(paths, options);
});
program
@@ -37,18 +37,18 @@ program
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default(false))
.addOption(new Option('--no-read-only', 'Import files without read-only protection, allowing Immich to manage them'))
.argument('[paths...]', 'One or more paths to assets to be imported')
.action((paths, options) => {
.action(async (paths, options) => {
options.import = true;
options.excludePatterns = options.ignore;
new Upload().run(paths, options);
await new Upload().run(paths, options);
});
program
.command('server-info')
.description('Display server information')
.action(() => {
new ServerInfo().run();
.action(async () => {
await new ServerInfo().run();
});
program
@@ -56,8 +56,8 @@ program
.description('Login using an API key')
.argument('[instanceUrl]')
.argument('[apiKey]')
.action((paths, options) => {
new LoginKey().run(paths, options);
.action(async (paths, options) => {
await new LoginKey().run(paths, options);
});
program.parse(process.argv);

View File

@@ -67,7 +67,7 @@ describe('SessionService', () => {
});
});
it('should create auth file when logged in', async () => {
it.skip('should create auth file when logged in', async () => {
mockfs();
await sessionService.keyLogin('https://test/api', 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg');

View File

@@ -53,7 +53,14 @@ export class SessionService {
if (!fs.existsSync(this.configDir)) {
// Create config folder if it doesn't exist
fs.mkdirSync(this.configDir, { recursive: true });
const created = await fs.promises.mkdir(this.configDir, { recursive: true });
if (!created) {
throw new Error(`Failed to create config folder ${this.configDir}`);
}
}
if (!fs.existsSync(this.configDir)) {
console.error('waah');
}
fs.writeFileSync(this.authPath, yaml.stringify({ instanceUrl, apiKey }));

View File

@@ -1,35 +1,24 @@
import { UploadService } from './upload.service';
import mockfs from 'mock-fs';
import axios from 'axios';
import mockAxios from 'jest-mock-axios';
import FormData from 'form-data';
import { ApiConfiguration } from '../cores/api-configuration';
jest.mock('axios', () => jest.fn());
describe('UploadService', () => {
let uploadService: UploadService;
beforeAll(() => {
// Write a dummy output before mock-fs to prevent some annoying errors
console.log();
});
beforeEach(() => {
const apiConfiguration = new ApiConfiguration('https://example.com/api', 'key');
uploadService = new UploadService(apiConfiguration);
});
it('should upload a single file', async () => {
it('should call axios', async () => {
const data = new FormData();
uploadService.upload(data);
await uploadService.upload(data);
mockAxios.mockResponse();
expect(axios).toHaveBeenCalled();
});
afterEach(() => {
mockfs.restore();
mockAxios.reset();
});
});

View File

@@ -42,21 +42,21 @@ export class UploadService {
};
}
public checkIfAssetAlreadyExists(path: string, checksum: string): Promise<any> {
public checkIfAssetAlreadyExists(path: string, checksum: string) {
this.checkAssetExistenceConfig.data = JSON.stringify({ assets: [{ id: path, checksum: checksum }] });
// TODO: retry on 500 errors?
return axios(this.checkAssetExistenceConfig);
}
public upload(data: FormData): Promise<any> {
public upload(data: FormData) {
this.uploadConfig.data = data;
// TODO: retry on 500 errors?
return axios(this.uploadConfig);
}
public import(data: any): Promise<any> {
public import(data: any) {
this.importConfig.data = data;
// TODO: retry on 500 errors?

View File

@@ -1,16 +0,0 @@
# Database
DB_HOSTNAME=immich-database-test
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=e2e_test
# Redis
REDIS_HOSTNAME=immich-redis-test
# Upload File Config
UPLOAD_LOCATION=./upload
# WEB
VITE_SERVER_ENDPOINT=http://localhost:2283/api
TYPESENSE_ENABLED=false

View File

@@ -11,8 +11,9 @@ services:
command: npm run start:debug immich
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
ports:
- 3001:3001
- 9230:9230
@@ -25,25 +26,6 @@ services:
- database
- typesense
immich-machine-learning:
container_name: immich_machine_learning
image: immich-machine-learning-dev:latest
build:
context: ../machine-learning
dockerfile: Dockerfile
ports:
- 3003:3003
volumes:
- ../machine-learning:/usr/src/app
- model-cache:/cache
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
restart: unless-stopped
immich-microservices:
container_name: immich_microservices
image: immich-microservices:latest
@@ -57,8 +39,9 @@ services:
command: npm run start:debug microservices
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
@@ -94,6 +77,25 @@ services:
depends_on:
- immich-server
immich-machine-learning:
container_name: immich_machine_learning
image: immich-machine-learning-dev:latest
build:
context: ../machine-learning
dockerfile: Dockerfile
ports:
- 3003:3003
volumes:
- ../machine-learning:/usr/src/app
- model-cache:/cache
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
restart: unless-stopped
typesense:
container_name: immich_typesense
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
@@ -103,7 +105,7 @@ services:
# remove this to get debug messages
- GLOG_minloglevel=1
volumes:
- tsdata:/data
- ${UPLOAD_LOCATION}/typesense:/data
redis:
container_name: immich_redis
@@ -119,7 +121,7 @@ services:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports:
- 5432:5432
@@ -141,6 +143,4 @@ services:
restart: unless-stopped
volumes:
pgdata:
model-cache:
tsdata:

View File

@@ -10,6 +10,7 @@ services:
command: ["./start-server.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
@@ -29,7 +30,7 @@ services:
env_file:
- .env
restart: always
immich-microservices:
container_name: immich_microservices
image: immich-microservices:latest
@@ -42,6 +43,7 @@ services:
command: ["./start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:

View File

@@ -1,5 +1,7 @@
version: "3.8"
# Compose file for dockerized end-to-end testing of the backend
services:
immich-server-test:
image: immich-server-test
@@ -8,39 +10,31 @@ services:
dockerfile: Dockerfile
target: builder
command: npm run test:e2e
expose:
- "3000"
volumes:
- ../server:/usr/src/app
- /usr/src/app/node_modules
env_file:
- .env.test
environment:
- NODE_ENV=development
- TYPESENSE_ENABLED=false
- DB_HOSTNAME=immich-database-test
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=e2e_test
- IMMICH_RUN_ALL_TESTS=true
depends_on:
- immich-redis-test
- immich-database-test
networks:
- immich-test-network
immich-redis-test:
container_name: immich-redis-test
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
networks:
- immich-test-network
immich-database-test:
container_name: immich-database-test
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
env_file:
- .env.test
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- /var/lib/postgresql/data
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: e2e_test
networks:
- immich-test-network
logging:
driver: none
networks:
immich-test-network:

View File

@@ -4,9 +4,10 @@ services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "immich" ]
command: ["start.sh", "immich"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
@@ -21,9 +22,10 @@ services:
# extends:
# file: hwaccel.yml
# service: hwaccel
command: [ "start.sh", "microservices" ]
command: ["start.sh", "microservices"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:

View File

@@ -20,22 +20,9 @@ Immich doesn't have two-way synchronization ([yet](https://github.com/immich-app
The initial approach of Immich is to become a backup tool, primarily for mobile device usage. Thus, all the assets must be uploaded from the mobile client. The app was architectured to perform that job well.
### Why does my uploaded photo show up with the wrong date or time in Immich?
When a photo is initially uploaded Immich uses the create date of the file to determine where it belongs in the timeline. After that, background jobs will run that extract [exif metadata](https://en.wikipedia.org/wiki/Exif), including the CreateDate, to provide a more accurate date for the photo. If that is not available it will fallback to the modified date. If you want to ensure your photo has the right date, check the exif metadata before uploading.
If the timezone is incorrect in an uploaded photo, check the `DateTimeOriginal` exif field of the uploaded file. Immich uses the very competent library [exiftool-vendored.js](https://github.com/photostructure/exiftool-vendored.js#dates) to handle timezone parsing, but in some cases (like photos taken with DSLR cameras) it has to fallback on the local timezone. If you are using docker, this fallback will be UTC. (Note that even the photo backup app that can't be named [has the same bug!](https://photo.stackexchange.com/a/126978)) In Immich, it is possible to change this assumed fallback timezone system-wide by setting the timezone in the microservices docker container. You might need to run the "Extract Metadata" job after to effect the change.
As an example, the following modification of `docker-compose.yml` will set the timezone of the microservices container to be `Europe/Stockholm`
```
environment:
- TZ=Europe/Stockholm # <---- Add this line in the microservices config
```
### Why are only photos and not videos being uploaded to Immich?
This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes.
This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes. Also check the disk space of your reverse proxy, in some cases proxies caches requests to disk before passing them on, and if disk space runs out the request fails.
### Why is Immich slow on low-memory systems like the Raspberry Pi?

View File

@@ -19,7 +19,7 @@ Users can deploy a custom reverse proxy that forwards requests to Immich's rever
### Nginx example config
Below is an example config for nginx:
Below is an example config for nginx. Make sure to include `client_max_body_size 50000M;` also in a `http` block in `/etc/nginx/nginx.conf`.
```nginx
server {

View File

@@ -0,0 +1,17 @@
# Testing
## Server
### Unit tests
Unit are run by calling `npm run test` from the `server` directory.
### End to end tests
The backend has an end-to-end test suite that can be called with `npm run test:e2e` from the `server` directory. This will set up a dummy database inside a temporary container and run the tests against it. Setup and teardown is automatically taken care of. That test, however, can not set up all prerequisites to parse file formats, as that is very complex and error-prone. As such, this test excludes some test cases like HEIC file imports. The test suite will also print a friendly warning to remind you that not all tests are being run.
Note that there is a bug in nodejs <20.8 that causes segmentation faults when running these tests. If you run into segfaults, ensure you are using at least version 20.8.
To perform a full e2e test, you need to run e2e tests inside docker. The easiest way to do that is to run `make test-e2e` in the root directory. This will build and start a docker-compose consisting of the server, microservices, and a postgres database. It will then perfom the tests and exit.
If you manually install the dependencies (see the DOCKERFILE) on your development machine, you can also run the full e2e tests manually by setting the `IMMICH_RUN_ALL_TESTS` environment value to true, i.e. `IMMICH_RUN_ALL_TESTS=true npm run test:e2e`.

View File

@@ -42,8 +42,26 @@ Finally, files can be deleted from Immich via the `Remove Offline Files` job. An
External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. If the import paths are edited in a way that an external file is no longer in any import path, it will be removed from the library in the same way a deleted file would. If the file is moved back to an import path, it will be added again as if it was a new file.
### Troubleshooting
Sometimes, an external library will not scan correctly. This can happen if the immich_server or immich_microservices can't access the files. Here are some things to check:
- Is the external path set correctly?
- In the docker-compose file, are the volumes mounted correctly?
- Are the volumes identical between the `server` and `microservices` container?
- Are the import paths set correctly, and do they match the path set in docker-compose file?
- Are the permissions set correctly?
If all else fails, you can always start a shell inside the container and check if the path is accessible. For example, `docker exec -it immich_microservices /bin/bash` will start a bash shell. If your import path, for instance, is `/data/import/photos`, you can check if the files are accessible by running `ls /data/import/photos`. Also check the `immich_server` container in the same way.
### Security Considerations
:::caution
Please read and understand this section before setting external paths, as there are important security considerations.
:::
For security purposes, each Immich user is disallowed to add external files by default. This is to prevent devastating [path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal). An admin can allow individual users to use external path feature via the `external path` setting found in the admin panel. Without the external path restriction, a user can add any image or video file on the Immich host filesystem to be imported into Immich, potentially allowing sensitive data to be accessed. If you are running Immich as root in your Docker setup (which is the default), all external file reads are done with root privileges. This is particularly dangerous if the Immich host is a shared server.
With the `external path` set, a user is restricted to accessing external files to files or directories within that path. The Immich admin should still be careful not set the external path too generously. For example, `user1` wants to read their photos in to `/home/user1`. A lazy admin sets that user's external path to `/home/` since it "gets the job done". However, that user will then be able to read all photos in `/home/user2/private-photos`, too! Please set the external path as specific as possible. If multiple folders must be added, do this using the docker volume mount feature described below.
@@ -59,6 +77,10 @@ Some basic examples:
- `**/Raw/**` will exclude all files in any directory named `Raw`
- `*.(tif,jpg)` will exclude all files with the extension `.tif` or `.jpg`
### Nightly job
There is an automatic job that's run once a day and refreshes all modified files in all libraries as well as cleans up any libraries stuck in deletion.
## Usage
Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add:

View File

@@ -66,6 +66,10 @@ ORDER BY
"users"."email";
```
```sql title="Failed file movements"
SELECT * FROM "move_history";
```
## Users
```sql title="List"

View File

@@ -89,6 +89,7 @@ const config = {
},
},
navbar: {
title: 'IMMICH',
logo: {
alt: 'Immich University Logo',
src: 'img/color-logo.png',

9112
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,10 +17,11 @@
"check": "tsc"
},
"dependencies": {
"@docusaurus/core": "^2.4.1",
"@docusaurus/preset-classic": "^2.4.1",
"@docusaurus/core": "^2.4.3",
"@docusaurus/preset-classic": "^2.4.3",
"@mdx-js/react": "^1.6.22",
"autoprefixer": "^10.4.13",
"classnames": "^2.3.2",
"clsx": "^1.2.1",
"docusaurus-lunr-search": "^2.3.2",
"docusaurus-preset-openapi": "^0.6.3",

View File

@@ -40,3 +40,7 @@ button {
--ifm-background-color: #000000;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
.navbar__brand .navbar__title {
@apply font-immich-title text-2xl font-normal text-immich-primary dark:text-immich-dark-primary;
}

View File

@@ -1,7 +1,9 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": {
"baseUrl": "."
"baseUrl": ".",
"module": "Node16"
}
}

View File

@@ -16,13 +16,6 @@ from ..config import log
from ..schemas import ModelType
from .base import InferenceModel
_ST_TO_JINA_MODEL_NAME = {
"clip-ViT-B-16": "ViT-B-16::openai",
"clip-ViT-B-32": "ViT-B-32::openai",
"clip-ViT-B-32-multilingual-v1": "M-CLIP/XLM-Roberta-Large-Vit-B-32",
"clip-ViT-L-14": "ViT-L-14::openai",
}
class CLIPEncoder(InferenceModel):
_model_type = ModelType.CLIP
@@ -36,11 +29,10 @@ class CLIPEncoder(InferenceModel):
) -> None:
if mode is not None and mode not in ("text", "vision"):
raise ValueError(f"Mode must be 'text', 'vision', or omitted; got '{mode}'")
if "vit-b" not in model_name.lower():
raise ValueError(f"Only ViT-B models are currently supported; got '{model_name}'")
if model_name not in _MODELS:
raise ValueError(f"Unknown model name {model_name}.")
self.mode = mode
jina_model_name = self._get_jina_model_name(model_name)
super().__init__(jina_model_name, cache_dir, **model_kwargs)
super().__init__(model_name, cache_dir, **model_kwargs)
def _download(self) -> None:
models: tuple[tuple[str, str], tuple[str, str]] = _MODELS[self.model_name]
@@ -104,20 +96,6 @@ class CLIPEncoder(InferenceModel):
return outputs[0][0].tolist()
def _get_jina_model_name(self, model_name: str) -> str:
if model_name in _MODELS:
return model_name
elif model_name in _ST_TO_JINA_MODEL_NAME:
log.warn(
(
f"Sentence-Transformer models like '{model_name}' are not supported."
f"Using '{_ST_TO_JINA_MODEL_NAME[model_name]}' instead as it is the best match for '{model_name}'."
),
)
return _ST_TO_JINA_MODEL_NAME[model_name]
else:
raise ValueError(f"Unknown model name {model_name}.")
def _download_model(self, model_name: str, model_md5: str) -> bool:
# downloading logic is adapted from clip-server's CLIPOnnxModel class
download_model(

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.80.0"
version = "1.82.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 104,
"android.injected.version.name" => "1.80.0",
"android.injected.version.code" => 106,
"android.injected.version.name" => "1.82.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -163,7 +163,6 @@
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albums",
@@ -174,6 +173,8 @@
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"library_page_sort_most_recent_photo": "Most recent photo",
"library_page_sort_last_modified": "Last modified",
"login_disabled": "Login has been disabled",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
"login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
@@ -225,6 +226,7 @@
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
"profile_drawer_app_logs": "Logs",
"profile_drawer_trash": "Trash",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_settings": "Settings",
"profile_drawer_sign_out": "Sign Out",
@@ -312,6 +314,7 @@
"map_settings_dialog_title": "Map Settings",
"map_settings_dark_mode": "Dark mode",
"map_settings_only_show_favorites": "Show Favorite Only",
"map_settings_include_show_archived": "Include Archived",
"map_settings_only_relative_range": "Date range",
"map_settings_dialog_cancel": "Cancel",
"map_settings_dialog_save": "Save",
@@ -321,5 +324,17 @@
"map_no_location_permission_title": "Location Permission denied",
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
"map_location_dialog_cancel": "Cancel",
"map_location_dialog_yes": "Yes"
"map_location_dialog_yes": "Yes",
"trash_page_title": "Trash ({})",
"trash_page_info": "Trashed items will be permanently deleted after {} days",
"trash_page_no_assets": "No trashed assets",
"trash_page_delete": "Delete",
"trash_page_delete_all": "Delete All",
"trash_page_restore": "Restore",
"trash_page_restore_all": "Restore All",
"trash_page_select_btn": "Select",
"trash_page_select_assets_btn": "Select assets",
"trash_page_empty_trash_btn": "Empty trash",
"trash_page_empty_trash_dialog_ok": "Ok",
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich"
}

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.80.0"
version_number: "1.82.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -47,6 +47,7 @@ class LibraryPage extends HookConsumerWidget {
useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
List<Album> sortedAlbums() {
// Created.
if (selectedAlbumSortOrder.value == 0) {
return albums
.where((a) => a.isRemote)
@@ -54,6 +55,34 @@ class LibraryPage extends HookConsumerWidget {
.reversed
.toList();
}
// Album title.
if (selectedAlbumSortOrder.value == 1) {
return albums.where((a) => a.isRemote).sortedBy((album) => album.name);
}
// Most recent photo, if unset (e.g. empty album, use modifiedAt / updatedAt).
if (selectedAlbumSortOrder.value == 2) {
return albums
.where((a) => a.isRemote)
.sorted(
(a, b) => a.lastModifiedAssetTimestamp != null &&
b.lastModifiedAssetTimestamp != null
? a.lastModifiedAssetTimestamp!
.compareTo(b.lastModifiedAssetTimestamp!)
: a.modifiedAt.compareTo(b.modifiedAt),
)
.reversed
.toList();
}
// Last modified.
if (selectedAlbumSortOrder.value == 3) {
return albums
.where((a) => a.isRemote)
.sortedBy((album) => album.modifiedAt)
.reversed
.toList();
}
// Fallback: Album title.
return albums.where((a) => a.isRemote).sortedBy((album) => album.name);
}
@@ -61,6 +90,8 @@ class LibraryPage extends HookConsumerWidget {
final options = [
"library_page_sort_created".tr(),
"library_page_sort_title".tr(),
"library_page_sort_most_recent_photo".tr(),
"library_page_sort_last_modified".tr(),
];
return PopupMenuButton(

View File

@@ -17,6 +17,7 @@ final archiveProvider = StreamProvider<RenderList>((ref) async* {
.ownerIdEqualToAnyChecksum(user.isarId)
.filter()
.isArchivedEqualTo(true)
.isTrashedEqualTo(false)
.sortByFileCreatedAt();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =

View File

@@ -364,7 +364,18 @@ class ExifBottomSheet extends HookConsumerWidget {
children: [
buildDragHeader(),
buildDate(),
if (asset.isRemote) DescriptionInput(asset: asset),
assetWithExif.when(
data: (data) => DescriptionInput(asset: data),
error: (error, stackTrace) => Icon(
Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor,
),
loading: () => const SizedBox(
width: 75,
height: 75,
child: CircularProgressIndicator.adaptive(),
),
),
const SizedBox(height: 8.0),
buildLocation(),
SizedBox(height: hasCoordinates(exifInfo) ? 16.0 : 0.0),

View File

@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
class TopControlAppBar extends HookConsumerWidget {
const TopControlAppBar({
@@ -14,7 +15,6 @@ class TopControlAppBar extends HookConsumerWidget {
required this.isPlayingMotionVideo,
required this.onFavorite,
required this.onUploadPressed,
required this.isFavorite,
}) : super(key: key);
final Asset asset;
@@ -23,19 +23,19 @@ class TopControlAppBar extends HookConsumerWidget {
final VoidCallback? onDownloadPressed;
final VoidCallback onToggleMotionVideo;
final VoidCallback onAddToAlbumPressed;
final VoidCallback? onFavorite;
final Function(Asset) onFavorite;
final bool isPlayingMotionVideo;
final bool isFavorite;
@override
Widget build(BuildContext context, WidgetRef ref) {
const double iconSize = 22.0;
final a = ref.watch(assetWatcher(asset)).value ?? asset;
Widget buildFavoriteButton() {
Widget buildFavoriteButton(a) {
return IconButton(
onPressed: onFavorite,
onPressed: () => onFavorite(a),
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
a.isFavorite ? Icons.favorite : Icons.favorite_border,
color: Colors.grey[200],
),
);
@@ -123,7 +123,7 @@ class TopControlAppBar extends HookConsumerWidget {
size: iconSize,
),
actions: [
if (asset.isRemote) buildFavoriteButton(),
if (asset.isRemote) buildFavoriteButton(a),
if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
if (asset.isRemote && !asset.isLocal) buildDownloadButton(),

View File

@@ -6,6 +6,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
@@ -19,11 +20,14 @@ import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
import 'package:immich_mobile/shared/cache/original_image_provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart';
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
@@ -67,6 +71,12 @@ class GalleryViewerPage extends HookConsumerWidget {
final header = {"Authorization": authToken};
final currentIndex = useState(initialIndex);
final currentAsset = loadAsset(currentIndex.value);
final isTrashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final navStack = AutoRouter.of(context).stackData;
final isFromTrash = isTrashEnabled &&
navStack.length > 2 &&
navStack.elementAt(navStack.length - 2).name == TrashRoute.name;
Asset asset() => currentAsset;
@@ -161,25 +171,47 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
void handleDelete(Asset deleteAsset) {
void handleDelete(Asset deleteAsset) async {
Future<bool> onDelete(bool force) async {
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
{deleteAsset},
force: force,
);
if (isDeleted) {
if (totalAssets == 1) {
// Handle only one asset
AutoRouter.of(context).pop();
} else {
// Go to next page otherwise
controller.nextPage(
duration: const Duration(milliseconds: 100),
curve: Curves.fastLinearToSlowEaseIn,
);
}
}
return isDeleted;
}
// Asset is trashed
if (isTrashEnabled && !isFromTrash) {
final isDeleted = await onDelete(false);
// Can only trash assets stored in server. Local assets are always permanently removed for now
if (context.mounted && isDeleted && deleteAsset.isRemote) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'Asset trashed',
gravity: ToastGravity.BOTTOM,
);
}
return;
}
// Asset is permanently removed
showDialog(
context: context,
builder: (BuildContext _) {
return DeleteDialog(
onDelete: () {
if (totalAssets == 1) {
// Handle only one asset
AutoRouter.of(context).pop();
} else {
// Go to next page otherwise
controller.nextPage(
duration: const Duration(milliseconds: 100),
curve: Curves.fastLinearToSlowEaseIn,
);
}
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
},
);
return DeleteDialog(onDelete: () => onDelete(true));
},
);
}
@@ -265,10 +297,8 @@ class GalleryViewerPage extends HookConsumerWidget {
child: TopControlAppBar(
isPlayingMotionVideo: isPlayingMotionVideo.value,
asset: asset(),
isFavorite: asset().isFavorite,
onMoreInfoPressed: showInfo,
onFavorite:
asset().isRemote ? () => toggleFavorite(asset()) : null,
onFavorite: toggleFavorite,
onUploadPressed:
asset().isLocal ? () => handleUpload(asset()) : null,
onDownloadPressed: asset().isLocal

View File

@@ -81,7 +81,9 @@ class BackupControllerPage extends HookConsumerWidget {
context: context,
msg: "Deleting ${assets.length} assets on the server...",
);
await ref.read(assetProvider.notifier).deleteAssets(assets);
await ref
.read(assetProvider.notifier)
.deleteAssets(assets, force: true);
ImmichToast.show(
context: context,
msg: "Deleted ${assets.length} assets on the server. "

View File

@@ -17,6 +17,7 @@ final favoriteAssetsProvider = StreamProvider<RenderList>((ref) async* {
.ownerIdEqualToAnyChecksum(user.isarId)
.filter()
.isFavoriteEqualTo(true)
.isTrashedEqualTo(false)
.sortByFileCreatedAt();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =

View File

@@ -60,6 +60,39 @@ class ThumbnailImage extends StatelessWidget {
}
}
Widget buildVideoIcon() {
final minutes = asset.duration.inMinutes;
final durationString = asset.duration.toString();
return Positioned(
top: 5,
right: 5,
child: Row(
children: [
Text(
minutes > 59
? durationString.substring(0, 7) // h:mm:ss
: minutes > 0
? durationString.substring(2, 7) // mm:ss
: durationString.substring(3, 7), // m:ss
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 3,
),
const Icon(
Icons.play_circle_fill_rounded,
color: Colors.white,
size: 18,
),
],
),
);
}
Widget buildImage() {
final image = SizedBox(
width: 300,
@@ -162,26 +195,7 @@ class ThumbnailImage extends StatelessWidget {
size: 18,
),
),
if (!asset.isImage)
Positioned(
top: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
),
if (!asset.isImage) buildVideoIcon(),
],
),
);

View File

@@ -7,6 +7,7 @@ import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/shared/models/album.dart';
@@ -43,6 +44,8 @@ class ControlBottomAppBar extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
var hasRemote = selectionAssetState == AssetState.remote;
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
Widget renderActionButtons() {
return Row(
@@ -70,14 +73,20 @@ class ControlBottomAppBar extends ConsumerWidget {
iconData: Icons.delete_outline_rounded,
label: "control_bottom_app_bar_delete".tr(),
onPressed: enabled
? () => showDialog(
context: context,
builder: (BuildContext context) {
return DeleteDialog(
onDelete: onDelete,
);
},
)
? () {
if (!trashEnabled) {
showDialog(
context: context,
builder: (BuildContext context) {
return DeleteDialog(
onDelete: onDelete,
);
},
);
} else {
onDelete();
}
}
: null,
),
if (!hasRemote)

View File

@@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dar
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
class ProfileDrawer extends HookConsumerWidget {
@@ -16,6 +17,9 @@ class ProfileDrawer extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
buildSignOutButton() {
return ListTile(
leading: SizedBox(
@@ -91,6 +95,29 @@ class ProfileDrawer extends HookConsumerWidget {
);
}
buildTrashButton() {
return ListTile(
leading: SizedBox(
height: double.infinity,
child: Icon(
Icons.delete_rounded,
color: Theme.of(context).textTheme.labelMedium?.color,
size: 20,
),
),
title: Text(
"profile_drawer_trash",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(fontWeight: FontWeight.bold),
).tr(),
onTap: () {
AutoRouter.of(context).push(const TrashRoute());
},
);
}
return Drawer(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
@@ -105,6 +132,7 @@ class ProfileDrawer extends HookConsumerWidget {
const ProfileDrawerHeader(),
buildSettingButton(),
buildAppLogButton(),
if (trashEnabled) buildTrashButton(),
buildSignOutButton(),
],
),

View File

@@ -43,6 +43,8 @@ class HomePage extends HookConsumerWidget {
final sharedAlbums = ref.watch(sharedAlbumProvider);
final albumService = ref.watch(albumServiceProvider);
final currentUser = ref.watch(currentUserProvider);
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final tipOneOpacity = useState(0.0);
final refreshCount = useState(0);
@@ -139,30 +141,34 @@ class HomePage extends HookConsumerWidget {
void onDelete() async {
processing.value = true;
try {
await ref.read(assetProvider.notifier).deleteAssets(selection.value);
await ref
.read(assetProvider.notifier)
.deleteAssets(selection.value, force: !trashEnabled);
final hasRemote = selection.value.any((a) => a.isRemote);
final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
final trashOrRemoved =
!trashEnabled ? 'deleted permanently' : 'trashed';
if (hasRemote) {
ImmichToast.show(
context: context,
msg: '${selection.value.length} $assetOrAssets $trashOrRemoved',
gravity: ToastGravity.BOTTOM,
);
}
selectionEnabledHook.value = false;
} finally {
processing.value = false;
}
}
void onUpload() async {
void onUpload() {
processing.value = true;
selectionEnabledHook.value = false;
try {
final Set<Asset> assets = selection.value;
if (assets.length > 30) {
ImmichToast.show(
context: context,
msg: 'home_page_upload_err_limit'.tr(),
gravity: ToastGravity.BOTTOM,
);
} else {
processing.value = false;
selectionEnabledHook.value = false;
await ref
.read(manualUploadProvider.notifier)
.uploadAssets(context, assets);
}
ref
.read(manualUploadProvider.notifier)
.uploadAssets(context, selection.value);
} finally {
processing.value = false;
}

View File

@@ -1,29 +1,33 @@
class MapState {
final bool isDarkTheme;
final bool showFavoriteOnly;
final bool includeArchived;
final int relativeTime;
MapState({
this.isDarkTheme = false,
this.showFavoriteOnly = false,
this.includeArchived = false,
this.relativeTime = 0,
});
MapState copyWith({
bool? isDarkTheme,
bool? showFavoriteOnly,
bool? includeArchived,
int? relativeTime,
}) {
return MapState(
isDarkTheme: isDarkTheme ?? this.isDarkTheme,
showFavoriteOnly: showFavoriteOnly ?? this.showFavoriteOnly,
includeArchived: includeArchived ?? this.includeArchived,
relativeTime: relativeTime ?? this.relativeTime,
);
}
@override
String toString() {
return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime)';
return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime, includeArchived: $includeArchived)';
}
@override
@@ -33,13 +37,15 @@ class MapState {
return other is MapState &&
other.isDarkTheme == isDarkTheme &&
other.showFavoriteOnly == showFavoriteOnly &&
other.relativeTime == relativeTime;
other.relativeTime == relativeTime &&
other.includeArchived == includeArchived;
}
@override
int get hashCode {
return isDarkTheme.hashCode ^
showFavoriteOnly.hashCode ^
relativeTime.hashCode;
relativeTime.hashCode ^
includeArchived.hashCode;
}
}

View File

@@ -10,6 +10,7 @@ final mapMarkersProvider =
final mapState = ref.read(mapStateNotifier);
DateTime? fileCreatedAfter;
bool? isFavorite;
bool? isIncludeArchived;
if (mapState.relativeTime != 0) {
fileCreatedAfter =
@@ -20,8 +21,13 @@ final mapMarkersProvider =
isFavorite = true;
}
if (!mapState.includeArchived) {
isIncludeArchived = false;
}
final markers = await service.getMapMarkers(
isFavorite: isFavorite,
withArchived: isIncludeArchived,
fileCreatedAfter: fileCreatedAfter,
);

View File

@@ -11,6 +11,8 @@ class MapStateNotifier extends StateNotifier<MapState> {
.getSetting<bool>(AppSettingsEnum.mapThemeMode),
showFavoriteOnly: appSettingsProvider
.getSetting<bool>(AppSettingsEnum.mapShowFavoriteOnly),
includeArchived: appSettingsProvider
.getSetting<bool>(AppSettingsEnum.mapIncludeArchived),
relativeTime: appSettingsProvider
.getSetting<int>(AppSettingsEnum.mapRelativeDate),
),
@@ -31,11 +33,19 @@ class MapStateNotifier extends StateNotifier<MapState> {
void switchFavoriteOnly(bool isFavoriteOnly) {
appSettingsProvider.setSetting(
AppSettingsEnum.mapShowFavoriteOnly,
appSettingsProvider,
isFavoriteOnly,
);
state = state.copyWith(showFavoriteOnly: isFavoriteOnly);
}
void switchIncludeArchived(bool isIncludeArchived) {
appSettingsProvider.setSetting(
AppSettingsEnum.mapIncludeArchived,
isIncludeArchived,
);
state = state.copyWith(includeArchived: isIncludeArchived);
}
void setRelativeTime(int relativeTime) {
appSettingsProvider.setSetting(
AppSettingsEnum.mapRelativeDate,

View File

@@ -23,12 +23,14 @@ class MapSerivce {
Future<List<MapMarkerResponseDto>> getMapMarkers({
bool? isFavorite,
bool? withArchived,
DateTime? fileCreatedAfter,
DateTime? fileCreatedBefore,
}) async {
try {
final markers = await _apiService.assetApi.getMapMarkers(
isFavorite: isFavorite,
isArchived: withArchived,
fileCreatedAfter: fileCreatedAfter,
fileCreatedBefore: fileCreatedBefore,
);

View File

@@ -13,6 +13,7 @@ class MapSettingsDialog extends HookConsumerWidget {
final mapSettings = ref.read(mapStateNotifier);
final isDarkMode = useState(mapSettings.isDarkTheme);
final showFavoriteOnly = useState(mapSettings.showFavoriteOnly);
final showIncludeArchived = useState(mapSettings.includeArchived);
final showRelativeDate = useState(mapSettings.relativeTime);
final ThemeData theme = Theme.of(context);
@@ -48,6 +49,22 @@ class MapSettingsDialog extends HookConsumerWidget {
);
}
Widget buildIncludeArchivedSetting() {
return SwitchListTile.adaptive(
value: showIncludeArchived.value,
onChanged: (value) {
showIncludeArchived.value = value;
},
activeColor: theme.primaryColor,
dense: true,
title: Text(
"map_settings_include_show_archived".tr(),
style:
theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
),
);
}
Widget buildDateRangeSetting() {
final now = DateTime.now();
return DropdownMenu(
@@ -127,6 +144,8 @@ class MapSettingsDialog extends HookConsumerWidget {
mapSettingsNotifier.switchTheme(isDarkMode.value);
mapSettingsNotifier.switchFavoriteOnly(showFavoriteOnly.value);
mapSettingsNotifier.setRelativeTime(showRelativeDate.value);
mapSettingsNotifier
.switchIncludeArchived(showIncludeArchived.value);
Navigator.of(context).pop();
},
style: TextButton.styleFrom(
@@ -166,6 +185,7 @@ class MapSettingsDialog extends HookConsumerWidget {
children: [
buildMapThemeSetting(),
buildFavoriteOnlySetting(),
buildIncludeArchivedSetting(),
const SizedBox(
height: 10,
),

View File

@@ -155,7 +155,8 @@ class MapPageState extends ConsumerState<MapPage> {
ref.listen(mapStateNotifier, (previous, next) {
bool shouldRefetch =
previous?.showFavoriteOnly != next.showFavoriteOnly ||
previous?.relativeTime != next.relativeTime;
previous?.relativeTime != next.relativeTime ||
previous?.includeArchived != next.includeArchived;
if (shouldRefetch) {
refetchMarkers.value = shouldRefetch;
ref.invalidate(mapMarkersProvider);

View File

@@ -22,9 +22,9 @@ class MemoryService {
Future<List<Memory>?> getMemoryLane() async {
try {
final now = DateTime.now();
final beginningOfDate = DateTime(now.year, now.month, now.day);
final data = await _apiService.assetApi.getMemoryLane(
beginningOfDate,
now.day,
now.month,
);
if (data == null) {

View File

@@ -48,6 +48,7 @@ enum AppSettingsEnum<T> {
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
mapThemeMode<bool>(StoreKey.mapThemeMode, null, false),
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
;

View File

@@ -0,0 +1,125 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/trash/services/trash.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
class TrashNotifier extends StateNotifier<bool> {
final Isar _db;
final Ref _ref;
final TrashService _trashService;
final _log = Logger('TrashNotifier');
TrashNotifier(
this._trashService,
this._db,
this._ref,
) : super(false);
Future<void> emptyTrash() async {
try {
final user = _ref.read(currentUserProvider);
if (user == null) {
return;
}
await _trashService.emptyTrash();
final dbIds = await _db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(user.isarId)
.isTrashedEqualTo(true)
.idProperty()
.findAll();
await _db.writeTxn(() async {
await _db.exifInfos.deleteAll(dbIds);
await _db.assets.deleteAll(dbIds);
});
} catch (error, stack) {
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
}
}
Future<bool> restoreAssets(Iterable<Asset> assetList) async {
try {
final result = await _trashService.restoreAssets(assetList);
if (result) {
final remoteAssets = assetList.where((a) => a.isRemote).toList();
final updatedAssets = remoteAssets.map((e) {
e.isTrashed = false;
return e;
}).toList();
await _db.writeTxn(() async {
await _db.assets.putAll(updatedAssets);
});
return true;
}
} catch (error, stack) {
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
}
return false;
}
Future<void> restoreTrash() async {
try {
final user = _ref.read(currentUserProvider);
if (user == null) {
return;
}
await _trashService.restoreTrash();
final assets = await _db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(user.isarId)
.isTrashedEqualTo(true)
.findAll();
final updatedAssets = assets.map((e) {
e.isTrashed = false;
return e;
}).toList();
await _db.writeTxn(() async {
await _db.assets.putAll(updatedAssets);
});
} catch (error, stack) {
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
}
}
}
final trashProvider = StateNotifierProvider<TrashNotifier, bool>((ref) {
return TrashNotifier(
ref.watch(trashServiceProvider),
ref.watch(dbProvider),
ref,
);
});
final trashedAssetsProvider = StreamProvider<RenderList>((ref) async* {
final user = ref.read(currentUserProvider);
if (user == null) return;
final query = ref
.watch(dbProvider)
.assets
.filter()
.ownerIdEqualTo(user.isarId)
.isTrashedEqualTo(true)
.sortByFileCreatedAt();
const groupBy = GroupAssetsBy.none;
yield await RenderList.fromQuery(query, groupBy);
await for (final _ in query.watchLazy()) {
yield await RenderList.fromQuery(query, groupBy);
}
});

View File

@@ -0,0 +1,48 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final trashServiceProvider = Provider<TrashService>((ref) {
return TrashService(
ref.watch(apiServiceProvider),
);
});
class TrashService {
final _log = Logger("TrashService");
final ApiService _apiService;
TrashService(this._apiService);
Future<bool> restoreAssets(Iterable<Asset> assetList) async {
try {
List<String> remoteIds =
assetList.where((a) => a.isRemote).map((e) => e.remoteId!).toList();
await _apiService.assetApi.restoreAssets(BulkIdsDto(ids: remoteIds));
return true;
} catch (error, stack) {
_log.severe("Cannot restore assets ${error.toString()}", error, stack);
return false;
}
}
Future<void> emptyTrash() async {
try {
await _apiService.assetApi.emptyTrash();
} catch (error, stack) {
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
}
}
Future<void> restoreTrash() async {
try {
await _apiService.assetApi.restoreTrash();
} catch (error, stack) {
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
}
}
}

View File

@@ -0,0 +1,276 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/trash/providers/trashed_asset.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class TrashPage extends HookConsumerWidget {
const TrashPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final trashedAssets = ref.watch(trashedAssetsProvider);
final trashDays =
ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays));
final selectionEnabledHook = useState(false);
final selection = useState(<Asset>{});
final processing = useState(false);
void selectionListener(
bool multiselect,
Set<Asset> selectedAssets,
) {
selectionEnabledHook.value = multiselect;
selection.value = selectedAssets;
}
onEmptyTrash() async {
processing.value = true;
await ref.read(trashProvider.notifier).emptyTrash();
processing.value = false;
selectionEnabledHook.value = false;
if (context.mounted) {
ImmichToast.show(
context: context,
msg: 'Emptied trash',
gravity: ToastGravity.BOTTOM,
);
}
}
handleEmptyTrash() async {
await showDialog(
context: context,
builder: (context) => ConfirmDialog(
onOk: () => onEmptyTrash(),
title: "trash_page_empty_trash_btn".tr(),
ok: "trash_page_empty_trash_dialog_ok".tr(),
content: "trash_page_empty_trash_dialog_content".tr(),
),
);
}
Future<void> onPermanentlyDelete() async {
processing.value = true;
try {
if (selection.value.isNotEmpty) {
await ref
.read(assetProvider.notifier)
.deleteAssets(selection.value, force: true);
final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
if (context.mounted) {
ImmichToast.show(
context: context,
msg:
'${selection.value.length} $assetOrAssets deleted permanently',
gravity: ToastGravity.BOTTOM,
);
}
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
handlePermanentDelete() async {
await showDialog(
context: context,
builder: (context) => DeleteDialog(
onDelete: () => onPermanentlyDelete(),
),
);
}
Future<void> handleRestoreAll() async {
processing.value = true;
await ref.read(trashProvider.notifier).restoreTrash();
processing.value = false;
selectionEnabledHook.value = false;
}
Future<void> handleRestore() async {
processing.value = true;
try {
if (selection.value.isNotEmpty) {
final result = await ref
.read(trashProvider.notifier)
.restoreAssets(selection.value);
final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
if (result && context.mounted) {
ImmichToast.show(
context: context,
msg:
'${selection.value.length} $assetOrAssets restored successfully',
gravity: ToastGravity.BOTTOM,
);
}
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
String getAppBarTitle(String count) {
if (selectionEnabledHook.value) {
return selection.value.isNotEmpty
? "${selection.value.length}"
: "trash_page_select_assets_btn".tr();
}
return 'trash_page_title'.tr(args: [count]);
}
AppBar buildAppBar(String count) {
return AppBar(
leading: IconButton(
onPressed: !selectionEnabledHook.value
? () => AutoRouter.of(context).pop()
: () {
selectionEnabledHook.value = false;
selection.value = {};
},
icon: !selectionEnabledHook.value
? const Icon(Icons.arrow_back_ios_rounded)
: const Icon(Icons.close_rounded),
),
centerTitle: !selectionEnabledHook.value,
automaticallyImplyLeading: false,
title: Text(getAppBarTitle(count)),
actions: <Widget>[
if (!selectionEnabledHook.value)
PopupMenuButton<void Function()>(
itemBuilder: (context) {
return [
PopupMenuItem(
value: () => selectionEnabledHook.value = true,
child: const Text('trash_page_select_btn').tr(),
),
PopupMenuItem(
value: handleEmptyTrash,
child: const Text('trash_page_empty_trash_btn').tr(),
),
];
},
onSelected: (fn) => fn(),
),
],
);
}
Widget buildBottomBar() {
return SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 64,
child: Container(
color: Theme.of(context).canvasColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton.icon(
icon: Icon(
Icons.delete_forever,
color: Colors.red[400],
),
label: Text(
selection.value.isEmpty
? 'trash_page_delete_all'.tr()
: 'trash_page_delete'.tr(),
style: TextStyle(
fontSize: 14,
color: Colors.red[400],
fontWeight: FontWeight.bold,
),
),
onPressed: processing.value
? null
: selection.value.isEmpty
? handleEmptyTrash
: handlePermanentDelete,
),
TextButton.icon(
icon: const Icon(
Icons.history_rounded,
),
label: Text(
selection.value.isEmpty
? 'trash_page_restore_all'.tr()
: 'trash_page_restore'.tr(),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
onPressed: processing.value
? null
: selection.value.isEmpty
? handleRestoreAll
: handleRestore,
),
],
),
),
),
),
);
}
return trashedAssets.when(
loading: () => Scaffold(
appBar: buildAppBar("?"),
body: const Center(child: CircularProgressIndicator()),
),
error: (error, stackTrace) => Scaffold(
appBar: buildAppBar("!"),
body: Center(child: Text(error.toString())),
),
data: (data) => Scaffold(
appBar: buildAppBar(data.totalAssets.toString()),
body: data.isEmpty
? Center(
child: Text('trash_page_no_assets'.tr()),
)
: Stack(
children: [
SafeArea(
child: ImmichAssetGrid(
renderList: data,
listener: selectionListener,
selectionActive: selectionEnabledHook.value,
showMultiSelectIndicator: false,
topWidget: Padding(
padding: const EdgeInsets.only(
top: 24,
bottom: 24,
left: 12,
right: 12,
),
child: const Text(
"trash_page_info",
).tr(args: ["$trashDays"]),
),
),
),
if (selectionEnabledHook.value) buildBottomBar(),
if (processing.value)
const Center(child: ImmichLoadingIndicator()),
],
),
),
);
}
}

View File

@@ -28,6 +28,7 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart';
import 'package:immich_mobile/modules/login/views/login_page.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart';
import 'package:immich_mobile/modules/trash/views/trash_page.dart';
import 'package:immich_mobile/modules/search/views/all_motion_videos_page.dart';
import 'package:immich_mobile/modules/search/views/all_people_page.dart';
import 'package:immich_mobile/modules/search/views/all_videos_page.dart';
@@ -155,6 +156,7 @@ part 'router.gr.dart';
AutoRoute(page: MemoryPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: MapPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: AlbumOptionsPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: TrashPage, guards: [AuthGuard, DuplicateGuard]),
],
)
class AppRouter extends _$AppRouter {

View File

@@ -312,6 +312,12 @@ class _$AppRouter extends RootStackRouter {
),
);
},
TrashRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const TrashPage(),
);
},
HomeRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
@@ -624,6 +630,14 @@ class _$AppRouter extends RootStackRouter {
duplicateGuard,
],
),
RouteConfig(
TrashRoute.name,
path: '/trash-page',
guards: [
authGuard,
duplicateGuard,
],
),
];
}
@@ -1394,6 +1408,18 @@ class AlbumOptionsRouteArgs {
}
}
/// generated route for
/// [TrashPage]
class TrashRoute extends PageRouteInfo<void> {
const TrashRoute()
: super(
TrashRoute.name,
path: '/trash-page',
);
static const String name = 'TrashRoute';
}
/// generated route for
/// [HomePage]
class HomeRoute extends PageRouteInfo<void> {

View File

@@ -18,6 +18,7 @@ class Album {
required this.name,
required this.createdAt,
required this.modifiedAt,
this.lastModifiedAssetTimestamp,
required this.shared,
});
@@ -29,6 +30,7 @@ class Album {
String name;
DateTime createdAt;
DateTime modifiedAt;
DateTime? lastModifiedAssetTimestamp;
bool shared;
final IsarLink<User> owner = IsarLink<User>();
final IsarLink<Asset> thumbnail = IsarLink<Asset>();
@@ -83,12 +85,21 @@ class Album {
@override
bool operator ==(other) {
if (other is! Album) return false;
final lastModifiedAssetTimestampIsSetAndEqual =
lastModifiedAssetTimestamp != null &&
other.lastModifiedAssetTimestamp != null
? lastModifiedAssetTimestamp!
.isAtSameMomentAs(other.lastModifiedAssetTimestamp!)
: true;
return id == other.id &&
remoteId == other.remoteId &&
localId == other.localId &&
name == other.name &&
createdAt.isAtSameMomentAs(other.createdAt) &&
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
lastModifiedAssetTimestampIsSetAndEqual &&
shared == other.shared &&
owner.value == other.owner.value &&
thumbnail.value == other.thumbnail.value &&
@@ -105,6 +116,7 @@ class Album {
name.hashCode ^
createdAt.hashCode ^
modifiedAt.hashCode ^
lastModifiedAssetTimestamp.hashCode ^
shared.hashCode ^
owner.value.hashCode ^
thumbnail.value.hashCode ^
@@ -130,6 +142,7 @@ class Album {
name: dto.albumName,
createdAt: dto.createdAt,
modifiedAt: dto.updatedAt,
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
shared: dto.shared,
);
a.owner.value = await db.users.getById(dto.ownerId);

View File

@@ -22,28 +22,33 @@ const AlbumSchema = CollectionSchema(
name: r'createdAt',
type: IsarType.dateTime,
),
r'localId': PropertySchema(
r'lastModifiedAssetTimestamp': PropertySchema(
id: 1,
name: r'lastModifiedAssetTimestamp',
type: IsarType.dateTime,
),
r'localId': PropertySchema(
id: 2,
name: r'localId',
type: IsarType.string,
),
r'modifiedAt': PropertySchema(
id: 2,
id: 3,
name: r'modifiedAt',
type: IsarType.dateTime,
),
r'name': PropertySchema(
id: 3,
id: 4,
name: r'name',
type: IsarType.string,
),
r'remoteId': PropertySchema(
id: 4,
id: 5,
name: r'remoteId',
type: IsarType.string,
),
r'shared': PropertySchema(
id: 5,
id: 6,
name: r'shared',
type: IsarType.bool,
)
@@ -143,11 +148,12 @@ void _albumSerialize(
Map<Type, List<int>> allOffsets,
) {
writer.writeDateTime(offsets[0], object.createdAt);
writer.writeString(offsets[1], object.localId);
writer.writeDateTime(offsets[2], object.modifiedAt);
writer.writeString(offsets[3], object.name);
writer.writeString(offsets[4], object.remoteId);
writer.writeBool(offsets[5], object.shared);
writer.writeDateTime(offsets[1], object.lastModifiedAssetTimestamp);
writer.writeString(offsets[2], object.localId);
writer.writeDateTime(offsets[3], object.modifiedAt);
writer.writeString(offsets[4], object.name);
writer.writeString(offsets[5], object.remoteId);
writer.writeBool(offsets[6], object.shared);
}
Album _albumDeserialize(
@@ -158,11 +164,12 @@ Album _albumDeserialize(
) {
final object = Album(
createdAt: reader.readDateTime(offsets[0]),
localId: reader.readStringOrNull(offsets[1]),
modifiedAt: reader.readDateTime(offsets[2]),
name: reader.readString(offsets[3]),
remoteId: reader.readStringOrNull(offsets[4]),
shared: reader.readBool(offsets[5]),
lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[1]),
localId: reader.readStringOrNull(offsets[2]),
modifiedAt: reader.readDateTime(offsets[3]),
name: reader.readString(offsets[4]),
remoteId: reader.readStringOrNull(offsets[5]),
shared: reader.readBool(offsets[6]),
);
object.id = id;
return object;
@@ -178,14 +185,16 @@ P _albumDeserializeProp<P>(
case 0:
return (reader.readDateTime(offset)) as P;
case 1:
return (reader.readStringOrNull(offset)) as P;
return (reader.readDateTimeOrNull(offset)) as P;
case 2:
return (reader.readDateTime(offset)) as P;
case 3:
return (reader.readString(offset)) as P;
case 4:
return (reader.readStringOrNull(offset)) as P;
case 3:
return (reader.readDateTime(offset)) as P;
case 4:
return (reader.readString(offset)) as P;
case 5:
return (reader.readStringOrNull(offset)) as P;
case 6:
return (reader.readBool(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -520,6 +529,80 @@ extension AlbumQueryFilter on QueryBuilder<Album, Album, QFilterCondition> {
});
}
QueryBuilder<Album, Album, QAfterFilterCondition>
lastModifiedAssetTimestampIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'lastModifiedAssetTimestamp',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition>
lastModifiedAssetTimestampIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'lastModifiedAssetTimestamp',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition>
lastModifiedAssetTimestampEqualTo(DateTime? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'lastModifiedAssetTimestamp',
value: value,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition>
lastModifiedAssetTimestampGreaterThan(
DateTime? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'lastModifiedAssetTimestamp',
value: value,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition>
lastModifiedAssetTimestampLessThan(
DateTime? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'lastModifiedAssetTimestamp',
value: value,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition>
lastModifiedAssetTimestampBetween(
DateTime? lower,
DateTime? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'lastModifiedAssetTimestamp',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> localIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@@ -1158,6 +1241,19 @@ extension AlbumQuerySortBy on QueryBuilder<Album, Album, QSortBy> {
});
}
QueryBuilder<Album, Album, QAfterSortBy> sortByLastModifiedAssetTimestamp() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc);
});
}
QueryBuilder<Album, Album, QAfterSortBy>
sortByLastModifiedAssetTimestampDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> sortByLocalId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'localId', Sort.asc);
@@ -1244,6 +1340,19 @@ extension AlbumQuerySortThenBy on QueryBuilder<Album, Album, QSortThenBy> {
});
}
QueryBuilder<Album, Album, QAfterSortBy> thenByLastModifiedAssetTimestamp() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc);
});
}
QueryBuilder<Album, Album, QAfterSortBy>
thenByLastModifiedAssetTimestampDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> thenByLocalId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'localId', Sort.asc);
@@ -1312,6 +1421,12 @@ extension AlbumQueryWhereDistinct on QueryBuilder<Album, Album, QDistinct> {
});
}
QueryBuilder<Album, Album, QDistinct> distinctByLastModifiedAssetTimestamp() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'lastModifiedAssetTimestamp');
});
}
QueryBuilder<Album, Album, QDistinct> distinctByLocalId(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@@ -1359,6 +1474,13 @@ extension AlbumQueryProperty on QueryBuilder<Album, Album, QQueryProperty> {
});
}
QueryBuilder<Album, DateTime?, QQueryOperations>
lastModifiedAssetTimestampProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'lastModifiedAssetTimestamp');
});
}
QueryBuilder<Album, String?, QQueryOperations> localIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'localId');

View File

@@ -30,7 +30,8 @@ class Asset {
exifInfo =
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
isFavorite = remote.isFavorite,
isArchived = remote.isArchived;
isArchived = remote.isArchived,
isTrashed = remote.isTrashed;
Asset.local(AssetEntity local, List<int> hash)
: localId = local.id,
@@ -45,6 +46,7 @@ class Asset {
updatedAt = local.modifiedDateTime,
isFavorite = local.isFavorite,
isArchived = false,
isTrashed = false,
fileCreatedAt = local.createDateTime {
if (fileCreatedAt.year == 1970) {
fileCreatedAt = fileModifiedAt;
@@ -74,6 +76,7 @@ class Asset {
this.exifInfo,
required this.isFavorite,
required this.isArchived,
required this.isTrashed,
});
@ignore
@@ -138,6 +141,8 @@ class Asset {
bool isArchived;
bool isTrashed;
@ignore
ExifInfo? exifInfo;
@@ -194,7 +199,8 @@ class Asset {
livePhotoVideoId == other.livePhotoVideoId &&
isFavorite == other.isFavorite &&
isLocal == other.isLocal &&
isArchived == other.isArchived;
isArchived == other.isArchived &&
isTrashed == other.isTrashed;
}
@override
@@ -216,7 +222,8 @@ class Asset {
livePhotoVideoId.hashCode ^
isFavorite.hashCode ^
isLocal.hashCode ^
isArchived.hashCode;
isArchived.hashCode ^
isTrashed.hashCode;
/// Returns `true` if this [Asset] can updated with values from parameter [a]
bool canUpdate(Asset a) {
@@ -229,8 +236,9 @@ class Asset {
width == null && a.width != null ||
height == null && a.height != null ||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
!isRemote && a.isRemote && isFavorite != a.isFavorite ||
!isRemote && a.isRemote && isArchived != a.isArchived;
isFavorite != a.isFavorite ||
isArchived != a.isArchived ||
isTrashed != a.isTrashed;
}
/// Returns a new [Asset] with values from this and merged & updated with [a]
@@ -261,6 +269,7 @@ class Asset {
livePhotoVideoId: livePhotoVideoId,
isFavorite: isFavorite,
isArchived: isArchived,
isTrashed: isTrashed,
);
}
} else {
@@ -275,6 +284,7 @@ class Asset {
// isFavorite + isArchived are not set by device-only assets
isFavorite: a.isFavorite,
isArchived: a.isArchived,
isTrashed: a.isTrashed,
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
);
} else {
@@ -306,6 +316,7 @@ class Asset {
String? livePhotoVideoId,
bool? isFavorite,
bool? isArchived,
bool? isTrashed,
ExifInfo? exifInfo,
}) =>
Asset(
@@ -325,6 +336,7 @@ class Asset {
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
isFavorite: isFavorite ?? this.isFavorite,
isArchived: isArchived ?? this.isArchived,
isTrashed: isTrashed ?? this.isTrashed,
exifInfo: exifInfo ?? this.exifInfo,
);
@@ -378,7 +390,8 @@ class Asset {
"storage": "$storage",
"width": ${width ?? "N/A"},
"height": ${height ?? "N/A"},
"isArchived": $isArchived
"isArchived": $isArchived,
"isTrashed": $isTrashed,
}""";
}
}

View File

@@ -57,39 +57,44 @@ const AssetSchema = CollectionSchema(
name: r'isFavorite',
type: IsarType.bool,
),
r'livePhotoVideoId': PropertySchema(
r'isTrashed': PropertySchema(
id: 8,
name: r'isTrashed',
type: IsarType.bool,
),
r'livePhotoVideoId': PropertySchema(
id: 9,
name: r'livePhotoVideoId',
type: IsarType.string,
),
r'localId': PropertySchema(
id: 9,
id: 10,
name: r'localId',
type: IsarType.string,
),
r'ownerId': PropertySchema(
id: 10,
id: 11,
name: r'ownerId',
type: IsarType.long,
),
r'remoteId': PropertySchema(
id: 11,
id: 12,
name: r'remoteId',
type: IsarType.string,
),
r'type': PropertySchema(
id: 12,
id: 13,
name: r'type',
type: IsarType.byte,
enumMap: _AssettypeEnumValueMap,
),
r'updatedAt': PropertySchema(
id: 13,
id: 14,
name: r'updatedAt',
type: IsarType.dateTime,
),
r'width': PropertySchema(
id: 14,
id: 15,
name: r'width',
type: IsarType.int,
)
@@ -196,13 +201,14 @@ void _assetSerialize(
writer.writeInt(offsets[5], object.height);
writer.writeBool(offsets[6], object.isArchived);
writer.writeBool(offsets[7], object.isFavorite);
writer.writeString(offsets[8], object.livePhotoVideoId);
writer.writeString(offsets[9], object.localId);
writer.writeLong(offsets[10], object.ownerId);
writer.writeString(offsets[11], object.remoteId);
writer.writeByte(offsets[12], object.type.index);
writer.writeDateTime(offsets[13], object.updatedAt);
writer.writeInt(offsets[14], object.width);
writer.writeBool(offsets[8], object.isTrashed);
writer.writeString(offsets[9], object.livePhotoVideoId);
writer.writeString(offsets[10], object.localId);
writer.writeLong(offsets[11], object.ownerId);
writer.writeString(offsets[12], object.remoteId);
writer.writeByte(offsets[13], object.type.index);
writer.writeDateTime(offsets[14], object.updatedAt);
writer.writeInt(offsets[15], object.width);
}
Asset _assetDeserialize(
@@ -221,14 +227,15 @@ Asset _assetDeserialize(
id: id,
isArchived: reader.readBool(offsets[6]),
isFavorite: reader.readBool(offsets[7]),
livePhotoVideoId: reader.readStringOrNull(offsets[8]),
localId: reader.readStringOrNull(offsets[9]),
ownerId: reader.readLong(offsets[10]),
remoteId: reader.readStringOrNull(offsets[11]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[12])] ??
isTrashed: reader.readBool(offsets[8]),
livePhotoVideoId: reader.readStringOrNull(offsets[9]),
localId: reader.readStringOrNull(offsets[10]),
ownerId: reader.readLong(offsets[11]),
remoteId: reader.readStringOrNull(offsets[12]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[13])] ??
AssetType.other,
updatedAt: reader.readDateTime(offsets[13]),
width: reader.readIntOrNull(offsets[14]),
updatedAt: reader.readDateTime(offsets[14]),
width: reader.readIntOrNull(offsets[15]),
);
return object;
}
@@ -257,19 +264,21 @@ P _assetDeserializeProp<P>(
case 7:
return (reader.readBool(offset)) as P;
case 8:
return (reader.readStringOrNull(offset)) as P;
return (reader.readBool(offset)) as P;
case 9:
return (reader.readStringOrNull(offset)) as P;
case 10:
return (reader.readLong(offset)) as P;
case 11:
return (reader.readStringOrNull(offset)) as P;
case 11:
return (reader.readLong(offset)) as P;
case 12:
return (reader.readStringOrNull(offset)) as P;
case 13:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P;
case 13:
return (reader.readDateTime(offset)) as P;
case 14:
return (reader.readDateTime(offset)) as P;
case 15:
return (reader.readIntOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -1290,6 +1299,16 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isTrashed',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> livePhotoVideoIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@@ -2058,6 +2077,18 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByLivePhotoVideoId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
@@ -2252,6 +2283,18 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByLivePhotoVideoId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
@@ -2388,6 +2431,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isTrashed');
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByLivePhotoVideoId(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@@ -2490,6 +2539,12 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
});
}
QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isTrashed');
});
}
QueryBuilder<Asset, String?, QQueryOperations> livePhotoVideoIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'livePhotoVideoId');

View File

@@ -179,6 +179,7 @@ enum StoreKey<T> {
mapShowFavoriteOnly<bool>(118, type: bool),
mapRelativeDate<int>(119, type: int),
selfSignedCert<bool>(120, type: bool),
mapIncludeArchived<bool>(121, type: bool),
;
const StoreKey(

View File

@@ -15,7 +15,6 @@ import 'package:immich_mobile/shared/services/user.service.dart';
import 'package:immich_mobile/utils/db.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:photo_manager/photo_manager.dart';
class AssetNotifier extends StateNotifier<bool> {
@@ -92,23 +91,45 @@ class AssetNotifier extends StateNotifier<bool> {
await _syncService.syncNewAssetToDb(newAsset);
}
Future<void> deleteAssets(Iterable<Asset> deleteAssets) async {
Future<bool> deleteAssets(
Iterable<Asset> deleteAssets, {
bool? force = false,
}) async {
_deleteInProgress = true;
state = true;
try {
final localDeleted = await _deleteLocalAssets(deleteAssets);
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
final remoteDeleted = await _deleteRemoteAssets(deleteAssets, force);
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
final dbIds = deleteAssets.map((e) => e.id).toList();
List<Asset>? assetsToUpdate;
// Local only assets are permanently deleted for now. So always remove them from db
final dbIds = deleteAssets
.where((a) => a.isLocal && !a.isRemote)
.map((e) => e.id)
.toList();
if (force == null || !force) {
assetsToUpdate = remoteDeleted.map((e) {
e.isTrashed = true;
return e;
}).toList();
} else {
// Add all remote assets to be deleted from isar as since they are permanently deleted
dbIds.addAll(remoteDeleted.map((e) => e.id));
}
await _db.writeTxn(() async {
if (assetsToUpdate != null) {
await _db.assets.putAll(assetsToUpdate);
}
await _db.exifInfos.deleteAll(dbIds);
await _db.assets.deleteAll(dbIds);
});
return true;
}
} finally {
_deleteInProgress = false;
state = false;
}
return false;
}
Future<List<String>> _deleteLocalAssets(
@@ -127,15 +148,14 @@ class AssetNotifier extends StateNotifier<bool> {
return [];
}
Future<Iterable<String>> _deleteRemoteAssets(
Future<Iterable<Asset>> _deleteRemoteAssets(
Iterable<Asset> assetsToDelete,
bool? force,
) async {
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
final List<DeleteAssetResponseDto> deleteAssetResult =
await _assetService.deleteAssets(remote) ?? [];
return deleteAssetResult
.where((a) => a.status == DeleteAssetStatus.SUCCESS)
.map((a) => a.id);
final isSuccess = await _assetService.deleteAssets(remote, force: force);
return isSuccess ? remote : [];
}
Future<void> toggleFavorite(List<Asset> assets, bool status) async {
@@ -180,6 +200,12 @@ final assetDetailProvider =
}
});
final assetWatcher =
StreamProvider.autoDispose.family<Asset?, Asset>((ref, asset) {
final db = ref.watch(dbProvider);
return db.assets.watchObject(asset.id, fireImmediately: true);
});
final assetsProvider =
StreamProvider.family<RenderList, int?>((ref, userId) async* {
if (userId == null) return;
@@ -190,6 +216,7 @@ final assetsProvider =
.ownerIdEqualToAnyChecksum(userId)
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.sortByFileCreatedAtDesc();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =
@@ -210,6 +237,7 @@ final remoteAssetsProvider =
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(userId)
.isTrashedEqualTo(false)
.sortByFileCreatedAtDesc();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =

View File

@@ -26,12 +26,15 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
search: true,
sidecar: true,
tagImage: true,
trash: true,
reverseGeocoding: true,
),
serverConfig: ServerConfigDto(
loginPageMessage: "",
mapTileUrl: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
oauthButtonText: "",
trashDays: 30,
isInitialized: false,
),
isVersionMismatch: false,
versionMismatchErrorMessage: "",

View File

@@ -6,26 +6,44 @@ import 'package:immich_mobile/modules/login/providers/authentication.provider.da
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:socket_io_client/socket_io_client.dart';
enum PendingAction {
assetDelete,
}
class PendingChange {
final PendingAction action;
final dynamic value;
const PendingChange(this.action, this.value);
}
class WebsocketState {
final Socket? socket;
final bool isConnected;
final List<PendingChange> pendingChanges;
WebsocketState({
this.socket,
required this.isConnected,
required this.pendingChanges,
});
WebsocketState copyWith({
Socket? socket,
bool? isConnected,
List<PendingChange>? pendingChanges,
}) {
return WebsocketState(
socket: socket ?? this.socket,
isConnected: isConnected ?? this.isConnected,
pendingChanges: pendingChanges ?? this.pendingChanges,
);
}
@@ -48,10 +66,17 @@ class WebsocketState {
class WebsocketNotifier extends StateNotifier<WebsocketState> {
WebsocketNotifier(this.ref)
: super(WebsocketState(socket: null, isConnected: false));
: super(
WebsocketState(socket: null, isConnected: false, pendingChanges: []),
) {
debounce = Debounce(
const Duration(milliseconds: 500),
);
}
final log = Logger('WebsocketNotifier');
final Ref ref;
late final Debounce debounce;
connect() {
var authenticationState = ref.read(authenticationProvider);
@@ -78,20 +103,36 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.onConnect((_) {
debugPrint("Established Websocket Connection");
state = WebsocketState(isConnected: true, socket: socket);
state = WebsocketState(
isConnected: true,
socket: socket,
pendingChanges: state.pendingChanges,
);
});
socket.onDisconnect((_) {
debugPrint("Disconnect to Websocket Connection");
state = WebsocketState(isConnected: false, socket: null);
state = WebsocketState(
isConnected: false,
socket: null,
pendingChanges: state.pendingChanges,
);
});
socket.on('error', (errorMessage) {
log.severe("Websocket Error - $errorMessage");
state = WebsocketState(isConnected: false, socket: null);
state = WebsocketState(
isConnected: false,
socket: null,
pendingChanges: state.pendingChanges,
);
});
socket.on('on_upload_success', _handleOnUploadSuccess);
socket.on('on_config_update', _handleOnConfigUpdate);
socket.on('on_asset_delete', _handleOnAssetDelete);
socket.on('on_asset_trash', _handleServerUpdates);
socket.on('on_asset_restore', _handleServerUpdates);
} catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
}
@@ -104,7 +145,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
var socket = state.socket?.disconnect();
if (socket?.disconnected == true) {
state = WebsocketState(isConnected: false, socket: null);
state = WebsocketState(
isConnected: false,
socket: null,
pendingChanges: state.pendingChanges,
);
}
}
@@ -118,6 +163,29 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
}
addPendingChange(PendingAction action, dynamic value) {
state = state.copyWith(
pendingChanges: [...state.pendingChanges, PendingChange(action, value)],
);
}
handlePendingChanges() {
final deleteChanges = state.pendingChanges
.where((c) => c.action == PendingAction.assetDelete)
.toList();
if (deleteChanges.isNotEmpty) {
List<String> remoteIds = deleteChanges
.map((a) => jsonDecode(a.value.toString()).toString())
.toList();
ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
state = state.copyWith(
pendingChanges: state.pendingChanges
.where((c) => c.action != PendingAction.assetDelete)
.toList(),
);
}
}
_handleOnUploadSuccess(dynamic data) {
final jsonString = jsonDecode(data.toString());
final dto = AssetResponseDto.fromJson(jsonString);
@@ -126,6 +194,21 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
}
}
_handleOnConfigUpdate(dynamic data) {
ref.read(serverInfoProvider.notifier).getServerFeatures();
ref.read(serverInfoProvider.notifier).getServerConfig();
}
// Refresh updated assets
_handleServerUpdates(dynamic data) {
ref.read(assetProvider.notifier).getAllAsset();
}
_handleOnAssetDelete(dynamic data) {
addPendingChange(PendingAction.assetDelete, data);
debounce(handlePendingChanges);
}
}
final websocketProvider =

View File

@@ -64,7 +64,9 @@ class AssetService {
Future<List<Asset>?> _getRemoteAssets(User user) async {
try {
final List<AssetResponseDto>? assets =
await _apiService.assetApi.getAllAssets(userId: user.id);
await _apiService.assetApi.getAllAssets(
userId: user.id,
);
if (assets == null) {
return null;
} else if (assets.isNotEmpty && assets.first.ownerId != user.id) {
@@ -84,9 +86,10 @@ class AssetService {
}
}
Future<List<DeleteAssetResponseDto>?> deleteAssets(
Iterable<Asset> deleteAssets,
) async {
Future<bool> deleteAssets(
Iterable<Asset> deleteAssets, {
bool? force = false,
}) async {
try {
final List<String> payload = [];
@@ -94,12 +97,17 @@ class AssetService {
payload.add(asset.remoteId!);
}
return await _apiService.assetApi
.deleteAsset(DeleteAssetDto(ids: payload));
await _apiService.assetApi.deleteAssets(
AssetBulkDeleteDto(
ids: payload,
force: force,
),
);
return true;
} catch (error, stack) {
log.severe("Error deleteAssets ${error.toString()}", error, stack);
return null;
}
return false;
}
/// Loads the exif information from the database. If there is none, loads

View File

@@ -153,7 +153,7 @@ class SyncService {
if (toUpsert == null || toDelete == null) return null;
try {
if (toDelete.isNotEmpty) {
await _handleRemoteAssetRemoval(toDelete);
await handleRemoteAssetRemoval(toDelete);
}
if (toUpsert.isNotEmpty) {
final (_, updated) = await _linkWithExistingFromDb(toUpsert);
@@ -171,13 +171,14 @@ class SyncService {
}
/// Deletes remote-only assets, updates merged assets to be local-only
Future<void> _handleRemoteAssetRemoval(List<String> idsToDelete) {
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
return _db.writeTxn(() async {
await _db.assets.remote(idsToDelete).filter().localIdIsNull().deleteAll();
final onlyLocal = await _db.assets.remote(idsToDelete).findAll();
if (onlyLocal.isNotEmpty) {
for (final Asset a in onlyLocal) {
a.remoteId = null;
a.isTrashed = false;
}
await _db.assets.putAll(onlyLocal);
}
@@ -282,6 +283,9 @@ class SyncService {
if (!_hasAlbumResponseDtoChanged(dto, album)) {
return false;
}
// loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp,
// i.e. it will always be null. Save it here.
final originalDto = dto;
dto = await loadDetails(dto);
if (dto.assetCount != dto.assets.length) {
return false;
@@ -321,6 +325,7 @@ class SyncService {
album.name = dto.albumName;
album.shared = dto.shared;
album.modifiedAt = dto.updatedAt;
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
album.thumbnail.value = await _db.assets
.where()
@@ -808,5 +813,13 @@ bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
dto.shared != a.shared ||
dto.sharedUsers.length != a.sharedUsers.length ||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt);
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
(dto.lastModifiedAssetTimestamp == null &&
a.lastModifiedAssetTimestamp != null) ||
(dto.lastModifiedAssetTimestamp != null &&
a.lastModifiedAssetTimestamp == null) ||
(dto.lastModifiedAssetTimestamp != null &&
a.lastModifiedAssetTimestamp != null &&
!dto.lastModifiedAssetTimestamp!
.isAtSameMomentAs(a.lastModifiedAssetTimestamp!));
}

View File

@@ -15,6 +15,7 @@ doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md
doc/AllJobStatusResponseDto.md
doc/AssetApi.md
doc/AssetBulkDeleteDto.md
doc/AssetBulkUpdateDto.md
doc/AssetBulkUploadCheckDto.md
doc/AssetBulkUploadCheckItem.md
@@ -53,14 +54,16 @@ doc/CreateTagDto.md
doc/CreateUserDto.md
doc/CuratedLocationsResponseDto.md
doc/CuratedObjectsResponseDto.md
doc/DeleteAssetDto.md
doc/DeleteAssetResponseDto.md
doc/DeleteAssetStatus.md
doc/DownloadArchiveInfo.md
doc/DownloadInfoDto.md
doc/DownloadResponseDto.md
doc/EntityType.md
doc/ExifResponseDto.md
doc/FileChecksumDto.md
doc/FileChecksumResponseDto.md
doc/FileReportDto.md
doc/FileReportFixDto.md
doc/FileReportItemDto.md
doc/ImportAssetDto.md
doc/JobApi.md
doc/JobCommand.md
@@ -86,6 +89,8 @@ doc/OAuthCallbackDto.md
doc/OAuthConfigDto.md
doc/OAuthConfigResponseDto.md
doc/PartnerApi.md
doc/PathEntityType.md
doc/PathType.md
doc/PeopleResponseDto.md
doc/PeopleUpdateDto.md
doc/PeopleUpdateItem.md
@@ -131,6 +136,7 @@ doc/SystemConfigReverseGeocodingDto.md
doc/SystemConfigStorageTemplateDto.md
doc/SystemConfigTemplateStorageOptionDto.md
doc/SystemConfigThumbnailDto.md
doc/SystemConfigTrashDto.md
doc/TagApi.md
doc/TagResponseDto.md
doc/TagTypeEnum.md
@@ -186,6 +192,7 @@ lib/model/api_key_create_dto.dart
lib/model/api_key_create_response_dto.dart
lib/model/api_key_response_dto.dart
lib/model/api_key_update_dto.dart
lib/model/asset_bulk_delete_dto.dart
lib/model/asset_bulk_update_dto.dart
lib/model/asset_bulk_upload_check_dto.dart
lib/model/asset_bulk_upload_check_item.dart
@@ -222,14 +229,16 @@ lib/model/create_tag_dto.dart
lib/model/create_user_dto.dart
lib/model/curated_locations_response_dto.dart
lib/model/curated_objects_response_dto.dart
lib/model/delete_asset_dto.dart
lib/model/delete_asset_response_dto.dart
lib/model/delete_asset_status.dart
lib/model/download_archive_info.dart
lib/model/download_info_dto.dart
lib/model/download_response_dto.dart
lib/model/entity_type.dart
lib/model/exif_response_dto.dart
lib/model/file_checksum_dto.dart
lib/model/file_checksum_response_dto.dart
lib/model/file_report_dto.dart
lib/model/file_report_fix_dto.dart
lib/model/file_report_item_dto.dart
lib/model/import_asset_dto.dart
lib/model/job_command.dart
lib/model/job_command_dto.dart
@@ -251,6 +260,8 @@ lib/model/o_auth_authorize_response_dto.dart
lib/model/o_auth_callback_dto.dart
lib/model/o_auth_config_dto.dart
lib/model/o_auth_config_response_dto.dart
lib/model/path_entity_type.dart
lib/model/path_type.dart
lib/model/people_response_dto.dart
lib/model/people_update_dto.dart
lib/model/people_update_item.dart
@@ -291,6 +302,7 @@ lib/model/system_config_reverse_geocoding_dto.dart
lib/model/system_config_storage_template_dto.dart
lib/model/system_config_template_storage_option_dto.dart
lib/model/system_config_thumbnail_dto.dart
lib/model/system_config_trash_dto.dart
lib/model/tag_response_dto.dart
lib/model/tag_type_enum.dart
lib/model/thumbnail_format.dart
@@ -322,6 +334,7 @@ test/api_key_create_response_dto_test.dart
test/api_key_response_dto_test.dart
test/api_key_update_dto_test.dart
test/asset_api_test.dart
test/asset_bulk_delete_dto_test.dart
test/asset_bulk_update_dto_test.dart
test/asset_bulk_upload_check_dto_test.dart
test/asset_bulk_upload_check_item_test.dart
@@ -360,14 +373,16 @@ test/create_tag_dto_test.dart
test/create_user_dto_test.dart
test/curated_locations_response_dto_test.dart
test/curated_objects_response_dto_test.dart
test/delete_asset_dto_test.dart
test/delete_asset_response_dto_test.dart
test/delete_asset_status_test.dart
test/download_archive_info_test.dart
test/download_info_dto_test.dart
test/download_response_dto_test.dart
test/entity_type_test.dart
test/exif_response_dto_test.dart
test/file_checksum_dto_test.dart
test/file_checksum_response_dto_test.dart
test/file_report_dto_test.dart
test/file_report_fix_dto_test.dart
test/file_report_item_dto_test.dart
test/import_asset_dto_test.dart
test/job_api_test.dart
test/job_command_dto_test.dart
@@ -393,6 +408,8 @@ test/o_auth_callback_dto_test.dart
test/o_auth_config_dto_test.dart
test/o_auth_config_response_dto_test.dart
test/partner_api_test.dart
test/path_entity_type_test.dart
test/path_type_test.dart
test/people_response_dto_test.dart
test/people_update_dto_test.dart
test/people_update_item_test.dart
@@ -438,6 +455,7 @@ test/system_config_reverse_geocoding_dto_test.dart
test/system_config_storage_template_dto_test.dart
test/system_config_template_storage_option_dto_test.dart
test/system_config_thumbnail_dto_test.dart
test/system_config_trash_dto_test.dart
test/tag_api_test.dart
test/tag_response_dto_test.dart
test/tag_type_enum_test.dart

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.80.0
- API version: 1.82.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -90,9 +90,10 @@ Class | Method | HTTP request | Description
*AssetApi* | [**bulkUploadCheck**](doc//AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check |
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset |
*AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset |
*AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download/archive |
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} |
*AssetApi* | [**emptyTrash**](doc//AssetApi.md#emptytrash) | **POST** /asset/trash/empty |
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} |
*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
@@ -108,13 +109,18 @@ Class | Method | HTTP request | Description
*AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
*AssetApi* | [**importFile**](doc//AssetApi.md#importfile) | **POST** /asset/import |
*AssetApi* | [**restoreAssets**](doc//AssetApi.md#restoreassets) | **POST** /asset/restore |
*AssetApi* | [**restoreTrash**](doc//AssetApi.md#restoretrash) | **POST** /asset/trash/restore |
*AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs |
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} |
*AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset |
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report |
*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
*AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
*AuthenticationApi* | [**getAuthDevices**](doc//AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
@@ -151,6 +157,7 @@ Class | Method | HTTP request | Description
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
@@ -201,6 +208,7 @@ Class | Method | HTTP request | Description
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
- [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md)
- [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md)
- [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md)
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
@@ -237,14 +245,16 @@ Class | Method | HTTP request | Description
- [CreateUserDto](doc//CreateUserDto.md)
- [CuratedLocationsResponseDto](doc//CuratedLocationsResponseDto.md)
- [CuratedObjectsResponseDto](doc//CuratedObjectsResponseDto.md)
- [DeleteAssetDto](doc//DeleteAssetDto.md)
- [DeleteAssetResponseDto](doc//DeleteAssetResponseDto.md)
- [DeleteAssetStatus](doc//DeleteAssetStatus.md)
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
- [DownloadInfoDto](doc//DownloadInfoDto.md)
- [DownloadResponseDto](doc//DownloadResponseDto.md)
- [EntityType](doc//EntityType.md)
- [ExifResponseDto](doc//ExifResponseDto.md)
- [FileChecksumDto](doc//FileChecksumDto.md)
- [FileChecksumResponseDto](doc//FileChecksumResponseDto.md)
- [FileReportDto](doc//FileReportDto.md)
- [FileReportFixDto](doc//FileReportFixDto.md)
- [FileReportItemDto](doc//FileReportItemDto.md)
- [ImportAssetDto](doc//ImportAssetDto.md)
- [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md)
@@ -266,6 +276,8 @@ Class | Method | HTTP request | Description
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
- [OAuthConfigDto](doc//OAuthConfigDto.md)
- [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
- [PathEntityType](doc//PathEntityType.md)
- [PathType](doc//PathType.md)
- [PeopleResponseDto](doc//PeopleResponseDto.md)
- [PeopleUpdateDto](doc//PeopleUpdateDto.md)
- [PeopleUpdateItem](doc//PeopleUpdateItem.md)
@@ -306,6 +318,7 @@ Class | Method | HTTP request | Description
- [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)
- [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md)
- [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md)
- [SystemConfigTrashDto](doc//SystemConfigTrashDto.md)
- [TagResponseDto](doc//TagResponseDto.md)
- [TagTypeEnum](doc//TagTypeEnum.md)
- [ThumbnailFormat](doc//ThumbnailFormat.md)

View File

@@ -12,9 +12,10 @@ Method | HTTP request | Description
[**bulkUploadCheck**](AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check |
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |
[**deleteAssets**](AssetApi.md#deleteassets) | **DELETE** /asset |
[**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download/archive |
[**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} |
[**emptyTrash**](AssetApi.md#emptytrash) | **POST** /asset/trash/empty |
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} |
[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
@@ -30,6 +31,8 @@ Method | HTTP request | Description
[**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
[**importFile**](AssetApi.md#importfile) | **POST** /asset/import |
[**restoreAssets**](AssetApi.md#restoreassets) | **POST** /asset/restore |
[**restoreTrash**](AssetApi.md#restoretrash) | **POST** /asset/trash/restore |
[**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs |
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
@@ -211,8 +214,8 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **deleteAsset**
> List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto)
# **deleteAssets**
> deleteAssets(assetBulkDeleteDto)
@@ -235,13 +238,12 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final deleteAssetDto = DeleteAssetDto(); // DeleteAssetDto |
final assetBulkDeleteDto = AssetBulkDeleteDto(); // AssetBulkDeleteDto |
try {
final result = api_instance.deleteAsset(deleteAssetDto);
print(result);
api_instance.deleteAssets(assetBulkDeleteDto);
} catch (e) {
print('Exception when calling AssetApi->deleteAsset: $e\n');
print('Exception when calling AssetApi->deleteAssets: $e\n');
}
```
@@ -249,11 +251,11 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**deleteAssetDto** | [**DeleteAssetDto**](DeleteAssetDto.md)| |
**assetBulkDeleteDto** | [**AssetBulkDeleteDto**](AssetBulkDeleteDto.md)| |
### Return type
[**List<DeleteAssetResponseDto>**](DeleteAssetResponseDto.md)
void (empty response body)
### Authorization
@@ -262,7 +264,7 @@ Name | Type | Description | Notes
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
@@ -380,6 +382,56 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **emptyTrash**
> emptyTrash()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
try {
api_instance.emptyTrash();
} catch (e) {
print('Exception when calling AssetApi->emptyTrash: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAllAssets**
> List<AssetResponseDto> getAllAssets(userId, isFavorite, isArchived, skip, updatedAfter, ifNoneMatch)
@@ -558,7 +610,7 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAssetStats**
> AssetStatsResponseDto getAssetStats(isArchived, isFavorite)
> AssetStatsResponseDto getAssetStats(isArchived, isFavorite, isTrashed)
@@ -583,9 +635,10 @@ import 'package:openapi/api.dart';
final api_instance = AssetApi();
final isArchived = true; // bool |
final isFavorite = true; // bool |
final isTrashed = true; // bool |
try {
final result = api_instance.getAssetStats(isArchived, isFavorite);
final result = api_instance.getAssetStats(isArchived, isFavorite, isTrashed);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getAssetStats: $e\n');
@@ -598,6 +651,7 @@ Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**isArchived** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**isTrashed** | **bool**| | [optional]
### Return type
@@ -674,7 +728,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getByTimeBucket**
> List<AssetResponseDto> getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key)
> List<AssetResponseDto> getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, key)
@@ -704,10 +758,11 @@ final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final isArchived = true; // bool |
final isFavorite = true; // bool |
final isTrashed = true; // bool |
final key = key_example; // String |
try {
final result = api_instance.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key);
final result = api_instance.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, key);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getByTimeBucket: $e\n');
@@ -725,6 +780,7 @@ Name | Type | Description | Notes
**personId** | **String**| | [optional]
**isArchived** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**isTrashed** | **bool**| | [optional]
**key** | **String**| | [optional]
### Return type
@@ -902,7 +958,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getMapMarkers**
> List<MapMarkerResponseDto> getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore)
> List<MapMarkerResponseDto> getMapMarkers(isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore)
@@ -925,12 +981,13 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final isArchived = true; // bool |
final isFavorite = true; // bool |
final fileCreatedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final fileCreatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
try {
final result = api_instance.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore);
final result = api_instance.getMapMarkers(isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getMapMarkers: $e\n');
@@ -941,6 +998,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**isArchived** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**fileCreatedAfter** | **DateTime**| | [optional]
**fileCreatedBefore** | **DateTime**| | [optional]
@@ -961,7 +1019,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getMemoryLane**
> List<MemoryLaneResponseDto> getMemoryLane(timestamp)
> List<MemoryLaneResponseDto> getMemoryLane(day, month)
@@ -984,10 +1042,11 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final timestamp = 2013-10-20T19:20:30+01:00; // DateTime | Get pictures for +24 hours from this time going back x years
final day = 56; // int |
final month = 56; // int |
try {
final result = api_instance.getMemoryLane(timestamp);
final result = api_instance.getMemoryLane(day, month);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getMemoryLane: $e\n');
@@ -998,7 +1057,8 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**timestamp** | **DateTime**| Get pictures for +24 hours from this time going back x years |
**day** | **int**| |
**month** | **int**| |
### Return type
@@ -1071,7 +1131,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getTimeBuckets**
> List<TimeBucketResponseDto> getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key)
> List<TimeBucketResponseDto> getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key)
@@ -1100,10 +1160,11 @@ final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final isArchived = true; // bool |
final isFavorite = true; // bool |
final isTrashed = true; // bool |
final key = key_example; // String |
try {
final result = api_instance.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key);
final result = api_instance.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getTimeBuckets: $e\n');
@@ -1120,6 +1181,7 @@ Name | Type | Description | Notes
**personId** | **String**| | [optional]
**isArchived** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**isTrashed** | **bool**| | [optional]
**key** | **String**| | [optional]
### Return type
@@ -1249,6 +1311,110 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **restoreAssets**
> restoreAssets(bulkIdsDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final bulkIdsDto = BulkIdsDto(); // BulkIdsDto |
try {
api_instance.restoreAssets(bulkIdsDto);
} catch (e) {
print('Exception when calling AssetApi->restoreAssets: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **restoreTrash**
> restoreTrash()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
try {
api_instance.restoreTrash();
} catch (e) {
print('Exception when calling AssetApi->restoreTrash: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **runAssetJobs**
> runAssetJobs(assetJobsDto)

View File

@@ -1,4 +1,4 @@
# openapi.model.DeleteAssetDto
# openapi.model.AssetBulkDeleteDto
## Load the model package
```dart
@@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**force** | **bool** | | [optional]
**ids** | **List<String>** | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -15,14 +15,17 @@ Name | Type | Description | Notes
**exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) | | [optional]
**fileCreatedAt** | [**DateTime**](DateTime.md) | |
**fileModifiedAt** | [**DateTime**](DateTime.md) | |
**hasMetadata** | **bool** | |
**id** | **String** | |
**isArchived** | **bool** | |
**isExternal** | **bool** | |
**isFavorite** | **bool** | |
**isOffline** | **bool** | |
**isReadOnly** | **bool** | |
**isTrashed** | **bool** | |
**libraryId** | **String** | |
**livePhotoVideoId** | **String** | | [optional]
**localDateTime** | [**DateTime**](DateTime.md) | |
**originalFileName** | **String** | |
**originalPath** | **String** | |
**owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional]
@@ -31,7 +34,7 @@ Name | Type | Description | Notes
**resized** | **bool** | |
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
**tags** | [**List<TagResponseDto>**](TagResponseDto.md) | | [optional] [default to const []]
**thumbhash** | **String** | base64 encoded thumbhash |
**thumbhash** | **String** | |
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | |
**updatedAt** | [**DateTime**](DateTime.md) | |

View File

@@ -9,9 +9,66 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**fixAuditFiles**](AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
[**getAuditDeletes**](AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
[**getAuditFiles**](AuditApi.md#getauditfiles) | **GET** /audit/file-report |
[**getFileChecksums**](AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
# **fixAuditFiles**
> fixAuditFiles(fileReportFixDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AuditApi();
final fileReportFixDto = FileReportFixDto(); // FileReportFixDto |
try {
api_instance.fixAuditFiles(fileReportFixDto);
} catch (e) {
print('Exception when calling AuditApi->fixAuditFiles: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAuditDeletes**
> AuditDeletesResponseDto getAuditDeletes(entityType, after, userId)
@@ -71,3 +128,109 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAuditFiles**
> FileReportDto getAuditFiles()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AuditApi();
try {
final result = api_instance.getAuditFiles();
print(result);
} catch (e) {
print('Exception when calling AuditApi->getAuditFiles: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**FileReportDto**](FileReportDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getFileChecksums**
> List<FileChecksumResponseDto> getFileChecksums(fileChecksumDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AuditApi();
final fileChecksumDto = FileChecksumDto(); // FileChecksumDto |
try {
final result = api_instance.getFileChecksums(fileChecksumDto);
print(result);
} catch (e) {
print('Exception when calling AuditApi->getFileChecksums: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| |
### Return type
[**List<FileChecksumResponseDto>**](FileChecksumResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View File

@@ -1,4 +1,4 @@
# openapi.model.DeleteAssetResponseDto
# openapi.model.FileChecksumDto
## Load the model package
```dart
@@ -8,8 +8,7 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | |
**status** | [**DeleteAssetStatus**](DeleteAssetStatus.md) | |
**filenames** | **List<String>** | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,16 @@
# openapi.model.FileChecksumResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**checksum** | **String** | |
**filename** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

16
mobile/openapi/doc/FileReportDto.md generated Normal file
View File

@@ -0,0 +1,16 @@
# openapi.model.FileReportDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**extras** | **List<String>** | | [default to const []]
**orphans** | [**List<FileReportItemDto>**](FileReportItemDto.md) | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

15
mobile/openapi/doc/FileReportFixDto.md generated Normal file
View File

@@ -0,0 +1,15 @@
# openapi.model.FileReportFixDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**items** | [**List<FileReportItemDto>**](FileReportItemDto.md) | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

19
mobile/openapi/doc/FileReportItemDto.md generated Normal file
View File

@@ -0,0 +1,19 @@
# openapi.model.FileReportItemDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**checksum** | **String** | | [optional]
**entityId** | **String** | |
**entityType** | [**PathEntityType**](PathEntityType.md) | |
**pathType** | [**PathType**](PathType.md) | |
**pathValue** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -1,4 +1,4 @@
# openapi.model.DeleteAssetStatus
# openapi.model.PathEntityType
## Load the model package
```dart

14
mobile/openapi/doc/PathType.md generated Normal file
View File

@@ -0,0 +1,14 @@
# openapi.model.PathType
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -11,6 +11,7 @@ Method | HTTP request | Description
------------- | ------------- | -------------
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
[**search**](SearchApi.md#search) | **GET** /search |
[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person |
# **getExploreData**
@@ -149,3 +150,58 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **searchPerson**
> List<PersonResponseDto> searchPerson(name)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SearchApi();
final name = name_example; // String |
try {
final result = api_instance.searchPerson(name);
print(result);
} catch (e) {
print('Exception when calling SearchApi->searchPerson: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**name** | **String**| |
### Return type
[**List<PersonResponseDto>**](PersonResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View File

@@ -8,9 +8,11 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**isInitialized** | **bool** | |
**loginPageMessage** | **String** | |
**mapTileUrl** | **String** | |
**oauthButtonText** | **String** | |
**trashDays** | **int** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -19,6 +19,7 @@ Name | Type | Description | Notes
**search** | **bool** | |
**sidecar** | **bool** | |
**tagImage** | **bool** | |
**trash** | **bool** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -14,7 +14,7 @@ Name | Type | Description | Notes
**assetIds** | **List<String>** | | [optional] [default to const []]
**description** | **String** | | [optional]
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**showExif** | **bool** | | [optional] [default to true]
**showMetadata** | **bool** | | [optional] [default to true]
**type** | [**SharedLinkType**](SharedLinkType.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -12,7 +12,7 @@ Name | Type | Description | Notes
**allowUpload** | **bool** | | [optional]
**description** | **String** | | [optional]
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**showExif** | **bool** | | [optional]
**showMetadata** | **bool** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -17,7 +17,7 @@ Name | Type | Description | Notes
**expiresAt** | [**DateTime**](DateTime.md) | |
**id** | **String** | |
**key** | **String** | |
**showExif** | **bool** | |
**showMetadata** | **bool** | |
**type** | [**SharedLinkType**](SharedLinkType.md) | |
**userId** | **String** | |

View File

@@ -17,6 +17,7 @@ Name | Type | Description | Notes
**reverseGeocoding** | [**SystemConfigReverseGeocodingDto**](SystemConfigReverseGeocodingDto.md) | |
**storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | |
**thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) | |
**trash** | [**SystemConfigTrashDto**](SystemConfigTrashDto.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,16 @@
# openapi.model.SystemConfigTrashDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**days** | **int** | |
**enabled** | **bool** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -410,6 +410,20 @@ Name | Type | Description | Notes
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = UserApi();
final admin = true; // bool |
@@ -434,7 +448,7 @@ Name | Type | Description | Notes
### Authorization
No authorization required
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers

View File

@@ -54,6 +54,7 @@ part 'model/admin_signup_response_dto.dart';
part 'model/album_count_response_dto.dart';
part 'model/album_response_dto.dart';
part 'model/all_job_status_response_dto.dart';
part 'model/asset_bulk_delete_dto.dart';
part 'model/asset_bulk_update_dto.dart';
part 'model/asset_bulk_upload_check_dto.dart';
part 'model/asset_bulk_upload_check_item.dart';
@@ -90,14 +91,16 @@ part 'model/create_tag_dto.dart';
part 'model/create_user_dto.dart';
part 'model/curated_locations_response_dto.dart';
part 'model/curated_objects_response_dto.dart';
part 'model/delete_asset_dto.dart';
part 'model/delete_asset_response_dto.dart';
part 'model/delete_asset_status.dart';
part 'model/download_archive_info.dart';
part 'model/download_info_dto.dart';
part 'model/download_response_dto.dart';
part 'model/entity_type.dart';
part 'model/exif_response_dto.dart';
part 'model/file_checksum_dto.dart';
part 'model/file_checksum_response_dto.dart';
part 'model/file_report_dto.dart';
part 'model/file_report_fix_dto.dart';
part 'model/file_report_item_dto.dart';
part 'model/import_asset_dto.dart';
part 'model/job_command.dart';
part 'model/job_command_dto.dart';
@@ -119,6 +122,8 @@ part 'model/o_auth_authorize_response_dto.dart';
part 'model/o_auth_callback_dto.dart';
part 'model/o_auth_config_dto.dart';
part 'model/o_auth_config_response_dto.dart';
part 'model/path_entity_type.dart';
part 'model/path_type.dart';
part 'model/people_response_dto.dart';
part 'model/people_update_dto.dart';
part 'model/people_update_item.dart';
@@ -159,6 +164,7 @@ part 'model/system_config_reverse_geocoding_dto.dart';
part 'model/system_config_storage_template_dto.dart';
part 'model/system_config_template_storage_option_dto.dart';
part 'model/system_config_thumbnail_dto.dart';
part 'model/system_config_trash_dto.dart';
part 'model/tag_response_dto.dart';
part 'model/tag_type_enum.dart';
part 'model/thumbnail_format.dart';

View File

@@ -183,13 +183,13 @@ class AssetApi {
/// Performs an HTTP 'DELETE /asset' operation and returns the [Response].
/// Parameters:
///
/// * [DeleteAssetDto] deleteAssetDto (required):
Future<Response> deleteAssetWithHttpInfo(DeleteAssetDto deleteAssetDto,) async {
/// * [AssetBulkDeleteDto] assetBulkDeleteDto (required):
Future<Response> deleteAssetsWithHttpInfo(AssetBulkDeleteDto assetBulkDeleteDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset';
// ignore: prefer_final_locals
Object? postBody = deleteAssetDto;
Object? postBody = assetBulkDeleteDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
@@ -211,23 +211,12 @@ class AssetApi {
/// Parameters:
///
/// * [DeleteAssetDto] deleteAssetDto (required):
Future<List<DeleteAssetResponseDto>?> deleteAsset(DeleteAssetDto deleteAssetDto,) async {
final response = await deleteAssetWithHttpInfo(deleteAssetDto,);
/// * [AssetBulkDeleteDto] assetBulkDeleteDto (required):
Future<void> deleteAssets(AssetBulkDeleteDto assetBulkDeleteDto,) async {
final response = await deleteAssetsWithHttpInfo(assetBulkDeleteDto,);
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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<DeleteAssetResponseDto>') as List)
.cast<DeleteAssetResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'POST /asset/download/archive' operation and returns the [Response].
@@ -341,6 +330,39 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'POST /asset/trash/empty' operation and returns the [Response].
Future<Response> emptyTrashWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/trash/empty';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<void> emptyTrash() async {
final response = await emptyTrashWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Get all AssetEntity belong to the user
///
/// Note: This method returns the HTTP [Response].
@@ -549,7 +571,9 @@ class AssetApi {
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
Future<Response> getAssetStatsWithHttpInfo({ bool? isArchived, bool? isFavorite, }) async {
///
/// * [bool] isTrashed:
Future<Response> getAssetStatsWithHttpInfo({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/statistics';
@@ -566,6 +590,9 @@ class AssetApi {
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isTrashed != null) {
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
}
const contentTypes = <String>[];
@@ -586,8 +613,10 @@ class AssetApi {
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
Future<AssetStatsResponseDto?> getAssetStats({ bool? isArchived, bool? isFavorite, }) async {
final response = await getAssetStatsWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, );
///
/// * [bool] isTrashed:
Future<AssetStatsResponseDto?> getAssetStats({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async {
final response = await getAssetStatsWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -681,8 +710,10 @@ class AssetApi {
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
///
/// * [String] key:
Future<Response> getByTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async {
Future<Response> getByTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/time-bucket';
@@ -708,6 +739,9 @@ class AssetApi {
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isTrashed != null) {
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
}
queryParams.addAll(_queryParams('', 'timeBucket', timeBucket));
if (key != null) {
@@ -744,9 +778,11 @@ class AssetApi {
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
///
/// * [String] key:
Future<List<AssetResponseDto>?> getByTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async {
final response = await getByTimeBucketWithHttpInfo(size, timeBucket, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, key: key, );
Future<List<AssetResponseDto>?> getByTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async {
final response = await getByTimeBucketWithHttpInfo(size, timeBucket, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -909,12 +945,14 @@ class AssetApi {
/// Performs an HTTP 'GET /asset/map-marker' operation and returns the [Response].
/// Parameters:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [DateTime] fileCreatedAfter:
///
/// * [DateTime] fileCreatedBefore:
Future<Response> getMapMarkersWithHttpInfo({ bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
Future<Response> getMapMarkersWithHttpInfo({ bool? isArchived, bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/map-marker';
@@ -925,6 +963,9 @@ class AssetApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (isArchived != null) {
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
@@ -951,13 +992,15 @@ class AssetApi {
/// Parameters:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [DateTime] fileCreatedAfter:
///
/// * [DateTime] fileCreatedBefore:
Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, );
Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isArchived, bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
final response = await getMapMarkersWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -977,9 +1020,10 @@ class AssetApi {
/// Performs an HTTP 'GET /asset/memory-lane' operation and returns the [Response].
/// Parameters:
///
/// * [DateTime] timestamp (required):
/// Get pictures for +24 hours from this time going back x years
Future<Response> getMemoryLaneWithHttpInfo(DateTime timestamp,) async {
/// * [int] day (required):
///
/// * [int] month (required):
Future<Response> getMemoryLaneWithHttpInfo(int day, int month,) async {
// ignore: prefer_const_declarations
final path = r'/asset/memory-lane';
@@ -990,7 +1034,8 @@ class AssetApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'timestamp', timestamp));
queryParams.addAll(_queryParams('', 'day', day));
queryParams.addAll(_queryParams('', 'month', month));
const contentTypes = <String>[];
@@ -1008,10 +1053,11 @@ class AssetApi {
/// Parameters:
///
/// * [DateTime] timestamp (required):
/// Get pictures for +24 hours from this time going back x years
Future<List<MemoryLaneResponseDto>?> getMemoryLane(DateTime timestamp,) async {
final response = await getMemoryLaneWithHttpInfo(timestamp,);
/// * [int] day (required):
///
/// * [int] month (required):
Future<List<MemoryLaneResponseDto>?> getMemoryLane(int day, int month,) async {
final response = await getMemoryLaneWithHttpInfo(day, month,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -1097,8 +1143,10 @@ class AssetApi {
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
///
/// * [String] key:
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async {
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/time-buckets';
@@ -1125,6 +1173,9 @@ class AssetApi {
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isTrashed != null) {
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
}
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
@@ -1157,9 +1208,11 @@ class AssetApi {
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
///
/// * [String] key:
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async {
final response = await getTimeBucketsWithHttpInfo(size, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, key: key, );
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async {
final response = await getTimeBucketsWithHttpInfo(size, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -1279,6 +1332,78 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'POST /asset/restore' operation and returns the [Response].
/// Parameters:
///
/// * [BulkIdsDto] bulkIdsDto (required):
Future<Response> restoreAssetsWithHttpInfo(BulkIdsDto bulkIdsDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/restore';
// ignore: prefer_final_locals
Object? postBody = bulkIdsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [BulkIdsDto] bulkIdsDto (required):
Future<void> restoreAssets(BulkIdsDto bulkIdsDto,) async {
final response = await restoreAssetsWithHttpInfo(bulkIdsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /asset/trash/restore' operation and returns the [Response].
Future<Response> restoreTrashWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/trash/restore';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<void> restoreTrash() async {
final response = await restoreTrashWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /asset/jobs' operation and returns the [Response].
/// Parameters:
///

View File

@@ -16,6 +16,45 @@ class AuditApi {
final ApiClient apiClient;
/// Performs an HTTP 'POST /audit/file-report/fix' operation and returns the [Response].
/// Parameters:
///
/// * [FileReportFixDto] fileReportFixDto (required):
Future<Response> fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async {
// ignore: prefer_const_declarations
final path = r'/audit/file-report/fix';
// ignore: prefer_final_locals
Object? postBody = fileReportFixDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [FileReportFixDto] fileReportFixDto (required):
Future<void> fixAuditFiles(FileReportFixDto fileReportFixDto,) async {
final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'GET /audit/deletes' operation and returns the [Response].
/// Parameters:
///
@@ -76,4 +115,95 @@ class AuditApi {
}
return null;
}
/// Performs an HTTP 'GET /audit/file-report' operation and returns the [Response].
Future<Response> getAuditFilesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/audit/file-report';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<FileReportDto?> getAuditFiles() async {
final response = await getAuditFilesWithHttpInfo();
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), 'FileReportDto',) as FileReportDto;
}
return null;
}
/// Performs an HTTP 'POST /audit/file-report/checksum' operation and returns the [Response].
/// Parameters:
///
/// * [FileChecksumDto] fileChecksumDto (required):
Future<Response> getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async {
// ignore: prefer_const_declarations
final path = r'/audit/file-report/checksum';
// ignore: prefer_final_locals
Object? postBody = fileChecksumDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [FileChecksumDto] fileChecksumDto (required):
Future<List<FileChecksumResponseDto>?> getFileChecksums(FileChecksumDto fileChecksumDto,) async {
final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,);
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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<FileChecksumResponseDto>') as List)
.cast<FileChecksumResponseDto>()
.toList();
}
return null;
}
}

View File

@@ -215,4 +215,56 @@ class SearchApi {
}
return null;
}
/// Performs an HTTP 'GET /search/person' operation and returns the [Response].
/// Parameters:
///
/// * [String] name (required):
Future<Response> searchPersonWithHttpInfo(String name,) async {
// ignore: prefer_const_declarations
final path = r'/search/person';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'name', name));
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] name (required):
Future<List<PersonResponseDto>?> searchPerson(String name,) async {
final response = await searchPersonWithHttpInfo(name,);
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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<PersonResponseDto>') as List)
.cast<PersonResponseDto>()
.toList();
}
return null;
}
}

View File

@@ -199,6 +199,8 @@ class ApiClient {
return AlbumResponseDto.fromJson(value);
case 'AllJobStatusResponseDto':
return AllJobStatusResponseDto.fromJson(value);
case 'AssetBulkDeleteDto':
return AssetBulkDeleteDto.fromJson(value);
case 'AssetBulkUpdateDto':
return AssetBulkUpdateDto.fromJson(value);
case 'AssetBulkUploadCheckDto':
@@ -271,12 +273,6 @@ class ApiClient {
return CuratedLocationsResponseDto.fromJson(value);
case 'CuratedObjectsResponseDto':
return CuratedObjectsResponseDto.fromJson(value);
case 'DeleteAssetDto':
return DeleteAssetDto.fromJson(value);
case 'DeleteAssetResponseDto':
return DeleteAssetResponseDto.fromJson(value);
case 'DeleteAssetStatus':
return DeleteAssetStatusTypeTransformer().decode(value);
case 'DownloadArchiveInfo':
return DownloadArchiveInfo.fromJson(value);
case 'DownloadInfoDto':
@@ -287,6 +283,16 @@ class ApiClient {
return EntityTypeTypeTransformer().decode(value);
case 'ExifResponseDto':
return ExifResponseDto.fromJson(value);
case 'FileChecksumDto':
return FileChecksumDto.fromJson(value);
case 'FileChecksumResponseDto':
return FileChecksumResponseDto.fromJson(value);
case 'FileReportDto':
return FileReportDto.fromJson(value);
case 'FileReportFixDto':
return FileReportFixDto.fromJson(value);
case 'FileReportItemDto':
return FileReportItemDto.fromJson(value);
case 'ImportAssetDto':
return ImportAssetDto.fromJson(value);
case 'JobCommand':
@@ -329,6 +335,10 @@ class ApiClient {
return OAuthConfigDto.fromJson(value);
case 'OAuthConfigResponseDto':
return OAuthConfigResponseDto.fromJson(value);
case 'PathEntityType':
return PathEntityTypeTypeTransformer().decode(value);
case 'PathType':
return PathTypeTypeTransformer().decode(value);
case 'PeopleResponseDto':
return PeopleResponseDto.fromJson(value);
case 'PeopleUpdateDto':
@@ -409,6 +419,8 @@ class ApiClient {
return SystemConfigTemplateStorageOptionDto.fromJson(value);
case 'SystemConfigThumbnailDto':
return SystemConfigThumbnailDto.fromJson(value);
case 'SystemConfigTrashDto':
return SystemConfigTrashDto.fromJson(value);
case 'TagResponseDto':
return TagResponseDto.fromJson(value);
case 'TagTypeEnum':

View File

@@ -76,9 +76,6 @@ String parameterToString(dynamic value) {
if (value is Colorspace) {
return ColorspaceTypeTransformer().encode(value).toString();
}
if (value is DeleteAssetStatus) {
return DeleteAssetStatusTypeTransformer().encode(value).toString();
}
if (value is EntityType) {
return EntityTypeTypeTransformer().encode(value).toString();
}
@@ -94,6 +91,12 @@ String parameterToString(dynamic value) {
if (value is ModelType) {
return ModelTypeTypeTransformer().encode(value).toString();
}
if (value is PathEntityType) {
return PathEntityTypeTypeTransformer().encode(value).toString();
}
if (value is PathType) {
return PathTypeTypeTransformer().encode(value).toString();
}
if (value is SharedLinkType) {
return SharedLinkTypeTypeTransformer().encode(value).toString();
}

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