Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b258f3552a | ||
|
|
e803bc909f | ||
|
|
d078aea32b | ||
|
|
fb2cfcb640 | ||
|
|
99f85fb359 | ||
|
|
454fb106d2 | ||
|
|
7d078a2f0e | ||
|
|
b015648bfe | ||
|
|
a58482cb2b | ||
|
|
9dd1d81536 | ||
|
|
a2f5674bbb | ||
|
|
837ad24f58 | ||
|
|
058c62b111 | ||
|
|
bbb6bca605 | ||
|
|
a8e5a1de15 | ||
|
|
bba4c44182 | ||
|
|
7c76249e1f | ||
|
|
294955db17 | ||
|
|
752ad2d2eb | ||
|
|
02a268c7c6 | ||
|
|
b2dc7adf3b | ||
|
|
6e62558d81 | ||
|
|
0d0866d5d9 | ||
|
|
00f65a53dd | ||
|
|
751922990f | ||
|
|
4311d385fc | ||
|
|
3e2f335a4c | ||
|
|
cf1eddb449 | ||
|
|
e171fec5aa | ||
|
|
7f44d508dc | ||
|
|
2c924e4c1c | ||
|
|
0f0375a67e | ||
|
|
069c68bfe4 | ||
|
|
c03d8e312a | ||
|
|
de7f66f983 | ||
|
|
82b89aa20b | ||
|
|
80d02e8a8d | ||
|
|
868f629f32 | ||
|
|
746ca5d5ed | ||
|
|
3c5fefde2e | ||
|
|
26f58d3335 | ||
|
|
6baeca654b | ||
|
|
1b15b5414c | ||
|
|
48e4ea5231 | ||
|
|
f9fbf1a2a5 | ||
|
|
f003ff3c98 | ||
|
|
81e2b18531 | ||
|
|
c404ea20ee | ||
|
|
cc45564d84 | ||
|
|
8d560ec55f | ||
|
|
df74111427 | ||
|
|
93c35efe67 | ||
|
|
296c77ac73 | ||
|
|
9c0f444e4d | ||
|
|
6b0f91cafd | ||
|
|
3f71d2d33d | ||
|
|
f2942588f2 | ||
|
|
b47027efc2 | ||
|
|
34201be74c | ||
|
|
3e804f16df | ||
|
|
3512140148 | ||
|
|
bff6914a73 | ||
|
|
652add635f | ||
|
|
fde410e2ac | ||
|
|
f04e47803c | ||
|
|
61d74263d9 | ||
|
|
66ee065c0c | ||
|
|
09bcf6974e | ||
|
|
5d7d615433 | ||
|
|
5387048dc3 | ||
|
|
6930df71cf | ||
|
|
52bbf6da5d | ||
|
|
1cd5df7558 | ||
|
|
74429798e2 | ||
|
|
651f3ea5eb | ||
|
|
0909335d02 | ||
|
|
827e4b5f75 | ||
|
|
c8ff07fff0 | ||
|
|
4a21cb2d00 | ||
|
|
07f7fffae7 | ||
|
|
441ee2ef90 | ||
|
|
acad133e3a | ||
|
|
ef8714fda9 | ||
|
|
16171eee8d | ||
|
|
d3c1781478 | ||
|
|
329b52e670 | ||
|
|
a1b9a1d244 | ||
|
|
377cec9fb1 | ||
|
|
48b9c63268 |
3
.github/workflows/prepare-release.yml
vendored
3
.github/workflows/prepare-release.yml
vendored
@@ -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 }}"
|
||||
|
||||
|
||||
@@ -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
104
README_tr_TR.md
Normal 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
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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".
|
||||
###################################################################################
|
||||
|
||||
|
||||
105
docs/blog/2023/06-24/update.mdx
Normal file
105
docs/blog/2023/06-24/update.mdx
Normal 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 Immich’s 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 Immich’s 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 Immich’s 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 Immich’s 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 Immich’s 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
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
docs/docs/features/img/me.png
Normal file
BIN
docs/docs/features/img/me.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/docs/features/img/my-wife.png
Normal file
BIN
docs/docs/features/img/my-wife.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
103
docs/docs/features/read-only-gallery.md
Normal file
103
docs/docs/features/read-only-gallery.md
Normal 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.
|
||||
@@ -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`).
|
||||
|
||||
|
||||
186
docs/docs/install/environment-variables.md
Normal file
186
docs/docs/install/environment-variables.md
Normal 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 |
|
||||
@@ -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
776
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": [
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
0
machine-learning/app/__init__.py
Normal file
0
machine-learning/app/__init__.py
Normal file
31
machine-learning/app/config.py
Normal file
31
machine-learning/app/config.py
Normal 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()
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
3
machine-learning/app/models/__init__.py
Normal file
3
machine-learning/app/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .clip import CLIPSTTextEncoder, CLIPSTVisionEncoder
|
||||
from .facial_recognition import FaceRecognizer
|
||||
from .image_classification import ImageClassifier
|
||||
52
machine-learning/app/models/base.py
Normal file
52
machine-learning/app/models/base.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
37
machine-learning/app/models/clip.py
Normal file
37
machine-learning/app/models/clip.py
Normal 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
|
||||
59
machine-learning/app/models/facial_recognition.py
Normal file
59
machine-learning/app/models/facial_recognition.py
Normal 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
|
||||
40
machine-learning/app/models/image_classification.py
Normal file
40
machine-learning/app/models/image_classification.py
Normal 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
|
||||
@@ -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
24
machine-learning/load_test.sh
Executable 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
|
||||
52
machine-learning/locustfile.py
Normal file
52
machine-learning/locustfile.py
Normal 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,
|
||||
)
|
||||
1488
machine-learning/poetry.lock
generated
1488
machine-learning/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -72,6 +72,11 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix '.debug'
|
||||
versionNameSuffix '-DEBUG'
|
||||
}
|
||||
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
57
mobile/lib/modules/asset_viewer/ui/animated_play_pause.dart
Normal file
57
mobile/lib/modules/asset_viewer/ui/animated_play_pause.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
53
mobile/lib/modules/asset_viewer/ui/center_play_button.dart
Normal file
53
mobile/lib/modules/asset_viewer/ui/center_play_button.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
207
mobile/lib/modules/asset_viewer/ui/video_player_controls.dart
Normal file
207
mobile/lib/modules/asset_viewer/ui/video_player_controls.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
44
mobile/lib/modules/search/providers/people.provider.dart
Normal file
44
mobile/lib/modules/search/providers/people.provider.dart
Normal 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);
|
||||
}
|
||||
});
|
||||
56
mobile/lib/modules/search/services/person.service.dart
Normal file
56
mobile/lib/modules/search/services/person.service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
mobile/lib/modules/search/ui/curated_people_row.dart
Normal file
114
mobile/lib/modules/search/ui/curated_people_row.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}'),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
82
mobile/lib/modules/search/ui/person_name_edit_form.dart
Normal file
82
mobile/lib/modules/search/ui/person_name_edit_form.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
46
mobile/lib/modules/search/ui/search_row_title.dart
Normal file
46
mobile/lib/modules/search/ui/search_row_title.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
51
mobile/lib/modules/search/views/all_people_page.dart
Normal file
51
mobile/lib/modules/search/views/all_people_page.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
152
mobile/lib/modules/search/views/person_result_page.dart
Normal file
152
mobile/lib/modules/search/views/person_result_page.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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", "")),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,6 @@ class TabControllerPage extends HookConsumerWidget {
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
size: 24,
|
||||
Icons.photo_library,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
|
||||
@@ -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"};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
24
mobile/openapi/.openapi-generator/FILES
generated
24
mobile/openapi/.openapi-generator/FILES
generated
@@ -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
|
||||
|
||||
30
mobile/openapi/README.md
generated
30
mobile/openapi/README.md
generated
@@ -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)
|
||||
|
||||
66
mobile/openapi/doc/AlbumApi.md
generated
66
mobile/openapi/doc/AlbumApi.md
generated
@@ -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');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
2
mobile/openapi/doc/AlbumCountResponseDto.md
generated
2
mobile/openapi/doc/AlbumCountResponseDto.md
generated
@@ -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)
|
||||
|
||||
|
||||
1
mobile/openapi/doc/AlbumResponseDto.md
generated
1
mobile/openapi/doc/AlbumResponseDto.md
generated
@@ -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)
|
||||
|
||||
|
||||
142
mobile/openapi/doc/AssetApi.md
generated
142
mobile/openapi/doc/AssetApi.md
generated
@@ -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]
|
||||
|
||||
1
mobile/openapi/doc/AssetResponseDto.md
generated
1
mobile/openapi/doc/AssetResponseDto.md
generated
@@ -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) | |
|
||||
|
||||
20
mobile/openapi/doc/CreateAssetsShareLinkDto.md
generated
20
mobile/openapi/doc/CreateAssetsShareLinkDto.md
generated
@@ -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)
|
||||
|
||||
|
||||
1
mobile/openapi/doc/CreateUserDto.md
generated
1
mobile/openapi/doc/CreateUserDto.md
generated
@@ -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
26
mobile/openapi/doc/ImportAssetDto.md
generated
Normal 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)
|
||||
|
||||
|
||||
10
mobile/openapi/doc/JobApi.md
generated
10
mobile/openapi/doc/JobApi.md
generated
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# openapi.model.EditSharedLinkDto
|
||||
# openapi.model.SharedLinkEditDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
2
mobile/openapi/doc/SharedLinkResponseDto.md
generated
2
mobile/openapi/doc/SharedLinkResponseDto.md
generated
@@ -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) | |
|
||||
|
||||
1
mobile/openapi/doc/UpdateUserDto.md
generated
1
mobile/openapi/doc/UpdateUserDto.md
generated
@@ -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]
|
||||
|
||||
|
||||
1
mobile/openapi/doc/UserResponseDto.md
generated
1
mobile/openapi/doc/UserResponseDto.md
generated
@@ -13,6 +13,7 @@ Name | Type | Description | Notes
|
||||
**firstName** | **String** | |
|
||||
**lastName** | **String** | |
|
||||
**storageLabel** | **String** | |
|
||||
**externalPath** | **String** | |
|
||||
**profileImagePath** | **String** | |
|
||||
**shouldChangePassword** | **bool** | |
|
||||
**isAdmin** | **bool** | |
|
||||
|
||||
8
mobile/openapi/lib/api.dart
generated
8
mobile/openapi/lib/api.dart
generated
@@ -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';
|
||||
|
||||
57
mobile/openapi/lib/api/album_api.dart
generated
57
mobile/openapi/lib/api/album_api.dart
generated
@@ -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));
|
||||
}
|
||||
|
||||
160
mobile/openapi/lib/api/asset_api.dart
generated
160
mobile/openapi/lib/api/asset_api.dart
generated
@@ -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));
|
||||
}
|
||||
|
||||
16
mobile/openapi/lib/api/job_api.dart
generated
16
mobile/openapi/lib/api/job_api.dart
generated
@@ -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));
|
||||
}
|
||||
|
||||
253
mobile/openapi/lib/api/share_api.dart
generated
253
mobile/openapi/lib/api/share_api.dart
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
426
mobile/openapi/lib/api/shared_link_api.dart
generated
Normal file
426
mobile/openapi/lib/api/shared_link_api.dart
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
12
mobile/openapi/lib/api_client.dart
generated
12
mobile/openapi/lib/api_client.dart
generated
@@ -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
Reference in New Issue
Block a user