Compare commits

...

89 Commits

Author SHA1 Message Date
Alex The Bot
b258f3552a Version v1.64.0 2023-06-26 18:06:11 +00:00
Ethan Margaillan
e803bc909f feat(web): store albums sorting policy in local storage (#2966) 2023-06-26 11:54:20 -05:00
Sergey Kondrikov
d078aea32b Normalize progress bar value (#2967) 2023-06-26 11:54:08 -05:00
Sergey Kondrikov
fb2cfcb640 feat(mobile): custom video player controls (#2960)
* Remove toggle fullscreen button

* Implement custom video player controls

* Move Padding into Container
2023-06-26 10:27:47 -05:00
Fynn Petersen-Frey
99f85fb359 feat(Android): guard against missing EXIF info (#2965) 2023-06-26 10:27:32 -05:00
Keszei Balázs
454fb106d2 Updated OSM tile URLs immich-app/immich#2874 (#2961)
Co-authored-by: Balazs Keszei <balazs.keszei@clbr.hu>
2023-06-26 08:47:37 -05:00
Nurullah Türkoğlu
7d078a2f0e add Turkish README file (#2943)
* add Turkish README file

* fixed a typo
2023-06-25 21:29:41 -05:00
Alex
b015648bfe chore(mobile): Add more error log (#2949)
Co-authored-by: alex <alex@pop-os.localdomain>
2023-06-25 18:59:35 -05:00
Mert
a58482cb2b added locustfile (#2926) 2023-06-25 13:20:45 -05:00
Elliot Lee
9dd1d81536 Allow Windows' version of the MIME types for raw photos. (#2945)
* Allow Windows' version of the MIME types for raw photos.

* Fix prettier warning.

---------

Co-authored-by: Elliot Lee <sopwith@gmail.com>
2023-06-25 13:10:39 -05:00
Mert
a2f5674bbb refactor(ml): modularization and styling (#2835)
* basic refactor and styling

* removed batching

* module entrypoint

* removed unused imports

* model superclass,  model cache now in app state

* fixed cache dir and enforced abstract method

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-24 22:18:09 -05:00
Mert
837ad24f58 fix(deps): install poetry in pump workflow (#2938)
* install poetry in pump workflow

* formatting
2023-06-24 22:03:50 -05:00
bo0tzz
058c62b111 feat(blog): Make blog public, add June 2023 update post (#2935)
* feat(blog): Add blog link to website header

* feat(blog): June 2023 update post

* chore(blog): Reorder folder structure

* chore(blog): Add discord link

* chore(blog): Formatting

* chore(blog): Use youtube embeds

* fix format

---------

Co-authored-by: alex <alex@pop-os.localdomain>
2023-06-24 22:02:39 -05:00
Alex The Bot
bbb6bca605 Version v1.63.2 2023-06-25 02:53:18 +00:00
Alex
a8e5a1de15 chore(server): support DNG file (#2940) 2023-06-24 21:50:01 -05:00
Alex The Bot
bba4c44182 Version v1.63.1 2023-06-24 15:31:16 +00:00
Alex
7c76249e1f fix(server): Share link with expire creation time error (#2934)
* fix(server): Share link with expire creation time error

* better
2023-06-24 10:24:55 -05:00
Alex Tran
294955db17 wording 2023-06-23 23:32:39 -05:00
Alex
752ad2d2eb chore(docs): ReadOnly documentation (#2925)
* update

* update

* update'

* update

* update

* update

* update

* chore: typoes etc

* update

* prettier

---------

Co-authored-by: bo0tzz <git@bo0tzz.me>
2023-06-23 21:06:52 -05:00
Alex The Bot
02a268c7c6 Version v1.63.0 2023-06-24 01:41:12 +00:00
Alex
b2dc7adf3b fix(web): memory pause autoplay when scrolling down (#2923) 2023-06-23 11:13:05 -05:00
416c616e
6e62558d81 Fix Canon CR3 mime type (#2922) 2023-06-23 11:12:11 -05:00
Alex
0d0866d5d9 feat(mobile): Facial recognition (#2507)
* Add API service

* Added service, provider

* merge main

* update pubspec

* styling

* dev: add person search result page

* dev: display person asset on page

* dev: add rename form

* style form

* dev: mechanism to add name to faces

* styling

* fix bad merge

* update api

* test

* revert

* Add header widget

* change name

* show all people page

* fix test

* pr feedback

* Add name to app bar

* feedback

* styling
2023-06-23 10:44:02 -05:00
Alex
00f65a53dd fix(server): Fix person's assets retrival order (#2920) 2023-06-23 09:34:55 -05:00
Alex
751922990f chore/remove openapi assertion for dart 2 (#2916)
* chore(server): patch dart openapi assertion 2

* removed usused file
2023-06-22 13:00:07 -05:00
Alex
4311d385fc chore(server): patch dart openapi assertion (#2914)
* chore(server): patch dart openapi assertion

* remove unused file
2023-06-22 12:48:57 -05:00
Fynn Petersen-Frey
3e2f335a4c feat(mobile): optimize screen space usage (#2911)
* feat(mobile): optimize screen space usage

* undo nav bar changes
2023-06-22 09:50:27 -05:00
Alex
cf1eddb449 fix(server): transform isReadOnly DTO to boolean (#2912) 2023-06-22 09:46:21 -05:00
Alex Phillips
e171fec5aa feat(server): support for read-only assets and importing existing items in the filesystem (#2715)
* Added read-only flag for assets, endpoint to trigger file import vs upload

* updated fixtures with new property

* if upload is 'read-only', ensure there is no existing asset at the designated originalPath

* added test for file import as well as detecting existing image at read-only destination location

* Added storage service test for a case where it should not move read-only assets

* upload doesn't need the read-only flag available, just importing

* default isReadOnly on import endpoint to true

* formatting fixes

* create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation

* updated code to reflect changes in MR

* fixed read stream promise return type

* new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates

* refactor: import asset

* chore: open api

* chore: tests

* Added externalPath support for individual users, updated UI to allow this to be set by admin

* added missing var for externalPath in ui

* chore: open api

* fix: compilation issues

* fix: server test

* built api, fixed user-response dto to include externalPath

* reverted accidental commit

* bad commit of duplicate externalPath in user response  dto

* fixed tests to include externalPath on expected result

* fix: unit tests

* centralized supported filetypes, perform file type checking of asset and sidecar during file import process

* centralized supported filetype check method to keep regex DRY

* fixed typo

* combined migrations into one

* update api

* Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not

* update mimetype

* Fixed detect correct mimetype

* revert asset-upload config

* reverted domain.constant

* refactor

* fix mime-type issue

* fix format

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-21 21:33:20 -05:00
Fynn Petersen-Frey
7f44d508dc feat(mobile): pinch to zoom on asset grid (#2905) 2023-06-21 21:13:23 -05:00
Krisjanis Lejejs
2c924e4c1c feature (web): Add keyboard event support to memory view (#2890)
* Add keyboard event support to memory view in web

* Implement PR suggestions
2023-06-21 15:28:58 -05:00
Alex
0f0375a67e feat(web): add album to search result (#2900)
* Add album to search result page

* Update web/src/routes/(user)/search/+page.svelte

Co-authored-by: Thomas <9749173+uhthomas@users.noreply.github.com>

* Update web/src/routes/(user)/search/+page.svelte

Co-authored-by: Thomas <9749173+uhthomas@users.noreply.github.com>

* change font weight

* hide context menu in this view

---------

Co-authored-by: Thomas <9749173+uhthomas@users.noreply.github.com>
2023-06-21 15:18:00 -05:00
Thomas
069c68bfe4 feat: M2TS (#2896)
Support the Blu-ray disc Audio-Video (BDAV) MPEG-2 Transport Stream (M2TS) format.

https://en.wikipedia.org/wiki/.m2ts

Fixes: #2350
2023-06-21 13:50:12 -05:00
Fynn Petersen-Frey
c03d8e312a chore(readme): mention offline support feature (#2902) 2023-06-21 13:49:35 -05:00
faupau
de7f66f983 fix(web): keep video volume (#2897)
* save video volume in asset-interaction.store.ts

* move video-viewer-volume to preferences store
save in localstorage by using persisted
2023-06-21 09:59:13 -05:00
Alex
82b89aa20b feat(web): custom drop down button (#2887)
* feat(web): custom drop down button

* fix test

* fix test
2023-06-21 08:05:59 -05:00
Thomas
80d02e8a8d feat: JPEG XL (#2893)
Support the JPEG XL format (.jxl).

JPEG XL is reported as supported by `sharp.format`:

```
jxl: {
  id: 'jxl',
  input: { file: true, buffer: true, stream: true, fileSuffix: [Array] },
  output: { file: true, buffer: true, stream: true }
}
```

Fixes: #2743
2023-06-21 07:29:02 -05:00
Jason Rasmussen
868f629f32 refactor(server, web): create shared link (#2879)
* refactor: shared links

* chore: open api

* fix: tsc error
2023-06-20 20:08:43 -05:00
Krisjanis Lejejs
746ca5d5ed feat(web): Add album sorting to albums view (#2861)
* Add album sorting to web albums view

* generate api

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-20 20:00:59 -05:00
Skyler Mäntysaari
3c5fefde2e fix(web): Mov files should show up in file picker. (#2886) 2023-06-20 19:58:35 -05:00
martyfuhry
26f58d3335 Fixes local position late initialization (#2884) 2023-06-20 19:58:17 -05:00
Alex
6baeca654b chore(server): Improve moveAsset log (#2878)
* chore(server): Improve moveAsset log

* Update storage-template.service.ts
2023-06-20 16:24:47 -05:00
martyfuhry
1b15b5414c Adds photo thumbnail to videos (#2880)
* Motion photos use placeholder image for more seamless loading

* Fixes merge conflicts
2023-06-20 16:17:43 -05:00
Manuel Taberna
48e4ea5231 feat(web): add zoom toggle icon (#2873)
* feat(web): add zoom toggle icon

* update zoom-image dependency

* fix lint issues

* remove variable testing line

* Simplify code using ternary conditional

Co-authored-by: Thomas <9749173+uhthomas@users.noreply.github.com>

* fix typo

---------

Co-authored-by: Thomas <9749173+uhthomas@users.noreply.github.com>
2023-06-20 09:36:38 -05:00
Thomas
f9fbf1a2a5 fix(server): use HTTP status OK instead of CREATED for assets (#2876)
The NGINX gzip module does not compress responses with a status of 201, which is
a major issue specifically for the /api/asset/time-bucket endpoint where
responses can be upwards of 5Mi. The size of the response is dramatically
reduced with gzip to 500Ki in some cases.

https://trac.nginx.org/nginx/ticket/471
https://trac.nginx.org/nginx/ticket/394

The signature of these endpoints should be GET rather than POST anyway, but that
is a bigger discussion.
2023-06-20 08:49:36 -05:00
Jonathan Jogenfors
f003ff3c98 Add dependency on immich-web to immich-proxy (#2875) 2023-06-20 08:46:48 -05:00
Elliot Lee
81e2b18531 Add support for many missing raw formats (#2834)
* Allow upload of AVIF and x-canon-cr2 mime types

* Allow generic RAW file mime type image/x-dcraw

* Another place to uploading avif and cr2

* Determine mime type for .avif and .cr2 files correctly

* Update asset-upload.config.spec.ts for CR2 and AVIF files

* More changes for AVIF & CR2 files

Found some other places where avif and cr2 should be mentioned.

* Merge in upstream changes

* Allow uploading and using most of the formats that libraw supports

* Add raw files to allowable mobile uploads

* Update asset-upload.config.spec.ts

Fix errant commas.

* Update asset-utils.ts

Remove duplicate entry in hash table.

* Fix missing k25 mime type in server upload check.
Fix prettier formatting message in web file-uploader.

* fix test

---------

Co-authored-by: Elliot Lee <sopwith@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-19 21:10:29 -05:00
Sophie
c404ea20ee fix(web): redirect to parent folder if asset viewer was open before reload (#2863)
* fix(web): redirect to parent folder if asset viewer was open before reload

* chore(web): use route constants in all routes
2023-06-19 21:06:08 -05:00
Stavros Kois
cc45564d84 move public msg to general section (#2864) 2023-06-19 16:42:59 -05:00
Alex The Bot
8d560ec55f Version v1.62.1 2023-06-19 21:31:38 +00:00
Thomas
df74111427 chore(web): fade between thumbhash and thumbnail (#2856) 2023-06-19 16:21:06 -05:00
Stavros Kois
93c35efe67 [docs]: Document environment variables (#2814)
* draft env vars

* remove mapbox refs, fixes #2535

* formatting and add some notes

* add examples for redis and typesense url

* [skipci] add note for redis socket

* do some formatting

* update md

* fix url

* fix variable

* add web for NODE_ENV

* fix variable name

* Apply suggestions from code review

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>

* address review feedback

* Update docker/example.env

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>

* add section for docker compose envs

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-06-19 15:55:12 -05:00
cycneuramus
296c77ac73 feat(server): support rclone as storage backend (#2832) 2023-06-19 11:58:10 -05:00
Alex The Bot
9c0f444e4d Version v1.62.0 2023-06-19 15:43:49 +00:00
Jason Rasmussen
6b0f91cafd fix(server): only show assets 'on-this-day' with thumbnails (#2851) 2023-06-19 09:12:18 -05:00
Dan Cowell
3f71d2d33d chore(deps): change compose service dependencies to use alpine variants (#2825)
* chore(deps): change compose service dependencies to use alpine variants

* chore(deps): pin manifest hashes for dependency containers
2023-06-18 20:51:46 -05:00
Sergey Kondrikov
f2942588f2 chore(mobile): Add debug build type suffix to the applicationId and version (#2826) 2023-06-17 23:10:57 -05:00
Alex
b47027efc2 fix(mobile): Sort newest first for asset selection in album (#2833) 2023-06-17 23:09:55 -05:00
Zeeshan Khan
34201be74c feat(ml) backend takes image over HTTP (#2783)
* using pydantic BaseSetting

* ML API takes image file as input

* keeping image in memory

* reducing duplicate code

* using bytes instead of UploadFile & other small code improvements

* removed form-multipart, using HTTP body

* format code

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-17 22:49:19 -05:00
Covalent
3e804f16df feat(web,server): add thumbhash support (#2649)
* add thumbhash: server generation and web impl

* move logic to infra & use byta in db

* remove unnecesary logs

* update generated API and simplify thumbhash gen

* fix check errors

* removed unnecessary library and css tag

* style edits

* syntax mistake

* update server test, change thumbhash job name

* fix tests

* Update server/src/domain/asset/response-dto/asset-response.dto.ts

Co-authored-by: Thomas <9749173+uhthomas@users.noreply.github.com>

* add unit test, change migration date

* change to official thumbhash impl

* update call method to not use eval

* "generate missing" looks for thumbhash

* improve queue & improve syntax

* update syntax again

* update tests

* fix thumbhash generation

* consolidate queueing to avoid duplication

* cover all types of incorrect thumbnail cases

* split out jest tasks

* put back thumbnail duration loading for images without thumbhash

* Remove stray package.json

---------

Co-authored-by: Luke McCarthy <mail@lukehmcc.com>
Co-authored-by: Thomas <9749173+uhthomas@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-17 22:22:31 -05:00
Thomas
3512140148 feat(web): add padding to memory asset navigation (#2822)
The bars are 2 pixels tall, which can be tricky to click. Additional padding
increases the height to 16 pixels, without changing how it looks, and makes for
much easier clicking.

In addition, remove the onDestroy lifecycle for the tween as it's not
necessary. It was a relic from using animation frames.
2023-06-16 23:37:11 +01:00
Jason Rasmussen
bff6914a73 chore(server): organize imports (#2779)
* feat: lint rule for organize imports

* chore: organize imports
2023-06-16 19:54:17 +00:00
Jason Rasmussen
652add635f refactor: rename get auth user decorator (#2778)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-06-16 19:39:53 +00:00
Jason Rasmussen
fde410e2ac refactor(server): send job command (#2777)
* refactor: send job command

* chore: open api
2023-06-16 14:36:07 -05:00
Jason Rasmussen
f04e47803c refactor(server): access checks (#2776)
* refactor(server): access checks

* chore: simply asset module
2023-06-16 14:01:34 -05:00
Thomas
61d74263d9 fix(web): hide memory lane navigation properly on scaled resolutions (#2819)
Fixes: #2817
2023-06-16 13:55:11 -05:00
Alex Tran
66ee065c0c pause renovate 2023-06-16 13:52:29 -05:00
Thomas
09bcf6974e feat(web): show number of assets in memory progress bar (#2813)
Fixes: #2810
2023-06-16 13:17:39 -05:00
renovate[bot]
5d7d615433 chore(deps): update web (#2806)
* chore(deps): update web

* fixed svelte-check being a nuisance

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-16 12:45:05 -05:00
renovate[bot]
5387048dc3 fix(deps): update dependency tailwindcss to v3.3.2 (#2808)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 17:14:29 +00:00
renovate[bot]
6930df71cf fix(deps): update dependency docusaurus-preset-openapi to v0.6.4 (#2800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 11:40:04 -05:00
renovate[bot]
52bbf6da5d fix(deps): update dependency url to v0.11.1 (#2802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 11:39:54 -05:00
Alex
1cd5df7558 fix(web): not displaying assets in album after adding shared user (#2804) 2023-06-16 11:39:40 -05:00
renovate[bot]
74429798e2 fix(deps): update dependency autoprefixer to v10.4.14 (#2799)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 11:39:25 -05:00
renovate[bot]
651f3ea5eb chore(deps): update typesense/typesense docker tag to v0.24.1 (#2798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 11:39:11 -05:00
renovate[bot]
0909335d02 chore(deps): update python:3.11.4-slim-bullseye docker digest to 91d194f (#2797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 11:38:59 -05:00
renovate[bot]
827e4b5f75 chore(deps): update python:3.11.4-bullseye docker digest to 5b40167 (#2796)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 11:17:23 -05:00
renovate[bot]
c8ff07fff0 fix(deps): update dependency postcss to v8.4.24 (#2801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 11:16:44 -05:00
Thomas
4a21cb2d00 chore(web): hide memory lane navigation when it's no longer possible to scroll (#2791)
Fixes: #2790
2023-06-16 11:06:38 -05:00
Jason Rasmussen
07f7fffae7 refactor(server): album count (#2746)
* refactor(server): album count

* chore: open api
2023-06-16 10:48:48 -05:00
renovate[bot]
441ee2ef90 chore(deps): update dependency typescript to v5 (#2795)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 10:43:40 -05:00
renovate[bot]
acad133e3a chore(deps): update dependency @tsconfig/docusaurus to v1.0.7 (#2793)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 15:30:14 +00:00
renovate[bot]
ef8714fda9 chore(deps): update dependency vite to v4.1.5 [security] (#2792)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 15:28:49 +00:00
Thomas
16171eee8d pin image digests (#2754)
Manifest list digests can be found with:

```sh
docker buildx imagetools inspect python:3.11.4-bullseye
docker buildx imagetools inspect python:3.11.4-slim-bullseye
docker buildx imagetools inspect ghcr.io/nginxinc/nginx-unprivileged:1.25.0-alpine3.17
```

The node images are pinned in #2736

Fixes #2751
Partially fixes #2752
2023-06-16 10:28:41 -05:00
renovate[bot]
d3c1781478 Configure Renovate (#2739)
* Add renovate.json

* Update renovate.json

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-06-16 10:22:52 -05:00
Thomas
329b52e670 use svelte motion tweening for animation (#2788)
It look like Svelte has a concept of 'tweening' for writing animations, which should reduce the complexity of the animation code.

Thanks to @probablykasper for finding this.

A lot of the logic has been rewritten for reactivity, which further reduces
complexity.
2023-06-16 10:09:28 -05:00
Alex
a1b9a1d244 fix(web): error when refreshing asset view in memory page (#2789) 2023-06-16 10:09:16 -05:00
phillibl
377cec9fb1 Update xmp-sidecars.md (#2785)
Fixed a spelling mistake
2023-06-16 09:42:49 -05:00
phillibl
48b9c63268 Update README.md (#2787)
Changed Partner Sharing to Yes for mobile
2023-06-16 09:39:06 -05:00
418 changed files with 12721 additions and 8368 deletions

View File

@@ -34,6 +34,9 @@ jobs:
with:
token: ${{ secrets.ORG_RELEASE_TOKEN }}
- name: Install Poetry
run: pipx install poetry
- name: Bump version
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"

View File

@@ -19,6 +19,7 @@
<br/>
<p align="center">
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
</p>
## Disclaimer
@@ -82,8 +83,9 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| Public Sharing | No | Yes |
| Archive and Favorites | Yes | Yes |
| Global Map | No | Yes |
| Partner Sharing | No | Yes |
| Partner Sharing | Yes | Yes |
| Facial recognition and clustering | No | Yes |
| Offline support | Yes | No |
# Support the project

104
README_tr_TR.md Normal file
View File

@@ -0,0 +1,104 @@
<p align="center">
<br/>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
<a href="https://discord.gg/D8JsnBEuKb">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a>
<br/>
<br/>
</p>
<p align="center">
<img src="design/immich-logo.svg" width="150" title="Login With Custom URL">
</p>
<h3 align="center">Immich - Yüksek performanslı, kendine ait barındırılan fotoğraf ve video yedekleme çözümü</h3>
<br/>
<a href="https://immich.app">
<img src="design/immich-screenshots.png" title="Main Screenshot">
</a>
<br/>
<p align="center">
<a href="README.md">English</a>
<a href="README_zh_CN.md">中文</a>
</p>
## Feragatname
- ⚠️ Proje **çok aktif** bir şekilde geliştirilmektedir.
- ⚠️ Hatalar ve uygulama yapısını bozan değişiklikler olabilir.
- ⚠️ **Uygulamayı, fotoğraflarınızı ve videolarınızı saklamanın tek yöntemi olarak kullanmayın!**
## Content
- [Resmi Belgeler](https://immich.app/docs)
- [Yol Haritası](https://github.com/orgs/immich-app/projects/1)
- [Demo](#demo)
- [Özellikler](#özellikler)
- [Giriş](https://immich.app/docs/overview/introduction)
- [Kurulum](https://immich.app/docs/install/requirements)
- [Katkı Sağlama Rehberi](https://immich.app/docs/overview/support-the-project)
- [Projeyi Destekle](#projeyi-destekle)
## Belgeler
Kurulum dahil olmak üzere resmi belgeleri https://immich.app/ adresinde bulabilirsiniz.
## Demo
Web demo adresi: https://demo.immich.app
Mobil uygulama için `Server Endpoint URL` olarak `https://demo.immich.app/api` adresini kullanabilirsiniz.
```bash title="Demo Bilgileri"
Giriş bilgileri:
email: demo@immich.app
password: demo
```
```
Server Özellikleri: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
```
# Özellikler
| Özellikler | Mobile | Web |
| ----------------------------------------------------| ------ | --- |
| Videoları ve fotoğrafları yükleme ve görüntüleme | Evet | Evet |
| Uygulama açıldığında otomatik yedekleme | Evet | N/A |
| Yedekleme için seçilebilir albüm(ler) | Evet | N/A |
| Fotoğrafları ve videoları yerel cihaza yükleme | Evet | Evet |
| Çoklu kullanıcı desteği | Evet | Evet |
| Albüm ve paylaşılan albümler | Evet | Evet |
| Silinebilir/sürüklenebilir kaydırma çubuğu | Evet | Evet |
| RAW (HEIC, HEIF, DNG, Apple ProRaw) format desteği | Evet | Evet |
| Metadata'ya uygun görüntüleme (EXIF, map) | Evet | Evet |
| Metadata, objects, faces ve CLIP'e göre arama | Evet | Evet |
| Yönetimsel işlevler (kullanıcı yönetimi) | Hayır | Evet |
| Arka planda yedekleme | Evet | N/A |
| Sanal kaydırma | Evet | Evet |
| OAuth desteği | Evet | Evet |
| API anahtarları | N/A | Evet |
| LivePhoto yedekleme ve oynatma | iOS | Evet |
| Kullanıcı tanımlı depolama yapısı | Evet | Evet |
| Herkese açık paylaşım | Hayır | Evet |
| Arşiv ve Favoriler | Evet | Evet |
| Dünya haritası | Hayır | Evet |
| Partner paylaşımı | Evet | Evet |
| Yüz tanıma ve kümeleme | Hayır | Evet |
| Çevrimdışı destek | Evet | Hayır|
# Projeyi Destekle
Bu projeye bağlı kaldım ve durmayacağım. Belgeleri güncellemeye, yeni özellikler eklemeye ve hataları düzeltmeye devam edeceğim. Ancak bunu tek başıma yapamam. Bu yüzden devam etme konusunda bana motivasyon sağlamanız için yardımınıza ihtiyacım var.
[selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) bölümünde söylendiği üzere,bu projede takımımın ve benim projeye harcadağımız büyük bir çaba var. Bir gün bunu tam zamanlı olarak yapabilmeyi çok isterim. Bunu gerçekleştirebilmek için gerçekten sizlerin desteğine ihtiyacım var.
Eğer bu size doğru bir amaç gibi geliyorsa ve uygulamanın uzun bir süre boyunca kullanacağınız bir şey olduğunu düşünüyorsanız, aşağıdaki bağlantılardan birini kullanarak bana destek olabilirsiniz.
## Bağış
- [Aylık bağış](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [Bir seferlik bağış](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX

View File

@@ -23,6 +23,7 @@
<p align="center">
<a href="README.md">English</a>
<a href="README_tr_TR.md">Türkçe</a>
</p>

View File

@@ -10,12 +10,7 @@ REDIS_HOSTNAME=immich-redis-test
# Upload File Config
UPLOAD_LOCATION=./upload
# MAPBOX
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
ENABLE_MAPBOX=false
# WEB
MAPBOX_KEY=
VITE_SERVER_ENDPOINT=http://localhost:2283/api
TYPESENSE_ENABLED=false

View File

@@ -36,7 +36,6 @@ services:
- 3003:3003
volumes:
- ../machine-learning/app:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- model-cache:/cache
env_file:
- .env
@@ -95,7 +94,7 @@ services:
typesense:
container_name: immich_typesense
image: typesense/typesense:0.24.0
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
environment:
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
- TYPESENSE_DATA_DIR=/data
@@ -106,11 +105,11 @@ services:
redis:
container_name: immich_redis
image: redis:6.2
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
database:
container_name: immich_postgres
image: postgres:14
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
env_file:
- .env
environment:
@@ -137,6 +136,7 @@ services:
- 2283:8080
depends_on:
- immich-server
- immich-web
restart: always
volumes:

View File

@@ -25,12 +25,12 @@ services:
- immich-test-network
immich-redis-test:
container_name: immich-redis-test
image: redis:6.2
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
networks:
- immich-test-network
immich-database-test:
container_name: immich-database-test
image: postgres:14
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
env_file:
- .env.test
environment:

View File

@@ -33,7 +33,6 @@ services:
container_name: immich_machine_learning
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- model-cache:/cache
env_file:
- .env
@@ -48,7 +47,7 @@ services:
typesense:
container_name: immich_typesense
image: typesense/typesense:0.24.0
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
environment:
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
- TYPESENSE_DATA_DIR=/data
@@ -60,12 +59,12 @@ services:
redis:
container_name: immich_redis
image: redis:6.2
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
restart: always
database:
container_name: immich_postgres
image: postgres:14
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
env_file:
- .env
environment:
@@ -88,6 +87,7 @@ services:
- 2283:8080
depends_on:
- immich-server
- immich-web
restart: always
volumes:

View File

@@ -52,11 +52,11 @@ TYPESENSE_API_KEY=some-random-text
# TYPESENSE_URL uses base64 encoding for the nodes json.
# Example JSON that was used:
# [
# { 'host': 'typesense-1.example.net', 'port': '443', 'protocol': 'https' },
# { 'host': 'typesense-2.example.net', 'port': '443', 'protocol': 'https' },
# { 'host': 'typesense-3.example.net', 'port': '443', 'protocol': 'https' },
# ]
# TYPESENSE_URL=ha://WwogICAgeyAnaG9zdCc6ICd0eXBlc2Vuc2UtMS5leGFtcGxlLm5ldCcsICdwb3J0JzogJzQ0MycsICdwcm90b2NvbCc6ICdodHRwcycgfSwKICAgIHsgJ2hvc3QnOiAndHlwZXNlbnNlLTIuZXhhbXBsZS5uZXQnLCAncG9ydCc6ICc0NDMnLCAncHJvdG9jb2wnOiAnaHR0cHMnIH0sCiAgICB7ICdob3N0JzogJ3R5cGVzZW5zZS0zLmV4YW1wbGUubmV0JywgJ3BvcnQnOiAnNDQzJywgJ3Byb3RvY29sJzogJ2h0dHBzJyB9LApd
# { "host": "typesense-1.example.net", "port": "443", "protocol": "https" },
# { "host": "typesense-2.example.net", "port": "443", "protocol": "https" },
# { "host": "typesense-3.example.net", "port": "443", "protocol": "https" },
# ]
# TYPESENSE_URL=ha://WwogIHsgImhvc3QiOiAidHlwZXNlbnNlLTEuZXhhbXBsZS5uZXQiLCAicG9ydCI6ICI0NDMiLCAicHJvdG9jb2wiOiAiaHR0cHMiIH0sCiAgeyAiaG9zdCI6ICJ0eXBlc2Vuc2UtMi5leGFtcGxlLm5ldCIsICJwb3J0IjogIjQ0MyIsICJwcm90b2NvbCI6ICJodHRwcyIgfSwKICB7ICJob3N0IjogInR5cGVzZW5zZS0zLmV4YW1wbGUubmV0IiwgInBvcnQiOiAiNDQzIiwgInByb3RvY29sIjogImh0dHBzIiB9Cl0=
###################################################################################
# Reverse Geocoding
@@ -109,7 +109,7 @@ IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003
###################################################################################
# Immich Version - Optional
#
# This allows all immich docker images to be pinned to a specific version. By default,
# This allows all immich docker images to be pinned to a specific version. By default,
# the version is "release" but could be a specific version, like "v1.59.0".
###################################################################################

View File

@@ -0,0 +1,105 @@
---
title: June 2023 update
authors: [alextran]
tags: [update]
---
Hello everybody, Alex here!
I am back with another update on Immich. It has been only a month since my last update (May 18th, 2023), but it seems forever. I think the rapid releases of Immich and the amount of work make the perspective of time change in Immichs world. We have some exciting updates that I think you will like.
Before going into detail, on behalf of the core team, I would like to thank all of you for loving Immich and contributing to the project. Thank you for helping me make Immich an enjoyable alternative solution to Google Photos so that you have complete control of your data and privacy. I know we are still young and have a lot of work to do, but I am confident we will get there with help from the community. I appreciate all of you from the bottom of my heart!
<!--truncate-->
And now, to the exciting part, what is new in Immichs world?
- Initial support for existing gallery.
- Memory feature.
- Support XMP sidecar.
- Support more raw formats.
- Justified layout for web timeline and blurred thumbnail hash.
- Mechanism to host machine learning on a completely different machine.
## Support for existing gallery
I know this is the most controversial feature when it comes to Immichs way of ingesting photos and videos. For many users, having to upload photos and videos to Immich is simply not working. We listen, discuss, and digest this feature internally more than you imagine because it is not a simple feature to tackle while keeping the performance and the user experience at the top level, which is Immichs primary goal.
Thankfully, we have many great contributors and developers that want to make this come true. So we came up with an initial implementation of this feature in the form of a supporting read-only gallery.
To be concise, Immich can now read in the gallery files, register the path into the database, and then generate necessary files and put them through Immichs machine learning pipeline so you can use all the goodness of Immich without the need to upload them. Since this is the initial implementation, some actions/behavior are not yet supported, and we aim to build toward them in future releases, namely:
- Assets are not automatically synced and must instead be manually synced with the CLI tool.
- Only new files that are added to the gallery will be detected.
- Deleted and moved files will not be detected.
You can find more information on how to use the feature by reading the documentation [here](/docs/features/read-only-gallery).
## Memory feature
This is considered a fun feature that the team and I wanted to build for so long, but we had to put it off because of the refactoring of the code base. The code base is now in a good enough form to circle back and add more exciting features.
This memory feature is very much similar to GPhotos' implementation of “x years since…”. We are aiming to add more categories of memories in the future, such as “Spotlight of the day” or “Day of the Week highlights”
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/j5XZKvViPew"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
This feature is now available on the web and will be ported to the mobile app in the near future.
## Support XMP Sidecar
Immich can now import/upload XMP sidecars from the CLI and use the information as the metadata of assets.
## Support more raw formats.
With the recent updates on the dependencies of Immich, we are now extending and hardening support for multiple raw formats. So users with DSLR or mirrorless cameras can now upload their original files to Immich and have them displayed in high-quality thumbnails on the web and mobile view.
## Justified layout for web timeline and blurred thumbnail hash
This is an aesthetic improvement in user experience when browsing the timeline. Photos and videos are now displayed correctly with perspective orientation, making the browsing experience more pleasurable.
To further improve the browsing experience, we now added a blur hash to the thumbnail, so the transition is more natural with a dreamy fade in effect, similar to how our brain goes from faded to vivid memory
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/b95FLmGHRFc"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
## Hosting machine learning container on a different machine
With more capabilities Immich is building toward, machine learning will get more powerful and therefore require more resources to run effectively. However, we understand that users might not have the best server resources where they host the Immich instance. Therefore, we changed how machine learning interacts and receives the photos and videos to run through its inference pipeline.
The machine learning container is now a headless system that can run on any machine. As long as your Immich instance can communicate with the system running the machine learning container, it can send the files and receive the required information to make Immich powerful in terms of searching and intelligence. This helps you to utilize a more powerful machine in your home/infrastructure to perform the CPU-intensive tasks while letting Immich only handle the I/O operations for a pleasant and smooth experience.
---
So, those are the highlights for the team and the community after a busy month. There are a lot more changes and improvements. I encourage you to read some release notes, starting from version [v1.57.0](https://github.com/immich-app/immich/releases/tag/v1.57.0) to now.
Thank you, and I am asking for your support for the project. I hope to be a full-time maintainer of Immich one day to dedicate myself to the project as my life works for the community and my family. You can find the support channels below:
- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502)
- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
- [Liberapay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything
Cheer!
Until next time!
Alex

View File

@@ -42,6 +42,7 @@ immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recur
| --server / -s | Immich's server address |
| --threads / -t | Number of threads to use (Default 5) |
| --album/ -al | Create albums for assets based on the parent folder or a given name |
| --import/ -i | Import gallery |
### Obtain the API Key

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -0,0 +1,103 @@
# Read-only Gallery [Experimental]
## Overview
This feature enables users to use an existing gallery without uploading the assets to Immich.
Upon syncing the file information, it will be read by Immich to generate supported files.
:::caution
This feature is still in an experimental stage. And this is an initial implementation and will receive improvements in the future.
The current limitations of this feature are:
- Assets are not automatically synced and must instead be manually synced with the CLI tool.
- Only new files that are added to the gallery will be detected.
- Deleted and moved files will not be detected.
:::
## Usage
:::tip Example scenario
On the VM/system that Immich is running, I have 2 galleries that I want to use with Immich.
- My gallery is stored at `/mnt/media/precious-memory`
- My wife's gallery is stored at `/mnt/media/childhood-memory`
We will use those values in the steps below.
:::
### Mount the gallery to the containers.
`immich-server` and `immich-microservices` containers will need access to the gallery. Mount the directory path as in the example below
```diff title="docker-compose.yml"
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "immich" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/media/precious-memory:/mnt/media/precious-memory
+ - /mnt/media/childhood-memory:/mnt/media/childhood-memory
env_file:
- .env
depends_on:
- redis
- database
- typesense
restart: always
immich-microservices:
container_name: immich_microservices
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "microservices" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/media/precious-memory:/mnt/media/precious-memory
+ - /mnt/media/childhood-memory:/mnt/media/childhood-memory
env_file:
- .env
depends_on:
- redis
- database
- typesense
restart: always
```
:::tip
Internal and external path have to be identical.
:::
_Remember to bring the container down/up to register the changes. Make sure you can see the mounted path in the container._
### Register the path for the user.
This action is done by the admin of the instance.
- Navigate to `Administration > Users` page on the web.
- Click on the user edit button.
- Add the gallery path to the `External Path` field for the corresponding user and confirm the changes.
<img src={require('./img/me.png').default} width='33%' title='My Account Storage Path' />
<img src={require('./img/my-wife.png').default} width='33%' title='My Wifes Account Storage Path' />
### Sync with the CLI tool.
- Install or update the [CLI Tool](/docs/features/bulk-upload.md). The import feature is supported from version `v0.39.0` of the CLI
- Run the command below to sync the gallery with Immich.
```bash title="Import my gallery"
immich upload --key <my-api-key> --server http://my-server-ip:2283/api /mnt/media/precious-memory --recursive --import
```
```bash title="Import my wife gallery"
immich upload --key <my-wife-api-key> --server http://my-server-ip:2283/api /mnt/media/childhood-memory --recursive --import
```
The `--import` flag will tell Immich to import the files by path instead of uploading them.

View File

@@ -4,7 +4,7 @@ Immich can ingest XMP sidecars on file upload (via the CLI) as well as detect ne
<img src={require('./img/xmp-sidecars.png').default} title='XMP sidecars' />
XMP sidecars are external XML files that contain metadata related to media files. Many applications read and write these files either exclusively or in addition to the metadata written to image files. They can be a powerful tool for editing and storing metadata of a media file without modifying the mdia file itself. When Immich receives or detects an XMP sidecar for a media file, it will attempt to extract the metadata from both the sidecar as well as the media file. It will prioritize the metadata for fields in the sidecar but will fall back and use the metadata in the media file if necessary.
XMP sidecars are external XML files that contain metadata related to media files. Many applications read and write these files either exclusively or in addition to the metadata written to image files. They can be a powerful tool for editing and storing metadata of a media file without modifying the media file itself. When Immich receives or detects an XMP sidecar for a media file, it will attempt to extract the metadata from both the sidecar as well as the media file. It will prioritize the metadata for fields in the sidecar but will fall back and use the metadata in the media file if necessary.
When importing files via the CLI bulk uploader, Immich will automatically detect XMP sidecar files as files that exist next to the original media file and have the exact same name with an additional `.xmp` file extension (i.e., `PXL_20230401_203352928.MP.jpg` and `PXL_20230401_203352928.MP.jpg.xmp`).

View File

@@ -0,0 +1,186 @@
# Environment Variables
## Docker Compose
| Variable | Description | Default | Services |
| :---------------- | :-------------------- | :-------: | :------------------------------------------------------------- |
| `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning, web, proxy, typesense |
| `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices |
:::tip
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
:::
## General
| Variable | Description | Default | Services |
| :-------------------------- | :------------------------------------------- | :----------: | :------------------------------------------- |
| `TZ` | Timezone | | microservices |
| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning, web |
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload` | server, microservices |
| `PUBLIC_LOGIN_PAGE_MESSAGE` | Public Login Page Message | | web |
:::tip
`TZ` is only used by the `exiftool` as a fallback in case the timezone cannot be determined from the image metadata.
`exiftool` is only present in the microservices container.
:::
## Geocoding
| Variable | Description | Default | Services |
| :--------------------------------- | :---------------------------------- | :--------------------------: | :------------ |
| `DISABLE_REVERSE_GEOCODING` | Disable Reverse Geocoding Precision | `false` | microservices |
| `REVERSE_GEOCODING_PRECISION` | Reverse Geocoding Precision | `3` | microservices |
| `REVERSE_GEOCODING_DUMP_DIRECTORY` | Reverse Geocoding Dump Directory | `./.reverse-geocoding-dump/` | microservices |
## Ports
| Variable | Description | Default | Services |
| :---------------------- | :-------------------- | :-----: | :--------------- |
| `PORT` | Web Port | `3000` | web |
| `SERVER_PORT` | Server Port | `3001` | server |
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
## URLs
| Variable | Description | Default | Services |
| :---------------------------- | :------------------------------------------------------- | :-----------------------------------: | :-------------------- |
| `IMMICH_WEB_URL` | Immich Web URL | `http://immich-web:3000` | proxy |
| `IMMICH_SERVER_URL` | Immich Server URL | `http://immich-server:3001` | web, proxy |
| `IMMICH_MACHINE_LEARNING_URL` | Immich Machine Learning URL, set `"false"` to disable ML | `http://immich-machine-learning:3003` | server, microservices |
| `PUBLIC_IMMICH_SERVER_URL` | Public Immich URL | `http://immich-server:3001` | web |
| `IMMICH_API_URL_EXTERNAL` | Immich API URL External | `/api` | web |
:::info
The above paths are modifying the internal paths of the containers.
:::
## Database
| Variable | Description | Default | Services |
| :------------ | :---------------- | :---------: | :-------------------- |
| `DB_URL` | Database URL | | server, microservices |
| `DB_HOSTNAME` | Database Host | `localhost` | server, microservices |
| `DB_PORT` | Database Port | `5432` | server, microservices |
| `DB_USERNAME` | Database User | `postgres` | server, microservices |
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices |
| `DB_DATABASE` | Database Name | `immich` | server, microservices |
:::info
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored.
:::
## Redis
| Variable | Description | Default | Services |
| :--------------- | :------------- | :------------: | :-------------------- |
| `REDIS_URL` | Redis URL | | server, microservices |
| `REDIS_HOSTNAME` | Redis Host | `immich_redis` | server, microservices |
| `REDIS_PORT` | Redis Port | `6379` | server, microservices |
| `REDIS_DBINDEX` | Redis DB Index | `0` | server, microservices |
| `REDIS_USERNAME` | Redis Username | | server, microservices |
| `REDIS_PASSWORD` | Redis Password | | server, microservices |
| `REDIS_SOCKET` | Redis Socket | | server, microservices |
:::info
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
More info can be found in the upstream [ioredis](https://ioredis.readthedocs.io/en/latest/API/) documentation.
- When `REDIS_URL` is defined, the other redis (`REDIS_*`) variables are ignored.
- When `REDIS_SOCKET` is defined, the other redis (`REDIS_*`) variables are ignored.
:::
Redis (Sentinel) URL example JSON before encoding:
```json
{
"sentinels": [
{
"host": "redis-sentinel-node-0",
"port": 26379
},
{
"host": "redis-sentinel-node-1",
"port": 26379
},
{
"host": "redis-sentinel-node-2",
"port": 26379
}
],
"name": "redis-sentinel"
}
```
## Typesense
| Variable | Description | Default | Services |
| :------------------- | :----------------------- | :---------: | :------------------------------- |
| `TYPESENSE_ENABLED` | Enable Typesense | | server, microservices |
| `TYPESENSE_URL` | Typesense URL | | server, microservices |
| `TYPESENSE_HOST` | Typesense Host | `typesense` | server, microservices |
| `TYPESENSE_PORT` | Typesense Port | `8108` | server, microservices |
| `TYPESENSE_PROTOCOL` | Typesense Protocol | `http` | server, microservices |
| `TYPESENSE_API_KEY` | Typesense API Key | | server, microservices, typesense |
| `TYPESENSE_DATA_DIR` | Typesense Data Directory | `/data` | typesense |
:::info
`TYPESENSE_URL` must start with `ha://` and then include a `base64` encoded JSON string for the configuration.
`TYPESENSE_ENABLED`: Anything other than `false`, behaves as `true`.
Even undefined is treated as `true`.
- When `TYPESENSE_URL` is defined, the other typesense (`TYPESENSE_*`) variables are ignored.
:::
Typesense URL example JSON before encoding:
```json
[
{
"host": "typesense-1.example.net",
"port": "443",
"protocol": "https"
},
{
"host": "typesense-2.example.net",
"port": "443",
"protocol": "https"
},
{
"host": "typesense-3.example.net",
"port": "443",
"protocol": "https"
}
]
```
## Machine Learning
| Variable | Description | Default | Services |
| :------------------------------------------ | :----------------------------- | :-------------------: | :--------------- |
| `MACHINE_LEARNING_MIN_FACE_SCORE` | Minimum Face Score | `0.7` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL` | Model TTL | `300` | machine learning |
| `MACHINE_LEARNING_EAGER_STARTUP` | Eager Startup | `true` | machine learning |
| `MACHINE_LEARNING_MIN_TAG_SCORE` | Minimum Tag Score | `0.9` | machine learning |
| `MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL` | Facial Recognition Model | `buffalo_l` | machine learning |
| `MACHINE_LEARNING_CLIP_TEXT_MODEL` | Clip Text Model | `clip-ViT-B-32` | machine learning |
| `MACHINE_LEARNING_CLIP_IMAGE_MODEL` | Clip Image Model | `clip-ViT-B-32` | machine learning |
| `MACHINE_LEARNING_CLASSIFICATION_MODEL` | Classification Model | `microsoft/resnet-50` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | ML Cache Location | `/cache` | machine learning |
| `TRANSFORMERS_CACHE` | ML Transformers Cache Location | `/cache` | machine learning |

View File

@@ -105,6 +105,11 @@ const config = {
position: 'right',
label: 'API',
},
{
to: '/blog',
position: 'right',
label: 'Blog',
},
{
href: 'https://github.com/immich-app/immich',
label: 'GitHub',

776
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@
"@docusaurus/module-type-aliases": "2.1.0",
"@tsconfig/docusaurus": "^1.0.5",
"prettier": "^2.8.8",
"typescript": "^4.7.4"
"typescript": "^5.0.0"
},
"browserslist": {
"production": [

View File

@@ -1,4 +1,5 @@
FROM python:3.11 as builder
FROM python:3.11.4-bullseye@sha256:5b401676aff858495a5c9c726c60b8b73fe52833e9e16eccdb59e93d52741727 as builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=true
@@ -12,15 +13,16 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
FROM python:3.11-slim
FROM python:3.11.4-slim-bullseye@sha256:91d194f58f50594cda71dcd2e8fdefd90e7ecc57d07823813b67c8521e565dcd
WORKDIR /usr/src/app
ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \
PYTHONPATH=`pwd`
PYTHONPATH=/usr/src
COPY --from=builder /opt/venv /opt/venv
COPY app .
ENTRYPOINT ["python", "main.py"]
ENTRYPOINT ["python", "-m", "app.main"]

View File

@@ -11,3 +11,12 @@ Running `poetry install --no-root --with dev` will install everything you need i
To add or remove dependencies, you can use the commands `poetry add $PACKAGE_NAME` and `poetry remove $PACKAGE_NAME`, respectively.
Be sure to commit the `poetry.lock` and `pyproject.toml` files to reflect any changes in dependencies.
# Load Testing
To measure inference throughput and latency, you can use [Locust](https://locust.io/) using the provided `locustfile.py`.
Locust works by querying the model endpoints and aggregating their statistics, meaning the app must be deployed.
You can run `load_test.sh` to automatically deploy the app locally and start Locust, optionally adjusting its env variables as needed.
Alternatively, for more custom testing, you may also run `locust` directly: see the [documentation](https://docs.locust.io/en/stable/index.html). Note that in Locust's jargon, concurrency is measured in `users`, and each user runs one task at a time. To achieve a particular per-endpoint concurrency, multiply that number by the number of endpoints to be queried. For example, if there are 3 endpoints and you want each of them to receive 8 requests at a time, you should set the number of users to 24.

View File

View File

@@ -0,0 +1,31 @@
from pathlib import Path
from pydantic import BaseSettings
from .schemas import ModelType
class Settings(BaseSettings):
cache_folder: str = "/cache"
classification_model: str = "microsoft/resnet-50"
clip_image_model: str = "clip-ViT-B-32"
clip_text_model: str = "clip-ViT-B-32"
facial_recognition_model: str = "buffalo_l"
min_tag_score: float = 0.9
eager_startup: bool = True
model_ttl: int = 300
host: str = "0.0.0.0"
port: int = 3003
workers: int = 1
min_face_score: float = 0.7
class Config(BaseSettings.Config):
env_prefix = "MACHINE_LEARNING_"
case_sensitive = False
def get_cache_dir(model_name: str, model_type: ModelType) -> Path:
return Path(settings.cache_folder, model_type.value, model_name)
settings = Settings()

View File

@@ -1,58 +1,57 @@
import os
from io import BytesIO
from typing import Any
from cache import ModelCache
from schemas import (
import cv2
import numpy as np
import uvicorn
from fastapi import Body, Depends, FastAPI
from PIL import Image
from .config import settings
from .models.base import InferenceModel
from .models.cache import ModelCache
from .schemas import (
EmbeddingResponse,
FaceResponse,
TagResponse,
MessageResponse,
ModelType,
TagResponse,
TextModelRequest,
TextResponse,
VisionModelRequest,
)
import uvicorn
from PIL import Image
from fastapi import FastAPI, HTTPException
from models import get_model, run_classification, run_facial_recognition
classification_model = os.getenv(
"MACHINE_LEARNING_CLASSIFICATION_MODEL", "microsoft/resnet-50"
)
clip_image_model = os.getenv("MACHINE_LEARNING_CLIP_IMAGE_MODEL", "clip-ViT-B-32")
clip_text_model = os.getenv("MACHINE_LEARNING_CLIP_TEXT_MODEL", "clip-ViT-B-32")
facial_recognition_model = os.getenv(
"MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL", "buffalo_l"
)
min_tag_score = float(os.getenv("MACHINE_LEARNING_MIN_TAG_SCORE", 0.9))
eager_startup = (
os.getenv("MACHINE_LEARNING_EAGER_STARTUP", "true") == "true"
) # loads all models at startup
model_ttl = int(os.getenv("MACHINE_LEARNING_MODEL_TTL", 300))
_model_cache = None
app = FastAPI()
@app.on_event("startup")
async def startup_event() -> None:
global _model_cache
_model_cache = ModelCache(ttl=model_ttl, revalidate=True)
app.state.model_cache = ModelCache(ttl=settings.model_ttl, revalidate=True)
same_clip = settings.clip_image_model == settings.clip_text_model
app.state.clip_vision_type = ModelType.CLIP if same_clip else ModelType.CLIP_VISION
app.state.clip_text_type = ModelType.CLIP if same_clip else ModelType.CLIP_TEXT
models = [
(classification_model, "image-classification"),
(clip_image_model, "clip"),
(clip_text_model, "clip"),
(facial_recognition_model, "facial-recognition"),
(settings.classification_model, ModelType.IMAGE_CLASSIFICATION),
(settings.clip_image_model, app.state.clip_vision_type),
(settings.clip_text_model, app.state.clip_text_type),
(settings.facial_recognition_model, ModelType.FACIAL_RECOGNITION),
]
# Get all models
for model_name, model_type in models:
if eager_startup:
await _model_cache.get_cached_model(model_name, model_type)
if settings.eager_startup:
await app.state.model_cache.get(model_name, model_type)
else:
get_model(model_name, model_type)
InferenceModel.from_model_type(model_type, model_name)
def dep_pil_image(byte_image: bytes = Body(...)) -> Image.Image:
return Image.open(BytesIO(byte_image))
def dep_cv_image(byte_image: bytes = Body(...)) -> cv2.Mat:
byte_image_np = np.frombuffer(byte_image, np.uint8)
return cv2.imdecode(byte_image_np, cv2.IMREAD_COLOR)
@app.get("/", response_model=MessageResponse)
@@ -65,15 +64,18 @@ def ping() -> str:
return "pong"
@app.post("/image-classifier/tag-image", response_model=TagResponse, status_code=200)
async def image_classification(payload: VisionModelRequest) -> list[str]:
if _model_cache is None:
raise HTTPException(status_code=500, detail="Unable to load model.")
model = await _model_cache.get_cached_model(
classification_model, "image-classification"
@app.post(
"/image-classifier/tag-image",
response_model=TagResponse,
status_code=200,
)
async def image_classification(
image: Image.Image = Depends(dep_pil_image),
) -> list[str]:
model = await app.state.model_cache.get(
settings.classification_model, ModelType.IMAGE_CLASSIFICATION
)
labels = run_classification(model, payload.image_path, min_tag_score)
labels = model.predict(image)
return labels
@@ -82,13 +84,13 @@ async def image_classification(payload: VisionModelRequest) -> list[str]:
response_model=EmbeddingResponse,
status_code=200,
)
async def clip_encode_image(payload: VisionModelRequest) -> list[float]:
if _model_cache is None:
raise HTTPException(status_code=500, detail="Unable to load model.")
model = await _model_cache.get_cached_model(clip_image_model, "clip")
image = Image.open(payload.image_path)
embedding = model.encode(image).tolist()
async def clip_encode_image(
image: Image.Image = Depends(dep_pil_image),
) -> list[float]:
model = await app.state.model_cache.get(
settings.clip_image_model, app.state.clip_vision_type
)
embedding = model.predict(image)
return embedding
@@ -98,31 +100,34 @@ async def clip_encode_image(payload: VisionModelRequest) -> list[float]:
status_code=200,
)
async def clip_encode_text(payload: TextModelRequest) -> list[float]:
if _model_cache is None:
raise HTTPException(status_code=500, detail="Unable to load model.")
model = await _model_cache.get_cached_model(clip_text_model, "clip")
embedding = model.encode(payload.text).tolist()
model = await app.state.model_cache.get(
settings.clip_text_model, app.state.clip_text_type
)
embedding = model.predict(payload.text)
return embedding
@app.post(
"/facial-recognition/detect-faces", response_model=FaceResponse, status_code=200
"/facial-recognition/detect-faces",
response_model=FaceResponse,
status_code=200,
)
async def facial_recognition(payload: VisionModelRequest) -> list[dict[str, Any]]:
if _model_cache is None:
raise HTTPException(status_code=500, detail="Unable to load model.")
model = await _model_cache.get_cached_model(
facial_recognition_model, "facial-recognition"
async def facial_recognition(
image: cv2.Mat = Depends(dep_cv_image),
) -> list[dict[str, Any]]:
model = await app.state.model_cache.get(
settings.facial_recognition_model, ModelType.FACIAL_RECOGNITION
)
faces = run_facial_recognition(model, payload.image_path)
faces = model.predict(image)
return faces
if __name__ == "__main__":
host = os.getenv("MACHINE_LEARNING_HOST", "0.0.0.0")
port = int(os.getenv("MACHINE_LEARNING_PORT", 3003))
is_dev = os.getenv("NODE_ENV") == "development"
uvicorn.run("main:app", host=host, port=port, reload=is_dev, workers=1)
uvicorn.run(
"app.main:app",
host=settings.host,
port=settings.port,
reload=is_dev,
workers=settings.workers,
)

View File

@@ -1,117 +0,0 @@
import torch
from insightface.app import FaceAnalysis
from pathlib import Path
import os
from transformers import pipeline, Pipeline
from sentence_transformers import SentenceTransformer
from typing import Any
import cv2 as cv
cache_folder = os.getenv("MACHINE_LEARNING_CACHE_FOLDER", "/cache")
device = "cuda" if torch.cuda.is_available() else "cpu"
def get_model(model_name: str, model_type: str, **model_kwargs):
"""
Instantiates the specified model.
Args:
model_name: Name of model in the model hub used for the task.
model_type: Model type or task, which determines which model zoo is used.
`facial-recognition` uses Insightface, while all other models use the HF Model Hub.
Options:
`image-classification`, `clip`,`facial-recognition`, `tokenizer`, `processor`
Returns:
model: The requested model.
"""
cache_dir = _get_cache_dir(model_name, model_type)
match model_type:
case "facial-recognition":
model = _load_facial_recognition(
model_name, cache_dir=cache_dir, **model_kwargs
)
case "clip":
model = SentenceTransformer(
model_name, cache_folder=cache_dir, **model_kwargs
)
case _:
model = pipeline(
model_type,
model_name,
model_kwargs={"cache_dir": cache_dir, **model_kwargs},
)
return model
def run_classification(
model: Pipeline, image_path: str, min_score: float | None = None
):
predictions: list[dict[str, Any]] = model(image_path) # type: ignore
result = {
tag
for pred in predictions
for tag in pred["label"].split(", ")
if min_score is None or pred["score"] >= min_score
}
return list(result)
def run_facial_recognition(
model: FaceAnalysis, image_path: str
) -> list[dict[str, Any]]:
img = cv.imread(image_path)
height, width, _ = img.shape
results = []
faces = model.get(img)
for face in faces:
x1, y1, x2, y2 = face.bbox
results.append(
{
"imageWidth": width,
"imageHeight": height,
"boundingBox": {
"x1": round(x1),
"y1": round(y1),
"x2": round(x2),
"y2": round(y2),
},
"score": face.det_score.item(),
"embedding": face.normed_embedding.tolist(),
}
)
return results
def _load_facial_recognition(
model_name: str,
min_face_score: float | None = None,
cache_dir: Path | str | None = None,
**model_kwargs,
):
if cache_dir is None:
cache_dir = _get_cache_dir(model_name, "facial-recognition")
if isinstance(cache_dir, Path):
cache_dir = cache_dir.as_posix()
if min_face_score is None:
min_face_score = float(os.getenv("MACHINE_LEARNING_MIN_FACE_SCORE", 0.7))
model = FaceAnalysis(
name=model_name,
root=cache_dir,
allowed_modules=["detection", "recognition"],
**model_kwargs,
)
model.prepare(ctx_id=0, det_thresh=min_face_score, det_size=(640, 640))
return model
def _get_cache_dir(model_name: str, model_type: str) -> Path:
return Path(cache_folder, device, model_type, model_name)

View File

@@ -0,0 +1,3 @@
from .clip import CLIPSTTextEncoder, CLIPSTVisionEncoder
from .facial_recognition import FaceRecognizer
from .image_classification import ImageClassifier

View File

@@ -0,0 +1,52 @@
from __future__ import annotations
from abc import abstractmethod, ABC
from pathlib import Path
from typing import Any
from ..config import get_cache_dir
from ..schemas import ModelType
class InferenceModel(ABC):
_model_type: ModelType
def __init__(
self,
model_name: str,
cache_dir: Path | None = None,
):
self.model_name = model_name
self._cache_dir = (
cache_dir
if cache_dir is not None
else get_cache_dir(model_name, self.model_type)
)
@abstractmethod
def predict(self, inputs: Any) -> Any:
...
@property
def model_type(self) -> ModelType:
return self._model_type
@property
def cache_dir(self) -> Path:
return self._cache_dir
@cache_dir.setter
def cache_dir(self, cache_dir: Path):
self._cache_dir = cache_dir
@classmethod
def from_model_type(
cls, model_type: ModelType, model_name, **model_kwargs
) -> InferenceModel:
subclasses = {
subclass._model_type: subclass for subclass in cls.__subclasses__()
}
if model_type not in subclasses:
raise ValueError(f"Unsupported model type: {model_type}")
return subclasses[model_type](model_name, **model_kwargs)

View File

@@ -1,8 +1,11 @@
from aiocache.plugins import TimingPlugin, BasePlugin
import asyncio
from aiocache.backends.memory import SimpleMemoryCache
from aiocache.lock import OptimisticLock
from typing import Any
from models import get_model
from aiocache.plugins import BasePlugin, TimingPlugin
from ..schemas import ModelType
from .base import InferenceModel
class ModelCache:
@@ -10,7 +13,7 @@ class ModelCache:
def __init__(
self,
ttl: int | None = None,
ttl: float | None = None,
revalidate: bool = False,
timeout: int | None = None,
profiling: bool = False,
@@ -35,9 +38,9 @@ class ModelCache:
ttl=ttl, timeout=timeout, plugins=plugins, namespace=None
)
async def get_cached_model(
self, model_name: str, model_type: str, **model_kwargs
) -> Any:
async def get(
self, model_name: str, model_type: ModelType, **model_kwargs
) -> InferenceModel:
"""
Args:
model_name: Name of model in the model hub used for the task.
@@ -47,11 +50,16 @@ class ModelCache:
model: The requested model.
"""
key = self.cache.build_key(model_name, model_type)
key = self.cache.build_key(model_name, model_type.value)
model = await self.cache.get(key)
if model is None:
async with OptimisticLock(self.cache, key) as lock:
model = get_model(model_name, model_type, **model_kwargs)
model = await asyncio.get_running_loop().run_in_executor(
None,
lambda: InferenceModel.from_model_type(
model_type, model_name, **model_kwargs
),
)
await lock.cas(model, ttl=self.ttl)
return model

View File

@@ -0,0 +1,37 @@
from pathlib import Path
from PIL.Image import Image
from sentence_transformers import SentenceTransformer
from ..schemas import ModelType
from .base import InferenceModel
class CLIPSTEncoder(InferenceModel):
_model_type = ModelType.CLIP
def __init__(
self,
model_name: str,
cache_dir: Path | None = None,
**model_kwargs,
):
super().__init__(model_name, cache_dir)
self.model = SentenceTransformer(
self.model_name,
cache_folder=self.cache_dir.as_posix(),
**model_kwargs,
)
def predict(self, image_or_text: Image | str) -> list[float]:
return self.model.encode(image_or_text).tolist()
# stubs to allow different behavior between the two in the future
# and handle loading different image and text clip models
class CLIPSTVisionEncoder(CLIPSTEncoder):
_model_type = ModelType.CLIP_VISION
class CLIPSTTextEncoder(CLIPSTEncoder):
_model_type = ModelType.CLIP_TEXT

View File

@@ -0,0 +1,59 @@
from pathlib import Path
from typing import Any
import cv2
from insightface.app import FaceAnalysis
from ..config import settings
from ..schemas import ModelType
from .base import InferenceModel
class FaceRecognizer(InferenceModel):
_model_type = ModelType.FACIAL_RECOGNITION
def __init__(
self,
model_name: str,
min_score: float = settings.min_face_score,
cache_dir: Path | None = None,
**model_kwargs,
):
super().__init__(model_name, cache_dir)
self.min_score = min_score
model = FaceAnalysis(
name=self.model_name,
root=self.cache_dir.as_posix(),
allowed_modules=["detection", "recognition"],
**model_kwargs,
)
model.prepare(
ctx_id=0,
det_thresh=self.min_score,
det_size=(640, 640),
)
self.model = model
def predict(self, image: cv2.Mat) -> list[dict[str, Any]]:
height, width, _ = image.shape
results = []
faces = self.model.get(image)
for face in faces:
x1, y1, x2, y2 = face.bbox
results.append(
{
"imageWidth": width,
"imageHeight": height,
"boundingBox": {
"x1": round(x1),
"y1": round(y1),
"x2": round(x2),
"y2": round(y2),
},
"score": face.det_score.item(),
"embedding": face.normed_embedding.tolist(),
}
)
return results

View File

@@ -0,0 +1,40 @@
from pathlib import Path
from PIL.Image import Image
from transformers.pipelines import pipeline
from ..config import settings
from ..schemas import ModelType
from .base import InferenceModel
class ImageClassifier(InferenceModel):
_model_type = ModelType.IMAGE_CLASSIFICATION
def __init__(
self,
model_name: str,
min_score: float = settings.min_tag_score,
cache_dir: Path | None = None,
**model_kwargs,
):
super().__init__(model_name, cache_dir)
self.min_score = min_score
self.model = pipeline(
self.model_type.value,
self.model_name,
model_kwargs={"cache_dir": self.cache_dir, **model_kwargs},
)
def predict(self, image: Image) -> list[str]:
predictions = self.model(image)
tags = list(
{
tag
for pred in predictions
for tag in pred["label"].split(", ")
if pred["score"] >= self.min_score
}
)
return tags

View File

@@ -1,3 +1,5 @@
from enum import Enum
from pydantic import BaseModel
@@ -9,14 +11,6 @@ def to_lower_camel(string: str) -> str:
return "".join(tokens)
class VisionModelRequest(BaseModel):
image_path: str
class Config:
alias_generator = to_lower_camel
allow_population_by_field_name = True
class TextModelRequest(BaseModel):
text: str
@@ -62,3 +56,11 @@ class Face(BaseModel):
class FaceResponse(BaseModel):
__root__: list[Face]
class ModelType(Enum):
IMAGE_CLASSIFICATION = "image-classification"
CLIP = "clip"
CLIP_VISION = "clip-vision"
CLIP_TEXT = "clip-text"
FACIAL_RECOGNITION = "facial-recognition"

24
machine-learning/load_test.sh Executable file
View File

@@ -0,0 +1,24 @@
export MACHINE_LEARNING_CACHE_FOLDER=/tmp/model_cache
export MACHINE_LEARNING_MIN_FACE_SCORE=0.034 # returns 1 face per request; setting this to 0 blows up the number of faces to the thousands
export MACHINE_LEARNING_MIN_TAG_SCORE=0.0
export PID_FILE=/tmp/locust_pid
export LOG_FILE=/tmp/gunicorn.log
export HEADLESS=false
export HOST=127.0.0.1:3003
export CONCURRENCY=4
export NUM_ENDPOINTS=3
export PYTHONPATH=app
gunicorn app.main:app --worker-class uvicorn.workers.UvicornWorker \
--bind $HOST --daemon --error-logfile $LOG_FILE --pid $PID_FILE
while true ; do
echo "Loading models..."
sleep 5
if cat $LOG_FILE | grep -q -E "startup complete"; then break; fi
done
# "users" are assigned only one task, so multiply concurrency by the number of tasks
locust --host http://$HOST --web-host 127.0.0.1 \
--run-time 120s --users $(($CONCURRENCY * $NUM_ENDPOINTS)) $(if $HEADLESS; then echo "--headless"; fi)
if [[ -e $PID_FILE ]]; then kill $(cat $PID_FILE); fi

View File

@@ -0,0 +1,52 @@
from io import BytesIO
from locust import HttpUser, events, task
from PIL import Image
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
global byte_image
image = Image.new("RGB", (1000, 1000))
byte_image = BytesIO()
image.save(byte_image, format="jpeg")
class InferenceLoadTest(HttpUser):
abstract: bool = True
host = "http://127.0.0.1:3003"
data: bytes
headers: dict[str, str] = {"Content-Type": "image/jpg"}
# re-use the image across all instances in a process
def on_start(self):
global byte_image
self.data = byte_image.getvalue()
class ClassificationLoadTest(InferenceLoadTest):
@task
def classify(self):
self.client.post(
"/image-classifier/tag-image", data=self.data, headers=self.headers
)
class CLIPLoadTest(InferenceLoadTest):
@task
def encode_image(self):
self.client.post(
"/sentence-transformer/encode-image",
data=self.data,
headers=self.headers,
)
class RecognitionLoadTest(InferenceLoadTest):
@task
def recognize(self):
self.client.post(
"/facial-recognition/detect-faces",
data=self.data,
headers=self.headers,
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.59.1"
version = "1.64.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"
@@ -27,6 +27,8 @@ aiocache = "^0.12.1"
mypy = "^1.3.0"
black = "^23.3.0"
pytest = "^7.3.1"
locust = "^2.15.1"
gunicorn = "^20.1.0"
[[tool.poetry.source]]
name = "pytorch-cpu"

View File

@@ -72,6 +72,11 @@ android {
}
buildTypes {
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
}
release {
signingConfig signingConfigs.release
}

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 84,
"android.injected.version.name" => "1.61.0",
"android.injected.version.code" => 87,
"android.injected.version.name" => "1.64.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

@@ -217,6 +217,7 @@
"search_page_selfies": "Selfies",
"search_page_things": "Things",
"search_page_videos": "Videos",
"search_page_people": "People",
"search_page_view_all_button": "View all",
"search_page_your_activity": "Your activity",
"search_result_page_new_search_hint": "New Search",
@@ -285,5 +286,6 @@
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
}
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"all_people_page_title": "People"
}

View File

@@ -39,7 +39,7 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.2):
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- Toast (4.0.0)
@@ -128,21 +128,21 @@ SPEC CHECKSUMS:
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker_ios: 58b9c4269cb176f89acea5e5d043c9358f2d25f8
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
integration_test: 13825b8a9334a850581300559b8839134b124670
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
video_player_avfoundation: 6d971a232d72e6ee25368378d48a079dea01f1cf
video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382

View File

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

View File

@@ -0,0 +1,21 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
final showControlsProvider = StateNotifierProvider<ShowControls, bool>((ref) {
return ShowControls(ref);
});
class ShowControls extends StateNotifier<bool> {
ShowControls(this.ref) : super(true);
final Ref ref;
bool get show => state;
set show(bool value) {
state = value;
}
void toggle() {
state = !state;
}
}

View File

@@ -0,0 +1,46 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
class VideoPlaybackControls {
VideoPlaybackControls({required this.position, required this.mute});
final double position;
final bool mute;
}
final videoPlayerControlsProvider =
StateNotifierProvider<VideoPlayerControls, VideoPlaybackControls>((ref) {
return VideoPlayerControls(ref);
});
class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
VideoPlayerControls(this.ref)
: super(
VideoPlaybackControls(
position: 0,
mute: false,
),
);
final Ref ref;
VideoPlaybackControls get value => state;
set value(VideoPlaybackControls value) {
state = value;
}
double get position => state.position;
bool get mute => state.mute;
set position(double value) {
state = VideoPlaybackControls(position: value, mute: state.mute);
}
set mute(bool value) {
state = VideoPlaybackControls(position: state.position, mute: value);
}
void toggleMute() {
state = VideoPlaybackControls(position: state.position, mute: !state.mute);
}
}

View File

@@ -0,0 +1,35 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
class VideoPlaybackValue {
VideoPlaybackValue({required this.position, required this.duration});
final Duration position;
final Duration duration;
}
final videoPlaybackValueProvider =
StateNotifierProvider<VideoPlaybackValueState, VideoPlaybackValue>((ref) {
return VideoPlaybackValueState(ref);
});
class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
VideoPlaybackValueState(this.ref)
: super(
VideoPlaybackValue(
position: Duration.zero,
duration: Duration.zero,
),
);
final Ref ref;
VideoPlaybackValue get value => state;
set value(VideoPlaybackValue value) {
state = value;
}
set position(Duration value) {
state = VideoPlaybackValue(position: value, duration: state.duration);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
/// A widget that animates implicitly between a play and a pause icon.
class AnimatedPlayPause extends StatefulWidget {
const AnimatedPlayPause({
Key? key,
required this.playing,
this.size,
this.color,
}) : super(key: key);
final double? size;
final bool playing;
final Color? color;
@override
State<StatefulWidget> createState() => AnimatedPlayPauseState();
}
class AnimatedPlayPauseState extends State<AnimatedPlayPause>
with SingleTickerProviderStateMixin {
late final animationController = AnimationController(
vsync: this,
value: widget.playing ? 1 : 0,
duration: const Duration(milliseconds: 300),
);
@override
void didUpdateWidget(AnimatedPlayPause oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.playing != oldWidget.playing) {
if (widget.playing) {
animationController.forward();
} else {
animationController.reverse();
}
}
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedIcon(
color: widget.color,
size: widget.size,
icon: AnimatedIcons.play_pause,
progress: animationController,
),
);
}
}

View File

@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/animated_play_pause.dart';
class CenterPlayButton extends StatelessWidget {
const CenterPlayButton({
Key? key,
required this.backgroundColor,
this.iconColor,
required this.show,
required this.isPlaying,
required this.isFinished,
this.onPressed,
}) : super(key: key);
final Color backgroundColor;
final Color? iconColor;
final bool show;
final bool isPlaying;
final bool isFinished;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.transparent,
child: Center(
child: UnconstrainedBox(
child: AnimatedOpacity(
opacity: show ? 1.0 : 0.0,
duration: const Duration(milliseconds: 100),
child: DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
shape: BoxShape.circle,
),
child: IconButton(
iconSize: 32,
padding: const EdgeInsets.all(12.0),
icon: isFinished
? Icon(Icons.replay, color: iconColor)
: AnimatedPlayPause(
color: iconColor,
playing: isPlaying,
),
onPressed: onPressed,
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,207 @@
import 'dart:async';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.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';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:video_player/video_player.dart';
class VideoPlayerControls extends ConsumerStatefulWidget {
const VideoPlayerControls({
Key? key,
}) : super(key: key);
@override
VideoPlayerControlsState createState() => VideoPlayerControlsState();
}
class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
with SingleTickerProviderStateMixin {
late VideoPlayerController controller;
late VideoPlayerValue _latestValue;
bool _displayBufferingIndicator = false;
double? _latestVolume;
Timer? _hideTimer;
ChewieController? _chewieController;
ChewieController get chewieController => _chewieController!;
@override
Widget build(BuildContext context) {
ref.listen(videoPlayerControlsProvider.select((value) => value.mute),
(_, value) {
_mute(value);
_cancelAndRestartTimer();
});
ref.listen(videoPlayerControlsProvider.select((value) => value.position),
(_, position) {
_seekTo(position);
_cancelAndRestartTimer();
});
if (_latestValue.hasError) {
return chewieController.errorBuilder?.call(
context,
chewieController.videoPlayerController.value.errorDescription!,
) ??
const Center(
child: Icon(
Icons.error,
color: Colors.white,
size: 42,
),
);
}
return GestureDetector(
onTap: () => _cancelAndRestartTimer(),
child: AbsorbPointer(
absorbing: !ref.watch(showControlsProvider),
child: Stack(
children: [
if (_displayBufferingIndicator)
const Center(
child: ImmichLoadingIndicator(),
)
else
_buildHitArea(),
],
),
),
);
}
@override
void dispose() {
_dispose();
super.dispose();
}
void _dispose() {
controller.removeListener(_updateState);
_hideTimer?.cancel();
}
@override
void didChangeDependencies() {
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
controller = chewieController.videoPlayerController;
if (oldController != chewieController) {
_dispose();
_initialize();
}
super.didChangeDependencies();
}
Widget _buildHitArea() {
final bool isFinished = _latestValue.position >= _latestValue.duration;
return GestureDetector(
onTap: () {
if (_latestValue.isPlaying) {
ref.read(showControlsProvider.notifier).show = false;
} else {
_playPause();
ref.read(showControlsProvider.notifier).show = false;
}
},
child: CenterPlayButton(
backgroundColor: Colors.black54,
iconColor: Colors.white,
isFinished: isFinished,
isPlaying: controller.value.isPlaying,
show: ref.watch(showControlsProvider),
onPressed: _playPause,
),
);
}
void _cancelAndRestartTimer() {
_hideTimer?.cancel();
_startHideTimer();
ref.read(showControlsProvider.notifier).show = true;
}
Future<void> _initialize() async {
_mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute)));
controller.addListener(_updateState);
_latestValue = controller.value;
if (controller.value.isPlaying || chewieController.autoPlay) {
_startHideTimer();
}
}
void _playPause() {
final isFinished = _latestValue.position >= _latestValue.duration;
setState(() {
if (controller.value.isPlaying) {
ref.read(showControlsProvider.notifier).show = true;
_hideTimer?.cancel();
controller.pause();
} else {
_cancelAndRestartTimer();
if (!controller.value.isInitialized) {
controller.initialize().then((_) {
controller.play();
});
} else {
if (isFinished) {
controller.seekTo(Duration.zero);
}
controller.play();
}
}
});
}
void _startHideTimer() {
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
? ChewieController.defaultHideControlsTimer
: chewieController.hideControlsTimer;
_hideTimer = Timer(hideControlsTimer, () {
ref.read(showControlsProvider.notifier).show = false;
});
}
void _updateState() {
if (!mounted) return;
_displayBufferingIndicator = controller.value.isBuffering;
setState(() {
_latestValue = controller.value;
ref.read(videoPlaybackValueProvider.notifier).value = VideoPlaybackValue(
position: _latestValue.position,
duration: _latestValue.duration,
);
});
}
void _mute(bool mute) {
if (mute) {
_latestVolume = controller.value.volume;
controller.setVolume(0);
} else {
controller.setVolume(_latestVolume ?? 0.5);
}
}
void _seekTo(double position) {
final Duration pos = controller.value.duration * (position / 100.0);
if (pos != controller.value.position) {
controller.seekTo(pos);
}
}
}

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
@@ -6,8 +7,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
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';
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
@@ -49,10 +53,10 @@ class GalleryViewerPage extends HookConsumerWidget {
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
final isZoomed = useState<bool>(false);
final showAppBar = useState<bool>(true);
final isPlayingMotionVideo = useState(false);
final isPlayingVideo = useState(false);
late Offset localPosition;
final progressValue = useState(0.0);
Offset? localPosition;
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
final currentIndex = useState(initialIndex);
final currentAsset = loadAsset(currentIndex.value);
@@ -60,15 +64,6 @@ class GalleryViewerPage extends HookConsumerWidget {
Asset asset() => watchedAsset.value ?? currentAsset;
showAppBar.addListener(() {
// Change to and from immersive mode, hiding navigation and app bar
if (showAppBar.value) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
});
useEffect(
() {
isLoadPreview.value =
@@ -246,8 +241,13 @@ class GalleryViewerPage extends HookConsumerWidget {
return;
}
// Guard [localPosition] null
if (localPosition == null) {
return;
}
// Check for delta from initial down point
final d = details.localPosition - localPosition;
final d = details.localPosition - localPosition!;
// If the magnitude of the dx swipe is large, we probably didn't mean to go down
if (d.dx.abs() > dxThreshold) {
return;
@@ -272,15 +272,11 @@ class GalleryViewerPage extends HookConsumerWidget {
}
buildAppBar() {
final show = (showAppBar.value || // onTap has the final say
(showAppBar.value && !isZoomed.value)) &&
!isPlayingVideo.value;
return IgnorePointer(
ignoring: !show,
ignoring: !ref.watch(showControlsProvider),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: show ? 1.0 : 0.0,
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
child: Container(
color: Colors.black.withOpacity(0.4),
child: TopControlAppBar(
@@ -308,65 +304,180 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
buildBottomBar() {
final show = (showAppBar.value || // onTap has the final say
(showAppBar.value && !isZoomed.value)) &&
!isPlayingVideo.value;
Widget buildProgressBar() {
final playerValue = ref.watch(videoPlaybackValueProvider);
return Expanded(
child: Slider(
value: playerValue.duration == Duration.zero
? 0.0
: min(
playerValue.position.inMicroseconds /
playerValue.duration.inMicroseconds *
100,
100,
),
min: 0,
max: 100,
thumbColor: Colors.white,
activeColor: Colors.white,
inactiveColor: Colors.white.withOpacity(0.75),
onChanged: (position) {
progressValue.value = position;
ref.read(videoPlayerControlsProvider.notifier).position = position;
},
),
);
}
Text buildPosition() {
final position = ref
.watch(videoPlaybackValueProvider.select((value) => value.position));
return Text(
_formatDuration(position),
style: TextStyle(
fontSize: 14.0,
color: Colors.white.withOpacity(.75),
fontWeight: FontWeight.normal,
),
);
}
Text buildDuration() {
final duration = ref
.watch(videoPlaybackValueProvider.select((value) => value.duration));
return Text(
_formatDuration(duration),
style: TextStyle(
fontSize: 14.0,
color: Colors.white.withOpacity(.75),
fontWeight: FontWeight.normal,
),
);
}
Widget buildMuteButton() {
return IconButton(
icon: Icon(
ref.watch(videoPlayerControlsProvider.select((value) => value.mute))
? Icons.volume_off
: Icons.volume_up,
),
onPressed: () =>
ref.read(videoPlayerControlsProvider.notifier).toggleMute(),
color: Colors.white,
);
}
buildBottomBar() {
return IgnorePointer(
ignoring: !show,
ignoring: !ref.watch(showControlsProvider),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: show ? 1.0 : 0.0,
child: BottomNavigationBar(
backgroundColor: Colors.black.withOpacity(0.4),
unselectedIconTheme: const IconThemeData(color: Colors.white),
selectedIconTheme: const IconThemeData(color: Colors.white),
unselectedLabelStyle: const TextStyle(color: Colors.black),
selectedLabelStyle: const TextStyle(color: Colors.black),
showSelectedLabels: false,
showUnselectedLabels: false,
items: [
BottomNavigationBarItem(
icon: const Icon(Icons.ios_share_rounded),
label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(),
),
asset().isArchived
? BottomNavigationBarItem(
icon: const Icon(Icons.unarchive_rounded),
label: 'control_bottom_app_bar_unarchive'.tr(),
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
)
: BottomNavigationBarItem(
icon: const Icon(Icons.archive_outlined),
label: 'control_bottom_app_bar_archive'.tr(),
tooltip: 'control_bottom_app_bar_archive'.tr(),
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
child: Column(
children: [
Visibility(
visible: !asset().isImage && !isPlayingMotionVideo.value,
child: Container(
color: Colors.black.withOpacity(0.4),
child: Padding(
padding: MediaQuery.of(context).orientation ==
Orientation.portrait
? const EdgeInsets.symmetric(horizontal: 12.0)
: const EdgeInsets.symmetric(horizontal: 64.0),
child: Row(
children: [
buildPosition(),
buildProgressBar(),
buildDuration(),
buildMuteButton(),
],
),
BottomNavigationBarItem(
icon: const Icon(Icons.delete_outline),
label: 'control_bottom_app_bar_delete'.tr(),
tooltip: 'control_bottom_app_bar_delete'.tr(),
),
),
),
BottomNavigationBar(
backgroundColor: Colors.black.withOpacity(0.4),
unselectedIconTheme: const IconThemeData(color: Colors.white),
selectedIconTheme: const IconThemeData(color: Colors.white),
unselectedLabelStyle: const TextStyle(color: Colors.black),
selectedLabelStyle: const TextStyle(color: Colors.black),
showSelectedLabels: false,
showUnselectedLabels: false,
items: [
BottomNavigationBarItem(
icon: const Icon(Icons.ios_share_rounded),
label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(),
),
asset().isArchived
? BottomNavigationBarItem(
icon: const Icon(Icons.unarchive_rounded),
label: 'control_bottom_app_bar_unarchive'.tr(),
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
)
: BottomNavigationBarItem(
icon: const Icon(Icons.archive_outlined),
label: 'control_bottom_app_bar_archive'.tr(),
tooltip: 'control_bottom_app_bar_archive'.tr(),
),
BottomNavigationBarItem(
icon: const Icon(Icons.delete_outline),
label: 'control_bottom_app_bar_delete'.tr(),
tooltip: 'control_bottom_app_bar_delete'.tr(),
),
],
onTap: (index) {
switch (index) {
case 0:
shareAsset();
break;
case 1:
handleArchive(asset());
break;
case 2:
handleDelete(asset());
break;
}
},
),
],
onTap: (index) {
switch (index) {
case 0:
shareAsset();
break;
case 1:
handleArchive(asset());
break;
case 2:
handleDelete(asset());
break;
}
},
),
),
);
}
ref.listen(showControlsProvider, (_, show) {
if (show) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
});
ImageProvider imageProvider(Asset asset) {
if (asset.isLocal) {
return localImageProvider(asset);
} else {
if (isLoadOriginal.value) {
return originalImageProvider(asset);
} else if (isLoadPreview.value) {
return remoteThumbnailImageProvider(
asset,
api.ThumbnailFormat.JPEG,
);
} else {
return remoteThumbnailImageProvider(
asset,
api.ThumbnailFormat.WEBP,
);
}
}
}
return Scaffold(
backgroundColor: Colors.black,
body: WillPopScope(
@@ -380,7 +491,6 @@ class GalleryViewerPage extends HookConsumerWidget {
PhotoViewGallery.builder(
scaleStateChangedCallback: (state) {
isZoomed.value = state != PhotoViewScaleState.initial;
showAppBar.value = !isZoomed.value;
},
pageController: controller,
scrollPhysics: isZoomed.value
@@ -401,6 +511,8 @@ class GalleryViewerPage extends HookConsumerWidget {
precacheNextImage(value - 1);
}
currentIndex.value = value;
progressValue.value = 0.0;
HapticFeedback.selectionClick();
},
loadingBuilder: isLoadPreview.value
@@ -460,33 +572,17 @@ class GalleryViewerPage extends HookConsumerWidget {
: null,
builder: (context, index) {
final asset = loadAsset(index);
final ImageProvider provider = imageProvider(asset);
if (asset.isImage && !isPlayingMotionVideo.value) {
// Show photo
final ImageProvider provider;
if (asset.isLocal) {
provider = localImageProvider(asset);
} else {
if (isLoadOriginal.value) {
provider = originalImageProvider(asset);
} else if (isLoadPreview.value) {
provider = remoteThumbnailImageProvider(
asset,
api.ThumbnailFormat.JPEG,
);
} else {
provider = remoteThumbnailImageProvider(
asset,
api.ThumbnailFormat.WEBP,
);
}
}
return PhotoViewGalleryPageOptions(
onDragStart: (_, details, __) =>
localPosition = details.localPosition,
onDragUpdate: (_, details, __) =>
handleSwipeUpDown(details),
onTapDown: (_, __, ___) =>
showAppBar.value = !showAppBar.value,
onTapDown: (_, __, ___) {
ref.read(showControlsProvider.notifier).toggle();
},
imageProvider: provider,
heroAttributes: PhotoViewHeroAttributes(
tag: asset.id,
@@ -511,19 +607,24 @@ class GalleryViewerPage extends HookConsumerWidget {
filterQuality: FilterQuality.high,
maxScale: 1.0,
minScale: 1.0,
basePosition: Alignment.bottomCenter,
child: SafeArea(
child: VideoViewerPage(
onPlaying: () => isPlayingVideo.value = true,
onPaused: () => isPlayingVideo.value = false,
asset: asset,
isMotionVideo: isPlayingMotionVideo.value,
onVideoEnded: () {
if (isPlayingMotionVideo.value) {
isPlayingMotionVideo.value = false;
}
},
basePosition: Alignment.center,
child: VideoViewerPage(
onPlaying: () => isPlayingVideo.value = true,
onPaused: () => isPlayingVideo.value = false,
asset: asset,
isMotionVideo: isPlayingMotionVideo.value,
placeholder: Image(
image: provider,
fit: BoxFit.fitWidth,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
),
onVideoEnded: () {
if (isPlayingMotionVideo.value) {
isPlayingMotionVideo.value = false;
}
},
),
);
}
@@ -546,4 +647,37 @@ class GalleryViewerPage extends HookConsumerWidget {
),
);
}
String _formatDuration(Duration position) {
final ms = position.inMilliseconds;
int seconds = ms ~/ 1000;
final int hours = seconds ~/ 3600;
seconds = seconds % 3600;
final minutes = seconds ~/ 60;
seconds = seconds % 60;
final hoursString = hours >= 10
? '$hours'
: hours == 0
? '00'
: '0$hours';
final minutesString = minutes >= 10
? '$minutes'
: minutes == 0
? '00'
: '0$minutes';
final secondsString = seconds >= 10
? '$seconds'
: seconds == 0
? '00'
: '0$seconds';
final formattedTime =
'${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString';
return formattedTime;
}
}

View File

@@ -5,16 +5,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:chewie/chewie.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:video_player/video_player.dart';
import 'package:wakelock/wakelock.dart';
// ignore: must_be_immutable
class VideoViewerPage extends HookConsumerWidget {
final Asset asset;
final bool isMotionVideo;
final Widget? placeholder;
final VoidCallback onVideoEnded;
final VoidCallback? onPlaying;
final VoidCallback? onPaused;
@@ -26,6 +29,7 @@ class VideoViewerPage extends HookConsumerWidget {
required this.onVideoEnded,
this.onPlaying,
this.onPaused,
this.placeholder,
}) : super(key: key);
@override
@@ -66,6 +70,7 @@ class VideoViewerPage extends HookConsumerWidget {
onVideoEnded: onVideoEnded,
onPaused: onPaused,
onPlaying: onPlaying,
placeholder: placeholder,
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
@@ -95,6 +100,10 @@ class VideoPlayer extends StatefulWidget {
final Function()? onPlaying;
final Function()? onPaused;
/// The placeholder to show while the video is loading
/// usually, a thumbnail of the video
final Widget? placeholder;
const VideoPlayer({
Key? key,
this.url,
@@ -104,6 +113,7 @@ class VideoPlayer extends StatefulWidget {
required this.isMotionVideo,
this.onPlaying,
this.onPaused,
this.placeholder,
}) : super(key: key);
@override
@@ -122,13 +132,16 @@ class _VideoPlayerState extends State<VideoPlayer> {
videoPlayerController.addListener(() {
if (videoPlayerController.value.isInitialized) {
if (videoPlayerController.value.isPlaying) {
Wakelock.enable();
widget.onPlaying?.call();
} else if (!videoPlayerController.value.isPlaying) {
Wakelock.disable();
widget.onPaused?.call();
}
if (videoPlayerController.value.position ==
videoPlayerController.value.duration) {
Wakelock.disable();
widget.onVideoEnded();
}
}
@@ -162,9 +175,10 @@ class _VideoPlayerState extends State<VideoPlayer> {
videoPlayerController: videoPlayerController,
autoPlay: true,
autoInitialize: true,
allowFullScreen: true,
allowFullScreen: false,
allowedScreenSleep: false,
showControls: !widget.isMotionVideo,
customControls: const VideoPlayerControls(),
hideControlsTimer: const Duration(seconds: 5),
);
}
@@ -186,12 +200,18 @@ class _VideoPlayerState extends State<VideoPlayer> {
),
);
} else {
return const Center(
child: SizedBox(
width: 75,
height: 75,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
return SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Center(
child: Stack(
children: [
if (widget.placeholder != null)
widget.placeholder!,
const Center(
child: ImmichLoadingIndicator(),
),
],
),
),
);

View File

@@ -16,7 +16,9 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/utils/files_helper.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart' as p;
@@ -33,6 +35,7 @@ class BackupService {
final httpClient = http.Client();
final ApiService _apiService;
final Isar _db;
final Logger _log = Logger("BackupService");
BackupService(this._apiService, this._db);
@@ -203,6 +206,14 @@ class BackupService {
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
Function(ErrorUploadAsset) errorCb,
) async {
if (Platform.isAndroid &&
!(await Permission.accessMediaLocation.status).isGranted) {
// double check that permission is granted here, to guard against
// uploading corrupt assets without EXIF information
_log.warning("Media location permission is not granted. "
"Cannot access original assets for backup.");
return false;
}
final String deviceId = Store.get(StoreKey.deviceId);
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
File? file;

View File

@@ -29,8 +29,8 @@ class GroupDividerTitle extends ConsumerWidget {
return Padding(
padding: const EdgeInsets.only(
top: 29.0,
bottom: 10.0,
top: 12.0,
bottom: 4.0,
left: 12.0,
right: 12.0,
),

View File

@@ -1,3 +1,6 @@
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -25,6 +28,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
final bool showMultiSelectIndicator;
final void Function(ItemPosition start, ItemPosition end)?
visibleItemsListener;
final Widget? topWidget;
const ImmichAssetGrid({
super.key,
@@ -41,6 +45,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
this.dynamicLayout,
this.showMultiSelectIndicator = true,
this.visibleItemsListener,
this.topWidget,
});
@override
@@ -51,6 +56,12 @@ class ImmichAssetGrid extends HookConsumerWidget {
final enableHeroAnimations = useState(false);
final transitionDuration = ModalRoute.of(context)?.transitionDuration;
final perRow = useState(
assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!,
);
final scaleFactor = useState(7.0 - perRow.value);
final baseScaleFactor = useState(7.0 - perRow.value);
useEffect(
() {
// Wait for transition to complete, then re-enable
@@ -80,22 +91,44 @@ class ImmichAssetGrid extends HookConsumerWidget {
onWillPop: onWillPop,
child: HeroMode(
enabled: enableHeroAnimations.value,
child: ImmichAssetGridView(
onRefresh: onRefresh,
assetsPerRow: assetsPerRow ??
settings.getSetting(AppSettingsEnum.tilesPerRow),
listener: listener,
showStorageIndicator: showStorageIndicator ??
settings.getSetting(AppSettingsEnum.storageIndicator),
renderList: renderList,
margin: margin,
selectionActive: selectionActive,
preselectedAssets: preselectedAssets,
canDeselect: canDeselect,
dynamicLayout: dynamicLayout ??
settings.getSetting(AppSettingsEnum.dynamicLayout),
showMultiSelectIndicator: showMultiSelectIndicator,
visibleItemsListener: visibleItemsListener,
child: RawGestureDetector(
gestures: {
CustomScaleGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
CustomScaleGestureRecognizer>(
() => CustomScaleGestureRecognizer(),
(CustomScaleGestureRecognizer scale) {
scale.onStart = (details) {
baseScaleFactor.value = scaleFactor.value;
};
scale.onUpdate = (details) {
scaleFactor.value =
max(min(5.0, baseScaleFactor.value * details.scale), 1.0);
if (7 - scaleFactor.value.toInt() != perRow.value) {
perRow.value = 7 - scaleFactor.value.toInt();
}
};
scale.onEnd = (details) {};
})
},
child: ImmichAssetGridView(
onRefresh: onRefresh,
assetsPerRow: perRow.value,
listener: listener,
showStorageIndicator: showStorageIndicator ??
settings.getSetting(AppSettingsEnum.storageIndicator),
renderList: renderList,
margin: margin,
selectionActive: selectionActive,
preselectedAssets: preselectedAssets,
canDeselect: canDeselect,
dynamicLayout: dynamicLayout ??
settings.getSetting(AppSettingsEnum.dynamicLayout),
showMultiSelectIndicator: showMultiSelectIndicator,
visibleItemsListener: visibleItemsListener,
topWidget: topWidget,
),
),
),
);
@@ -113,3 +146,11 @@ class ImmichAssetGrid extends HookConsumerWidget {
);
}
}
/// accepts a gesture even though it should reject it (because child won)
class CustomScaleGestureRecognizer extends ScaleGestureRecognizer {
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}

View File

@@ -127,7 +127,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
width: width * widthDistribution[index],
height: width,
margin: EdgeInsets.only(
top: widget.margin,
bottom: widget.margin,
right: last ? 0.0 : widget.margin,
),
child: _buildThumbnailOrPlaceholder(asset, absoluteOffset + index),
@@ -157,7 +157,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
final String title = monthFormat.format(date);
return Padding(
key: Key("month-$title"),
padding: const EdgeInsets.only(left: 12.0, top: 30),
padding: const EdgeInsets.only(left: 12.0, top: 24.0),
child: Text(
title,
style: TextStyle(
@@ -179,7 +179,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
width: width,
height: height,
margin: EdgeInsets.only(
top: widget.margin,
bottom: widget.margin,
right: i + 1 == num ? 0.0 : widget.margin,
),
color: Colors.grey,
@@ -206,6 +206,8 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
key: ValueKey(section.offset),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (section.offset == 0 && widget.topWidget != null)
widget.topWidget!,
if (section.type == RenderAssetGridElementType.monthTitle)
_buildMonthTitle(context, section.date),
if (section.type == RenderAssetGridElementType.groupDividerTitle ||
@@ -401,6 +403,7 @@ class ImmichAssetGridView extends StatefulWidget {
final bool showMultiSelectIndicator;
final void Function(ItemPosition start, ItemPosition end)?
visibleItemsListener;
final Widget? topWidget;
const ImmichAssetGridView({
super.key,
@@ -416,6 +419,7 @@ class ImmichAssetGridView extends StatefulWidget {
this.dynamicLayout = true,
this.showMultiSelectIndicator = true,
this.visibleItemsListener,
this.topWidget,
});
@override

View File

@@ -1,4 +1,5 @@
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:flutter_web_auth/flutter_web_auth.dart';
@@ -7,7 +8,7 @@ import 'package:flutter_web_auth/flutter_web_auth.dart';
class OAuthService {
final ApiService _apiService;
final callbackUrlScheme = 'app.immich';
final log = Logger('OAuthService');
OAuthService(this._apiService);
Future<OAuthConfigResponseDto?> getOAuthServerConfig(
@@ -33,7 +34,8 @@ class OAuthService {
url: result,
),
);
} catch (e) {
} catch (e, stack) {
log.severe("Error performing oAuthLogin: ${e.toString()}", e, stack);
return null;
}
}

View File

@@ -6,7 +6,7 @@ import 'package:permission_handler/permission_handler.dart';
class GalleryPermissionNotifier extends StateNotifier<PermissionStatus> {
GalleryPermissionNotifier()
: super(PermissionStatus.denied) // Denied is the intitial state
: super(PermissionStatus.denied) // Denied is the intitial state
{
// Sets the initial state
getGalleryPermissionStatus();
@@ -16,19 +16,20 @@ class GalleryPermissionNotifier extends StateNotifier<PermissionStatus> {
/// Requests the gallery permission
Future<PermissionStatus> requestGalleryPermission() async {
PermissionStatus result;
// Android 32 and below uses Permission.storage
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt <= 32) {
// Android 32 and below need storage
final permission = await Permission.storage.request();
state = permission;
return permission;
result = permission;
} else {
// Android 33 need photo & video
final photos = await Permission.photos.request();
if (!photos.isGranted) {
// Don't ask twice for the same permission
state = photos;
return photos;
}
final videos = await Permission.videos.request();
@@ -45,28 +46,32 @@ class GalleryPermissionNotifier extends StateNotifier<PermissionStatus> {
status = PermissionStatus.denied;
}
state = status;
return status;
result = status;
}
if (result == PermissionStatus.granted &&
androidInfo.version.sdkInt >= 29) {
result = await Permission.accessMediaLocation.request();
}
} else {
// iOS can use photos
final photos = await Permission.photos.request();
state = photos;
return photos;
result = photos;
}
state = result;
return result;
}
/// Checks the current state of the gallery permissions without
/// requesting them again
Future<PermissionStatus> getGalleryPermissionStatus() async {
PermissionStatus result;
// Android 32 and below uses Permission.storage
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt <= 32) {
// Android 32 and below need storage
final permission = await Permission.storage.status;
state = permission;
return permission;
result = permission;
} else {
// Android 33 needs photo & video
final photos = await Permission.photos.status;
@@ -84,18 +89,23 @@ class GalleryPermissionNotifier extends StateNotifier<PermissionStatus> {
status = PermissionStatus.denied;
}
state = status;
return status;
result = status;
}
if (state == PermissionStatus.granted &&
androidInfo.version.sdkInt >= 29) {
result = await Permission.accessMediaLocation.status;
}
} else {
// iOS can use photos
final photos = await Permission.photos.status;
state = photos;
return photos;
result = photos;
}
state = result;
return result;
}
}
final galleryPermissionNotifier
= StateNotifierProvider<GalleryPermissionNotifier, PermissionStatus>
((ref) => GalleryPermissionNotifier());
final galleryPermissionNotifier =
StateNotifierProvider<GalleryPermissionNotifier, PermissionStatus>(
(ref) => GalleryPermissionNotifier(),
);

View File

@@ -0,0 +1,44 @@
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/search/services/person.service.dart';
import 'package:openapi/api.dart';
final personAssetsProvider = FutureProvider.family
.autoDispose<RenderList, String>((ref, personId) async {
final PersonService personService = ref.watch(personServiceProvider);
final assets = await personService.getPersonAssets(personId);
if (assets == null) {
return RenderList.empty();
}
return RenderList.fromAssets(assets, GroupAssetsBy.auto);
});
final getCuratedPeopleProvider =
FutureProvider.autoDispose<List<PersonResponseDto>>((ref) async {
final PersonService personService = ref.watch(personServiceProvider);
final curatedPeople = await personService.getCuratedPeople();
return curatedPeople ?? [];
});
class UpdatePersonName {
final String id;
final String name;
UpdatePersonName(this.id, this.name);
}
final updatePersonNameProvider =
StateProvider.family<void, UpdatePersonName>((ref, dto) async {
final PersonService personService = ref.watch(personServiceProvider);
final person = await personService.updateName(dto.id, dto.name);
if (person != null && person.name == dto.name) {
ref.invalidate(getCuratedPeopleProvider);
}
});

View File

@@ -0,0 +1,56 @@
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/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
final personServiceProvider = Provider(
(ref) => PersonService(
ref.watch(apiServiceProvider),
),
);
class PersonService {
final ApiService _apiService;
PersonService(this._apiService);
Future<List<PersonResponseDto>?> getCuratedPeople() async {
try {
return await _apiService.personApi.getAllPeople();
} catch (e) {
debugPrint("Error [getCuratedPeople] ${e.toString()}");
return null;
}
}
Future<List<Asset>?> getPersonAssets(String id) async {
try {
final assets = await _apiService.personApi.getPersonAssets(id);
if (assets == null) {
return null;
}
return assets.map((e) => Asset.remote(e)).toList();
} catch (e) {
debugPrint("Error [getPersonAssets] ${e.toString()}");
return null;
}
}
Future<PersonResponseDto?> updateName(String id, String name) async {
try {
return await _apiService.personApi.updatePerson(
id,
PersonUpdateDto(
name: name,
),
);
} catch (e) {
debugPrint("Error [updateName] ${e.toString()}");
return null;
}
}
}

View File

@@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
class CuratedPeopleRow extends StatelessWidget {
final List<CuratedContent> content;
/// Callback with the content and the index when tapped
final Function(CuratedContent, int)? onTap;
final Function(CuratedContent, int)? onNameTap;
const CuratedPeopleRow({
super.key,
required this.content,
this.onTap,
required this.onNameTap,
});
@override
Widget build(BuildContext context) {
const imageSize = 85.0;
// Guard empty [content]
if (content.isEmpty) {
// Return empty thumbnail
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: imageSize,
height: imageSize,
child: ThumbnailWithInfo(
textInfo: '',
onTap: () {},
),
),
),
);
}
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(
left: 16,
top: 8,
),
itemBuilder: (context, index) {
final person = content[index];
final headers = {
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"
};
return Padding(
padding: const EdgeInsets.only(right: 18.0),
child: SizedBox(
width: imageSize,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
onTap: () => onTap?.call(person, index),
child: SizedBox(
height: imageSize,
child: Material(
shape: const CircleBorder(side: BorderSide.none),
elevation: 3,
child: CircleAvatar(
maxRadius: imageSize / 2,
backgroundImage: NetworkImage(
getFaceThumbnailUrl(person.id),
headers: headers,
),
),
),
),
),
if (person.label == "")
GestureDetector(
onTap: () => onNameTap?.call(person, index),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
"Add name",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
)
else
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
person.label,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13.0,
),
),
)
],
),
),
);
},
itemCount: content.length,
);
}
}

View File

@@ -4,12 +4,16 @@ import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
class ExploreGrid extends StatelessWidget {
final List<CuratedContent> curatedContent;
final bool isPeople;
const ExploreGrid({
super.key,
required this.curatedContent,
this.isPeople = false,
});
@override
@@ -36,16 +40,25 @@ class ExploreGrid extends StatelessWidget {
),
itemBuilder: (context, index) {
final content = curatedContent[index];
final thumbnailRequestUrl =
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${content.id}';
final thumbnailRequestUrl = isPeople
? getFaceThumbnailUrl(content.id)
: '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${content.id}';
return ThumbnailWithInfo(
imageUrl: thumbnailRequestUrl,
textInfo: content.label,
borderRadius: 0,
onTap: () {
AutoRouter.of(context).push(
SearchResultRoute(searchTerm: 'm:${content.label}'),
);
isPeople
? AutoRouter.of(context).push(
PersonResultRoute(
personId: content.id,
personName: content.label,
),
)
: AutoRouter.of(context).push(
SearchResultRoute(searchTerm: 'm:${content.label}'),
);
},
);
},

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
class PersonNameEditFormResult {
final bool success;
final String updatedName;
PersonNameEditFormResult(this.success, this.updatedName);
}
class PersonNameEditForm extends HookConsumerWidget {
final String personId;
final String personName;
const PersonNameEditForm({
super.key,
required this.personId,
required this.personName,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = useTextEditingController(text: personName);
return AlertDialog(
title: const Text(
"Add a name",
style: TextStyle(fontWeight: FontWeight.bold),
),
content: SingleChildScrollView(
child: TextFormField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Name',
),
),
),
actions: [
TextButton(
style: TextButton.styleFrom(),
onPressed: () {
Navigator.of(context, rootNavigator: true)
.pop<PersonNameEditFormResult>(
PersonNameEditFormResult(false, ''),
);
},
child: Text(
"Cancel",
style: TextStyle(
color: Colors.red[300],
fontWeight: FontWeight.bold,
),
),
),
TextButton(
onPressed: () {
ref.read(
updatePersonNameProvider(
UpdatePersonName(personId, controller.text),
),
);
Navigator.of(context, rootNavigator: true)
.pop<PersonNameEditFormResult>(
PersonNameEditFormResult(true, controller.text),
);
},
child: Text(
"Save",
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,46 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class SearchRowTitle extends StatelessWidget {
final Function() onViewAllPressed;
final String title;
final double top;
const SearchRowTitle({
super.key,
required this.onViewAllPressed,
required this.title,
this.top = 12,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
top: top,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleSmall,
),
TextButton(
onPressed: onViewAllPressed,
child: Text(
'search_page_view_all_button',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 14.0,
),
).tr(),
),
],
),
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class AllPeoplePage extends HookConsumerWidget {
const AllPeoplePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final curatedPeople = ref.watch(getCuratedPeopleProvider);
return Scaffold(
appBar: AppBar(
title: Text(
'all_people_page_title',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
).tr(),
leading: IconButton(
onPressed: () => AutoRouter.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_rounded),
),
),
body: curatedPeople.when(
loading: () => const Center(child: ImmichLoadingIndicator()),
error: (err, stack) => Center(
child: Text('Error: $err'),
),
data: (people) => ExploreGrid(
isPeople: true,
curatedContent: people
.map(
(person) => CuratedContent(
label: person.name,
id: person.id,
),
)
.toList(),
),
),
);
}
}

View File

@@ -0,0 +1,152 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
import 'package:immich_mobile/shared/models/store.dart' as isar_store;
import 'package:immich_mobile/utils/image_url_builder.dart';
class PersonResultPage extends HookConsumerWidget {
final String personId;
final String personName;
const PersonResultPage({
super.key,
required this.personId,
required this.personName,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final name = useState(personName);
showEditNameDialog() {
showDialog<PersonNameEditFormResult>(
context: context,
builder: (BuildContext context) {
return PersonNameEditForm(
personId: personId,
personName: personName,
);
},
).then((result) {
if (result != null && result.success) {
name.value = result.updatedName;
}
});
}
void buildBottomSheet() {
showModalBottomSheet(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: false,
context: context,
useSafeArea: true,
builder: (context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.edit_outlined),
title: const Text(
'Edit name',
style: TextStyle(fontWeight: FontWeight.bold),
),
onTap: showEditNameDialog,
)
],
),
);
},
);
}
buildTitleBlock() {
if (name.value == "") {
return GestureDetector(
onTap: showEditNameDialog,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Add a name',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
Text(
'Find them fast by name with search',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name.value,
style: Theme.of(context).textTheme.titleLarge,
),
],
);
}
return Scaffold(
appBar: AppBar(
title: Text(name.value),
leading: IconButton(
onPressed: () => AutoRouter.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_rounded),
),
actions: [
IconButton(
onPressed: buildBottomSheet,
icon: const Icon(Icons.more_vert_rounded),
),
],
),
body: ref.watch(personAssetsProvider(personId)).when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Center(
child: Text(
error.toString(),
),
),
data: (data) => data.isEmpty
? const Center(
child: Text('Opps'),
)
: ImmichAssetGrid(
renderList: data,
topWidget: Padding(
padding: const EdgeInsets.only(left: 8.0, top: 24),
child: Row(
children: [
CircleAvatar(
radius: 36,
backgroundImage: NetworkImage(
getFaceThumbnailUrl(personId),
headers: {
"Authorization":
"Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}"
},
),
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: buildTitleBlock(),
),
],
),
),
),
),
);
}
}

View File

@@ -4,13 +4,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/search/ui/curated_people_row.dart';
import 'package:immich_mobile/modules/search/ui/curated_row.dart';
import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart';
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
import 'package:immich_mobile/modules/search/ui/search_row_title.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:openapi/api.dart';
// ignore: must_be_immutable
class SearchPage extends HookConsumerWidget {
@@ -21,10 +24,9 @@ class SearchPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation =
ref.watch(getCuratedLocationProvider);
AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects =
ref.watch(getCuratedObjectProvider);
final curatedLocation = ref.watch(getCuratedLocationProvider);
final curatedObjects = ref.watch(getCuratedObjectProvider);
final curatedPeople = ref.watch(getCuratedPeopleProvider);
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
double imageSize = MediaQuery.of(context).size.width / 3;
@@ -54,6 +56,50 @@ class SearchPage extends HookConsumerWidget {
);
}
showNameEditModel(
String personId,
String personName,
) {
return showDialog(
context: context,
builder: (BuildContext context) {
return PersonNameEditForm(personId: personId, personName: personName);
},
);
}
buildPeople() {
return SizedBox(
height: MediaQuery.of(context).size.width / 3,
child: curatedPeople.when(
loading: () => const Center(child: ImmichLoadingIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')),
data: (people) => CuratedPeopleRow(
content: people
.map(
(person) => CuratedContent(
id: person.id,
label: person.name,
),
)
.take(12)
.toList(),
onTap: (content, index) {
AutoRouter.of(context).push(
PersonResultRoute(
personId: content.id,
personName: content.label,
),
);
},
onNameTap: (person, index) => {
showNameEditModel(person.id, person.label),
},
),
),
);
}
buildPlaces() {
return SizedBox(
height: imageSize,
@@ -130,63 +176,25 @@ class SearchPage extends HookConsumerWidget {
children: [
ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"search_page_places",
style: Theme.of(context).textTheme.titleSmall,
).tr(),
TextButton(
child: Text(
'search_page_view_all_button',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 14.0,
),
).tr(),
onPressed: () => AutoRouter.of(context).push(
const CuratedLocationRoute(),
),
),
],
SearchRowTitle(
title: "search_page_people".tr(),
onViewAllPressed: () => AutoRouter.of(context).push(
const AllPeopleRoute(),
),
),
buildPlaces(),
Padding(
padding: const EdgeInsets.only(
top: 24.0,
bottom: 4.0,
left: 16.0,
right: 16.0,
buildPeople(),
SearchRowTitle(
title: "search_page_places".tr(),
onViewAllPressed: () => AutoRouter.of(context).push(
const CuratedLocationRoute(),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"search_page_things",
style: Theme.of(context).textTheme.titleSmall,
).tr(),
TextButton(
child: Text(
'search_page_view_all_button',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 14.0,
),
).tr(),
onPressed: () => AutoRouter.of(context).push(
const CuratedObjectRoute(),
),
),
],
top: 0,
),
buildPlaces(),
SearchRowTitle(
title: "search_page_things".tr(),
onViewAllPressed: () => AutoRouter.of(context).push(
const CuratedObjectRoute(),
),
),
buildThings(),
@@ -200,7 +208,7 @@ class SearchPage extends HookConsumerWidget {
),
ListTile(
leading: Icon(
Icons.favorite_border,
Icons.star_outline,
color: categoryIconColor,
),
title:

View File

@@ -25,9 +25,11 @@ import 'package:immich_mobile/modules/login/views/login_page.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart';
import 'package:immich_mobile/modules/search/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';
import 'package:immich_mobile/modules/search/views/curated_location_page.dart';
import 'package:immich_mobile/modules/search/views/curated_object_page.dart';
import 'package:immich_mobile/modules/search/views/person_result_page.dart';
import 'package:immich_mobile/modules/search/views/recently_added_page.dart';
import 'package:immich_mobile/modules/search/views/search_page.dart';
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
@@ -37,8 +39,8 @@ import 'package:immich_mobile/routing/duplicate_guard.dart';
import 'package:immich_mobile/routing/gallery_permission_guard.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/models/logger_message.model.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/shared/views/app_log_detail_page.dart';
@@ -140,7 +142,15 @@ part 'router.gr.dart';
],
),
AutoRoute(page: PartnerPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: PartnerDetailPage, guards: [AuthGuard, DuplicateGuard])
AutoRoute(page: PartnerDetailPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(
page: PersonResultPage,
guards: [
AuthGuard,
DuplicateGuard,
],
),
AutoRoute(page: AllPeoplePage, guards: [AuthGuard, DuplicateGuard]),
],
)
class AppRouter extends _$AppRouter {

View File

@@ -84,6 +84,7 @@ class _$AppRouter extends RootStackRouter {
onVideoEnded: args.onVideoEnded,
onPlaying: args.onPlaying,
onPaused: args.onPaused,
placeholder: args.placeholder,
),
);
},
@@ -272,6 +273,23 @@ class _$AppRouter extends RootStackRouter {
),
);
},
PersonResultRoute.name: (routeData) {
final args = routeData.argsAs<PersonResultRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData,
child: PersonResultPage(
key: args.key,
personId: args.personId,
personName: args.personName,
),
);
},
AllPeopleRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const AllPeoplePage(),
);
},
HomeRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
@@ -555,6 +573,22 @@ class _$AppRouter extends RootStackRouter {
duplicateGuard,
],
),
RouteConfig(
PersonResultRoute.name,
path: '/person-result-page',
guards: [
authGuard,
duplicateGuard,
],
),
RouteConfig(
AllPeopleRoute.name,
path: '/all-people-page',
guards: [
authGuard,
duplicateGuard,
],
),
];
}
@@ -670,9 +704,10 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
Key? key,
required Asset asset,
required bool isMotionVideo,
required dynamic onVideoEnded,
dynamic onPlaying,
dynamic onPaused,
required void Function() onVideoEnded,
void Function()? onPlaying,
void Function()? onPaused,
Widget? placeholder,
}) : super(
VideoViewerRoute.name,
path: '/video-viewer-page',
@@ -683,6 +718,7 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
onVideoEnded: onVideoEnded,
onPlaying: onPlaying,
onPaused: onPaused,
placeholder: placeholder,
),
);
@@ -697,6 +733,7 @@ class VideoViewerRouteArgs {
required this.onVideoEnded,
this.onPlaying,
this.onPaused,
this.placeholder,
});
final Key? key;
@@ -705,15 +742,17 @@ class VideoViewerRouteArgs {
final bool isMotionVideo;
final dynamic onVideoEnded;
final void Function() onVideoEnded;
final dynamic onPlaying;
final void Function()? onPlaying;
final dynamic onPaused;
final void Function()? onPaused;
final Widget? placeholder;
@override
String toString() {
return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded, onPlaying: $onPlaying, onPaused: $onPaused}';
return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded, onPlaying: $onPlaying, onPaused: $onPaused, placeholder: $placeholder}';
}
}
@@ -1191,6 +1230,57 @@ class PartnerDetailRouteArgs {
}
}
/// generated route for
/// [PersonResultPage]
class PersonResultRoute extends PageRouteInfo<PersonResultRouteArgs> {
PersonResultRoute({
Key? key,
required String personId,
required String personName,
}) : super(
PersonResultRoute.name,
path: '/person-result-page',
args: PersonResultRouteArgs(
key: key,
personId: personId,
personName: personName,
),
);
static const String name = 'PersonResultRoute';
}
class PersonResultRouteArgs {
const PersonResultRouteArgs({
this.key,
required this.personId,
required this.personName,
});
final Key? key;
final String personId;
final String personName;
@override
String toString() {
return 'PersonResultRouteArgs{key: $key, personId: $personId, personName: $personName}';
}
}
/// generated route for
/// [AllPeoplePage]
class AllPeopleRoute extends PageRouteInfo<void> {
const AllPeopleRoute()
: super(
AllPeopleRoute.name,
path: '/all-people-page',
);
static const String name = 'AllPeopleRoute';
}
/// generated route for
/// [HomePage]
class HomeRoute extends PageRouteInfo<void> {

View File

@@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@@ -32,6 +33,7 @@ class TabNavigationObserver extends AutoRouterObserver {
// Refresh Location State
ref.invalidate(getCuratedLocationProvider);
ref.invalidate(getCuratedObjectProvider);
ref.invalidate(getCuratedPeopleProvider);
}
if (route.name == 'SharingRoute') {

View File

@@ -190,7 +190,7 @@ final remoteAssetsProvider =
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(userId)
.sortByFileCreatedAt();
.sortByFileCreatedAtDesc();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];

View File

@@ -76,7 +76,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
return {
"major": int.parse(major),
"minor": int.parse(minor),
"patch": int.parse(patch),
"patch": int.parse(patch.replaceAll("-DEBUG", "")),
};
}
}

View File

@@ -17,6 +17,7 @@ class ApiService {
late SearchApi searchApi;
late ServerInfoApi serverInfoApi;
late PartnerApi partnerApi;
late PersonApi personApi;
ApiService() {
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
@@ -39,6 +40,7 @@ class ApiService {
serverInfoApi = ServerInfoApi(_apiClient);
searchApi = SearchApi(_apiClient);
partnerApi = PartnerApi(_apiClient);
personApi = PersonApi(_apiClient);
}
Future<String> resolveAndSetEndpoint(String serverUrl) async {

View File

@@ -82,8 +82,12 @@ class AssetService {
_db.writeTxn(() => _db.eTags.put(ETag(id: user.id, value: newETag)));
}
return assets.map(Asset.remote).toList();
} catch (e, stack) {
log.severe('Error while getting remote assets', e, stack);
} catch (error, stack) {
log.severe(
'Error while getting remote assets: ${error.toString()}',
error,
stack,
);
return null;
}
}
@@ -100,8 +104,8 @@ class AssetService {
return await _apiService.assetApi
.deleteAsset(DeleteAssetDto(ids: payload));
} catch (e) {
debugPrint("Error getAllAsset ${e.toString()}");
} catch (error, stack) {
log.severe("Error deleteAssets ${error.toString()}", error, stack);
return null;
}
}

View File

@@ -112,7 +112,6 @@ class TabControllerPage extends HookConsumerWidget {
),
selectedIcon: buildIcon(
Icon(
size: 24,
Icons.photo_library,
color: Theme.of(context).primaryColor,
),

View File

@@ -47,6 +47,9 @@ class FileHelper {
case 'webm':
return {"type": "video", "subType": "webm"};
case 'avif':
return {"type": "image", "subType": "avif"};
case 'insp':
return {"type": "image", "subType": "jpeg"};
@@ -56,6 +59,90 @@ class FileHelper {
case 'arw':
return {"type": "image", "subType": "x-sony-arw"};
case 'raf':
return {"type": "image", "subType": "x-fuji-raf"};
case 'nef':
return {"type": "image", "subType": "x-nikon-nef"};
case 'srw':
return {"type": "image", "subType": "x-samsung-srw"};
case 'crw':
return {"type": "image", "subType": "x-canon-crw"};
case 'cr2':
return {"type": "image", "subType": "x-canon-cr2"};
case 'cr3':
return {"type": "image", "subType": "x-canon-cr3"};
case 'erf':
return {"type": "image", "subType": "x-epson-erf"};
case 'dcr':
return {"type": "image", "subType": "x-kodak-dcr"};
case 'k25':
return {"type": "image", "subType": "x-kodak-k25"};
case 'kdc':
return {"type": "image", "subType": "x-kodak-kdc"};
case 'mrw':
return {"type": "image", "subType": "x-minolta-mrw"};
case 'orf':
return {"type": "image", "subType": "x-olympus-orf"};
case 'raw':
return {"type": "image", "subType": "x-panasonic-raw"};
case 'pef':
return {"type": "image", "subType": "x-panasonic-pef"};
case 'x3f':
return {"type": "image", "subType": "x-sigma-x3f"};
case 'srf':
return {"type": "image", "subType": "x-sony-srf"};
case 'sr2':
return {"type": "image", "subType": "x-sony-sr2"};
case '3fr':
return {"type": "image", "subType": "x-hasselblad-3fr"};
case 'fff':
return {"type": "image", "subType": "x-hasselblad-fff"};
case 'rwl':
return {"type": "image", "subType": "x-leica-rwl"};
case 'ori':
return {"type": "image", "subType": "x-olympus-ori"};
case 'iiq':
return {"type": "image", "subType": "x-phaseone-iiq"};
case 'ari':
return {"type": "image", "subType": "x-arriflex-ari"};
case 'cap':
return {"type": "image", "subType": "x-phaseone-cap"};
case 'cin':
return {"type": "image", "subType": "x-phantom-cin"};
case 'jxl':
return {"type": "image", "subType": "jxl"};
case 'mts':
return {"type": "video", "subType": "mp2t"};
case 'm2ts':
return {"type": "video", "subType": "mp2t"};
default:
return {"type": "unsupport", "subType": "unsupport"};
}

View File

@@ -59,3 +59,7 @@ String _getThumbnailUrl(
}) {
return '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$id?format=${type.value}';
}
String getFaceThumbnailUrl(final String personId) {
return '${Store.get(StoreKey.serverEndpoint)}/person/$personId/thumbnail';
}

View File

@@ -24,6 +24,10 @@ ThemeData base = ThemeData(
chipTheme: const ChipThemeData(
side: BorderSide.none,
),
sliderTheme: const SliderThemeData(
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7),
trackHeight: 2.0,
),
);
ThemeData immichLightTheme = ThemeData(
@@ -32,6 +36,8 @@ ThemeData immichLightTheme = ThemeData(
primarySwatch: Colors.indigo,
primaryColor: Colors.indigo,
hintColor: Colors.indigo,
focusColor: Colors.indigo,
splashColor: Colors.indigo.withOpacity(0.15),
fontFamily: 'WorkSans',
scaffoldBackgroundColor: immichBackgroundColor,
snackBarTheme: const SnackBarThemeData(
@@ -97,6 +103,7 @@ ThemeData immichLightTheme = ThemeData(
),
),
chipTheme: base.chipTheme,
sliderTheme: base.sliderTheme,
popupMenuTheme: const PopupMenuThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
@@ -119,6 +126,26 @@ ThemeData immichLightTheme = ThemeData(
),
),
),
dialogTheme: const DialogTheme(
surfaceTintColor: Colors.transparent,
),
inputDecorationTheme: const InputDecorationTheme(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.indigo,
),
),
labelStyle: TextStyle(
color: Colors.indigo,
),
hintStyle: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
),
),
textSelectionTheme: const TextSelectionThemeData(
cursorColor: Colors.indigo,
),
);
ThemeData immichDarkTheme = ThemeData(
@@ -196,6 +223,7 @@ ThemeData immichDarkTheme = ThemeData(
),
),
chipTheme: base.chipTheme,
sliderTheme: base.sliderTheme,
popupMenuTheme: const PopupMenuThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
@@ -217,4 +245,24 @@ ThemeData immichDarkTheme = ThemeData(
),
),
),
dialogTheme: const DialogTheme(
surfaceTintColor: Colors.transparent,
),
inputDecorationTheme: InputDecorationTheme(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: immichDarkThemePrimaryColor,
),
),
labelStyle: TextStyle(
color: immichDarkThemePrimaryColor,
),
hintStyle: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
),
),
textSelectionTheme: TextSelectionThemeData(
cursorColor: immichDarkThemePrimaryColor,
),
);

View File

@@ -37,8 +37,6 @@ doc/CheckDuplicateAssetResponseDto.md
doc/CheckExistingAssetsDto.md
doc/CheckExistingAssetsResponseDto.md
doc/CreateAlbumDto.md
doc/CreateAlbumShareLinkDto.md
doc/CreateAssetsShareLinkDto.md
doc/CreateProfileImageResponseDto.md
doc/CreateTagDto.md
doc/CreateUserDto.md
@@ -48,10 +46,10 @@ doc/DeleteAssetDto.md
doc/DeleteAssetResponseDto.md
doc/DeleteAssetStatus.md
doc/DownloadFilesDto.md
doc/EditSharedLinkDto.md
doc/ExifResponseDto.md
doc/GetAssetByTimeBucketDto.md
doc/GetAssetCountByTimeBucketDto.md
doc/ImportAssetDto.md
doc/JobApi.md
doc/JobCommand.md
doc/JobCommandDto.md
@@ -89,7 +87,9 @@ doc/ServerInfoResponseDto.md
doc/ServerPingResponse.md
doc/ServerStatsResponseDto.md
doc/ServerVersionReponseDto.md
doc/ShareApi.md
doc/SharedLinkApi.md
doc/SharedLinkCreateDto.md
doc/SharedLinkEditDto.md
doc/SharedLinkResponseDto.md
doc/SharedLinkType.md
doc/SignUpDto.md
@@ -128,7 +128,7 @@ lib/api/partner_api.dart
lib/api/person_api.dart
lib/api/search_api.dart
lib/api/server_info_api.dart
lib/api/share_api.dart
lib/api/shared_link_api.dart
lib/api/system_config_api.dart
lib/api/tag_api.dart
lib/api/user_api.dart
@@ -170,8 +170,6 @@ lib/model/check_duplicate_asset_response_dto.dart
lib/model/check_existing_assets_dto.dart
lib/model/check_existing_assets_response_dto.dart
lib/model/create_album_dto.dart
lib/model/create_album_share_link_dto.dart
lib/model/create_assets_share_link_dto.dart
lib/model/create_profile_image_response_dto.dart
lib/model/create_tag_dto.dart
lib/model/create_user_dto.dart
@@ -181,10 +179,10 @@ lib/model/delete_asset_dto.dart
lib/model/delete_asset_response_dto.dart
lib/model/delete_asset_status.dart
lib/model/download_files_dto.dart
lib/model/edit_shared_link_dto.dart
lib/model/exif_response_dto.dart
lib/model/get_asset_by_time_bucket_dto.dart
lib/model/get_asset_count_by_time_bucket_dto.dart
lib/model/import_asset_dto.dart
lib/model/job_command.dart
lib/model/job_command_dto.dart
lib/model/job_counts_dto.dart
@@ -216,6 +214,8 @@ lib/model/server_info_response_dto.dart
lib/model/server_ping_response.dart
lib/model/server_stats_response_dto.dart
lib/model/server_version_reponse_dto.dart
lib/model/shared_link_create_dto.dart
lib/model/shared_link_edit_dto.dart
lib/model/shared_link_response_dto.dart
lib/model/shared_link_type.dart
lib/model/sign_up_dto.dart
@@ -274,8 +274,6 @@ test/check_duplicate_asset_response_dto_test.dart
test/check_existing_assets_dto_test.dart
test/check_existing_assets_response_dto_test.dart
test/create_album_dto_test.dart
test/create_album_share_link_dto_test.dart
test/create_assets_share_link_dto_test.dart
test/create_profile_image_response_dto_test.dart
test/create_tag_dto_test.dart
test/create_user_dto_test.dart
@@ -285,10 +283,10 @@ test/delete_asset_dto_test.dart
test/delete_asset_response_dto_test.dart
test/delete_asset_status_test.dart
test/download_files_dto_test.dart
test/edit_shared_link_dto_test.dart
test/exif_response_dto_test.dart
test/get_asset_by_time_bucket_dto_test.dart
test/get_asset_count_by_time_bucket_dto_test.dart
test/import_asset_dto_test.dart
test/job_api_test.dart
test/job_command_dto_test.dart
test/job_command_test.dart
@@ -326,7 +324,9 @@ test/server_info_response_dto_test.dart
test/server_ping_response_test.dart
test/server_stats_response_dto_test.dart
test/server_version_reponse_dto_test.dart
test/share_api_test.dart
test/shared_link_api_test.dart
test/shared_link_create_dto_test.dart
test/shared_link_edit_dto_test.dart
test/shared_link_response_dto_test.dart
test/shared_link_type_test.dart
test/sign_up_dto_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.61.0
- API version: 1.64.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -80,20 +80,17 @@ Class | Method | HTTP request | Description
*AlbumApi* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets |
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users |
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album |
*AlbumApi* | [**createAlbumSharedLink**](doc//AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link |
*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{id} |
*AlbumApi* | [**downloadArchive**](doc//AlbumApi.md#downloadarchive) | **GET** /album/{id}/download |
*AlbumApi* | [**getAlbumCountByUserId**](doc//AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
*AlbumApi* | [**getAlbumCount**](doc//AlbumApi.md#getalbumcount) | **GET** /album/count |
*AlbumApi* | [**getAlbumInfo**](doc//AlbumApi.md#getalbuminfo) | **GET** /album/{id} |
*AlbumApi* | [**getAllAlbums**](doc//AlbumApi.md#getallalbums) | **GET** /album |
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} |
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} |
*AssetApi* | [**addAssetsToSharedLink**](doc//AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |
*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* | [**createAssetsSharedLink**](doc//AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |
*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset |
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download/{id} |
*AssetApi* | [**downloadFiles**](doc//AssetApi.md#downloadfiles) | **POST** /asset/download-files |
@@ -111,7 +108,7 @@ Class | Method | HTTP request | Description
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
*AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
*AssetApi* | [**importFile**](doc//AssetApi.md#importfile) | **POST** /asset/import |
*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} |
@@ -125,7 +122,7 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
*OAuthApi* | [**callback**](doc//OAuthApi.md#callback) | **POST** /oauth/callback |
*OAuthApi* | [**generateConfig**](doc//OAuthApi.md#generateconfig) | **POST** /oauth/config |
*OAuthApi* | [**link**](doc//OAuthApi.md#link) | **POST** /oauth/link |
@@ -146,11 +143,14 @@ Class | Method | HTTP request | Description
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats |
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
*ShareApi* | [**getAllSharedLinks**](doc//ShareApi.md#getallsharedlinks) | **GET** /share |
*ShareApi* | [**getMySharedLink**](doc//ShareApi.md#getmysharedlink) | **GET** /share/me |
*ShareApi* | [**getSharedLinkById**](doc//ShareApi.md#getsharedlinkbyid) | **GET** /share/{id} |
*ShareApi* | [**removeSharedLink**](doc//ShareApi.md#removesharedlink) | **DELETE** /share/{id} |
*ShareApi* | [**updateSharedLink**](doc//ShareApi.md#updatesharedlink) | **PATCH** /share/{id} |
*SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets |
*SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link |
*SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link |
*SharedLinkApi* | [**getMySharedLink**](doc//SharedLinkApi.md#getmysharedlink) | **GET** /shared-link/me |
*SharedLinkApi* | [**getSharedLinkById**](doc//SharedLinkApi.md#getsharedlinkbyid) | **GET** /shared-link/{id} |
*SharedLinkApi* | [**removeSharedLink**](doc//SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} |
*SharedLinkApi* | [**removeSharedLinkAssets**](doc//SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets |
*SharedLinkApi* | [**updateSharedLink**](doc//SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} |
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
*SystemConfigApi* | [**getDefaults**](doc//SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults |
*SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |
@@ -207,8 +207,6 @@ Class | Method | HTTP request | Description
- [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
- [CreateAlbumDto](doc//CreateAlbumDto.md)
- [CreateAlbumShareLinkDto](doc//CreateAlbumShareLinkDto.md)
- [CreateAssetsShareLinkDto](doc//CreateAssetsShareLinkDto.md)
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
- [CreateTagDto](doc//CreateTagDto.md)
- [CreateUserDto](doc//CreateUserDto.md)
@@ -218,10 +216,10 @@ Class | Method | HTTP request | Description
- [DeleteAssetResponseDto](doc//DeleteAssetResponseDto.md)
- [DeleteAssetStatus](doc//DeleteAssetStatus.md)
- [DownloadFilesDto](doc//DownloadFilesDto.md)
- [EditSharedLinkDto](doc//EditSharedLinkDto.md)
- [ExifResponseDto](doc//ExifResponseDto.md)
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
- [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
- [ImportAssetDto](doc//ImportAssetDto.md)
- [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md)
- [JobCountsDto](doc//JobCountsDto.md)
@@ -253,6 +251,8 @@ Class | Method | HTTP request | Description
- [ServerPingResponse](doc//ServerPingResponse.md)
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
- [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
- [SharedLinkType](doc//SharedLinkType.md)
- [SignUpDto](doc//SignUpDto.md)

View File

@@ -12,10 +12,9 @@ Method | HTTP request | Description
[**addAssetsToAlbum**](AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets |
[**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users |
[**createAlbum**](AlbumApi.md#createalbum) | **POST** /album |
[**createAlbumSharedLink**](AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link |
[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{id} |
[**downloadArchive**](AlbumApi.md#downloadarchive) | **GET** /album/{id}/download |
[**getAlbumCountByUserId**](AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
[**getAlbumCount**](AlbumApi.md#getalbumcount) | **GET** /album/count |
[**getAlbumInfo**](AlbumApi.md#getalbuminfo) | **GET** /album/{id} |
[**getAllAlbums**](AlbumApi.md#getallalbums) | **GET** /album |
[**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |
@@ -194,61 +193,6 @@ 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)
# **createAlbumSharedLink**
> SharedLinkResponseDto createAlbumSharedLink(createAlbumShareLinkDto)
### 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 = AlbumApi();
final createAlbumShareLinkDto = CreateAlbumShareLinkDto(); // CreateAlbumShareLinkDto |
try {
final result = api_instance.createAlbumSharedLink(createAlbumShareLinkDto);
print(result);
} catch (e) {
print('Exception when calling AlbumApi->createAlbumSharedLink: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**createAlbumShareLinkDto** | [**CreateAlbumShareLinkDto**](CreateAlbumShareLinkDto.md)| |
### Return type
[**SharedLinkResponseDto**](SharedLinkResponseDto.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)
# **deleteAlbum**
> deleteAlbum(id)
@@ -364,8 +308,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)
# **getAlbumCountByUserId**
> AlbumCountResponseDto getAlbumCountByUserId()
# **getAlbumCount**
> AlbumCountResponseDto getAlbumCount()
@@ -390,10 +334,10 @@ import 'package:openapi/api.dart';
final api_instance = AlbumApi();
try {
final result = api_instance.getAlbumCountByUserId();
final result = api_instance.getAlbumCount();
print(result);
} catch (e) {
print('Exception when calling AlbumApi->getAlbumCountByUserId: $e\n');
print('Exception when calling AlbumApi->getAlbumCount: $e\n');
}
```

View File

@@ -10,7 +10,7 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**owned** | **int** | |
**shared** | **int** | |
**sharing** | **int** | |
**notShared** | **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
**sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | | [default to const []]
**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
**owner** | [**UserResponseDto**](UserResponseDto.md) | |
**lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [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

@@ -9,11 +9,9 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**addAssetsToSharedLink**](AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |
[**bulkUploadCheck**](AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check |
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
[**createAssetsSharedLink**](AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |
[**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download/{id} |
[**downloadFiles**](AssetApi.md#downloadfiles) | **POST** /asset/download-files |
@@ -31,70 +29,13 @@ Method | HTTP request | Description
[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
[**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
[**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
[**importFile**](AssetApi.md#importfile) | **POST** /asset/import |
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
[**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} |
[**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |
# **addAssetsToSharedLink**
> SharedLinkResponseDto addAssetsToSharedLink(addAssetsDto, key)
### 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 addAssetsDto = AddAssetsDto(); // AddAssetsDto |
final key = key_example; // String |
try {
final result = api_instance.addAssetsToSharedLink(addAssetsDto, key);
print(result);
} catch (e) {
print('Exception when calling AssetApi->addAssetsToSharedLink: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**addAssetsDto** | [**AddAssetsDto**](AddAssetsDto.md)| |
**key** | **String**| | [optional]
### Return type
[**SharedLinkResponseDto**](SharedLinkResponseDto.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)
# **bulkUploadCheck**
> AssetBulkUploadCheckResponseDto bulkUploadCheck(assetBulkUploadCheckDto)
@@ -268,61 +209,6 @@ 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)
# **createAssetsSharedLink**
> SharedLinkResponseDto createAssetsSharedLink(createAssetsShareLinkDto)
### 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 createAssetsShareLinkDto = CreateAssetsShareLinkDto(); // CreateAssetsShareLinkDto |
try {
final result = api_instance.createAssetsSharedLink(createAssetsShareLinkDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->createAssetsSharedLink: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**createAssetsShareLinkDto** | [**CreateAssetsShareLinkDto**](CreateAssetsShareLinkDto.md)| |
### Return type
[**SharedLinkResponseDto**](SharedLinkResponseDto.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)
# **deleteAsset**
> List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto)
@@ -1274,8 +1160,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)
# **removeAssetsFromSharedLink**
> SharedLinkResponseDto removeAssetsFromSharedLink(removeAssetsDto, key)
# **importFile**
> AssetFileUploadResponseDto importFile(importAssetDto)
@@ -1298,14 +1184,13 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto |
final key = key_example; // String |
final importAssetDto = ImportAssetDto(); // ImportAssetDto |
try {
final result = api_instance.removeAssetsFromSharedLink(removeAssetsDto, key);
final result = api_instance.importFile(importAssetDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->removeAssetsFromSharedLink: $e\n');
print('Exception when calling AssetApi->importFile: $e\n');
}
```
@@ -1313,12 +1198,11 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**removeAssetsDto** | [**RemoveAssetsDto**](RemoveAssetsDto.md)| |
**key** | **String**| | [optional]
**importAssetDto** | [**ImportAssetDto**](ImportAssetDto.md)| |
### Return type
[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
[**AssetFileUploadResponseDto**](AssetFileUploadResponseDto.md)
### Authorization
@@ -1507,7 +1391,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)
# **uploadFile**
> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration)
> AssetFileUploadResponseDto uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration)
@@ -1532,21 +1416,22 @@ import 'package:openapi/api.dart';
final api_instance = AssetApi();
final assetType = ; // AssetTypeEnum |
final assetData = BINARY_DATA_HERE; // MultipartFile |
final fileExtension = fileExtension_example; // String |
final deviceAssetId = deviceAssetId_example; // String |
final deviceId = deviceId_example; // String |
final fileCreatedAt = 2013-10-20T19:20:30+01:00; // DateTime |
final fileModifiedAt = 2013-10-20T19:20:30+01:00; // DateTime |
final isFavorite = true; // bool |
final fileExtension = fileExtension_example; // String |
final key = key_example; // String |
final livePhotoData = BINARY_DATA_HERE; // MultipartFile |
final sidecarData = BINARY_DATA_HERE; // MultipartFile |
final isReadOnly = true; // bool |
final isArchived = true; // bool |
final isVisible = true; // bool |
final duration = duration_example; // String |
try {
final result = api_instance.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration);
final result = api_instance.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration);
print(result);
} catch (e) {
print('Exception when calling AssetApi->uploadFile: $e\n');
@@ -1559,15 +1444,16 @@ Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**assetType** | [**AssetTypeEnum**](AssetTypeEnum.md)| |
**assetData** | **MultipartFile**| |
**fileExtension** | **String**| |
**deviceAssetId** | **String**| |
**deviceId** | **String**| |
**fileCreatedAt** | **DateTime**| |
**fileModifiedAt** | **DateTime**| |
**isFavorite** | **bool**| |
**fileExtension** | **String**| |
**key** | **String**| | [optional]
**livePhotoData** | **MultipartFile**| | [optional]
**sidecarData** | **MultipartFile**| | [optional]
**isReadOnly** | **bool**| | [optional] [default to false]
**isArchived** | **bool**| | [optional]
**isVisible** | **bool**| | [optional]
**duration** | **String**| | [optional]

View File

@@ -16,6 +16,7 @@ Name | Type | Description | Notes
**originalPath** | **String** | |
**originalFileName** | **String** | |
**resized** | **bool** | |
**thumbhash** | **String** | base64 encoded thumbhash |
**fileCreatedAt** | [**DateTime**](DateTime.md) | |
**fileModifiedAt** | [**DateTime**](DateTime.md) | |
**updatedAt** | [**DateTime**](DateTime.md) | |

View File

@@ -1,20 +0,0 @@
# openapi.model.CreateAssetsShareLinkDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assetIds** | **List<String>** | | [default to const []]
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**allowUpload** | **bool** | | [optional]
**allowDownload** | **bool** | | [optional]
**showExif** | **bool** | | [optional]
**description** | **String** | | [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

@@ -13,6 +13,7 @@ Name | Type | Description | Notes
**firstName** | **String** | |
**lastName** | **String** | |
**storageLabel** | **String** | | [optional]
**externalPath** | **String** | | [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)

26
mobile/openapi/doc/ImportAssetDto.md generated Normal file
View File

@@ -0,0 +1,26 @@
# openapi.model.ImportAssetDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assetType** | [**AssetTypeEnum**](AssetTypeEnum.md) | |
**isReadOnly** | **bool** | | [optional] [default to true]
**assetPath** | **String** | |
**sidecarPath** | **String** | | [optional]
**deviceAssetId** | **String** | |
**deviceId** | **String** | |
**fileCreatedAt** | [**DateTime**](DateTime.md) | |
**fileModifiedAt** | [**DateTime**](DateTime.md) | |
**isFavorite** | **bool** | |
**isArchived** | **bool** | | [optional]
**isVisible** | **bool** | | [optional]
**duration** | **String** | | [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

@@ -10,7 +10,7 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**getAllJobsStatus**](JobApi.md#getalljobsstatus) | **GET** /jobs |
[**sendJobCommand**](JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
[**sendJobCommand**](JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
# **getAllJobsStatus**
@@ -65,7 +65,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)
# **sendJobCommand**
> JobStatusDto sendJobCommand(jobId, jobCommandDto)
> JobStatusDto sendJobCommand(id, jobCommandDto)
@@ -88,11 +88,11 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = JobApi();
final jobId = ; // JobName |
final id = ; // JobName |
final jobCommandDto = JobCommandDto(); // JobCommandDto |
try {
final result = api_instance.sendJobCommand(jobId, jobCommandDto);
final result = api_instance.sendJobCommand(id, jobCommandDto);
print(result);
} catch (e) {
print('Exception when calling JobApi->sendJobCommand: $e\n');
@@ -103,7 +103,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**jobId** | [**JobName**](.md)| |
**id** | [**JobName**](.md)| |
**jobCommandDto** | [**JobCommandDto**](JobCommandDto.md)| |
### Return type

View File

@@ -1,4 +1,4 @@
# openapi.api.ShareApi
# openapi.api.SharedLinkApi
## Load the API package
```dart
@@ -9,13 +9,130 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**getAllSharedLinks**](ShareApi.md#getallsharedlinks) | **GET** /share |
[**getMySharedLink**](ShareApi.md#getmysharedlink) | **GET** /share/me |
[**getSharedLinkById**](ShareApi.md#getsharedlinkbyid) | **GET** /share/{id} |
[**removeSharedLink**](ShareApi.md#removesharedlink) | **DELETE** /share/{id} |
[**updateSharedLink**](ShareApi.md#updatesharedlink) | **PATCH** /share/{id} |
[**addSharedLinkAssets**](SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets |
[**createSharedLink**](SharedLinkApi.md#createsharedlink) | **POST** /shared-link |
[**getAllSharedLinks**](SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link |
[**getMySharedLink**](SharedLinkApi.md#getmysharedlink) | **GET** /shared-link/me |
[**getSharedLinkById**](SharedLinkApi.md#getsharedlinkbyid) | **GET** /shared-link/{id} |
[**removeSharedLink**](SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} |
[**removeSharedLinkAssets**](SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets |
[**updateSharedLink**](SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} |
# **addSharedLinkAssets**
> List<AssetIdsResponseDto> addSharedLinkAssets(id, assetIdsDto, key)
### 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 = SharedLinkApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final assetIdsDto = AssetIdsDto(); // AssetIdsDto |
final key = key_example; // String |
try {
final result = api_instance.addSharedLinkAssets(id, assetIdsDto, key);
print(result);
} catch (e) {
print('Exception when calling SharedLinkApi->addSharedLinkAssets: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
**assetIdsDto** | [**AssetIdsDto**](AssetIdsDto.md)| |
**key** | **String**| | [optional]
### Return type
[**List<AssetIdsResponseDto>**](AssetIdsResponseDto.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)
# **createSharedLink**
> SharedLinkResponseDto createSharedLink(sharedLinkCreateDto)
### 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 = SharedLinkApi();
final sharedLinkCreateDto = SharedLinkCreateDto(); // SharedLinkCreateDto |
try {
final result = api_instance.createSharedLink(sharedLinkCreateDto);
print(result);
} catch (e) {
print('Exception when calling SharedLinkApi->createSharedLink: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**sharedLinkCreateDto** | [**SharedLinkCreateDto**](SharedLinkCreateDto.md)| |
### Return type
[**SharedLinkResponseDto**](SharedLinkResponseDto.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)
# **getAllSharedLinks**
> List<SharedLinkResponseDto> getAllSharedLinks()
@@ -39,13 +156,13 @@ import 'package:openapi/api.dart';
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ShareApi();
final api_instance = SharedLinkApi();
try {
final result = api_instance.getAllSharedLinks();
print(result);
} catch (e) {
print('Exception when calling ShareApi->getAllSharedLinks: $e\n');
print('Exception when calling SharedLinkApi->getAllSharedLinks: $e\n');
}
```
@@ -90,14 +207,14 @@ import 'package:openapi/api.dart';
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ShareApi();
final api_instance = SharedLinkApi();
final key = key_example; // String |
try {
final result = api_instance.getMySharedLink(key);
print(result);
} catch (e) {
print('Exception when calling ShareApi->getMySharedLink: $e\n');
print('Exception when calling SharedLinkApi->getMySharedLink: $e\n');
}
```
@@ -145,14 +262,14 @@ import 'package:openapi/api.dart';
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ShareApi();
final api_instance = SharedLinkApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
final result = api_instance.getSharedLinkById(id);
print(result);
} catch (e) {
print('Exception when calling ShareApi->getSharedLinkById: $e\n');
print('Exception when calling SharedLinkApi->getSharedLinkById: $e\n');
}
```
@@ -200,13 +317,13 @@ import 'package:openapi/api.dart';
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ShareApi();
final api_instance = SharedLinkApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
api_instance.removeSharedLink(id);
} catch (e) {
print('Exception when calling ShareApi->removeSharedLink: $e\n');
print('Exception when calling SharedLinkApi->removeSharedLink: $e\n');
}
```
@@ -231,8 +348,8 @@ void (empty response body)
[[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)
# **updateSharedLink**
> SharedLinkResponseDto updateSharedLink(id, editSharedLinkDto)
# **removeSharedLinkAssets**
> List<AssetIdsResponseDto> removeSharedLinkAssets(id, assetIdsDto, key)
@@ -254,15 +371,16 @@ import 'package:openapi/api.dart';
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ShareApi();
final api_instance = SharedLinkApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final editSharedLinkDto = EditSharedLinkDto(); // EditSharedLinkDto |
final assetIdsDto = AssetIdsDto(); // AssetIdsDto |
final key = key_example; // String |
try {
final result = api_instance.updateSharedLink(id, editSharedLinkDto);
final result = api_instance.removeSharedLinkAssets(id, assetIdsDto, key);
print(result);
} catch (e) {
print('Exception when calling ShareApi->updateSharedLink: $e\n');
print('Exception when calling SharedLinkApi->removeSharedLinkAssets: $e\n');
}
```
@@ -271,7 +389,65 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
**editSharedLinkDto** | [**EditSharedLinkDto**](EditSharedLinkDto.md)| |
**assetIdsDto** | [**AssetIdsDto**](AssetIdsDto.md)| |
**key** | **String**| | [optional]
### Return type
[**List<AssetIdsResponseDto>**](AssetIdsResponseDto.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)
# **updateSharedLink**
> SharedLinkResponseDto updateSharedLink(id, sharedLinkEditDto)
### 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 = SharedLinkApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final sharedLinkEditDto = SharedLinkEditDto(); // SharedLinkEditDto |
try {
final result = api_instance.updateSharedLink(id, sharedLinkEditDto);
print(result);
} catch (e) {
print('Exception when calling SharedLinkApi->updateSharedLink: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
**sharedLinkEditDto** | [**SharedLinkEditDto**](SharedLinkEditDto.md)| |
### Return type

View File

@@ -1,4 +1,4 @@
# openapi.model.CreateAlbumShareLinkDto
# openapi.model.SharedLinkCreateDto
## Load the model package
```dart
@@ -8,12 +8,14 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**albumId** | **String** | |
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**allowUpload** | **bool** | | [optional]
**allowDownload** | **bool** | | [optional]
**showExif** | **bool** | | [optional]
**type** | [**SharedLinkType**](SharedLinkType.md) | |
**assetIds** | **List<String>** | | [optional] [default to const []]
**albumId** | **String** | | [optional]
**description** | **String** | | [optional]
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**allowUpload** | **bool** | | [optional] [default to false]
**allowDownload** | **bool** | | [optional] [default to true]
**showExif** | **bool** | | [optional] [default to true]
[[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.EditSharedLinkDto
# openapi.model.SharedLinkEditDto
## Load the model package
```dart

View File

@@ -10,7 +10,7 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**type** | [**SharedLinkType**](SharedLinkType.md) | |
**id** | **String** | |
**description** | **String** | | [optional]
**description** | **String** | |
**userId** | **String** | |
**key** | **String** | |
**createdAt** | [**DateTime**](DateTime.md) | |

View File

@@ -14,6 +14,7 @@ Name | Type | Description | Notes
**firstName** | **String** | | [optional]
**lastName** | **String** | | [optional]
**storageLabel** | **String** | | [optional]
**externalPath** | **String** | | [optional]
**isAdmin** | **bool** | | [optional]
**shouldChangePassword** | **bool** | | [optional]

View File

@@ -13,6 +13,7 @@ Name | Type | Description | Notes
**firstName** | **String** | |
**lastName** | **String** | |
**storageLabel** | **String** | |
**externalPath** | **String** | |
**profileImagePath** | **String** | |
**shouldChangePassword** | **bool** | |
**isAdmin** | **bool** | |

View File

@@ -38,7 +38,7 @@ part 'api/partner_api.dart';
part 'api/person_api.dart';
part 'api/search_api.dart';
part 'api/server_info_api.dart';
part 'api/share_api.dart';
part 'api/shared_link_api.dart';
part 'api/system_config_api.dart';
part 'api/tag_api.dart';
part 'api/user_api.dart';
@@ -73,8 +73,6 @@ part 'model/check_duplicate_asset_response_dto.dart';
part 'model/check_existing_assets_dto.dart';
part 'model/check_existing_assets_response_dto.dart';
part 'model/create_album_dto.dart';
part 'model/create_album_share_link_dto.dart';
part 'model/create_assets_share_link_dto.dart';
part 'model/create_profile_image_response_dto.dart';
part 'model/create_tag_dto.dart';
part 'model/create_user_dto.dart';
@@ -84,10 +82,10 @@ part 'model/delete_asset_dto.dart';
part 'model/delete_asset_response_dto.dart';
part 'model/delete_asset_status.dart';
part 'model/download_files_dto.dart';
part 'model/edit_shared_link_dto.dart';
part 'model/exif_response_dto.dart';
part 'model/get_asset_by_time_bucket_dto.dart';
part 'model/get_asset_count_by_time_bucket_dto.dart';
part 'model/import_asset_dto.dart';
part 'model/job_command.dart';
part 'model/job_command_dto.dart';
part 'model/job_counts_dto.dart';
@@ -119,6 +117,8 @@ part 'model/server_info_response_dto.dart';
part 'model/server_ping_response.dart';
part 'model/server_stats_response_dto.dart';
part 'model/server_version_reponse_dto.dart';
part 'model/shared_link_create_dto.dart';
part 'model/shared_link_edit_dto.dart';
part 'model/shared_link_response_dto.dart';
part 'model/shared_link_type.dart';
part 'model/sign_up_dto.dart';

View File

@@ -175,53 +175,6 @@ class AlbumApi {
return null;
}
/// Performs an HTTP 'POST /album/create-shared-link' operation and returns the [Response].
/// Parameters:
///
/// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required):
Future<Response> createAlbumSharedLinkWithHttpInfo(CreateAlbumShareLinkDto createAlbumShareLinkDto,) async {
// ignore: prefer_const_declarations
final path = r'/album/create-shared-link';
// ignore: prefer_final_locals
Object? postBody = createAlbumShareLinkDto;
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:
///
/// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required):
Future<SharedLinkResponseDto?> createAlbumSharedLink(CreateAlbumShareLinkDto createAlbumShareLinkDto,) async {
final response = await createAlbumSharedLinkWithHttpInfo(createAlbumShareLinkDto,);
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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
/// Performs an HTTP 'DELETE /album/{id}' operation and returns the [Response].
/// Parameters:
///
@@ -332,10 +285,10 @@ class AlbumApi {
return null;
}
/// Performs an HTTP 'GET /album/count-by-user-id' operation and returns the [Response].
Future<Response> getAlbumCountByUserIdWithHttpInfo() async {
/// Performs an HTTP 'GET /album/count' operation and returns the [Response].
Future<Response> getAlbumCountWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/album/count-by-user-id';
final path = r'/album/count';
// ignore: prefer_final_locals
Object? postBody;
@@ -358,8 +311,8 @@ class AlbumApi {
);
}
Future<AlbumCountResponseDto?> getAlbumCountByUserId() async {
final response = await getAlbumCountByUserIdWithHttpInfo();
Future<AlbumCountResponseDto?> getAlbumCount() async {
final response = await getAlbumCountWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -16,61 +16,6 @@ class AssetApi {
final ApiClient apiClient;
/// Performs an HTTP 'PATCH /asset/shared-link/add' operation and returns the [Response].
/// Parameters:
///
/// * [AddAssetsDto] addAssetsDto (required):
///
/// * [String] key:
Future<Response> addAssetsToSharedLinkWithHttpInfo(AddAssetsDto addAssetsDto, { String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/shared-link/add';
// ignore: prefer_final_locals
Object? postBody = addAssetsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [AddAssetsDto] addAssetsDto (required):
///
/// * [String] key:
Future<SharedLinkResponseDto?> addAssetsToSharedLink(AddAssetsDto addAssetsDto, { String? key, }) async {
final response = await addAssetsToSharedLinkWithHttpInfo(addAssetsDto, key: key, );
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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
/// Checks if assets exist by checksums
///
/// Note: This method returns the HTTP [Response].
@@ -235,53 +180,6 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'POST /asset/shared-link' operation and returns the [Response].
/// Parameters:
///
/// * [CreateAssetsShareLinkDto] createAssetsShareLinkDto (required):
Future<Response> createAssetsSharedLinkWithHttpInfo(CreateAssetsShareLinkDto createAssetsShareLinkDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/shared-link';
// ignore: prefer_final_locals
Object? postBody = createAssetsShareLinkDto;
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:
///
/// * [CreateAssetsShareLinkDto] createAssetsShareLinkDto (required):
Future<SharedLinkResponseDto?> createAssetsSharedLink(CreateAssetsShareLinkDto createAssetsShareLinkDto,) async {
final response = await createAssetsSharedLinkWithHttpInfo(createAssetsShareLinkDto,);
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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
/// Performs an HTTP 'DELETE /asset' operation and returns the [Response].
/// Parameters:
///
@@ -1225,33 +1123,27 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'PATCH /asset/shared-link/remove' operation and returns the [Response].
/// Performs an HTTP 'POST /asset/import' operation and returns the [Response].
/// Parameters:
///
/// * [RemoveAssetsDto] removeAssetsDto (required):
///
/// * [String] key:
Future<Response> removeAssetsFromSharedLinkWithHttpInfo(RemoveAssetsDto removeAssetsDto, { String? key, }) async {
/// * [ImportAssetDto] importAssetDto (required):
Future<Response> importFileWithHttpInfo(ImportAssetDto importAssetDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/shared-link/remove';
final path = r'/asset/import';
// ignore: prefer_final_locals
Object? postBody = removeAssetsDto;
Object? postBody = importAssetDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
'POST',
queryParams,
postBody,
headerParams,
@@ -1262,11 +1154,9 @@ class AssetApi {
/// Parameters:
///
/// * [RemoveAssetsDto] removeAssetsDto (required):
///
/// * [String] key:
Future<SharedLinkResponseDto?> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto, { String? key, }) async {
final response = await removeAssetsFromSharedLinkWithHttpInfo(removeAssetsDto, key: key, );
/// * [ImportAssetDto] importAssetDto (required):
Future<AssetFileUploadResponseDto?> importFile(ImportAssetDto importAssetDto,) async {
final response = await importFileWithHttpInfo(importAssetDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -1274,7 +1164,7 @@ class AssetApi {
// 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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetFileUploadResponseDto',) as AssetFileUploadResponseDto;
}
return null;
@@ -1464,6 +1354,8 @@ class AssetApi {
///
/// * [MultipartFile] assetData (required):
///
/// * [String] fileExtension (required):
///
/// * [String] deviceAssetId (required):
///
/// * [String] deviceId (required):
@@ -1474,20 +1366,20 @@ class AssetApi {
///
/// * [bool] isFavorite (required):
///
/// * [String] fileExtension (required):
///
/// * [String] key:
///
/// * [MultipartFile] livePhotoData:
///
/// * [MultipartFile] sidecarData:
///
/// * [bool] isReadOnly:
///
/// * [bool] isArchived:
///
/// * [bool] isVisible:
///
/// * [String] duration:
Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isArchived, bool? isVisible, String? duration, }) async {
Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/upload';
@@ -1525,6 +1417,14 @@ class AssetApi {
mp.fields[r'sidecarData'] = sidecarData.field;
mp.files.add(sidecarData);
}
if (isReadOnly != null) {
hasFields = true;
mp.fields[r'isReadOnly'] = parameterToString(isReadOnly);
}
if (fileExtension != null) {
hasFields = true;
mp.fields[r'fileExtension'] = parameterToString(fileExtension);
}
if (deviceAssetId != null) {
hasFields = true;
mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId);
@@ -1553,10 +1453,6 @@ class AssetApi {
hasFields = true;
mp.fields[r'isVisible'] = parameterToString(isVisible);
}
if (fileExtension != null) {
hasFields = true;
mp.fields[r'fileExtension'] = parameterToString(fileExtension);
}
if (duration != null) {
hasFields = true;
mp.fields[r'duration'] = parameterToString(duration);
@@ -1582,6 +1478,8 @@ class AssetApi {
///
/// * [MultipartFile] assetData (required):
///
/// * [String] fileExtension (required):
///
/// * [String] deviceAssetId (required):
///
/// * [String] deviceId (required):
@@ -1592,21 +1490,21 @@ class AssetApi {
///
/// * [bool] isFavorite (required):
///
/// * [String] fileExtension (required):
///
/// * [String] key:
///
/// * [MultipartFile] livePhotoData:
///
/// * [MultipartFile] sidecarData:
///
/// * [bool] isReadOnly:
///
/// * [bool] isArchived:
///
/// * [bool] isVisible:
///
/// * [String] duration:
Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isArchived, bool? isVisible, String? duration, }) async {
final response = await uploadFileWithHttpInfo(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isArchived: isArchived, isVisible: isVisible, duration: duration, );
Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async {
final response = await uploadFileWithHttpInfo(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isReadOnly: isReadOnly, isArchived: isArchived, isVisible: isVisible, duration: duration, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -57,16 +57,16 @@ class JobApi {
return null;
}
/// Performs an HTTP 'PUT /jobs/{jobId}' operation and returns the [Response].
/// Performs an HTTP 'PUT /jobs/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [JobName] jobId (required):
/// * [JobName] id (required):
///
/// * [JobCommandDto] jobCommandDto (required):
Future<Response> sendJobCommandWithHttpInfo(JobName jobId, JobCommandDto jobCommandDto,) async {
Future<Response> sendJobCommandWithHttpInfo(JobName id, JobCommandDto jobCommandDto,) async {
// ignore: prefer_const_declarations
final path = r'/jobs/{jobId}'
.replaceAll('{jobId}', jobId.toString());
final path = r'/jobs/{id}'
.replaceAll('{id}', id.toString());
// ignore: prefer_final_locals
Object? postBody = jobCommandDto;
@@ -91,11 +91,11 @@ class JobApi {
/// Parameters:
///
/// * [JobName] jobId (required):
/// * [JobName] id (required):
///
/// * [JobCommandDto] jobCommandDto (required):
Future<JobStatusDto?> sendJobCommand(JobName jobId, JobCommandDto jobCommandDto,) async {
final response = await sendJobCommandWithHttpInfo(jobId, jobCommandDto,);
Future<JobStatusDto?> sendJobCommand(JobName id, JobCommandDto jobCommandDto,) async {
final response = await sendJobCommandWithHttpInfo(id, jobCommandDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -1,253 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ShareApi {
ShareApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'GET /share' operation and returns the [Response].
Future<Response> getAllSharedLinksWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/share';
// 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<List<SharedLinkResponseDto>?> getAllSharedLinks() async {
final response = await getAllSharedLinksWithHttpInfo();
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<SharedLinkResponseDto>') as List)
.cast<SharedLinkResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'GET /share/me' operation and returns the [Response].
/// Parameters:
///
/// * [String] key:
Future<Response> getMySharedLinkWithHttpInfo({ String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/share/me';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] key:
Future<SharedLinkResponseDto?> getMySharedLink({ String? key, }) async {
final response = await getMySharedLinkWithHttpInfo( key: key, );
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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /share/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> getSharedLinkByIdWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/share/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<SharedLinkResponseDto?> getSharedLinkById(String id,) async {
final response = await getSharedLinkByIdWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
/// Performs an HTTP 'DELETE /share/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> removeSharedLinkWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/share/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> removeSharedLink(String id,) async {
final response = await removeSharedLinkWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'PATCH /share/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [EditSharedLinkDto] editSharedLinkDto (required):
Future<Response> updateSharedLinkWithHttpInfo(String id, EditSharedLinkDto editSharedLinkDto,) async {
// ignore: prefer_const_declarations
final path = r'/share/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = editSharedLinkDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [EditSharedLinkDto] editSharedLinkDto (required):
Future<SharedLinkResponseDto?> updateSharedLink(String id, EditSharedLinkDto editSharedLinkDto,) async {
final response = await updateSharedLinkWithHttpInfo(id, editSharedLinkDto,);
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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
}

View File

@@ -0,0 +1,426 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SharedLinkApi {
SharedLinkApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'PUT /shared-link/{id}/assets' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
///
/// * [String] key:
Future<Response> addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/shared-link/{id}/assets'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = assetIdsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
///
/// * [String] key:
Future<List<AssetIdsResponseDto>?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, );
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<AssetIdsResponseDto>') as List)
.cast<AssetIdsResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'POST /shared-link' operation and returns the [Response].
/// Parameters:
///
/// * [SharedLinkCreateDto] sharedLinkCreateDto (required):
Future<Response> createSharedLinkWithHttpInfo(SharedLinkCreateDto sharedLinkCreateDto,) async {
// ignore: prefer_const_declarations
final path = r'/shared-link';
// ignore: prefer_final_locals
Object? postBody = sharedLinkCreateDto;
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:
///
/// * [SharedLinkCreateDto] sharedLinkCreateDto (required):
Future<SharedLinkResponseDto?> createSharedLink(SharedLinkCreateDto sharedLinkCreateDto,) async {
final response = await createSharedLinkWithHttpInfo(sharedLinkCreateDto,);
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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /shared-link' operation and returns the [Response].
Future<Response> getAllSharedLinksWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/shared-link';
// 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<List<SharedLinkResponseDto>?> getAllSharedLinks() async {
final response = await getAllSharedLinksWithHttpInfo();
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<SharedLinkResponseDto>') as List)
.cast<SharedLinkResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'GET /shared-link/me' operation and returns the [Response].
/// Parameters:
///
/// * [String] key:
Future<Response> getMySharedLinkWithHttpInfo({ String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/shared-link/me';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] key:
Future<SharedLinkResponseDto?> getMySharedLink({ String? key, }) async {
final response = await getMySharedLinkWithHttpInfo( key: key, );
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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /shared-link/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> getSharedLinkByIdWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/shared-link/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<SharedLinkResponseDto?> getSharedLinkById(String id,) async {
final response = await getSharedLinkByIdWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
/// Performs an HTTP 'DELETE /shared-link/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> removeSharedLinkWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/shared-link/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> removeSharedLink(String id,) async {
final response = await removeSharedLinkWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'DELETE /shared-link/{id}/assets' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
///
/// * [String] key:
Future<Response> removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/shared-link/{id}/assets'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = assetIdsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
///
/// * [String] key:
Future<List<AssetIdsResponseDto>?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, );
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<AssetIdsResponseDto>') as List)
.cast<AssetIdsResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'PATCH /shared-link/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [SharedLinkEditDto] sharedLinkEditDto (required):
Future<Response> updateSharedLinkWithHttpInfo(String id, SharedLinkEditDto sharedLinkEditDto,) async {
// ignore: prefer_const_declarations
final path = r'/shared-link/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = sharedLinkEditDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [SharedLinkEditDto] sharedLinkEditDto (required):
Future<SharedLinkResponseDto?> updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto,) async {
final response = await updateSharedLinkWithHttpInfo(id, sharedLinkEditDto,);
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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
}

View File

@@ -241,10 +241,6 @@ class ApiClient {
return CheckExistingAssetsResponseDto.fromJson(value);
case 'CreateAlbumDto':
return CreateAlbumDto.fromJson(value);
case 'CreateAlbumShareLinkDto':
return CreateAlbumShareLinkDto.fromJson(value);
case 'CreateAssetsShareLinkDto':
return CreateAssetsShareLinkDto.fromJson(value);
case 'CreateProfileImageResponseDto':
return CreateProfileImageResponseDto.fromJson(value);
case 'CreateTagDto':
@@ -263,14 +259,14 @@ class ApiClient {
return DeleteAssetStatusTypeTransformer().decode(value);
case 'DownloadFilesDto':
return DownloadFilesDto.fromJson(value);
case 'EditSharedLinkDto':
return EditSharedLinkDto.fromJson(value);
case 'ExifResponseDto':
return ExifResponseDto.fromJson(value);
case 'GetAssetByTimeBucketDto':
return GetAssetByTimeBucketDto.fromJson(value);
case 'GetAssetCountByTimeBucketDto':
return GetAssetCountByTimeBucketDto.fromJson(value);
case 'ImportAssetDto':
return ImportAssetDto.fromJson(value);
case 'JobCommand':
return JobCommandTypeTransformer().decode(value);
case 'JobCommandDto':
@@ -333,6 +329,10 @@ class ApiClient {
return ServerStatsResponseDto.fromJson(value);
case 'ServerVersionReponseDto':
return ServerVersionReponseDto.fromJson(value);
case 'SharedLinkCreateDto':
return SharedLinkCreateDto.fromJson(value);
case 'SharedLinkEditDto':
return SharedLinkEditDto.fromJson(value);
case 'SharedLinkResponseDto':
return SharedLinkResponseDto.fromJson(value);
case 'SharedLinkType':

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