chore: pull main
This commit is contained in:
commit
16646d9946
152 changed files with 4494 additions and 617 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
|
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
run: docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
|
run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||||
|
|
||||||
doc-tests:
|
doc-tests:
|
||||||
name: Run documentation checks
|
name: Run documentation checks
|
||||||
|
|
7
Makefile
7
Makefile
|
@ -4,6 +4,9 @@ dev:
|
||||||
dev-new:
|
dev-new:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
|
dev-down:
|
||||||
|
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
||||||
|
|
||||||
dev-new-update:
|
dev-new-update:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
@ -20,7 +23,7 @@ pull-stage:
|
||||||
docker-compose -f ./docker/docker-compose.staging.yml pull
|
docker-compose -f ./docker/docker-compose.staging.yml pull
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
|
docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
@ -32,4 +35,4 @@ api:
|
||||||
cd ./server && npm run api:generate
|
cd ./server && npm run api:generate
|
||||||
|
|
||||||
attach-server:
|
attach-server:
|
||||||
docker exec -it docker_immich-server_1 sh
|
docker exec -it docker_immich-server_1 sh
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Avís legal
|
## Avís legal
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Descargo de responsabilidad
|
## Descargo de responsabilidad
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Clause de non-responsabilité
|
## Clause de non-responsabilité
|
||||||
|
|
113
README_it_IT.md
Normal file
113
README_it_IT.md
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<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 - Soluzione self-hosted ad alte prestazioni per backup di foto e video</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>
|
||||||
|
<a href="README_tr_TR.md">Türkçe</a>
|
||||||
|
<a href="README_ca_ES.md">Català</a>
|
||||||
|
<a href="README_es_ES.md">Español</a>
|
||||||
|
<a href="README_fr_FR.md">Français</a>
|
||||||
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Declino di responsabilità
|
||||||
|
|
||||||
|
- ⚠️ Il progetto è in fase di sviluppo **molto avanzato**.
|
||||||
|
- ⚠️ Possibilità di bug e cambiamenti rilevanti.
|
||||||
|
- ⚠️ **Non utilizzare l'app come unico salvataggio delle tue foto e dei tuoi video.**
|
||||||
|
- ⚠️ Utilizza sempre una tecnica [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) di backup per le foto e i video a cui tieni!
|
||||||
|
|
||||||
|
## Contenuto
|
||||||
|
|
||||||
|
- [Documentazione Ufficiale](https://immich.app/docs)
|
||||||
|
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
|
||||||
|
- [Demo](#demo)
|
||||||
|
- [Funzionalità](#features)
|
||||||
|
- [Introduzione](https://immich.app/docs/overview/introduction)
|
||||||
|
- [Installazione](https://immich.app/docs/install/requirements)
|
||||||
|
- [Linee Guida per Contribuire](https://immich.app/docs/overview/support-the-project)
|
||||||
|
- [Supporta il Progetto](#support-the-project)
|
||||||
|
|
||||||
|
## Documentazione
|
||||||
|
|
||||||
|
La documentazione ufficiale, inclusa la guida all'installazione, è disponibile qui: https://immich.app/.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
Prova la demo del progetto https://demo.immich.app
|
||||||
|
|
||||||
|
Sull'app mobile, imposta `https://demo.immich.app/api` come `Server Endpoint URL`
|
||||||
|
|
||||||
|
```bash title="Demo Credential"
|
||||||
|
Credenziali di accesso
|
||||||
|
email: demo@immich.app
|
||||||
|
password: demo
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||||
|
```
|
||||||
|
|
||||||
|
# Funzionalità
|
||||||
|
|
||||||
|
| Funzionalità | Mobile | Web |
|
||||||
|
| ---------------------------------------------- | ------ | --- |
|
||||||
|
| Caricamento e visualizzazione di foto e video | Sì | Sì |
|
||||||
|
| Backup automatico quando l'app è in esecuzione | Sì | N/A |
|
||||||
|
| Selezione degli album per backup | Sì | N/A |
|
||||||
|
| Download foto e video sul dispositivo | Sì | Sì |
|
||||||
|
| Supporto multi utente | Sì | Sì |
|
||||||
|
| Album e album condivisi | Sì | Sì |
|
||||||
|
| Barra di scorrimento con trascinamento | Sì | Sì |
|
||||||
|
| Supporto formati raw | Sì | Sì |
|
||||||
|
| Visualizzazione metadata (EXIF, map) | Sì | Sì |
|
||||||
|
| Ricerca per metadata, oggetti, volti e CLIP | Sì | Sì |
|
||||||
|
| Funzioni di amministrazione degli utenti | No | Sì |
|
||||||
|
| Backup in background | Sì | N/A |
|
||||||
|
| Scroll virtuale | Sì | Sì |
|
||||||
|
| Supporto OAuth | Sì | Sì |
|
||||||
|
| API Keys | N/A | Sì |
|
||||||
|
| Backup e riproduzione di LivePhoto | iOS | Sì |
|
||||||
|
| Archiviazione impostata dall'utente | Sì | Sì |
|
||||||
|
| Condivisione pubblica | No | Sì |
|
||||||
|
| Archivio e Preferiti | Sì | Sì |
|
||||||
|
| Mappa globale | Sì | Sì |
|
||||||
|
| Collaborazione con utenti | Sì | Sì |
|
||||||
|
| Riconoscimento facciale e categorizzazione | Sì | Sì |
|
||||||
|
| Ricordi (x anni fa) | Sì | Sì |
|
||||||
|
| Supporto offline | Sì | No |
|
||||||
|
| Galleria sola lettura | Sì | Sì |
|
||||||
|
|
||||||
|
# Supporta il progetto
|
||||||
|
|
||||||
|
Mi dedico al progetto e non smetterò di farlo. Manterrò aggiornata la documentazione, aggiungerò nuove funzioni e risolverò i bug, ma non posso farlo da solo. Ho bisogno del tuo aiuto che mi da motivazione per continuare.
|
||||||
|
|
||||||
|
Come detto dal nostro host [selfhosted.show - Nell'episodio 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418), quello che il team ed io stiamo facendo è un lavoro enorme. Mi piacerebbe dedicarmi al progetto full-time e chiedo il tuo aiuto affinchè sia possibile.
|
||||||
|
|
||||||
|
Se pensi che Immich sia una buona causa e che l'app sia qualcosa che useresti nel lungo termine, sappi che puoi supportare il progetto scegliendo tra le opzioni sotto elencate.
|
||||||
|
|
||||||
|
## Donazioni
|
||||||
|
|
||||||
|
- [Donazione mensile](https://github.com/sponsors/alextran1502) tramite GitHub Sponsors
|
||||||
|
- [Donazione una tantum](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) tramite GitHub Sponsors
|
||||||
|
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||||
|
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||||
|
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
|
@ -24,6 +24,7 @@
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 免責事項
|
## 免責事項
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Feragatname
|
## Feragatname
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
145
cli/src/api/open-api/api.ts
generated
145
cli/src/api/open-api/api.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.82.0
|
* The version of the OpenAPI document: 1.82.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
@ -399,6 +399,18 @@ export interface AssetBulkUpdateDto {
|
||||||
* @memberof AssetBulkUpdateDto
|
* @memberof AssetBulkUpdateDto
|
||||||
*/
|
*/
|
||||||
'isFavorite'?: boolean;
|
'isFavorite'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'removeParent'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'stackParentId'?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -748,6 +760,24 @@ export interface AssetResponseDto {
|
||||||
* @memberof AssetResponseDto
|
* @memberof AssetResponseDto
|
||||||
*/
|
*/
|
||||||
'smartInfo'?: SmartInfoResponseDto;
|
'smartInfo'?: SmartInfoResponseDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<AssetResponseDto>}
|
||||||
|
* @memberof AssetResponseDto
|
||||||
|
*/
|
||||||
|
'stack'?: Array<AssetResponseDto>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetResponseDto
|
||||||
|
*/
|
||||||
|
'stackCount': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetResponseDto
|
||||||
|
*/
|
||||||
|
'stackParentId'?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Array<TagResponseDto>}
|
* @type {Array<TagResponseDto>}
|
||||||
|
@ -3053,6 +3083,12 @@ export interface SharedLinkEditDto {
|
||||||
* @memberof SharedLinkEditDto
|
* @memberof SharedLinkEditDto
|
||||||
*/
|
*/
|
||||||
'allowUpload'?: boolean;
|
'allowUpload'?: boolean;
|
||||||
|
/**
|
||||||
|
* Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof SharedLinkEditDto
|
||||||
|
*/
|
||||||
|
'changeExpiryTime'?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -3981,6 +4017,25 @@ export interface UpdateLibraryDto {
|
||||||
*/
|
*/
|
||||||
'name'?: string;
|
'name'?: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface UpdateStackParentDto
|
||||||
|
*/
|
||||||
|
export interface UpdateStackParentDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UpdateStackParentDto
|
||||||
|
*/
|
||||||
|
'newParentId': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UpdateStackParentDto
|
||||||
|
*/
|
||||||
|
'oldParentId': string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -7135,6 +7190,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {UpdateStackParentDto} updateStackParentDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
updateStackParent: async (updateStackParentDto: UpdateStackParentDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'updateStackParentDto' is not null or undefined
|
||||||
|
assertParamExists('updateStackParent', 'updateStackParentDto', updateStackParentDto)
|
||||||
|
const localVarPath = `/asset/stack/parent`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded(updateStackParentDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {File} assetData
|
* @param {File} assetData
|
||||||
|
@ -7601,6 +7700,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssets(assetBulkUpdateDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssets(assetBulkUpdateDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {UpdateStackParentDto} updateStackParentDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async updateStackParent(updateStackParentDto: UpdateStackParentDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.updateStackParent(updateStackParentDto, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {File} assetData
|
* @param {File} assetData
|
||||||
|
@ -7892,6 +8001,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
updateAssets(requestParameters: AssetApiUpdateAssetsRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
|
updateAssets(requestParameters: AssetApiUpdateAssetsRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||||
return localVarFp.updateAssets(requestParameters.assetBulkUpdateDto, options).then((request) => request(axios, basePath));
|
return localVarFp.updateAssets(requestParameters.assetBulkUpdateDto, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetApiUpdateStackParentRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
updateStackParent(requestParameters: AssetApiUpdateStackParentRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||||
|
return localVarFp.updateStackParent(requestParameters.updateStackParentDto, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AssetApiUploadFileRequest} requestParameters Request parameters.
|
* @param {AssetApiUploadFileRequest} requestParameters Request parameters.
|
||||||
|
@ -8499,6 +8617,20 @@ export interface AssetApiUpdateAssetsRequest {
|
||||||
readonly assetBulkUpdateDto: AssetBulkUpdateDto
|
readonly assetBulkUpdateDto: AssetBulkUpdateDto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for updateStackParent operation in AssetApi.
|
||||||
|
* @export
|
||||||
|
* @interface AssetApiUpdateStackParentRequest
|
||||||
|
*/
|
||||||
|
export interface AssetApiUpdateStackParentRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UpdateStackParentDto}
|
||||||
|
* @memberof AssetApiUpdateStackParent
|
||||||
|
*/
|
||||||
|
readonly updateStackParentDto: UpdateStackParentDto
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request parameters for uploadFile operation in AssetApi.
|
* Request parameters for uploadFile operation in AssetApi.
|
||||||
* @export
|
* @export
|
||||||
|
@ -8939,6 +9071,17 @@ export class AssetApi extends BaseAPI {
|
||||||
return AssetApiFp(this.configuration).updateAssets(requestParameters.assetBulkUpdateDto, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).updateAssets(requestParameters.assetBulkUpdateDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetApiUpdateStackParentRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof AssetApi
|
||||||
|
*/
|
||||||
|
public updateStackParent(requestParameters: AssetApiUpdateStackParentRequest, options?: AxiosRequestConfig) {
|
||||||
|
return AssetApiFp(this.configuration).updateStackParent(requestParameters.updateStackParentDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AssetApiUploadFileRequest} requestParameters Request parameters.
|
* @param {AssetApiUploadFileRequest} requestParameters Request parameters.
|
||||||
|
|
2
cli/src/api/open-api/base.ts
generated
2
cli/src/api/open-api/base.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.82.0
|
* The version of the OpenAPI document: 1.82.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|
2
cli/src/api/open-api/common.ts
generated
2
cli/src/api/open-api/common.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.82.0
|
* The version of the OpenAPI document: 1.82.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|
2
cli/src/api/open-api/configuration.ts
generated
2
cli/src/api/open-api/configuration.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.82.0
|
* The version of the OpenAPI document: 1.82.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|
2
cli/src/api/open-api/index.ts
generated
2
cli/src/api/open-api/index.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.82.0
|
* The version of the OpenAPI document: 1.82.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|
|
@ -21,6 +21,10 @@ services:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 1048576
|
||||||
|
hard: 1048576
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
|
@ -48,6 +52,10 @@ services:
|
||||||
- 9231:9230
|
- 9231:9230
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 1048576
|
||||||
|
hard: 1048576
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
- immich-server
|
- immich-server
|
||||||
|
@ -73,6 +81,10 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ../web:/usr/src/app
|
- ../web:/usr/src/app
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 1048576
|
||||||
|
hard: 1048576
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-server
|
- immich-server
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
# Compose file for dockerized end-to-end testing of the backend
|
name: "immich-test-e2e"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server-test:
|
immich-server:
|
||||||
image: immich-server-test
|
image: immich-server-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
@ -14,27 +14,20 @@ services:
|
||||||
- ../server:/usr/src/app
|
- ../server:/usr/src/app
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
environment:
|
environment:
|
||||||
- DB_HOSTNAME=immich-database-test
|
- DB_HOSTNAME=database
|
||||||
- DB_USERNAME=postgres
|
- DB_USERNAME=postgres
|
||||||
- DB_PASSWORD=postgres
|
- DB_PASSWORD=postgres
|
||||||
- DB_DATABASE_NAME=e2e_test
|
- DB_DATABASE_NAME=e2e_test
|
||||||
- IMMICH_RUN_ALL_TESTS=true
|
- IMMICH_RUN_ALL_TESTS=true
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-database-test
|
- database
|
||||||
networks:
|
|
||||||
- immich-test-network
|
|
||||||
|
|
||||||
immich-database-test:
|
database:
|
||||||
container_name: immich-database-test
|
|
||||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||||
|
command: -c fsync=off
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_DB: e2e_test
|
POSTGRES_DB: e2e_test
|
||||||
networks:
|
|
||||||
- immich-test-network
|
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
|
|
||||||
networks:
|
|
||||||
immich-test-network:
|
|
||||||
|
|
|
@ -16,10 +16,6 @@ sidebar_position: 7
|
||||||
|
|
||||||
Immich doesn't have two-way synchronization ([yet](https://github.com/immich-app/immich/discussions/1006)), but the [command line tool](/docs/features/bulk-upload.md) can bulk upload items from a directory to Immich.
|
Immich doesn't have two-way synchronization ([yet](https://github.com/immich-app/immich/discussions/1006)), but the [command line tool](/docs/features/bulk-upload.md) can bulk upload items from a directory to Immich.
|
||||||
|
|
||||||
### Why doesn't Immich watch an existing photo gallery directory?
|
|
||||||
|
|
||||||
The initial approach of Immich is to become a backup tool, primarily for mobile device usage. Thus, all the assets must be uploaded from the mobile client. The app was architectured to perform that job well.
|
|
||||||
|
|
||||||
### Why are only photos and not videos being uploaded to Immich?
|
### Why are only photos and not videos being uploaded to Immich?
|
||||||
|
|
||||||
This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes. Also check the disk space of your reverse proxy, in some cases proxies caches requests to disk before passing them on, and if disk space runs out the request fails.
|
This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes. Also check the disk space of your reverse proxy, in some cases proxies caches requests to disk before passing them on, and if disk space runs out the request fails.
|
||||||
|
|
|
@ -11,6 +11,6 @@ Running into an issue or have a question? Try the following:
|
||||||
3. Search through existing [GitHub Issues][github-issues].
|
3. Search through existing [GitHub Issues][github-issues].
|
||||||
4. Open a help ticket on [Discord][discord-link].
|
4. Open a help ticket on [Discord][discord-link].
|
||||||
|
|
||||||
[github-issues]: https://github.com/immich-app/immich/releases
|
[github-issues]: https://github.com/immich-app/immich/issues
|
||||||
[github-releases]: https://github.com/immich-app/immich/releases
|
[github-releases]: https://github.com/immich-app/immich/releases
|
||||||
[discord-link]: https://discord.com/invite/D8JsnBEuKb
|
[discord-link]: https://discord.com/invite/D8JsnBEuKb
|
||||||
|
|
|
@ -4,7 +4,7 @@ sidebar_position: 1
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
<img src={require('./img/feature-panel.png').default} alt="Immich" />
|
<img src={require('./img/feature-panel.png').default} alt="Immich - Self-hosted photos and videos backup tool" />
|
||||||
|
|
||||||
## Welcome!
|
## Welcome!
|
||||||
|
|
||||||
|
|
|
@ -6,28 +6,31 @@ function HomepageHeader() {
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
<section className="text-center m-6 p-12 border border-red-400 rounded-[50px] bg-gray-100 dark:bg-immich-dark-gray">
|
<section className="text-center m-6 p-12 border border-red-400 rounded-[50px] bg-gray-100 dark:bg-immich-dark-gray">
|
||||||
<h1 className="md:text-6xl font-bold mb-10 font-immich-title text-immich-primary dark:text-immich-dark-primary">
|
<img src="img/immich-logo.svg" className="md:h-24 h-12 mb-2" alt="Immich logo" />
|
||||||
IMMICH
|
<h1 className="md:text-6xl font-immich-title mb-10 text-immich-primary dark:text-immich-dark-primary uppercase">
|
||||||
|
Immich
|
||||||
</h1>
|
</h1>
|
||||||
<div className="font-thin sm:text-base md:text-2xl my-12 sm:leading-tight">
|
<div className="font-thin sm:text-base md:text-2xl my-12 sm:leading-tight">
|
||||||
<p>SELF-HOSTED BACKUP SOLUTION </p>
|
<p className="mb-1 uppercase">
|
||||||
<p>FOR PHOTOS AND VIDEOS</p>
|
Self-hosted backup solution <span className="block"></span>
|
||||||
<p>ON MOBILE DEVICE</p>
|
for photos and videos <span className="block"></span>
|
||||||
|
on mobile device
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 mb-16 gap-4 ">
|
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 mb-16 gap-4 ">
|
||||||
<Link
|
<Link
|
||||||
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold"
|
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold uppercase"
|
||||||
to="docs/overview/introduction"
|
to="docs/overview/introduction"
|
||||||
>
|
>
|
||||||
GET STARTED
|
Get started
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold"
|
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
|
||||||
to="https://demo.immich.app/"
|
to="https://demo.immich.app/"
|
||||||
>
|
>
|
||||||
DEMO PORTAL
|
Demo portal
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
98
docs/static/img/immich-logo.svg
vendored
Normal file
98
docs/static/img/immich-logo.svg
vendored
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="svg2781" xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 564.2 553.5"
|
||||||
|
style="enable-background:new 0 0 564.2 553.5;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#4081EF;stroke:#512D8C;stroke-miterlimit:10;}
|
||||||
|
.st1{fill:#31A452;stroke:#512D8C;stroke-miterlimit:10;}
|
||||||
|
.st2{fill:#DE7FB3;stroke:#512D8C;stroke-miterlimit:10;}
|
||||||
|
.st3{fill:#FFB800;stroke:#512D8C;stroke-miterlimit:10;}
|
||||||
|
.st4{fill:#E64132;stroke:#512D8C;stroke-miterlimit:10;}
|
||||||
|
.st5{fill:#F2F5FB;stroke:#512D8C;stroke-miterlimit:10;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M210.5,549.6c-2.2-0.2-5.5-1-9.7-2.2c-52.4-15.7-99-46.5-133.8-88.5c-8.8-10.7-17.2-22.4-19.4-27.5
|
||||||
|
c-8.1-18.1-6.3-38.7,4.8-55.4c5-7.5,13.2-15,20.5-18.7c1.2-0.6,54.1-20,55.8-20.4c0.5-0.1,0.5,0.2-0.3,2.1c-0.7,1.7-1,3.1-1.1,5.5
|
||||||
|
l-0.1,3.2l2.8,5.8c8.7,17.9,19.2,32.7,33.2,46.4c6.3,6.2,7.8,7.6,13.8,12.3c22.7,18.1,52,30.7,79.9,34.3c2.5,0.3,5,0.8,5.7,1
|
||||||
|
c2.8,0.9,7.7-0.8,11-3.7l1.8-1.6l-0.2,4.8c-0.1,2.7-0.6,15.4-1,28.3c-0.6,20.3-0.8,24-1.5,27.5c-3.9,20.7-18.6,37.5-38.4,44.1
|
||||||
|
c-4.6,1.5-8,2.2-13.1,2.7C216.6,550.1,215.3,550,210.5,549.6z"/>
|
||||||
|
<path class="st1" d="M339.8,549.4c-4-0.4-9.4-1.6-13.2-2.9c-3.4-1.2-10-4.4-12.5-6.1c-10.9-7.4-19-17.9-23.1-30
|
||||||
|
c-2.2-6.7-2.3-7.5-3.3-36.9c-0.5-14.9-0.9-27.9-0.9-28.9l0-1.9l2.3,1.8c2.6,2,6.6,3.4,8.5,3.1c0.6-0.1,3-0.5,5.3-0.8
|
||||||
|
c37.7-5.3,71.2-22.2,97.4-49.1c12.2-12.5,21.4-25.5,29.9-42.4l3.5-7l0-3.6c0-3.1-0.1-3.8-1-5.7c-0.5-1.2-0.9-2.1-0.9-2.2
|
||||||
|
c0.2-0.2,55.3,20.1,56.9,20.9c2.6,1.3,6.6,4.1,9.9,7c9.2,7.7,16.1,19.4,18.8,31.8c0.7,3.1,0.8,4.8,0.8,11.3c0,8.6-0.5,11.7-2.9,18.7
|
||||||
|
c-1.7,5-2.9,7.2-7.1,13.1c-7.6,11-15.3,20.5-25.2,31.2c-32.8,35.4-76.5,62.5-123.4,76.3C351.6,549.6,347.2,550.1,339.8,549.4z"/>
|
||||||
|
<path class="st2" d="M255.6,438c-25.9-4.2-50.7-14.9-71.7-31c-5.2-4-8.7-7.1-14.1-12.4c-12.7-12.5-21.9-24.9-30.5-41.4
|
||||||
|
c-2.3-4.4-2.4-4.7-2.4-7.1c0-8.8,8.5-15.2,16.9-12.7c5.6,1.7,9.6,6.8,9.7,12.2c0,2.6-0.8,4.6-2.6,6.2c-1.2,1.1-3.2,1.9-4.6,1.9
|
||||||
|
c-1.2,0-3.3-0.8-4.3-1.6c-2.1-1.8-2-1,0.4,3.2c19.3,33.8,52.3,59.1,90,69.1c5.7,1.5,11.5,2.7,11.8,2.4c0.1-0.1-0.4-0.8-1.3-1.6
|
||||||
|
c-5.1-4.5-2.3-11.7,5-12.8c5.4-0.8,11.4,2.7,13.9,8c0.8,1.7,1,2.5,1,5.3s-0.1,3.5-1,5.3c-2,4.3-6.8,7.9-10.3,7.8
|
||||||
|
C260.6,438.7,257.9,438.3,255.6,438z"/>
|
||||||
|
<path class="st0" d="M297.6,438.2c-3.4-1.3-6.4-4.3-7.8-8.1c-1.1-2.9-0.9-7.3,0.5-10.2c2.6-5.3,8.7-8.5,14.4-7.5
|
||||||
|
c2.9,0.5,4.7,1.9,6,4.3c0.8,1.6,1,2.2,0.8,3.6c-0.3,2.2-0.9,3.3-2.7,4.8c-0.8,0.7-1.4,1.4-1.3,1.5c0.5,0.5,13.4-2.7,21.3-5.4
|
||||||
|
c33.6-11.3,62.5-35.1,80.4-66.1c2.5-4.4,2.6-5,0.5-3.2c-2.8,2.4-7,1.9-9.6-1c-4-4.6-0.7-13.8,6.1-16.9c2-0.9,2.7-1,5.5-1
|
||||||
|
c2.9,0,3.5,0.1,5.6,1.1c4.4,2.1,7.4,6.4,7.8,11c0.2,2.2,0.1,2.3-2.2,6.9c-23,45.9-67,78.1-117.2,85.9
|
||||||
|
C300.2,438.8,299.4,438.9,297.6,438.2z"/>
|
||||||
|
<path class="st1" d="M211.1,398.5c-4.7-0.9-8.7-2.7-12.9-5.9c-10.8-8.1-13.5-22.3-6.6-33.7c0.7-1.2,1.1-2.2,1-2.4
|
||||||
|
c-0.2-0.2-1.2-0.6-2.3-1.1c-7.6-3-13-10.6-13.5-19.1c-0.5-7.4,3.1-15,9-19.4c1-0.7,2.2-1.5,2.6-1.8c0.8-0.4,68.9-22.7,69.4-22.7
|
||||||
|
c0.2,0,0.7,0.7,1.2,1.5c0.5,0.8,1.6,2.3,2.4,3.3c1.2,1.4,1.5,1.9,1.2,2.3c-0.2,0.3-6.9,9.5-14.8,20.5
|
||||||
|
c-15.9,21.9-15.5,21.3-13.4,23.4c1.3,1.3,2.9,1.4,4.4,0.3c0.6-0.4,7.5-9.7,15.5-20.7c11.2-15.4,14.6-19.9,15-19.7
|
||||||
|
c0.9,0.4,5.5,1.9,6.6,2.1l1,0.2l0,35.3c0,39.7,0,38.8-2.5,44c-2.6,5.3-7.2,9.3-12.7,11.2c-3.7,1.3-6.8,1.6-10.2,1
|
||||||
|
c-5.5-0.9-9.8-3.2-13.7-7.4l-2.2-2.4l-0.6,0.9c-3,4.3-8.6,8.1-14,9.5C218.2,398.6,213.2,398.9,211.1,398.5z"/>
|
||||||
|
<path class="st3" d="M342.9,398.5c-5.5-0.9-9.9-3.2-14.3-7.6l-3.2-3.2l-0.7,1c-2.3,3.3-6.8,6.5-11.1,7.9c-3.7,1.2-9.2,1.4-12.6,0.3
|
||||||
|
c-7.1-2.1-12.7-7.4-15.2-14.3l-0.9-2.6v-37.1v-37.1l1.8-0.4c1-0.2,2.7-0.8,3.9-1.2c1.1-0.5,2.1-0.8,2.2-0.7c0.1,0.1,6.5,9,14.4,19.9
|
||||||
|
c7.8,10.9,14.7,20.1,15.2,20.5c2.2,1.9,5.4,0.4,5.4-2.6c0-1.4-1-2.9-13.8-20.5c-7.6-10.5-14.2-19.6-14.7-20.4l-0.9-1.3l1.4-1.7
|
||||||
|
c0.8-0.9,1.9-2.5,2.5-3.4l1-1.6l34.4,11.2c18.9,6.2,35.1,11.6,35.9,12.1c6.8,4,11.1,11.3,11.1,19.1c0,4.1-0.5,6.4-2.4,10.2
|
||||||
|
c-2,4.1-5.5,7.6-9.6,9.7c-1.6,0.8-3.2,1.5-3.4,1.5c-1,0-0.9,0.7,0.3,2.6c2.8,4.3,4,8.5,3.9,13.7c0,8.1-3.7,15.2-10.6,20.3
|
||||||
|
C356.4,397.6,349.5,399.5,342.9,398.5z"/>
|
||||||
|
<path class="st2" d="M53.9,341.9c-0.5-0.1-2.3-0.4-3.9-0.7c-15.6-2.6-30.4-12.6-38.8-26.2c-3.5-5.7-6.4-13.2-7.8-19.9
|
||||||
|
c-1.2-6.1-0.8-28.1,0.8-43.1c4.5-43,19-84.3,42.2-120.7c6.5-10.2,14.9-21.5,18.2-24.6c17.8-16.6,43.1-20.5,64.8-10
|
||||||
|
c4.3,2.1,8.8,5.1,12.7,8.6c2.8,2.4,5.8,6.1,20.9,25.5c9.7,12.5,17.8,22.8,17.9,23c0.2,0.2-0.9,0.4-3.2,0.4c-2.5,0-4.1,0.2-5.7,0.7
|
||||||
|
c-2.1,0.7-2.6,1.1-7.9,6.3c-8.2,8.1-14.4,15.3-20.3,23.9c-15.5,22.2-25.4,47.7-28.8,74.8c-2.2,16.9-1.6,37.5,1.6,52.3
|
||||||
|
c0.3,1.4,0.5,2.8,0.4,3c-0.1,0.2,0.2,1.3,0.8,2.4c1.1,2.4,4.3,5.7,6.5,6.8l1.5,0.8l-1.2,0.4c-0.7,0.2-13.1,3.8-27.6,8
|
||||||
|
c-16.4,4.7-27.7,7.8-29.8,8.1C64.1,342.1,56.1,342.3,53.9,341.9z"/>
|
||||||
|
<path class="st3" d="M494.7,341.7c-2.1-0.3-33.8-9.1-56.5-15.8l-2.5-0.7l1.6-0.8c3.4-1.7,7.2-6.6,7.3-9.6c0-0.7,0.4-3.3,0.8-5.8
|
||||||
|
c3.9-22.7,3.1-46.1-2.5-68.4c-6.4-25.5-18.6-49.2-35.8-69.1c-4.6-5.3-14.8-15.4-16.4-16.1c-2.4-1.1-5.1-1.6-8-1.4l-2.7,0.2l1.2-1.5
|
||||||
|
c0.7-0.8,8.5-10.8,17.5-22.3c8.9-11.5,17.2-21.8,18.5-23.1c2.6-2.7,7-6.2,10.3-8.2c19.3-11.6,43-11.1,61.6,1.2
|
||||||
|
c5.4,3.6,8.2,6.2,12.3,11.7c26.4,34.5,44,73.7,52.3,116.2c3.4,17.6,4.9,33.3,5,52.4c0,13-0.2,14.8-2.5,21.8
|
||||||
|
C547.8,328.6,521.7,345.2,494.7,341.7z"/>
|
||||||
|
<path class="st4" d="M133.9,318.5c-2-0.5-4.6-1.9-6-3.3c-2.5-2.4-3.1-3.5-3.7-7.3c-4.4-27.3-2.2-54,6.7-79.3
|
||||||
|
c5.3-15.1,13.5-30.5,23-43.1c5.8-7.8,16.6-19.5,19-20.7c4.7-2.4,11.3-1.2,15.2,2.7c5.4,5.4,5.2,13.9-0.3,19.1
|
||||||
|
c-4.3,4-9.4,4.4-12.6,0.9c-1.7-1.9-2.2-3.9-1.7-6.4c0.2-1.1,0.3-2,0.2-2.2c-0.3-0.3-3.6,3.3-8.3,9.1c-17.6,21.8-28.5,48-31.9,76.5
|
||||||
|
c-1.1,9.3-1,26.4,0.1,34.6c0.3,1.8,0.8,1.9,1.4,0.1c0.9-2.6,4-4.7,6.8-4.7c3,0,5.9,2.2,7.5,5.7c0.6,1.3,0.8,2.3,0.8,5.2
|
||||||
|
c0,3.3-0.1,3.8-1.1,5.7c-1.4,2.7-4.6,5.7-7.1,6.6C139.4,318.6,135.8,318.9,133.9,318.5z"/>
|
||||||
|
<path class="st1" d="M422.6,318.5c-3.7-0.6-7.7-3.6-9.4-7.1c-3.8-7.5,0.1-16.9,6.9-16.9c3.1,0,5.8,2,6.9,5.2
|
||||||
|
c0.4,1.2,0.5,1.3,0.7,0.7c1.3-3.7,1.7-26.4,0.6-35.7c-3.6-29.6-14.5-55.3-33-77.9c-5.5-6.7-8.4-9.4-7.1-6.6c0.7,1.4,0.5,4.3-0.3,5.9
|
||||||
|
c-0.9,1.7-3.2,3.5-5,3.8c-3.2,0.6-7.9-1.6-10.2-4.8c-6.5-8.8-0.5-21.2,10.4-21.4c4.6-0.1,5.2,0.3,11.2,6.4
|
||||||
|
c12.1,12.3,21.1,24.9,28.8,40.3c13.2,26.3,18.6,54.9,16.1,84.5c-0.5,5.6-2,15.7-2.6,17.1c-1.3,2.8-4.8,5.5-8.4,6.5
|
||||||
|
C425.9,318.9,425.1,318.9,422.6,318.5z"/>
|
||||||
|
<path class="st0" d="M178.2,307.2c-6-1.3-12.2-6.2-14.9-11.7c-3.4-7-3.1-15.1,0.9-21.6c0.7-1.2,1.2-2.3,1.1-2.4
|
||||||
|
c-0.1-0.1-1.1-0.6-2.1-1c-3.9-1.5-8.1-4.8-10.7-8.3c-4.6-6.2-6.1-14.6-3.9-22.1c2.9-10.3,9.4-16.8,19.1-19.3c2.8-0.7,9-0.8,11.7,0
|
||||||
|
c1.1,0.3,2.2,0.5,2.4,0.5c0.2,0,0.3-0.7,0.3-1.5c0-2.9,0.8-5.8,2.4-9.2c5.2-10.8,18.1-15.5,29-10.5c2.7,1.2,6.2,3.8,7.8,5.8
|
||||||
|
c0.7,0.8,10.3,14,21.5,29.4l20.3,27.9l-1.5,1.8c-0.8,1-1.9,2.6-2.5,3.5c-0.6,1-1.2,1.7-1.5,1.6c-4.5-1.7-46.7-15-47.7-15
|
||||||
|
c-1.9,0-3.1,1.3-3.1,3.2c0,1,0.2,1.7,0.8,2.3c0.6,0.6,7.8,3.1,24.5,8.5l23.7,7.7l-0.1,4.3l-0.1,4.3L223,295.9
|
||||||
|
c-18,5.9-33.9,10.9-35.2,11.2C184.7,307.8,181.2,307.8,178.2,307.2z"/>
|
||||||
|
<path class="st4" d="M372.5,306.8c-1.8-0.5-17.5-5.6-35-11.3l-31.8-10.4l1-4.3v-4.3l22.6-7.7c15-4.9,24-8,24.6-8.5
|
||||||
|
c0.7-0.6,0.9-1.1,0.9-2.2c0-2-1.2-3.3-3.1-3.3c-0.9,0-10.5,2.9-24.7,7.5c-12.8,4.1-23.4,7.5-23.6,7.5c-0.1,0-0.7-0.8-1.3-1.9
|
||||||
|
c-0.6-1-1.6-2.5-2.2-3.2c-0.7-0.7-1.2-1.5-1.2-1.6c0-0.2,9.6-13.5,21.4-29.6c18.9-26,21.6-29.6,23.6-31.1c5.7-4.4,13.1-5.8,19.7-3.9
|
||||||
|
c9,2.7,16.1,11.6,16.1,20.3c0,2.3-0.1,2.3,3.1,1.5c4.7-1.1,11.5-0.5,16,1.5c4.6,2,9,6,11.5,10.2c2.1,3.6,3.9,9.4,4.2,13.2
|
||||||
|
c0.3,5.2-1.1,10.7-4,15.3c-2.6,4.1-7.8,8.3-12.1,9.8c-0.9,0.3-1.7,0.8-1.7,1c0,0.2,0.4,1,0.9,1.7c2.4,3.6,3.6,7.7,3.5,12.7
|
||||||
|
c0,5.8-2.1,10.7-6.4,15.1c-4,4.1-8.9,6.3-14.9,6.5C376.3,307.7,375.3,307.6,372.5,306.8z"/>
|
||||||
|
<path class="st5" d="M276.2,298.9c-6.1-1.6-11.4-6.8-13.2-12.9c-0.7-2.4-0.7-7.5,0-9.9c1.7-5.8,6.6-10.8,12.3-12.5
|
||||||
|
c2.7-0.8,7.2-0.9,10-0.2c6.2,1.6,11.6,7.1,13.2,13.3c1.6,6-0.3,12.6-5,17.3C288.9,298.6,282.2,300.5,276.2,298.9z"/>
|
||||||
|
<path class="st2" d="M248.3,229.8c-13.3-18.3-21.2-29.6-22-31.1c-1.4-3-1.9-5.5-1.9-9.4c0-14.1,13.1-24.4,27.1-21.4
|
||||||
|
c1.4,0.3,2.6,0.5,2.7,0.5s0.3-1.3,0.4-2.8c0.8-10.7,8.4-19.6,18.9-22.4c3.9-1,10.6-1,14.5,0c8.9,2.3,15.9,9.3,18.2,18.2
|
||||||
|
c0.4,1.5,0.7,3.7,0.7,4.9c0,1.2,0.1,2.1,0.3,2.1s1.5-0.3,3-0.6c7.4-1.6,15.2,0.7,20.5,6c4.3,4.3,6.6,9.6,6.6,15.6
|
||||||
|
c0,4-0.6,6.5-2.4,10c-0.6,1.2-10.4,15-21.7,30.7c-17.8,24.5-20.8,28.5-21.4,28.3c-0.4-0.1-1.9-0.6-3.4-1.1c-1.5-0.5-2.9-0.9-3.3-0.9
|
||||||
|
c-0.7,0-0.7-0.8-0.3-25.5v-25.5l-1.4-0.9c-1-1.1-2.5-1.5-3.8-0.9c-2,0.8-2-0.5-1.8,27.2v25.8h-1.2c-0.5-0.2-2.4,0.3-4,0.9
|
||||||
|
s-3.1,1.1-3.2,1.1C269.2,258.5,259.8,245.6,248.3,229.8z"/>
|
||||||
|
<path class="st3" d="M210.9,164.8c-4.1-0.9-7.7-3.6-9.6-7.4c-1.4-2.8-1.7-7.3-0.5-10.3c1.7-4.5,3.9-6.1,15.6-11.2
|
||||||
|
c15.8-7,31.4-11.1,49.2-12.9c7.3-0.8,23.2-0.8,30.6,0c17.4,1.8,33.3,6,49.1,13c7.3,3.2,12.5,6.1,13.6,7.5c4.3,5.6,3.8,12.7-1.1,17.6
|
||||||
|
c-5.1,5.1-12.9,5.4-18.1,0.7c-2-1.8-3-3.5-3.4-5.6c-0.7-4,2.9-8.1,7.3-8.2c1.4,0,1.5-0.1,1.1-0.5c-0.3-0.3-2.2-1.2-4.3-2.1
|
||||||
|
c-33.2-14.5-70.5-16.4-105-5.4c-7.5,2.4-19,7.2-18.6,7.7c0.1,0.2,0.8,0.3,1.6,0.3c5.6,0,9.1,6.2,6.1,10.8
|
||||||
|
C221.6,163.3,215.9,165.9,210.9,164.8z"/>
|
||||||
|
<path class="st4" d="M174.7,123.4c-8.9-13.1-16.8-25.1-17.5-26.6c-1.6-3.3-3.6-9.2-4.4-13c-2.6-12.5-0.9-25.8,5-37.5
|
||||||
|
c4.2-8.3,11.2-16.3,18.6-21.3c5-3.4,6.1-3.9,12.8-6.3c23.1-8.2,47.2-13.1,73.4-15c7.5-0.6,28.5-0.6,36.3,0
|
||||||
|
c25.5,1.8,50.6,6.9,73,14.8c6.4,2.2,8.2,3.1,13.1,6.5c9.8,6.6,18.1,17.5,22,29.2c2.2,6.5,2.7,10,2.7,17.9c0,7.9-0.5,11.3-2.7,17.9
|
||||||
|
c-2.3,6.8-3.7,9.1-20.3,33.6l-16.1,23.8l-0.4-2.2c-0.2-1.2-0.9-3-1.4-4c-1-1.8-4.4-5.6-4.7-5.2c-0.1,0.1-1.2-0.4-2.4-1.1
|
||||||
|
c-9.1-5.2-21.9-10.5-33.2-13.9c-37-11-77.2-8.8-113,6.1c-4.9,2.1-17.7,8.4-19.2,9.5c-2.2,1.6-5.1,6.8-5.1,9c0,0.4-0.1,1-0.3,1.2
|
||||||
|
C191,147,184.7,138,174.7,123.4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 9.7 KiB |
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.82.0"
|
version = "1.82.1"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
@ -36,7 +36,7 @@ platform :android do
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 106,
|
"android.injected.version.code" => 106,
|
||||||
"android.injected.version.name" => "1.82.0",
|
"android.injected.version.name" => "1.82.1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
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')
|
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')
|
||||||
|
|
|
@ -131,7 +131,10 @@
|
||||||
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
"control_bottom_app_bar_favorite": "Favorite",
|
"control_bottom_app_bar_favorite": "Favorite",
|
||||||
"control_bottom_app_bar_share": "Share",
|
"control_bottom_app_bar_share": "Share",
|
||||||
|
"control_bottom_app_bar_share_to": "Share To",
|
||||||
|
"control_bottom_app_bar_stack": "Stack",
|
||||||
"control_bottom_app_bar_unarchive": "Unarchive",
|
"control_bottom_app_bar_unarchive": "Unarchive",
|
||||||
|
"control_bottom_app_bar_upload": "Upload",
|
||||||
"create_album_page_untitled": "Untitled",
|
"create_album_page_untitled": "Untitled",
|
||||||
"create_shared_album_page_create": "Create",
|
"create_shared_album_page_create": "Create",
|
||||||
"create_shared_album_page_share": "Share",
|
"create_shared_album_page_share": "Share",
|
||||||
|
@ -167,6 +170,7 @@
|
||||||
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
|
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
|
||||||
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
|
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
|
||||||
"image_viewer_page_state_provider_download_error": "Download Error",
|
"image_viewer_page_state_provider_download_error": "Download Error",
|
||||||
|
"image_viewer_page_state_provider_share_error": "Share Error",
|
||||||
"image_viewer_page_state_provider_download_success": "Download Success",
|
"image_viewer_page_state_provider_download_success": "Download Success",
|
||||||
"library_page_albums": "Albums",
|
"library_page_albums": "Albums",
|
||||||
"library_page_archive": "Archive",
|
"library_page_archive": "Archive",
|
||||||
|
@ -277,6 +281,7 @@
|
||||||
"setting_pages_app_bar_settings": "Settings",
|
"setting_pages_app_bar_settings": "Settings",
|
||||||
"settings_require_restart": "Please restart Immich to apply this setting",
|
"settings_require_restart": "Please restart Immich to apply this setting",
|
||||||
"share_add": "Add",
|
"share_add": "Add",
|
||||||
|
"share_done": "Done",
|
||||||
"share_add_photos": "Add photos",
|
"share_add_photos": "Add photos",
|
||||||
"share_add_title": "Add a title",
|
"share_add_title": "Add a title",
|
||||||
"share_create_album": "Create album",
|
"share_create_album": "Create album",
|
||||||
|
@ -286,6 +291,7 @@
|
||||||
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
|
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
|
||||||
"sharing_page_empty_list": "EMPTY LIST",
|
"sharing_page_empty_list": "EMPTY LIST",
|
||||||
"sharing_silver_appbar_create_shared_album": "Create shared album",
|
"sharing_silver_appbar_create_shared_album": "Create shared album",
|
||||||
|
"sharing_silver_appbar_shared_links": "Shared links",
|
||||||
"sharing_silver_appbar_share_partner": "Share with partner",
|
"sharing_silver_appbar_share_partner": "Share with partner",
|
||||||
"tab_controller_nav_library": "Library",
|
"tab_controller_nav_library": "Library",
|
||||||
"tab_controller_nav_photos": "Photos",
|
"tab_controller_nav_photos": "Photos",
|
||||||
|
@ -339,5 +345,24 @@
|
||||||
"trash_page_select_assets_btn": "Select assets",
|
"trash_page_select_assets_btn": "Select assets",
|
||||||
"trash_page_empty_trash_btn": "Empty trash",
|
"trash_page_empty_trash_btn": "Empty trash",
|
||||||
"trash_page_empty_trash_dialog_ok": "Ok",
|
"trash_page_empty_trash_dialog_ok": "Ok",
|
||||||
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich"
|
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
|
||||||
|
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||||
|
"viewer_remove_from_stack": "Remove from Stack",
|
||||||
|
"viewer_unstack": "Un-Stack",
|
||||||
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
|
"delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?",
|
||||||
|
"shared_link_create_app_bar_title": "Create link to share",
|
||||||
|
"shared_link_edit_app_bar_title": "Edit link",
|
||||||
|
"shared_link_create_info": "Let anyone with the link see the selected photo(s)",
|
||||||
|
"shared_link_edit_description": "Description",
|
||||||
|
"shared_link_edit_description_hint": "Enter the share description",
|
||||||
|
"shared_link_edit_show_meta": "Show metadata",
|
||||||
|
"shared_link_edit_allow_download": "Allow public user to download",
|
||||||
|
"shared_link_edit_allow_upload": "Allow public user to upload",
|
||||||
|
"shared_link_edit_change_expiry": "Change expiration time",
|
||||||
|
"shared_link_edit_submit_button": "Update link",
|
||||||
|
"shared_link_create_submit_button": "Create link",
|
||||||
|
"shared_link_app_bar_title": "Shared Links",
|
||||||
|
"shared_link_manage_links": "Manage Shared links",
|
||||||
|
"shared_link_empty": "You don't have any shared links"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ platform :ios do
|
||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.82.0"
|
version_number: "1.82.1"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|
|
@ -210,6 +210,18 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.share_rounded),
|
||||||
|
onTap: () {
|
||||||
|
AutoRouter.of(context)
|
||||||
|
.push(SharedLinkEditRoute(albumId: album.remoteId));
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
title: const Text(
|
||||||
|
"control_bottom_app_bar_share",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.settings_rounded),
|
leading: const Icon(Icons.settings_rounded),
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
|
|
|
@ -16,6 +16,7 @@ import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
@ -69,7 +70,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
|
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
|
||||||
AssetSelectionRoute(
|
AssetSelectionRoute(
|
||||||
existingAssets: albumInfo.assets,
|
existingAssets: albumInfo.assets,
|
||||||
isNewAlbum: false,
|
canDeselect: false,
|
||||||
|
query: getRemoteAssetQuery(ref),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -4,26 +4,27 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
|
||||||
|
|
||||||
class AssetSelectionPage extends HookConsumerWidget {
|
class AssetSelectionPage extends HookConsumerWidget {
|
||||||
const AssetSelectionPage({
|
const AssetSelectionPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.existingAssets,
|
required this.existingAssets,
|
||||||
this.isNewAlbum = false,
|
this.canDeselect = false,
|
||||||
|
required this.query,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Set<Asset> existingAssets;
|
final Set<Asset> existingAssets;
|
||||||
final bool isNewAlbum;
|
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
|
||||||
|
final bool canDeselect;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final currentUser = ref.watch(currentUserProvider);
|
final renderList = ref.watch(renderListQueryProvider(query));
|
||||||
final renderList = ref.watch(remoteAssetsProvider(currentUser?.isarId));
|
|
||||||
final selected = useState<Set<Asset>>(existingAssets);
|
final selected = useState<Set<Asset>>(existingAssets);
|
||||||
final selectionEnabledHook = useState(true);
|
final selectionEnabledHook = useState(true);
|
||||||
|
|
||||||
|
@ -39,8 +40,8 @@ class AssetSelectionPage extends HookConsumerWidget {
|
||||||
selected.value = assets;
|
selected.value = assets;
|
||||||
},
|
},
|
||||||
selectionActive: true,
|
selectionActive: true,
|
||||||
preselectedAssets: isNewAlbum ? selected.value : existingAssets,
|
preselectedAssets: existingAssets,
|
||||||
canDeselect: isNewAlbum,
|
canDeselect: canDeselect,
|
||||||
showMultiSelectIndicator: false,
|
showMultiSelectIndicator: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -65,7 +66,7 @@ class AssetSelectionPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
if (selected.value.isNotEmpty)
|
if (selected.value.isNotEmpty || canDeselect)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
var payload =
|
var payload =
|
||||||
|
@ -74,7 +75,7 @@ class AssetSelectionPage extends HookConsumerWidget {
|
||||||
.popForced<AssetSelectionPageResult>(payload);
|
.popForced<AssetSelectionPageResult>(payload);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"share_add",
|
canDeselect ? "share_done" : "share_add",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
|
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class CreateAlbumPage extends HookConsumerWidget {
|
class CreateAlbumPage extends HookConsumerWidget {
|
||||||
|
@ -31,7 +32,8 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||||
final isAlbumTitleTextFieldFocus = useState(false);
|
final isAlbumTitleTextFieldFocus = useState(false);
|
||||||
final isAlbumTitleEmpty = useState(true);
|
final isAlbumTitleEmpty = useState(true);
|
||||||
final selectedAssets = useState<Set<Asset>>(
|
final selectedAssets = useState<Set<Asset>>(
|
||||||
initialAssets != null ? Set.from(initialAssets!) : const {},);
|
initialAssets != null ? Set.from(initialAssets!) : const {},
|
||||||
|
);
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
showSelectUserPage() async {
|
showSelectUserPage() async {
|
||||||
|
@ -59,7 +61,8 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||||
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
|
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
|
||||||
AssetSelectionRoute(
|
AssetSelectionRoute(
|
||||||
existingAssets: selectedAssets.value,
|
existingAssets: selectedAssets.value,
|
||||||
isNewAlbum: true,
|
canDeselect: true,
|
||||||
|
query: getRemoteAssetQuery(ref),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (selectedAsset == null) {
|
if (selectedAsset == null) {
|
||||||
|
|
|
@ -147,13 +147,13 @@ class SharingPage extends HookConsumerWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
AutoRouter.of(context).push(const PartnerRoute()),
|
AutoRouter.of(context).push(const SharedLinkRoute()),
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.swap_horizontal_circle_outlined,
|
Icons.link,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"sharing_silver_appbar_share_partner",
|
"sharing_silver_appbar_shared_links",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
@ -179,6 +179,17 @@ class SharingPage extends HookConsumerWidget {
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
splashRadius: 25,
|
||||||
|
iconSize: 20,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.swap_horizontal_circle_outlined,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
onPressed: () => AutoRouter.of(context).push(const PartnerRoute()),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
||||||
|
final Asset _asset;
|
||||||
|
final Ref _ref;
|
||||||
|
|
||||||
|
AssetStackNotifier(
|
||||||
|
this._asset,
|
||||||
|
this._ref,
|
||||||
|
) : super([]) {
|
||||||
|
fetchStackChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetchStackChildren() async {
|
||||||
|
if (mounted) {
|
||||||
|
state = await _ref.read(assetStackProvider(_asset).future);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChild(int index) {
|
||||||
|
if (index < state.length) {
|
||||||
|
state.removeAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetStackStateProvider = StateNotifierProvider.autoDispose
|
||||||
|
.family<AssetStackNotifier, List<Asset>, Asset>(
|
||||||
|
(ref, asset) => AssetStackNotifier(asset, ref),
|
||||||
|
);
|
||||||
|
|
||||||
|
final assetStackProvider =
|
||||||
|
FutureProvider.autoDispose.family<List<Asset>, Asset>((ref, asset) async {
|
||||||
|
// Guard [local asset]
|
||||||
|
if (asset.remoteId == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ref
|
||||||
|
.watch(dbProvider)
|
||||||
|
.assets
|
||||||
|
.filter()
|
||||||
|
.isArchivedEqualTo(false)
|
||||||
|
.isTrashedEqualTo(false)
|
||||||
|
.stackParentIdEqualTo(asset.remoteId)
|
||||||
|
.findAll();
|
||||||
|
});
|
|
@ -57,9 +57,19 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext buildContext) {
|
builder: (BuildContext buildContext) {
|
||||||
_shareService
|
_shareService.shareAsset(asset).then(
|
||||||
.shareAsset(asset)
|
(bool status) {
|
||||||
.then((_) => Navigator.of(buildContext).pop());
|
if (!status) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'image_viewer_page_state_provider_share_error'.tr(),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Navigator.of(buildContext).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
return const ShareDialog();
|
return const ShareDialog();
|
||||||
},
|
},
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
final renderListProvider =
|
final renderListProvider =
|
||||||
FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
|
FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
|
||||||
|
@ -13,3 +14,19 @@ final renderListProvider =
|
||||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
|
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final renderListQueryProvider = StreamProvider.family<RenderList,
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy>?>(
|
||||||
|
(ref, query) async* {
|
||||||
|
if (query == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
final groupBy = GroupAssetsBy
|
||||||
|
.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
|
await for (final _ in query.watchLazy()) {
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
class AssetStackService {
|
||||||
|
AssetStackService(this._api);
|
||||||
|
|
||||||
|
final ApiService _api;
|
||||||
|
|
||||||
|
updateStack(
|
||||||
|
Asset parentAsset, {
|
||||||
|
List<Asset>? childrenToAdd,
|
||||||
|
List<Asset>? childrenToRemove,
|
||||||
|
}) async {
|
||||||
|
// Guard [local asset]
|
||||||
|
if (parentAsset.remoteId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (childrenToAdd != null) {
|
||||||
|
final toAdd = childrenToAdd
|
||||||
|
.where((e) => e.isRemote)
|
||||||
|
.map((e) => e.remoteId!)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
await _api.assetApi.updateAssets(
|
||||||
|
AssetBulkUpdateDto(ids: toAdd, stackParentId: parentAsset.remoteId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (childrenToRemove != null) {
|
||||||
|
final toRemove = childrenToRemove
|
||||||
|
.where((e) => e.isRemote)
|
||||||
|
.map((e) => e.remoteId!)
|
||||||
|
.toList();
|
||||||
|
await _api.assetApi.updateAssets(
|
||||||
|
AssetBulkUpdateDto(ids: toRemove, removeParent: true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("Error while updating stack children: ${error.toString()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStackParent(Asset oldParent, Asset newParent) async {
|
||||||
|
// Guard [local asset]
|
||||||
|
if (oldParent.remoteId == null || newParent.remoteId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _api.assetApi.updateStackParent(
|
||||||
|
UpdateStackParentDto(
|
||||||
|
oldParentId: oldParent.remoteId!,
|
||||||
|
newParentId: newParent.remoteId!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("Error while updating stack parent: ${error.toString()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetStackServiceProvider = Provider(
|
||||||
|
(ref) => AssetStackService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
|
@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
@ -14,6 +15,7 @@ final imageViewerServiceProvider =
|
||||||
|
|
||||||
class ImageViewerService {
|
class ImageViewerService {
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
|
final Logger _log = Logger("ImageViewerService");
|
||||||
|
|
||||||
ImageViewerService(this._apiService);
|
ImageViewerService(this._apiService);
|
||||||
|
|
||||||
|
@ -29,6 +31,16 @@ class ImageViewerService {
|
||||||
asset.livePhotoVideoId!,
|
asset.livePhotoVideoId!,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (imageResponse.statusCode != 200 ||
|
||||||
|
motionReponse.statusCode != 200) {
|
||||||
|
final failedResponse =
|
||||||
|
imageResponse.statusCode != 200 ? imageResponse : motionReponse;
|
||||||
|
_log.severe(
|
||||||
|
"Motion asset download failed with status - ${failedResponse.statusCode} and response - ${failedResponse.body}",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final AssetEntity? entity;
|
||||||
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
@ -48,6 +60,13 @@ class ImageViewerService {
|
||||||
var res = await _apiService.assetApi
|
var res = await _apiService.assetApi
|
||||||
.downloadFileWithHttpInfo(asset.remoteId!);
|
.downloadFileWithHttpInfo(asset.remoteId!);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
_log.severe(
|
||||||
|
"Asset download failed with status - ${res.statusCode} and response - ${res.body}",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final AssetEntity? entity;
|
||||||
|
|
||||||
if (asset.isImage) {
|
if (asset.isImage) {
|
||||||
|
|
|
@ -8,11 +8,13 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.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_controls_provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.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/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/providers/video_player_value_provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.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/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||||
|
@ -44,6 +46,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
final int totalAssets;
|
final int totalAssets;
|
||||||
final int initialIndex;
|
final int initialIndex;
|
||||||
final int heroOffset;
|
final int heroOffset;
|
||||||
|
final bool showStack;
|
||||||
|
|
||||||
GalleryViewerPage({
|
GalleryViewerPage({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -51,6 +54,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
required this.loadAsset,
|
required this.loadAsset,
|
||||||
required this.totalAssets,
|
required this.totalAssets,
|
||||||
this.heroOffset = 0,
|
this.heroOffset = 0,
|
||||||
|
this.showStack = false,
|
||||||
}) : controller = PageController(initialPage: initialIndex);
|
}) : controller = PageController(initialPage: initialIndex);
|
||||||
|
|
||||||
final PageController controller;
|
final PageController controller;
|
||||||
|
@ -77,8 +81,17 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
final isFromTrash = isTrashEnabled &&
|
final isFromTrash = isTrashEnabled &&
|
||||||
navStack.length > 2 &&
|
navStack.length > 2 &&
|
||||||
navStack.elementAt(navStack.length - 2).name == TrashRoute.name;
|
navStack.elementAt(navStack.length - 2).name == TrashRoute.name;
|
||||||
|
final stackIndex = useState(-1);
|
||||||
|
final stack = showStack && currentAsset.stackCount > 0
|
||||||
|
? ref.watch(assetStackStateProvider(currentAsset))
|
||||||
|
: <Asset>[];
|
||||||
|
final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
|
||||||
|
|
||||||
Asset asset() => currentAsset;
|
Asset asset() => stackIndex.value == -1
|
||||||
|
? currentAsset
|
||||||
|
: stackElements.elementAt(stackIndex.value);
|
||||||
|
|
||||||
|
bool isParent = stackIndex.value == -1 || stackIndex.value == 0;
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
|
@ -165,19 +178,28 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
),
|
),
|
||||||
child: ExifBottomSheet(asset: currentAsset),
|
child: ExifBottomSheet(asset: asset()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void removeAssetFromStack() {
|
||||||
|
if (stackIndex.value > 0 && showStack) {
|
||||||
|
ref
|
||||||
|
.read(assetStackStateProvider(currentAsset).notifier)
|
||||||
|
.removeChild(stackIndex.value - 1);
|
||||||
|
stackIndex.value = stackIndex.value - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void handleDelete(Asset deleteAsset) async {
|
void handleDelete(Asset deleteAsset) async {
|
||||||
Future<bool> onDelete(bool force) async {
|
Future<bool> onDelete(bool force) async {
|
||||||
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
|
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
|
||||||
{deleteAsset},
|
{deleteAsset},
|
||||||
force: force,
|
force: force,
|
||||||
);
|
);
|
||||||
if (isDeleted) {
|
if (isDeleted && isParent) {
|
||||||
if (totalAssets == 1) {
|
if (totalAssets == 1) {
|
||||||
// Handle only one asset
|
// Handle only one asset
|
||||||
AutoRouter.of(context).pop();
|
AutoRouter.of(context).pop();
|
||||||
|
@ -195,14 +217,17 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
// Asset is trashed
|
// Asset is trashed
|
||||||
if (isTrashEnabled && !isFromTrash) {
|
if (isTrashEnabled && !isFromTrash) {
|
||||||
final isDeleted = await onDelete(false);
|
final isDeleted = await onDelete(false);
|
||||||
// Can only trash assets stored in server. Local assets are always permanently removed for now
|
if (isDeleted) {
|
||||||
if (context.mounted && isDeleted && deleteAsset.isRemote) {
|
// Can only trash assets stored in server. Local assets are always permanently removed for now
|
||||||
ImmichToast.show(
|
if (context.mounted && deleteAsset.isRemote && isParent) {
|
||||||
durationInSecond: 1,
|
ImmichToast.show(
|
||||||
context: context,
|
durationInSecond: 1,
|
||||||
msg: 'Asset trashed',
|
context: context,
|
||||||
gravity: ToastGravity.BOTTOM,
|
msg: 'Asset trashed',
|
||||||
);
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
removeAssetFromStack();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -211,7 +236,14 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext _) {
|
builder: (BuildContext _) {
|
||||||
return DeleteDialog(onDelete: () => onDelete(true));
|
return DeleteDialog(
|
||||||
|
onDelete: () async {
|
||||||
|
final isDeleted = await onDelete(true);
|
||||||
|
if (isDeleted) {
|
||||||
|
removeAssetFromStack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -268,7 +300,11 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
ref
|
ref
|
||||||
.watch(assetProvider.notifier)
|
.watch(assetProvider.notifier)
|
||||||
.toggleArchive([asset], !asset.isArchived);
|
.toggleArchive([asset], !asset.isArchived);
|
||||||
AutoRouter.of(context).pop();
|
if (isParent) {
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeAssetFromStack();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpload(Asset asset) {
|
handleUpload(Asset asset) {
|
||||||
|
@ -385,7 +421,186 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildBottomBar() {
|
Widget buildStackedChildren() {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: stackElements.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final assetId = stackElements.elementAt(index).remoteId;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => stackIndex.value = index,
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: index == stackIndex.value
|
||||||
|
? Border.all(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 2,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
imageUrl:
|
||||||
|
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$assetId',
|
||||||
|
httpHeaders: {
|
||||||
|
"Authorization":
|
||||||
|
"Bearer ${Store.get(StoreKey.accessToken)}",
|
||||||
|
},
|
||||||
|
errorWidget: (context, url, error) =>
|
||||||
|
const Icon(Icons.image_not_supported_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showStackActionItems() {
|
||||||
|
showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
enableDrag: false,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (!isParent)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.bookmark_border_outlined,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await ref
|
||||||
|
.read(assetStackServiceProvider)
|
||||||
|
.updateStackParent(
|
||||||
|
currentAsset,
|
||||||
|
stackElements.elementAt(stackIndex.value),
|
||||||
|
);
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
title: const Text(
|
||||||
|
"viewer_stack_use_as_main_asset",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.copy_all_outlined,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
if (isParent) {
|
||||||
|
await ref
|
||||||
|
.read(assetStackServiceProvider)
|
||||||
|
.updateStackParent(
|
||||||
|
currentAsset,
|
||||||
|
stackElements
|
||||||
|
.elementAt(1), // Next asset as parent
|
||||||
|
);
|
||||||
|
// Remove itself from stack
|
||||||
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
stackElements.elementAt(1),
|
||||||
|
childrenToRemove: [currentAsset],
|
||||||
|
);
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
} else {
|
||||||
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
currentAsset,
|
||||||
|
childrenToRemove: [
|
||||||
|
stackElements.elementAt(stackIndex.value),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
removeAssetFromStack();
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: const Text(
|
||||||
|
"viewer_remove_from_stack",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.filter_none_outlined,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
currentAsset,
|
||||||
|
childrenToRemove: stack,
|
||||||
|
);
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
title: const Text(
|
||||||
|
"viewer_unstack",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBottomBar() {
|
||||||
|
// !!!! itemsList and actionlist should always be in sync
|
||||||
|
final itemsList = [
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(
|
||||||
|
Platform.isAndroid ? Icons.share_rounded : 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(),
|
||||||
|
),
|
||||||
|
if (stack.isNotEmpty)
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: const Icon(Icons.burst_mode_outlined),
|
||||||
|
label: 'control_bottom_app_bar_stack'.tr(),
|
||||||
|
tooltip: 'control_bottom_app_bar_stack'.tr(),
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
label: 'control_bottom_app_bar_delete'.tr(),
|
||||||
|
tooltip: 'control_bottom_app_bar_delete'.tr(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
List<Function(int)> actionslist = [
|
||||||
|
(_) => shareAsset(),
|
||||||
|
(_) => handleArchive(asset()),
|
||||||
|
if (stack.isNotEmpty) (_) => showStackActionItems(),
|
||||||
|
(_) => handleDelete(asset()),
|
||||||
|
];
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: !ref.watch(showControlsProvider),
|
ignoring: !ref.watch(showControlsProvider),
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
|
@ -393,6 +608,17 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
|
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (stack.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 10,
|
||||||
|
bottom: 30,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: buildStackedChildren(),
|
||||||
|
),
|
||||||
|
),
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: !asset().isImage && !isPlayingMotionVideo.value,
|
visible: !asset().isImage && !isPlayingMotionVideo.value,
|
||||||
child: Container(
|
child: Container(
|
||||||
|
@ -421,44 +647,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
selectedLabelStyle: const TextStyle(color: Colors.black),
|
selectedLabelStyle: const TextStyle(color: Colors.black),
|
||||||
showSelectedLabels: false,
|
showSelectedLabels: false,
|
||||||
showUnselectedLabels: false,
|
showUnselectedLabels: false,
|
||||||
items: [
|
items: itemsList,
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(
|
|
||||||
Platform.isAndroid
|
|
||||||
? Icons.share_rounded
|
|
||||||
: 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) {
|
onTap: (index) {
|
||||||
switch (index) {
|
if (index < actionslist.length) {
|
||||||
case 0:
|
actionslist[index].call(index);
|
||||||
shareAsset();
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
handleArchive(asset());
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
handleDelete(asset());
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -504,6 +696,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||||
precacheNextImage(next);
|
precacheNextImage(next);
|
||||||
currentIndex.value = value;
|
currentIndex.value = value;
|
||||||
|
stackIndex.value = -1;
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
},
|
},
|
||||||
loadingBuilder: (context, event, index) {
|
loadingBuilder: (context, event, index) {
|
||||||
|
@ -544,10 +737,11 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
: webPThumbnail;
|
: webPThumbnail;
|
||||||
},
|
},
|
||||||
builder: (context, index) {
|
builder: (context, index) {
|
||||||
final asset = loadAsset(index);
|
final a =
|
||||||
final ImageProvider provider = finalImageProvider(asset);
|
index == currentIndex.value ? asset() : loadAsset(index);
|
||||||
|
final ImageProvider provider = finalImageProvider(a);
|
||||||
|
|
||||||
if (asset.isImage && !isPlayingMotionVideo.value) {
|
if (a.isImage && !isPlayingMotionVideo.value) {
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
onDragStart: (_, details, __) =>
|
onDragStart: (_, details, __) =>
|
||||||
localPosition = details.localPosition,
|
localPosition = details.localPosition,
|
||||||
|
@ -558,13 +752,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
imageProvider: provider,
|
imageProvider: provider,
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
tag: asset.id + heroOffset,
|
tag: a.id + heroOffset,
|
||||||
),
|
),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
tightMode: true,
|
tightMode: true,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
errorBuilder: (context, error, stackTrace) => ImmichImage(
|
errorBuilder: (context, error, stackTrace) => ImmichImage(
|
||||||
asset,
|
a,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -575,7 +769,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
onDragUpdate: (_, details, __) =>
|
onDragUpdate: (_, details, __) =>
|
||||||
handleSwipeUpDown(details),
|
handleSwipeUpDown(details),
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
tag: asset.id + heroOffset,
|
tag: a.id + heroOffset,
|
||||||
),
|
),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
maxScale: 1.0,
|
maxScale: 1.0,
|
||||||
|
@ -584,7 +778,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
child: VideoViewerPage(
|
child: VideoViewerPage(
|
||||||
onPlaying: () => isPlayingVideo.value = true,
|
onPlaying: () => isPlayingVideo.value = true,
|
||||||
onPaused: () => isPlayingVideo.value = false,
|
onPaused: () => isPlayingVideo.value = false,
|
||||||
asset: asset,
|
asset: a,
|
||||||
isMotionVideo: isPlayingMotionVideo.value,
|
isMotionVideo: isPlayingMotionVideo.value,
|
||||||
placeholder: Image(
|
placeholder: Image(
|
||||||
image: provider,
|
image: provider,
|
||||||
|
|
47
mobile/lib/modules/home/models/selection_state.dart
Normal file
47
mobile/lib/modules/home/models/selection_state.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
|
class SelectionAssetState {
|
||||||
|
final bool hasRemote;
|
||||||
|
final bool hasLocal;
|
||||||
|
final bool hasMerged;
|
||||||
|
|
||||||
|
const SelectionAssetState({
|
||||||
|
this.hasRemote = false,
|
||||||
|
this.hasLocal = false,
|
||||||
|
this.hasMerged = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
SelectionAssetState copyWith({
|
||||||
|
bool? hasRemote,
|
||||||
|
bool? hasLocal,
|
||||||
|
bool? hasMerged,
|
||||||
|
}) {
|
||||||
|
return SelectionAssetState(
|
||||||
|
hasRemote: hasRemote ?? this.hasRemote,
|
||||||
|
hasLocal: hasLocal ?? this.hasLocal,
|
||||||
|
hasMerged: hasMerged ?? this.hasMerged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionAssetState.fromSelection(Set<Asset> selection)
|
||||||
|
: hasLocal = selection.any((e) => e.storage == AssetState.local),
|
||||||
|
hasMerged = selection.any((e) => e.storage == AssetState.merged),
|
||||||
|
hasRemote = selection.any((e) => e.storage == AssetState.remote);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'SelectionAssetState(hasRemote: $hasRemote, hasMerged: $hasMerged, hasMerged: $hasMerged)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant SelectionAssetState other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.hasRemote == hasRemote &&
|
||||||
|
other.hasLocal == hasLocal &&
|
||||||
|
other.hasMerged == hasMerged;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
hasRemote.hashCode ^ hasLocal.hashCode ^ hasMerged.hashCode;
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
||||||
final Widget? topWidget;
|
final Widget? topWidget;
|
||||||
final bool shrinkWrap;
|
final bool shrinkWrap;
|
||||||
final bool showDragScroll;
|
final bool showDragScroll;
|
||||||
|
final bool showStack;
|
||||||
|
|
||||||
const ImmichAssetGrid({
|
const ImmichAssetGrid({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -51,6 +52,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
||||||
this.topWidget,
|
this.topWidget,
|
||||||
this.shrinkWrap = false,
|
this.shrinkWrap = false,
|
||||||
this.showDragScroll = true,
|
this.showDragScroll = true,
|
||||||
|
this.showStack = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -114,6 +116,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
||||||
heroOffset: heroOffset(),
|
heroOffset: heroOffset(),
|
||||||
shrinkWrap: shrinkWrap,
|
shrinkWrap: shrinkWrap,
|
||||||
showDragScroll: showDragScroll,
|
showDragScroll: showDragScroll,
|
||||||
|
showStack: showStack,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ class ImmichAssetGridView extends StatefulWidget {
|
||||||
final int heroOffset;
|
final int heroOffset;
|
||||||
final bool shrinkWrap;
|
final bool shrinkWrap;
|
||||||
final bool showDragScroll;
|
final bool showDragScroll;
|
||||||
|
final bool showStack;
|
||||||
|
|
||||||
const ImmichAssetGridView({
|
const ImmichAssetGridView({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -56,6 +57,7 @@ class ImmichAssetGridView extends StatefulWidget {
|
||||||
this.heroOffset = 0,
|
this.heroOffset = 0,
|
||||||
this.shrinkWrap = false,
|
this.shrinkWrap = false,
|
||||||
this.showDragScroll = true,
|
this.showDragScroll = true,
|
||||||
|
this.showStack = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -71,7 +73,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||||
|
|
||||||
bool _scrolling = false;
|
bool _scrolling = false;
|
||||||
final Set<Asset> _selectedAssets =
|
final Set<Asset> _selectedAssets =
|
||||||
HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
||||||
|
|
||||||
Set<Asset> _getSelectedAssets() {
|
Set<Asset> _getSelectedAssets() {
|
||||||
return Set.from(_selectedAssets);
|
return Set.from(_selectedAssets);
|
||||||
|
@ -90,7 +92,13 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||||
|
|
||||||
void _deselectAssets(List<Asset> assets) {
|
void _deselectAssets(List<Asset> assets) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedAssets.removeAll(assets);
|
_selectedAssets.removeAll(
|
||||||
|
assets.where(
|
||||||
|
(a) =>
|
||||||
|
widget.canDeselect ||
|
||||||
|
!(widget.preselectedAssets?.contains(a) ?? false),
|
||||||
|
),
|
||||||
|
);
|
||||||
_callSelectionListener(_selectedAssets.isNotEmpty);
|
_callSelectionListener(_selectedAssets.isNotEmpty);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -129,6 +137,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||||
useGrayBoxPlaceholder: true,
|
useGrayBoxPlaceholder: true,
|
||||||
showStorageIndicator: widget.showStorageIndicator,
|
showStorageIndicator: widget.showStorageIndicator,
|
||||||
heroOffset: widget.heroOffset,
|
heroOffset: widget.heroOffset,
|
||||||
|
showStack: widget.showStack,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,10 +386,6 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedAssets.clear();
|
_selectedAssets.clear();
|
||||||
});
|
});
|
||||||
} else if (widget.preselectedAssets != null) {
|
|
||||||
setState(() {
|
|
||||||
_selectedAssets.addAll(widget.preselectedAssets!);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||||
final Asset Function(int index) loadAsset;
|
final Asset Function(int index) loadAsset;
|
||||||
final int totalAssets;
|
final int totalAssets;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
|
final bool showStack;
|
||||||
final bool useGrayBoxPlaceholder;
|
final bool useGrayBoxPlaceholder;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final bool multiselectEnabled;
|
final bool multiselectEnabled;
|
||||||
|
@ -26,6 +27,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||||
required this.loadAsset,
|
required this.loadAsset,
|
||||||
required this.totalAssets,
|
required this.totalAssets,
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator = true,
|
||||||
|
this.showStack = false,
|
||||||
this.useGrayBoxPlaceholder = false,
|
this.useGrayBoxPlaceholder = false,
|
||||||
this.isSelected = false,
|
this.isSelected = false,
|
||||||
this.multiselectEnabled = false,
|
this.multiselectEnabled = false,
|
||||||
|
@ -93,6 +95,35 @@ class ThumbnailImage extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildStackIcon() {
|
||||||
|
return Positioned(
|
||||||
|
top: 5,
|
||||||
|
right: 5,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (asset.stackCount > 1)
|
||||||
|
Text(
|
||||||
|
"${asset.stackCount}",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (asset.stackCount > 1)
|
||||||
|
const SizedBox(
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
Icons.burst_mode_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildImage() {
|
Widget buildImage() {
|
||||||
final image = SizedBox(
|
final image = SizedBox(
|
||||||
width: 300,
|
width: 300,
|
||||||
|
@ -113,9 +144,9 @@ class ThumbnailImage extends StatelessWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
width: 0,
|
width: 0,
|
||||||
color: assetContainerColor,
|
color: onDeselect == null ? Colors.grey : assetContainerColor,
|
||||||
),
|
),
|
||||||
color: assetContainerColor,
|
color: onDeselect == null ? Colors.grey : assetContainerColor,
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
|
@ -144,6 +175,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||||
loadAsset: loadAsset,
|
loadAsset: loadAsset,
|
||||||
totalAssets: totalAssets,
|
totalAssets: totalAssets,
|
||||||
heroOffset: heroOffset,
|
heroOffset: heroOffset,
|
||||||
|
showStack: showStack,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -196,6 +228,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!asset.isImage) buildVideoIcon(),
|
if (!asset.isImage) buildVideoIcon(),
|
||||||
|
if (asset.isImage && asset.stackCount > 0) buildStackIcon(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/models/selection_state.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
|
|
||||||
class ControlBottomAppBar extends ConsumerWidget {
|
class ControlBottomAppBar extends ConsumerWidget {
|
||||||
final void Function() onShare;
|
final void Function(bool shareLocal) onShare;
|
||||||
final void Function() onFavorite;
|
final void Function() onFavorite;
|
||||||
final void Function() onArchive;
|
final void Function() onArchive;
|
||||||
final void Function() onDelete;
|
final void Function() onDelete;
|
||||||
|
@ -20,11 +18,12 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||||
final Function(Album album) onAddToAlbum;
|
final Function(Album album) onAddToAlbum;
|
||||||
final void Function() onCreateNewAlbum;
|
final void Function() onCreateNewAlbum;
|
||||||
final void Function() onUpload;
|
final void Function() onUpload;
|
||||||
|
final void Function() onStack;
|
||||||
|
|
||||||
final List<Album> albums;
|
final List<Album> albums;
|
||||||
final List<Album> sharedAlbums;
|
final List<Album> sharedAlbums;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final AssetState selectionAssetState;
|
final SelectionAssetState selectionAssetState;
|
||||||
|
|
||||||
const ControlBottomAppBar({
|
const ControlBottomAppBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
@ -38,32 +37,37 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||||
required this.onAddToAlbum,
|
required this.onAddToAlbum,
|
||||||
required this.onCreateNewAlbum,
|
required this.onCreateNewAlbum,
|
||||||
required this.onUpload,
|
required this.onUpload,
|
||||||
this.selectionAssetState = AssetState.remote,
|
required this.onStack,
|
||||||
|
this.selectionAssetState = const SelectionAssetState(),
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||||
var hasRemote = selectionAssetState == AssetState.remote ||
|
var hasRemote =
|
||||||
selectionAssetState == AssetState.merged;
|
selectionAssetState.hasRemote || selectionAssetState.hasMerged;
|
||||||
var hasLocal = selectionAssetState == AssetState.merged ||
|
var hasLocal =
|
||||||
selectionAssetState == AssetState.local;
|
selectionAssetState.hasLocal || selectionAssetState.hasMerged;
|
||||||
final trashEnabled =
|
final trashEnabled =
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||||
|
|
||||||
List<Widget> renderActionButtons() {
|
List<Widget> renderActionButtons() {
|
||||||
return [
|
return [
|
||||||
|
if (hasRemote)
|
||||||
|
ControlBoxButton(
|
||||||
|
iconData: Icons.share_rounded,
|
||||||
|
label: "control_bottom_app_bar_share".tr(),
|
||||||
|
onPressed: enabled ? () => onShare(false) : null,
|
||||||
|
),
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Platform.isAndroid
|
iconData: Icons.ios_share_rounded,
|
||||||
? Icons.share_rounded
|
label: "control_bottom_app_bar_share_to".tr(),
|
||||||
: Icons.ios_share_rounded,
|
onPressed: enabled ? () => onShare(true) : null,
|
||||||
label: "control_bottom_app_bar_share".tr(),
|
|
||||||
onPressed: enabled ? onShare : null,
|
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
if (hasRemote)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.archive_outlined,
|
iconData: Icons.archive,
|
||||||
label: "control_bottom_app_bar_archive".tr(),
|
label: "control_bottom_app_bar_archive".tr(),
|
||||||
onPressed: enabled ? onArchive : null,
|
onPressed: enabled ? onArchive : null,
|
||||||
),
|
),
|
||||||
|
@ -114,6 +118,12 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (!hasLocal)
|
||||||
|
ControlBoxButton(
|
||||||
|
iconData: Icons.filter_none_rounded,
|
||||||
|
label: "control_bottom_app_bar_stack".tr(),
|
||||||
|
onPressed: enabled ? onStack : null,
|
||||||
|
),
|
||||||
if (!hasRemote)
|
if (!hasRemote)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.backup_outlined,
|
iconData: Icons.backup_outlined,
|
||||||
|
@ -135,7 +145,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||||
return DraggableScrollableSheet(
|
return DraggableScrollableSheet(
|
||||||
initialChildSize: hasRemote ? 0.30 : 0.18,
|
initialChildSize: hasRemote ? 0.30 : 0.18,
|
||||||
minChildSize: 0.18,
|
minChildSize: 0.18,
|
||||||
maxChildSize: hasRemote ? 0.57 : 0.18,
|
maxChildSize: hasRemote ? 0.60 : 0.18,
|
||||||
snap: true,
|
snap: true,
|
||||||
builder: (
|
builder: (
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
@ -191,10 +201,6 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
|
||||||
const SliverToBoxAdapter(
|
|
||||||
child: SizedBox(height: 200),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -227,7 +233,10 @@ class AddToAlbumTitleRow extends StatelessWidget {
|
||||||
).tr(),
|
).tr(),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: onCreateNewAlbum,
|
onPressed: onCreateNewAlbum,
|
||||||
icon: const Icon(Icons.add),
|
icon: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
"common_create_new_album",
|
"common_create_new_album",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
|
@ -7,11 +7,15 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/models/selection_state.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||||
|
@ -36,7 +40,7 @@ class HomePage extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
||||||
final selectionEnabledHook = useState(false);
|
final selectionEnabledHook = useState(false);
|
||||||
final selectionAssetState = useState(AssetState.remote);
|
final selectionAssetState = useState(const SelectionAssetState());
|
||||||
|
|
||||||
final selection = useState(<Asset>{});
|
final selection = useState(<Asset>{});
|
||||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||||
|
@ -83,17 +87,8 @@ class HomePage extends HookConsumerWidget {
|
||||||
) {
|
) {
|
||||||
selectionEnabledHook.value = multiselect;
|
selectionEnabledHook.value = multiselect;
|
||||||
selection.value = selectedAssets;
|
selection.value = selectedAssets;
|
||||||
selectionAssetState.value = selectedAssets.any((e) => e.isRemote)
|
selectionAssetState.value =
|
||||||
? selectedAssets.any((e) => e.isLocal)
|
SelectionAssetState.fromSelection(selectedAssets);
|
||||||
? AssetState.merged
|
|
||||||
: AssetState.remote
|
|
||||||
: AssetState.local;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onShareAssets() {
|
|
||||||
handleShareAssets(ref, context, selection.value.toList());
|
|
||||||
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Asset> remoteOnlySelection({String? localErrorMessage}) {
|
List<Asset> remoteOnlySelection({String? localErrorMessage}) {
|
||||||
|
@ -112,6 +107,19 @@ class HomePage extends HookConsumerWidget {
|
||||||
return assets.toList();
|
return assets.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onShareAssets(bool shareLocal) {
|
||||||
|
processing.value = true;
|
||||||
|
if (shareLocal) {
|
||||||
|
handleShareAssets(ref, context, selection.value.toList());
|
||||||
|
} else {
|
||||||
|
final ids = remoteOnlySelection().map((e) => e.remoteId!);
|
||||||
|
AutoRouter.of(context)
|
||||||
|
.push(SharedLinkEditRoute(assetsList: ids.toList()));
|
||||||
|
}
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
void onFavoriteAssets() async {
|
void onFavoriteAssets() async {
|
||||||
processing.value = true;
|
processing.value = true;
|
||||||
try {
|
try {
|
||||||
|
@ -270,6 +278,55 @@ class HomePage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onStack() async {
|
||||||
|
try {
|
||||||
|
processing.value = true;
|
||||||
|
if (!selectionEnabledHook.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedAsset = selection.value.elementAt(0);
|
||||||
|
|
||||||
|
if (selection.value.length == 1) {
|
||||||
|
final stackChildren =
|
||||||
|
(await ref.read(assetStackProvider(selectedAsset).future))
|
||||||
|
.toSet();
|
||||||
|
AssetSelectionPageResult? returnPayload =
|
||||||
|
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
|
||||||
|
AssetSelectionRoute(
|
||||||
|
existingAssets: stackChildren,
|
||||||
|
canDeselect: true,
|
||||||
|
query: getAssetStackSelectionQuery(ref, selectedAsset),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (returnPayload != null) {
|
||||||
|
Set<Asset> selectedAssets = returnPayload.selectedAssets;
|
||||||
|
// Do not add itself as its stack child
|
||||||
|
selectedAssets.remove(selectedAsset);
|
||||||
|
final removedChildren = stackChildren.difference(selectedAssets);
|
||||||
|
final addedChildren = selectedAssets.difference(stackChildren);
|
||||||
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
selectedAsset,
|
||||||
|
childrenToAdd: addedChildren.toList(),
|
||||||
|
childrenToRemove: removedChildren.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Merge assets
|
||||||
|
selection.value.remove(selectedAsset);
|
||||||
|
final selectedAssets = selection.value;
|
||||||
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
selectedAsset,
|
||||||
|
childrenToAdd: selectedAssets.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> refreshAssets() async {
|
Future<void> refreshAssets() async {
|
||||||
final fullRefresh = refreshCount.value > 0;
|
final fullRefresh = refreshCount.value > 0;
|
||||||
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
||||||
|
@ -346,6 +403,7 @@ class HomePage extends HookConsumerWidget {
|
||||||
currentUser.memoryEnabled!)
|
currentUser.memoryEnabled!)
|
||||||
? const MemoryLane()
|
? const MemoryLane()
|
||||||
: const SizedBox(),
|
: const SizedBox(),
|
||||||
|
showStack: true,
|
||||||
),
|
),
|
||||||
error: (error, _) => Center(child: Text(error.toString())),
|
error: (error, _) => Center(child: Text(error.toString())),
|
||||||
loading: buildLoadingIndicator,
|
loading: buildLoadingIndicator,
|
||||||
|
@ -364,6 +422,7 @@ class HomePage extends HookConsumerWidget {
|
||||||
onUpload: onUpload,
|
onUpload: onUpload,
|
||||||
enabled: !processing.value,
|
enabled: !processing.value,
|
||||||
selectionAssetState: selectionAssetState.value,
|
selectionAssetState: selectionAssetState.value,
|
||||||
|
onStack: onStack,
|
||||||
),
|
),
|
||||||
if (processing.value) const Center(child: ImmichLoadingIndicator()),
|
if (processing.value) const Center(child: ImmichLoadingIndicator()),
|
||||||
],
|
],
|
||||||
|
|
107
mobile/lib/modules/shared_link/models/shared_link.dart
Normal file
107
mobile/lib/modules/shared_link/models/shared_link.dart
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
enum SharedLinkSource { album, individual }
|
||||||
|
|
||||||
|
class SharedLink {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final bool allowDownload;
|
||||||
|
final bool allowUpload;
|
||||||
|
final String? thumbAssetId;
|
||||||
|
final String? description;
|
||||||
|
final DateTime? expiresAt;
|
||||||
|
final String key;
|
||||||
|
final bool showMetadata;
|
||||||
|
final SharedLinkSource type;
|
||||||
|
|
||||||
|
const SharedLink({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.allowDownload,
|
||||||
|
required this.allowUpload,
|
||||||
|
required this.thumbAssetId,
|
||||||
|
required this.description,
|
||||||
|
required this.expiresAt,
|
||||||
|
required this.key,
|
||||||
|
required this.showMetadata,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
SharedLink copyWith({
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? thumbAssetId,
|
||||||
|
bool? allowDownload,
|
||||||
|
bool? allowUpload,
|
||||||
|
String? description,
|
||||||
|
DateTime? expiresAt,
|
||||||
|
String? key,
|
||||||
|
bool? showMetadata,
|
||||||
|
SharedLinkSource? type,
|
||||||
|
}) {
|
||||||
|
return SharedLink(
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
thumbAssetId: thumbAssetId ?? this.thumbAssetId,
|
||||||
|
allowDownload: allowDownload ?? this.allowDownload,
|
||||||
|
allowUpload: allowUpload ?? this.allowUpload,
|
||||||
|
description: description ?? this.description,
|
||||||
|
expiresAt: expiresAt ?? this.expiresAt,
|
||||||
|
key: key ?? this.key,
|
||||||
|
showMetadata: showMetadata ?? this.showMetadata,
|
||||||
|
type: type ?? this.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedLink.fromDto(SharedLinkResponseDto dto)
|
||||||
|
: id = dto.id,
|
||||||
|
allowDownload = dto.allowDownload,
|
||||||
|
allowUpload = dto.allowUpload,
|
||||||
|
description = dto.description,
|
||||||
|
expiresAt = dto.expiresAt,
|
||||||
|
key = dto.key,
|
||||||
|
showMetadata = dto.showMetadata,
|
||||||
|
type = dto.type == SharedLinkType.ALBUM
|
||||||
|
? SharedLinkSource.album
|
||||||
|
: SharedLinkSource.individual,
|
||||||
|
title = dto.type == SharedLinkType.ALBUM
|
||||||
|
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
|
||||||
|
: "INDIVIDUAL SHARE",
|
||||||
|
thumbAssetId = dto.type == SharedLinkType.ALBUM
|
||||||
|
? dto.album?.albumThumbnailAssetId
|
||||||
|
: dto.assets.isNotEmpty
|
||||||
|
? dto.assets[0].id
|
||||||
|
: null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SharedLink &&
|
||||||
|
other.id == id &&
|
||||||
|
other.title == title &&
|
||||||
|
other.thumbAssetId == thumbAssetId &&
|
||||||
|
other.allowDownload == allowDownload &&
|
||||||
|
other.allowUpload == allowUpload &&
|
||||||
|
other.description == description &&
|
||||||
|
other.expiresAt == expiresAt &&
|
||||||
|
other.key == key &&
|
||||||
|
other.showMetadata == showMetadata &&
|
||||||
|
other.type == type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
id.hashCode ^
|
||||||
|
title.hashCode ^
|
||||||
|
thumbAssetId.hashCode ^
|
||||||
|
allowDownload.hashCode ^
|
||||||
|
allowUpload.hashCode ^
|
||||||
|
description.hashCode ^
|
||||||
|
expiresAt.hashCode ^
|
||||||
|
key.hashCode ^
|
||||||
|
showMetadata.hashCode ^
|
||||||
|
type.hashCode;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/services/shared_link.service.dart';
|
||||||
|
|
||||||
|
class SharedLinksNotifier extends StateNotifier<AsyncValue<List<SharedLink>>> {
|
||||||
|
final SharedLinkService _sharedLinkService;
|
||||||
|
|
||||||
|
SharedLinksNotifier(this._sharedLinkService) : super(const AsyncLoading()) {
|
||||||
|
fetchLinks();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchLinks() async {
|
||||||
|
state = await _sharedLinkService.getAllSharedLinks();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteLink(String id) async {
|
||||||
|
await _sharedLinkService.deleteSharedLink(id);
|
||||||
|
state = const AsyncLoading();
|
||||||
|
fetchLinks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final sharedLinksStateProvider =
|
||||||
|
StateNotifierProvider<SharedLinksNotifier, AsyncValue<List<SharedLink>>>(
|
||||||
|
(ref) {
|
||||||
|
return SharedLinksNotifier(
|
||||||
|
ref.watch(sharedLinkServiceProvider),
|
||||||
|
);
|
||||||
|
});
|
115
mobile/lib/modules/shared_link/services/shared_link.service.dart
Normal file
115
mobile/lib/modules/shared_link/services/shared_link.service.dart
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final sharedLinkServiceProvider = Provider(
|
||||||
|
(ref) => SharedLinkService(ref.watch(apiServiceProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
class SharedLinkService {
|
||||||
|
final ApiService _apiService;
|
||||||
|
final Logger _log = Logger("SharedLinkService");
|
||||||
|
|
||||||
|
SharedLinkService(this._apiService);
|
||||||
|
|
||||||
|
Future<AsyncValue<List<SharedLink>>> getAllSharedLinks() async {
|
||||||
|
try {
|
||||||
|
final list = await _apiService.sharedLinkApi.getAllSharedLinks();
|
||||||
|
return list != null
|
||||||
|
? AsyncData(list.map(SharedLink.fromDto).toList())
|
||||||
|
: const AsyncData([]);
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log.severe("failed to fetch shared links - $e");
|
||||||
|
return AsyncError(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteSharedLink(String id) async {
|
||||||
|
try {
|
||||||
|
return await _apiService.sharedLinkApi.removeSharedLink(id);
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe("failed to delete shared link id - $id with error - $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SharedLink?> createSharedLink({
|
||||||
|
required bool showMeta,
|
||||||
|
required bool allowDownload,
|
||||||
|
required bool allowUpload,
|
||||||
|
String? description,
|
||||||
|
String? albumId,
|
||||||
|
List<String>? assetIds,
|
||||||
|
DateTime? expiresAt,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final type =
|
||||||
|
albumId != null ? SharedLinkType.ALBUM : SharedLinkType.INDIVIDUAL;
|
||||||
|
SharedLinkCreateDto? dto;
|
||||||
|
if (type == SharedLinkType.ALBUM) {
|
||||||
|
dto = SharedLinkCreateDto(
|
||||||
|
type: type,
|
||||||
|
albumId: albumId,
|
||||||
|
showMetadata: showMeta,
|
||||||
|
allowDownload: allowDownload,
|
||||||
|
allowUpload: allowUpload,
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
description: description,
|
||||||
|
);
|
||||||
|
} else if (assetIds != null) {
|
||||||
|
dto = SharedLinkCreateDto(
|
||||||
|
type: type,
|
||||||
|
showMetadata: showMeta,
|
||||||
|
allowDownload: allowDownload,
|
||||||
|
allowUpload: allowUpload,
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
description: description,
|
||||||
|
assetIds: assetIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto != null) {
|
||||||
|
final responseDto =
|
||||||
|
await _apiService.sharedLinkApi.createSharedLink(dto);
|
||||||
|
if (responseDto != null) {
|
||||||
|
return SharedLink.fromDto(responseDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe("failed to create shared link with error - $e");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SharedLink?> updateSharedLink(
|
||||||
|
String id, {
|
||||||
|
required bool? showMeta,
|
||||||
|
required bool? allowDownload,
|
||||||
|
required bool? allowUpload,
|
||||||
|
bool? changeExpiry = false,
|
||||||
|
String? description,
|
||||||
|
DateTime? expiresAt,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final responseDto = await _apiService.sharedLinkApi.updateSharedLink(
|
||||||
|
id,
|
||||||
|
SharedLinkEditDto(
|
||||||
|
showMetadata: showMeta,
|
||||||
|
allowDownload: allowDownload,
|
||||||
|
allowUpload: allowUpload,
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
description: description,
|
||||||
|
changeExpiryTime: changeExpiry,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (responseDto != null) {
|
||||||
|
return SharedLink.fromDto(responseDto);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe("failed to update shared link id - $id with error - $e");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
307
mobile/lib/modules/shared_link/ui/shared_link_item.dart
Normal file
307
mobile/lib/modules/shared_link/ui/shared_link_item.dart
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
|
|
||||||
|
class SharedLinkItem extends ConsumerWidget {
|
||||||
|
final SharedLink sharedLink;
|
||||||
|
|
||||||
|
const SharedLinkItem(this.sharedLink, {super.key});
|
||||||
|
|
||||||
|
bool isExpired() {
|
||||||
|
if (sharedLink.expiresAt != null) {
|
||||||
|
return DateTime.now().isAfter(sharedLink.expiresAt!);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget getExpiryDuration(bool isDarkMode) {
|
||||||
|
var expiresText = "Expires ∞";
|
||||||
|
if (sharedLink.expiresAt != null) {
|
||||||
|
if (isExpired()) {
|
||||||
|
return Text(
|
||||||
|
"Expired",
|
||||||
|
style: TextStyle(color: Colors.red[300]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final difference = sharedLink.expiresAt!.difference(DateTime.now());
|
||||||
|
debugPrint("Difference: $difference");
|
||||||
|
if (difference.inDays > 0) {
|
||||||
|
var dayDifference = difference.inDays;
|
||||||
|
if (difference.inHours % 24 > 12) {
|
||||||
|
dayDifference += 1;
|
||||||
|
}
|
||||||
|
expiresText = "in $dayDifference days";
|
||||||
|
} else if (difference.inHours > 0) {
|
||||||
|
expiresText = "in ${difference.inHours} hours";
|
||||||
|
} else if (difference.inMinutes > 0) {
|
||||||
|
expiresText = "in ${difference.inMinutes} minutes";
|
||||||
|
} else if (difference.inSeconds > 0) {
|
||||||
|
expiresText = "in ${difference.inSeconds} seconds";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Text(
|
||||||
|
expiresText,
|
||||||
|
style: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final themeData = Theme.of(context);
|
||||||
|
final isDarkMode = themeData.brightness == Brightness.dark;
|
||||||
|
final thumbnailUrl = sharedLink.thumbAssetId != null
|
||||||
|
? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!)
|
||||||
|
: null;
|
||||||
|
final imageSize = math.min(MediaQuery.of(context).size.width / 4, 100.0);
|
||||||
|
|
||||||
|
void copyShareLinkToClipboard() {
|
||||||
|
final serverUrl = getServerUrl();
|
||||||
|
if (serverUrl == null) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.error,
|
||||||
|
msg: 'Cannot fetch the server url',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Clipboard.setData(
|
||||||
|
ClipboardData(
|
||||||
|
text: "$serverUrl/share/${sharedLink.key}",
|
||||||
|
),
|
||||||
|
).then((_) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"Copied to clipboard",
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteShareLink() async {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return ConfirmDialog(
|
||||||
|
title: "delete_shared_link_dialog_title",
|
||||||
|
content: "delete_shared_link_dialog_content",
|
||||||
|
onOk: () => ref
|
||||||
|
.read(sharedLinksStateProvider.notifier)
|
||||||
|
.deleteLink(sharedLink.id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildThumbnail() {
|
||||||
|
if (thumbnailUrl == null) {
|
||||||
|
return Container(
|
||||||
|
height: imageSize * 1.2,
|
||||||
|
width: imageSize,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
color: isDarkMode ? Colors.grey[100] : Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
height: imageSize * 1.2,
|
||||||
|
width: imageSize,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
|
child: ThumbnailWithInfo(
|
||||||
|
imageUrl: thumbnailUrl,
|
||||||
|
key: key,
|
||||||
|
textInfo: '',
|
||||||
|
noImageIcon: Icons.image_not_supported_outlined,
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildInfoChip(String labelText) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: Chip(
|
||||||
|
backgroundColor: themeData.primaryColor,
|
||||||
|
label: Text(
|
||||||
|
labelText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isDarkMode ? Colors.black : Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(25)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBottomInfo() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (sharedLink.allowUpload) buildInfoChip("Upload"),
|
||||||
|
if (sharedLink.allowDownload) buildInfoChip("Download"),
|
||||||
|
if (sharedLink.showMetadata) buildInfoChip("EXIF"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildSharedLinkActions() {
|
||||||
|
const actionIconSize = 20.0;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
splashRadius: 25,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
iconSize: actionIconSize,
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
style: const ButtonStyle(
|
||||||
|
tapTargetSize:
|
||||||
|
MaterialTapTargetSize.shrinkWrap, // the '2023' part
|
||||||
|
),
|
||||||
|
onPressed: deleteShareLink,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
splashRadius: 25,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
iconSize: actionIconSize,
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
style: const ButtonStyle(
|
||||||
|
tapTargetSize:
|
||||||
|
MaterialTapTargetSize.shrinkWrap, // the '2023' part
|
||||||
|
),
|
||||||
|
onPressed: () => AutoRouter.of(context)
|
||||||
|
.push(SharedLinkEditRoute(existingLink: sharedLink)),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
splashRadius: 25,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
iconSize: actionIconSize,
|
||||||
|
icon: const Icon(Icons.copy_outlined),
|
||||||
|
style: const ButtonStyle(
|
||||||
|
tapTargetSize:
|
||||||
|
MaterialTapTargetSize.shrinkWrap, // the '2023' part
|
||||||
|
),
|
||||||
|
onPressed: copyShareLinkToClipboard,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildSharedLinkDetails() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
getExpiryDuration(isDarkMode),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 5),
|
||||||
|
child: Tooltip(
|
||||||
|
verticalOffset: 0,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: themeData.primaryColor.withOpacity(0.9),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
textStyle: TextStyle(
|
||||||
|
color: isDarkMode ? Colors.black : Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
message: sharedLink.title,
|
||||||
|
preferBelow: false,
|
||||||
|
triggerMode: TooltipTriggerMode.tap,
|
||||||
|
child: Text(
|
||||||
|
sharedLink.title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: themeData.primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Tooltip(
|
||||||
|
verticalOffset: 0,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: themeData.primaryColor.withOpacity(0.9),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
textStyle: TextStyle(
|
||||||
|
color: isDarkMode ? Colors.black : Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
message: sharedLink.description ?? "",
|
||||||
|
preferBelow: false,
|
||||||
|
triggerMode: TooltipTriggerMode.tap,
|
||||||
|
child: Text(
|
||||||
|
sharedLink.description ?? "",
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 15),
|
||||||
|
child: buildSharedLinkActions(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
buildBottomInfo(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 15),
|
||||||
|
child: buildThumbnail(),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 15),
|
||||||
|
child: buildSharedLinkDetails(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(20),
|
||||||
|
child: Divider(
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
459
mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
Normal file
459
mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
Normal file
|
@ -0,0 +1,459 @@
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/services/shared_link.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
|
|
||||||
|
class SharedLinkEditPage extends HookConsumerWidget {
|
||||||
|
final SharedLink? existingLink;
|
||||||
|
final List<String>? assetsList;
|
||||||
|
final String? albumId;
|
||||||
|
|
||||||
|
const SharedLinkEditPage({
|
||||||
|
super.key,
|
||||||
|
this.existingLink,
|
||||||
|
this.assetsList,
|
||||||
|
this.albumId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
const padding = 20.0;
|
||||||
|
final themeData = Theme.of(context);
|
||||||
|
final descriptionController =
|
||||||
|
useTextEditingController(text: existingLink?.description ?? "");
|
||||||
|
final descriptionFocusNode = useFocusNode();
|
||||||
|
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
||||||
|
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
||||||
|
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
||||||
|
final editExpiry = useState(false);
|
||||||
|
final expiryAfter = useState(0);
|
||||||
|
final newShareLink = useState("");
|
||||||
|
|
||||||
|
Widget buildLinkTitle() {
|
||||||
|
if (existingLink != null) {
|
||||||
|
if (existingLink!.type == SharedLinkSource.album) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Public album | ",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
existingLink!.title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: themeData.primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingLink!.type == SharedLinkSource.individual) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Individual shared | ",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
existingLink!.description ?? "--",
|
||||||
|
style: TextStyle(
|
||||||
|
color: themeData.primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Text(
|
||||||
|
"shared_link_create_info",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildDescriptionField() {
|
||||||
|
return TextField(
|
||||||
|
controller: descriptionController,
|
||||||
|
enabled: newShareLink.value.isEmpty,
|
||||||
|
focusNode: descriptionFocusNode,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
autofocus: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'shared_link_edit_description'.tr(),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: themeData.primaryColor,
|
||||||
|
),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'shared_link_edit_description_hint'.tr(),
|
||||||
|
hintStyle: const TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => descriptionFocusNode.unfocus(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildShowMetaButton() {
|
||||||
|
return SwitchListTile.adaptive(
|
||||||
|
value: showMetadata.value,
|
||||||
|
onChanged: newShareLink.value.isEmpty
|
||||||
|
? (value) => showMetadata.value = value
|
||||||
|
: null,
|
||||||
|
activeColor: themeData.primaryColor,
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
"shared_link_edit_show_meta",
|
||||||
|
style: themeData.textTheme.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildAllowDownloadButton() {
|
||||||
|
return SwitchListTile.adaptive(
|
||||||
|
value: allowDownload.value,
|
||||||
|
onChanged: newShareLink.value.isEmpty
|
||||||
|
? (value) => allowDownload.value = value
|
||||||
|
: null,
|
||||||
|
activeColor: themeData.primaryColor,
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
"shared_link_edit_allow_download",
|
||||||
|
style: themeData.textTheme.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildAllowUploadButton() {
|
||||||
|
return SwitchListTile.adaptive(
|
||||||
|
value: allowUpload.value,
|
||||||
|
onChanged: newShareLink.value.isEmpty
|
||||||
|
? (value) => allowUpload.value = value
|
||||||
|
: null,
|
||||||
|
activeColor: themeData.primaryColor,
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
"shared_link_edit_allow_upload",
|
||||||
|
style: themeData.textTheme.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildEditExpiryButton() {
|
||||||
|
return SwitchListTile.adaptive(
|
||||||
|
value: editExpiry.value,
|
||||||
|
onChanged: newShareLink.value.isEmpty
|
||||||
|
? (value) => editExpiry.value = value
|
||||||
|
: null,
|
||||||
|
activeColor: themeData.primaryColor,
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
"shared_link_edit_change_expiry",
|
||||||
|
style: themeData.textTheme.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildExpiryAfterButton() {
|
||||||
|
return DropdownMenu(
|
||||||
|
enableSearch: false,
|
||||||
|
enableFilter: false,
|
||||||
|
width: MediaQuery.of(context).size.width - 40,
|
||||||
|
initialSelection: expiryAfter.value,
|
||||||
|
enabled: newShareLink.value.isEmpty &&
|
||||||
|
(existingLink == null || editExpiry.value),
|
||||||
|
onSelected: (value) {
|
||||||
|
expiryAfter.value = value!;
|
||||||
|
},
|
||||||
|
inputDecorationTheme: themeData.inputDecorationTheme.copyWith(
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)),
|
||||||
|
),
|
||||||
|
enabledBorder: const OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dropdownMenuEntries: const [
|
||||||
|
DropdownMenuEntry(value: 0, label: "Never"),
|
||||||
|
DropdownMenuEntry(
|
||||||
|
value: 30,
|
||||||
|
label: '30 minutes',
|
||||||
|
),
|
||||||
|
DropdownMenuEntry(
|
||||||
|
value: 60,
|
||||||
|
label: '1 hour',
|
||||||
|
),
|
||||||
|
DropdownMenuEntry(
|
||||||
|
value: 60 * 6,
|
||||||
|
label: '6 hours',
|
||||||
|
),
|
||||||
|
DropdownMenuEntry(
|
||||||
|
value: 60 * 24,
|
||||||
|
label: '1 day',
|
||||||
|
),
|
||||||
|
DropdownMenuEntry(
|
||||||
|
value: 60 * 24 * 7,
|
||||||
|
label: '7 days',
|
||||||
|
),
|
||||||
|
DropdownMenuEntry(
|
||||||
|
value: 60 * 24 * 30,
|
||||||
|
label: '30 days',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyLinkToClipboard() {
|
||||||
|
Clipboard.setData(
|
||||||
|
ClipboardData(
|
||||||
|
text: newShareLink.value,
|
||||||
|
),
|
||||||
|
).then((_) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"Copied to clipboard",
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildNewLinkField() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: 20,
|
||||||
|
bottom: 20,
|
||||||
|
),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
initialValue: newShareLink.value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
enabledBorder: themeData.inputDecorationTheme.focusedBorder,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: copyLinkToClipboard,
|
||||||
|
icon: const Icon(Icons.copy),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"Done",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime calculateExpiry() {
|
||||||
|
return DateTime.now().add(Duration(minutes: expiryAfter.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleNewLink() async {
|
||||||
|
final newLink =
|
||||||
|
await ref.read(sharedLinkServiceProvider).createSharedLink(
|
||||||
|
albumId: albumId,
|
||||||
|
assetIds: assetsList,
|
||||||
|
showMeta: showMetadata.value,
|
||||||
|
allowDownload: allowDownload.value,
|
||||||
|
allowUpload: allowUpload.value,
|
||||||
|
description: descriptionController.text.isEmpty
|
||||||
|
? null
|
||||||
|
: descriptionController.text,
|
||||||
|
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||||
|
);
|
||||||
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
|
final serverUrl = getServerUrl();
|
||||||
|
if (newLink != null && serverUrl != null) {
|
||||||
|
newShareLink.value = "$serverUrl/share/${newLink.key}";
|
||||||
|
copyLinkToClipboard();
|
||||||
|
} else if (newLink == null) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.error,
|
||||||
|
msg: 'Error while creating shared link',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleEditLink() async {
|
||||||
|
bool? download;
|
||||||
|
bool? upload;
|
||||||
|
bool? meta;
|
||||||
|
String? desc;
|
||||||
|
DateTime? expiry;
|
||||||
|
bool? changeExpiry;
|
||||||
|
|
||||||
|
if (allowDownload.value != existingLink!.allowDownload) {
|
||||||
|
download = allowDownload.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowUpload.value != existingLink!.allowUpload) {
|
||||||
|
upload = allowUpload.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showMetadata.value != existingLink!.showMetadata) {
|
||||||
|
meta = showMetadata.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptionController.text != existingLink!.description) {
|
||||||
|
desc = descriptionController.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editExpiry.value) {
|
||||||
|
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
||||||
|
changeExpiry = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ref.read(sharedLinkServiceProvider).updateSharedLink(
|
||||||
|
existingLink!.id,
|
||||||
|
showMeta: meta,
|
||||||
|
allowDownload: download,
|
||||||
|
allowUpload: upload,
|
||||||
|
description: desc,
|
||||||
|
expiresAt: expiry,
|
||||||
|
changeExpiry: changeExpiry,
|
||||||
|
);
|
||||||
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
existingLink == null
|
||||||
|
? "shared_link_create_app_bar_title"
|
||||||
|
: "shared_link_edit_app_bar_title",
|
||||||
|
).tr(),
|
||||||
|
elevation: 0,
|
||||||
|
leading: const CloseButton(),
|
||||||
|
centerTitle: false,
|
||||||
|
),
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(padding),
|
||||||
|
child: buildLinkTitle(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(padding),
|
||||||
|
child: buildDescriptionField(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: padding,
|
||||||
|
right: padding,
|
||||||
|
bottom: padding,
|
||||||
|
),
|
||||||
|
child: buildShowMetaButton(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: padding,
|
||||||
|
right: padding,
|
||||||
|
bottom: padding,
|
||||||
|
),
|
||||||
|
child: buildAllowDownloadButton(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(left: padding, right: 20, bottom: 20),
|
||||||
|
child: buildAllowUploadButton(),
|
||||||
|
),
|
||||||
|
if (existingLink != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: padding,
|
||||||
|
right: padding,
|
||||||
|
bottom: padding,
|
||||||
|
),
|
||||||
|
child: buildEditExpiryButton(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: padding,
|
||||||
|
right: padding,
|
||||||
|
bottom: padding,
|
||||||
|
),
|
||||||
|
child: buildExpiryAfterButton(),
|
||||||
|
),
|
||||||
|
if (newShareLink.value.isEmpty)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: padding + 10),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed:
|
||||||
|
existingLink != null ? handleEditLink : handleNewLink,
|
||||||
|
child: Text(
|
||||||
|
existingLink != null
|
||||||
|
? "shared_link_edit_submit_button"
|
||||||
|
: "shared_link_create_submit_button",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (newShareLink.value.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: padding,
|
||||||
|
right: padding,
|
||||||
|
),
|
||||||
|
child: buildNewLinkField(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
126
mobile/lib/modules/shared_link/views/shared_link_page.dart
Normal file
126
mobile/lib/modules/shared_link/views/shared_link_page.dart
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import 'package:easy_localization/easy_localization.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/shared_link/models/shared_link.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/ui/shared_link_item.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
|
||||||
|
class SharedLinkPage extends HookConsumerWidget {
|
||||||
|
const SharedLinkPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final sharedLinks = ref.watch(sharedLinksStateProvider);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
ref.read(sharedLinksStateProvider.notifier).fetchLinks();
|
||||||
|
return () => ref.invalidate(sharedLinksStateProvider);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget buildNoShares() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
||||||
|
child: const Text(
|
||||||
|
"shared_link_manage_links",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: const Text(
|
||||||
|
"shared_link_empty",
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.link_off,
|
||||||
|
size: 100,
|
||||||
|
color: Theme.of(context).iconTheme.color?.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildSharesList(List<SharedLink> links) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, top: 16.0, bottom: 30.0),
|
||||||
|
child: const Text(
|
||||||
|
"shared_link_manage_links",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
if (constraints.maxWidth > 600) {
|
||||||
|
// Two column
|
||||||
|
return GridView.builder(
|
||||||
|
gridDelegate:
|
||||||
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisExtent: 180,
|
||||||
|
),
|
||||||
|
itemCount: links.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return SharedLinkItem(links.elementAt(index));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single column
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: links.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return SharedLinkItem(links.elementAt(index));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("shared_link_app_bar_title").tr(),
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: false,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: sharedLinks.when(
|
||||||
|
data: (links) =>
|
||||||
|
links.isNotEmpty ? buildSharesList(links) : buildNoShares(),
|
||||||
|
error: (error, stackTrace) => buildNoShares(),
|
||||||
|
loading: () => const Center(child: ImmichLoadingIndicator()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -252,6 +252,7 @@ class TrashPage extends HookConsumerWidget {
|
||||||
listener: selectionListener,
|
listener: selectionListener,
|
||||||
selectionActive: selectionEnabledHook.value,
|
selectionActive: selectionEnabledHook.value,
|
||||||
showMultiSelectIndicator: false,
|
showMultiSelectIndicator: false,
|
||||||
|
showStack: true,
|
||||||
topWidget: Padding(
|
topWidget: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 24,
|
top: 24,
|
||||||
|
|
|
@ -28,6 +28,9 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart';
|
||||||
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
||||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.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/onboarding/views/permission_onboarding_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/views/shared_link_edit_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/shared_link/views/shared_link_page.dart';
|
||||||
import 'package:immich_mobile/modules/trash/views/trash_page.dart';
|
import 'package:immich_mobile/modules/trash/views/trash_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/all_motion_videos_page.dart';
|
import 'package:immich_mobile/modules/search/views/all_motion_videos_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/all_people_page.dart';
|
import 'package:immich_mobile/modules/search/views/all_people_page.dart';
|
||||||
|
@ -51,6 +54,7 @@ import 'package:immich_mobile/shared/views/app_log_detail_page.dart';
|
||||||
import 'package:immich_mobile/shared/views/app_log_page.dart';
|
import 'package:immich_mobile/shared/views/app_log_page.dart';
|
||||||
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
||||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
part 'router.gr.dart';
|
part 'router.gr.dart';
|
||||||
|
@ -157,6 +161,8 @@ part 'router.gr.dart';
|
||||||
AutoRoute(page: MapPage, guards: [AuthGuard, DuplicateGuard]),
|
AutoRoute(page: MapPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
AutoRoute(page: AlbumOptionsPage, guards: [AuthGuard, DuplicateGuard]),
|
AutoRoute(page: AlbumOptionsPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
AutoRoute(page: TrashPage, guards: [AuthGuard, DuplicateGuard]),
|
AutoRoute(page: TrashPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
|
AutoRoute(page: SharedLinkPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
|
AutoRoute(page: SharedLinkEditPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
|
|
|
@ -71,6 +71,7 @@ class _$AppRouter extends RootStackRouter {
|
||||||
loadAsset: args.loadAsset,
|
loadAsset: args.loadAsset,
|
||||||
totalAssets: args.totalAssets,
|
totalAssets: args.totalAssets,
|
||||||
heroOffset: args.heroOffset,
|
heroOffset: args.heroOffset,
|
||||||
|
showStack: args.showStack,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -153,7 +154,8 @@ class _$AppRouter extends RootStackRouter {
|
||||||
child: AssetSelectionPage(
|
child: AssetSelectionPage(
|
||||||
key: args.key,
|
key: args.key,
|
||||||
existingAssets: args.existingAssets,
|
existingAssets: args.existingAssets,
|
||||||
isNewAlbum: args.isNewAlbum,
|
canDeselect: args.canDeselect,
|
||||||
|
query: args.query,
|
||||||
),
|
),
|
||||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
opaque: true,
|
opaque: true,
|
||||||
|
@ -318,6 +320,25 @@ class _$AppRouter extends RootStackRouter {
|
||||||
child: const TrashPage(),
|
child: const TrashPage(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
SharedLinkRoute.name: (routeData) {
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const SharedLinkPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
SharedLinkEditRoute.name: (routeData) {
|
||||||
|
final args = routeData.argsAs<SharedLinkEditRouteArgs>(
|
||||||
|
orElse: () => const SharedLinkEditRouteArgs());
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: SharedLinkEditPage(
|
||||||
|
key: args.key,
|
||||||
|
existingLink: args.existingLink,
|
||||||
|
assetsList: args.assetsList,
|
||||||
|
albumId: args.albumId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
HomeRoute.name: (routeData) {
|
HomeRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
|
@ -638,6 +659,22 @@ class _$AppRouter extends RootStackRouter {
|
||||||
duplicateGuard,
|
duplicateGuard,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
RouteConfig(
|
||||||
|
SharedLinkRoute.name,
|
||||||
|
path: '/shared-link-page',
|
||||||
|
guards: [
|
||||||
|
authGuard,
|
||||||
|
duplicateGuard,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
RouteConfig(
|
||||||
|
SharedLinkEditRoute.name,
|
||||||
|
path: '/shared-link-edit-page',
|
||||||
|
guards: [
|
||||||
|
authGuard,
|
||||||
|
duplicateGuard,
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -711,6 +748,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||||
required Asset Function(int) loadAsset,
|
required Asset Function(int) loadAsset,
|
||||||
required int totalAssets,
|
required int totalAssets,
|
||||||
int heroOffset = 0,
|
int heroOffset = 0,
|
||||||
|
bool showStack = false,
|
||||||
}) : super(
|
}) : super(
|
||||||
GalleryViewerRoute.name,
|
GalleryViewerRoute.name,
|
||||||
path: '/gallery-viewer-page',
|
path: '/gallery-viewer-page',
|
||||||
|
@ -720,6 +758,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||||
loadAsset: loadAsset,
|
loadAsset: loadAsset,
|
||||||
totalAssets: totalAssets,
|
totalAssets: totalAssets,
|
||||||
heroOffset: heroOffset,
|
heroOffset: heroOffset,
|
||||||
|
showStack: showStack,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -733,6 +772,7 @@ class GalleryViewerRouteArgs {
|
||||||
required this.loadAsset,
|
required this.loadAsset,
|
||||||
required this.totalAssets,
|
required this.totalAssets,
|
||||||
this.heroOffset = 0,
|
this.heroOffset = 0,
|
||||||
|
this.showStack = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
@ -745,9 +785,11 @@ class GalleryViewerRouteArgs {
|
||||||
|
|
||||||
final int heroOffset;
|
final int heroOffset;
|
||||||
|
|
||||||
|
final bool showStack;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset}';
|
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset, showStack: $showStack}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -961,14 +1003,16 @@ class AssetSelectionRoute extends PageRouteInfo<AssetSelectionRouteArgs> {
|
||||||
AssetSelectionRoute({
|
AssetSelectionRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required Set<Asset> existingAssets,
|
required Set<Asset> existingAssets,
|
||||||
bool isNewAlbum = false,
|
bool canDeselect = false,
|
||||||
|
required QueryBuilder<Asset, Asset, QAfterSortBy>? query,
|
||||||
}) : super(
|
}) : super(
|
||||||
AssetSelectionRoute.name,
|
AssetSelectionRoute.name,
|
||||||
path: '/asset-selection-page',
|
path: '/asset-selection-page',
|
||||||
args: AssetSelectionRouteArgs(
|
args: AssetSelectionRouteArgs(
|
||||||
key: key,
|
key: key,
|
||||||
existingAssets: existingAssets,
|
existingAssets: existingAssets,
|
||||||
isNewAlbum: isNewAlbum,
|
canDeselect: canDeselect,
|
||||||
|
query: query,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -979,18 +1023,21 @@ class AssetSelectionRouteArgs {
|
||||||
const AssetSelectionRouteArgs({
|
const AssetSelectionRouteArgs({
|
||||||
this.key,
|
this.key,
|
||||||
required this.existingAssets,
|
required this.existingAssets,
|
||||||
this.isNewAlbum = false,
|
this.canDeselect = false,
|
||||||
|
required this.query,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final Set<Asset> existingAssets;
|
final Set<Asset> existingAssets;
|
||||||
|
|
||||||
final bool isNewAlbum;
|
final bool canDeselect;
|
||||||
|
|
||||||
|
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, isNewAlbum: $isNewAlbum}';
|
return 'AssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect, query: $query}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1420,6 +1467,62 @@ class TrashRoute extends PageRouteInfo<void> {
|
||||||
static const String name = 'TrashRoute';
|
static const String name = 'TrashRoute';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [SharedLinkPage]
|
||||||
|
class SharedLinkRoute extends PageRouteInfo<void> {
|
||||||
|
const SharedLinkRoute()
|
||||||
|
: super(
|
||||||
|
SharedLinkRoute.name,
|
||||||
|
path: '/shared-link-page',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'SharedLinkRoute';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [SharedLinkEditPage]
|
||||||
|
class SharedLinkEditRoute extends PageRouteInfo<SharedLinkEditRouteArgs> {
|
||||||
|
SharedLinkEditRoute({
|
||||||
|
Key? key,
|
||||||
|
SharedLink? existingLink,
|
||||||
|
List<String>? assetsList,
|
||||||
|
String? albumId,
|
||||||
|
}) : super(
|
||||||
|
SharedLinkEditRoute.name,
|
||||||
|
path: '/shared-link-edit-page',
|
||||||
|
args: SharedLinkEditRouteArgs(
|
||||||
|
key: key,
|
||||||
|
existingLink: existingLink,
|
||||||
|
assetsList: assetsList,
|
||||||
|
albumId: albumId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'SharedLinkEditRoute';
|
||||||
|
}
|
||||||
|
|
||||||
|
class SharedLinkEditRouteArgs {
|
||||||
|
const SharedLinkEditRouteArgs({
|
||||||
|
this.key,
|
||||||
|
this.existingLink,
|
||||||
|
this.assetsList,
|
||||||
|
this.albumId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final SharedLink? existingLink;
|
||||||
|
|
||||||
|
final List<String>? assetsList;
|
||||||
|
|
||||||
|
final String? albumId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SharedLinkEditRouteArgs{key: $key, existingLink: $existingLink, assetsList: $assetsList, albumId: $albumId}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [HomePage]
|
/// [HomePage]
|
||||||
class HomeRoute extends PageRouteInfo<void> {
|
class HomeRoute extends PageRouteInfo<void> {
|
||||||
|
|
|
@ -31,7 +31,9 @@ class Asset {
|
||||||
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
|
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
|
||||||
isFavorite = remote.isFavorite,
|
isFavorite = remote.isFavorite,
|
||||||
isArchived = remote.isArchived,
|
isArchived = remote.isArchived,
|
||||||
isTrashed = remote.isTrashed;
|
isTrashed = remote.isTrashed,
|
||||||
|
stackParentId = remote.stackParentId,
|
||||||
|
stackCount = remote.stackCount;
|
||||||
|
|
||||||
Asset.local(AssetEntity local, List<int> hash)
|
Asset.local(AssetEntity local, List<int> hash)
|
||||||
: localId = local.id,
|
: localId = local.id,
|
||||||
|
@ -47,6 +49,7 @@ class Asset {
|
||||||
isFavorite = local.isFavorite,
|
isFavorite = local.isFavorite,
|
||||||
isArchived = false,
|
isArchived = false,
|
||||||
isTrashed = false,
|
isTrashed = false,
|
||||||
|
stackCount = 0,
|
||||||
fileCreatedAt = local.createDateTime {
|
fileCreatedAt = local.createDateTime {
|
||||||
if (fileCreatedAt.year == 1970) {
|
if (fileCreatedAt.year == 1970) {
|
||||||
fileCreatedAt = fileModifiedAt;
|
fileCreatedAt = fileModifiedAt;
|
||||||
|
@ -77,6 +80,8 @@ class Asset {
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
required this.isArchived,
|
required this.isArchived,
|
||||||
required this.isTrashed,
|
required this.isTrashed,
|
||||||
|
this.stackParentId,
|
||||||
|
required this.stackCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
|
@ -146,6 +151,10 @@ class Asset {
|
||||||
@ignore
|
@ignore
|
||||||
ExifInfo? exifInfo;
|
ExifInfo? exifInfo;
|
||||||
|
|
||||||
|
String? stackParentId;
|
||||||
|
|
||||||
|
int stackCount;
|
||||||
|
|
||||||
/// `true` if this [Asset] is present on the device
|
/// `true` if this [Asset] is present on the device
|
||||||
@ignore
|
@ignore
|
||||||
bool get isLocal => localId != null;
|
bool get isLocal => localId != null;
|
||||||
|
@ -200,7 +209,9 @@ class Asset {
|
||||||
isFavorite == other.isFavorite &&
|
isFavorite == other.isFavorite &&
|
||||||
isLocal == other.isLocal &&
|
isLocal == other.isLocal &&
|
||||||
isArchived == other.isArchived &&
|
isArchived == other.isArchived &&
|
||||||
isTrashed == other.isTrashed;
|
isTrashed == other.isTrashed &&
|
||||||
|
stackCount == other.stackCount &&
|
||||||
|
stackParentId == other.stackParentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -223,7 +234,9 @@ class Asset {
|
||||||
isFavorite.hashCode ^
|
isFavorite.hashCode ^
|
||||||
isLocal.hashCode ^
|
isLocal.hashCode ^
|
||||||
isArchived.hashCode ^
|
isArchived.hashCode ^
|
||||||
isTrashed.hashCode;
|
isTrashed.hashCode ^
|
||||||
|
stackCount.hashCode ^
|
||||||
|
stackParentId.hashCode;
|
||||||
|
|
||||||
/// Returns `true` if this [Asset] can updated with values from parameter [a]
|
/// Returns `true` if this [Asset] can updated with values from parameter [a]
|
||||||
bool canUpdate(Asset a) {
|
bool canUpdate(Asset a) {
|
||||||
|
@ -236,9 +249,11 @@ class Asset {
|
||||||
width == null && a.width != null ||
|
width == null && a.width != null ||
|
||||||
height == null && a.height != null ||
|
height == null && a.height != null ||
|
||||||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
|
livePhotoVideoId == null && a.livePhotoVideoId != null ||
|
||||||
|
stackParentId == null && a.stackParentId != null ||
|
||||||
isFavorite != a.isFavorite ||
|
isFavorite != a.isFavorite ||
|
||||||
isArchived != a.isArchived ||
|
isArchived != a.isArchived ||
|
||||||
isTrashed != a.isTrashed;
|
isTrashed != a.isTrashed ||
|
||||||
|
stackCount != a.stackCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new [Asset] with values from this and merged & updated with [a]
|
/// Returns a new [Asset] with values from this and merged & updated with [a]
|
||||||
|
@ -267,6 +282,8 @@ class Asset {
|
||||||
id: id,
|
id: id,
|
||||||
remoteId: remoteId,
|
remoteId: remoteId,
|
||||||
livePhotoVideoId: livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
|
stackParentId: stackParentId,
|
||||||
|
stackCount: stackCount,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
isArchived: isArchived,
|
isArchived: isArchived,
|
||||||
isTrashed: isTrashed,
|
isTrashed: isTrashed,
|
||||||
|
@ -281,6 +298,8 @@ class Asset {
|
||||||
width: a.width,
|
width: a.width,
|
||||||
height: a.height,
|
height: a.height,
|
||||||
livePhotoVideoId: a.livePhotoVideoId,
|
livePhotoVideoId: a.livePhotoVideoId,
|
||||||
|
stackParentId: a.stackParentId,
|
||||||
|
stackCount: a.stackCount,
|
||||||
// isFavorite + isArchived are not set by device-only assets
|
// isFavorite + isArchived are not set by device-only assets
|
||||||
isFavorite: a.isFavorite,
|
isFavorite: a.isFavorite,
|
||||||
isArchived: a.isArchived,
|
isArchived: a.isArchived,
|
||||||
|
@ -318,6 +337,8 @@ class Asset {
|
||||||
bool? isArchived,
|
bool? isArchived,
|
||||||
bool? isTrashed,
|
bool? isTrashed,
|
||||||
ExifInfo? exifInfo,
|
ExifInfo? exifInfo,
|
||||||
|
String? stackParentId,
|
||||||
|
int? stackCount,
|
||||||
}) =>
|
}) =>
|
||||||
Asset(
|
Asset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
@ -338,6 +359,8 @@ class Asset {
|
||||||
isArchived: isArchived ?? this.isArchived,
|
isArchived: isArchived ?? this.isArchived,
|
||||||
isTrashed: isTrashed ?? this.isTrashed,
|
isTrashed: isTrashed ?? this.isTrashed,
|
||||||
exifInfo: exifInfo ?? this.exifInfo,
|
exifInfo: exifInfo ?? this.exifInfo,
|
||||||
|
stackParentId: stackParentId ?? this.stackParentId,
|
||||||
|
stackCount: stackCount ?? this.stackCount,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> put(Isar db) async {
|
Future<void> put(Isar db) async {
|
||||||
|
@ -379,6 +402,8 @@ class Asset {
|
||||||
"checksum": "$checksum",
|
"checksum": "$checksum",
|
||||||
"ownerId": $ownerId,
|
"ownerId": $ownerId,
|
||||||
"livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
|
"livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
|
||||||
|
"stackCount": "$stackCount",
|
||||||
|
"stackParentId": "${stackParentId ?? "N/A"}",
|
||||||
"fileCreatedAt": "$fileCreatedAt",
|
"fileCreatedAt": "$fileCreatedAt",
|
||||||
"fileModifiedAt": "$fileModifiedAt",
|
"fileModifiedAt": "$fileModifiedAt",
|
||||||
"updatedAt": "$updatedAt",
|
"updatedAt": "$updatedAt",
|
||||||
|
|
|
@ -82,19 +82,29 @@ const AssetSchema = CollectionSchema(
|
||||||
name: r'remoteId',
|
name: r'remoteId',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'type': PropertySchema(
|
r'stackCount': PropertySchema(
|
||||||
id: 13,
|
id: 13,
|
||||||
|
name: r'stackCount',
|
||||||
|
type: IsarType.long,
|
||||||
|
),
|
||||||
|
r'stackParentId': PropertySchema(
|
||||||
|
id: 14,
|
||||||
|
name: r'stackParentId',
|
||||||
|
type: IsarType.string,
|
||||||
|
),
|
||||||
|
r'type': PropertySchema(
|
||||||
|
id: 15,
|
||||||
name: r'type',
|
name: r'type',
|
||||||
type: IsarType.byte,
|
type: IsarType.byte,
|
||||||
enumMap: _AssettypeEnumValueMap,
|
enumMap: _AssettypeEnumValueMap,
|
||||||
),
|
),
|
||||||
r'updatedAt': PropertySchema(
|
r'updatedAt': PropertySchema(
|
||||||
id: 14,
|
id: 16,
|
||||||
name: r'updatedAt',
|
name: r'updatedAt',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
),
|
),
|
||||||
r'width': PropertySchema(
|
r'width': PropertySchema(
|
||||||
id: 15,
|
id: 17,
|
||||||
name: r'width',
|
name: r'width',
|
||||||
type: IsarType.int,
|
type: IsarType.int,
|
||||||
)
|
)
|
||||||
|
@ -184,6 +194,12 @@ int _assetEstimateSize(
|
||||||
bytesCount += 3 + value.length * 3;
|
bytesCount += 3 + value.length * 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
final value = object.stackParentId;
|
||||||
|
if (value != null) {
|
||||||
|
bytesCount += 3 + value.length * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
return bytesCount;
|
return bytesCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,9 +222,11 @@ void _assetSerialize(
|
||||||
writer.writeString(offsets[10], object.localId);
|
writer.writeString(offsets[10], object.localId);
|
||||||
writer.writeLong(offsets[11], object.ownerId);
|
writer.writeLong(offsets[11], object.ownerId);
|
||||||
writer.writeString(offsets[12], object.remoteId);
|
writer.writeString(offsets[12], object.remoteId);
|
||||||
writer.writeByte(offsets[13], object.type.index);
|
writer.writeLong(offsets[13], object.stackCount);
|
||||||
writer.writeDateTime(offsets[14], object.updatedAt);
|
writer.writeString(offsets[14], object.stackParentId);
|
||||||
writer.writeInt(offsets[15], object.width);
|
writer.writeByte(offsets[15], object.type.index);
|
||||||
|
writer.writeDateTime(offsets[16], object.updatedAt);
|
||||||
|
writer.writeInt(offsets[17], object.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
Asset _assetDeserialize(
|
Asset _assetDeserialize(
|
||||||
|
@ -232,10 +250,12 @@ Asset _assetDeserialize(
|
||||||
localId: reader.readStringOrNull(offsets[10]),
|
localId: reader.readStringOrNull(offsets[10]),
|
||||||
ownerId: reader.readLong(offsets[11]),
|
ownerId: reader.readLong(offsets[11]),
|
||||||
remoteId: reader.readStringOrNull(offsets[12]),
|
remoteId: reader.readStringOrNull(offsets[12]),
|
||||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[13])] ??
|
stackCount: reader.readLong(offsets[13]),
|
||||||
|
stackParentId: reader.readStringOrNull(offsets[14]),
|
||||||
|
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[15])] ??
|
||||||
AssetType.other,
|
AssetType.other,
|
||||||
updatedAt: reader.readDateTime(offsets[14]),
|
updatedAt: reader.readDateTime(offsets[16]),
|
||||||
width: reader.readIntOrNull(offsets[15]),
|
width: reader.readIntOrNull(offsets[17]),
|
||||||
);
|
);
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
@ -274,11 +294,15 @@ P _assetDeserializeProp<P>(
|
||||||
case 12:
|
case 12:
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
case 13:
|
case 13:
|
||||||
|
return (reader.readLong(offset)) as P;
|
||||||
|
case 14:
|
||||||
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
|
case 15:
|
||||||
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||||
AssetType.other) as P;
|
AssetType.other) as P;
|
||||||
case 14:
|
case 16:
|
||||||
return (reader.readDateTime(offset)) as P;
|
return (reader.readDateTime(offset)) as P;
|
||||||
case 15:
|
case 17:
|
||||||
return (reader.readIntOrNull(offset)) as P;
|
return (reader.readIntOrNull(offset)) as P;
|
||||||
default:
|
default:
|
||||||
throw IsarError('Unknown property with id $propertyId');
|
throw IsarError('Unknown property with id $propertyId');
|
||||||
|
@ -1801,6 +1825,205 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackCountEqualTo(
|
||||||
|
int value) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'stackCount',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackCountGreaterThan(
|
||||||
|
int value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
include: include,
|
||||||
|
property: r'stackCount',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackCountLessThan(
|
||||||
|
int value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.lessThan(
|
||||||
|
include: include,
|
||||||
|
property: r'stackCount',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackCountBetween(
|
||||||
|
int lower,
|
||||||
|
int upper, {
|
||||||
|
bool includeLower = true,
|
||||||
|
bool includeUpper = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.between(
|
||||||
|
property: r'stackCount',
|
||||||
|
lower: lower,
|
||||||
|
includeLower: includeLower,
|
||||||
|
upper: upper,
|
||||||
|
includeUpper: includeUpper,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdIsNull() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(const FilterCondition.isNull(
|
||||||
|
property: r'stackParentId',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdIsNotNull() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||||
|
property: r'stackParentId',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdEqualTo(
|
||||||
|
String? value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'stackParentId',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdGreaterThan(
|
||||||
|
String? value, {
|
||||||
|
bool include = false,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
include: include,
|
||||||
|
property: r'stackParentId',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdLessThan(
|
||||||
|
String? value, {
|
||||||
|
bool include = false,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.lessThan(
|
||||||
|
include: include,
|
||||||
|
property: r'stackParentId',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdBetween(
|
||||||
|
String? lower,
|
||||||
|
String? upper, {
|
||||||
|
bool includeLower = true,
|
||||||
|
bool includeUpper = true,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.between(
|
||||||
|
property: r'stackParentId',
|
||||||
|
lower: lower,
|
||||||
|
includeLower: includeLower,
|
||||||
|
upper: upper,
|
||||||
|
includeUpper: includeUpper,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdStartsWith(
|
||||||
|
String value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.startsWith(
|
||||||
|
property: r'stackParentId',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdEndsWith(
|
||||||
|
String value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.endsWith(
|
||||||
|
property: r'stackParentId',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdContains(
|
||||||
|
String value,
|
||||||
|
{bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.contains(
|
||||||
|
property: r'stackParentId',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdMatches(
|
||||||
|
String pattern,
|
||||||
|
{bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.matches(
|
||||||
|
property: r'stackParentId',
|
||||||
|
wildcard: pattern,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdIsEmpty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'stackParentId',
|
||||||
|
value: '',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdIsNotEmpty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
property: r'stackParentId',
|
||||||
|
value: '',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> typeEqualTo(
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> typeEqualTo(
|
||||||
AssetType value) {
|
AssetType value) {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
@ -2137,6 +2360,30 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByStackCount() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'stackCount', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByStackCountDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'stackCount', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByStackParentId() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'stackParentId', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByStackParentIdDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'stackParentId', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByType() {
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByType() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'type', Sort.asc);
|
return query.addSortBy(r'type', Sort.asc);
|
||||||
|
@ -2343,6 +2590,30 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByStackCount() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'stackCount', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByStackCountDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'stackCount', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByStackParentId() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'stackParentId', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByStackParentIdDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'stackParentId', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByType() {
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByType() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'type', Sort.asc);
|
return query.addSortBy(r'type', Sort.asc);
|
||||||
|
@ -2465,6 +2736,20 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QDistinct> distinctByStackCount() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addDistinctBy(r'stackCount');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QDistinct> distinctByStackParentId(
|
||||||
|
{bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addDistinctBy(r'stackParentId',
|
||||||
|
caseSensitive: caseSensitive);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QDistinct> distinctByType() {
|
QueryBuilder<Asset, Asset, QDistinct> distinctByType() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addDistinctBy(r'type');
|
return query.addDistinctBy(r'type');
|
||||||
|
@ -2569,6 +2854,18 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, int, QQueryOperations> stackCountProperty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addPropertyName(r'stackCount');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, String?, QQueryOperations> stackParentIdProperty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addPropertyName(r'stackParentId');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, AssetType, QQueryOperations> typeProperty() {
|
QueryBuilder<Asset, AssetType, QQueryOperations> typeProperty() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addPropertyName(r'type');
|
return query.addPropertyName(r'type');
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/asset.service.dart';
|
import 'package:immich_mobile/shared/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
@ -254,6 +255,7 @@ final assetsProvider =
|
||||||
.filter()
|
.filter()
|
||||||
.isArchivedEqualTo(false)
|
.isArchivedEqualTo(false)
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
|
.stackParentIdIsNull()
|
||||||
.sortByFileCreatedAtDesc();
|
.sortByFileCreatedAtDesc();
|
||||||
final settings = ref.watch(appSettingsServiceProvider);
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
final groupBy =
|
final groupBy =
|
||||||
|
@ -264,10 +266,12 @@ final assetsProvider =
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final remoteAssetsProvider =
|
QueryBuilder<Asset, Asset, QAfterSortBy>? getRemoteAssetQuery(WidgetRef ref) {
|
||||||
StreamProvider.family<RenderList, int?>((ref, userId) async* {
|
final userId = ref.watch(currentUserProvider)?.isarId;
|
||||||
if (userId == null) return;
|
if (userId == null) {
|
||||||
final query = ref
|
return null;
|
||||||
|
}
|
||||||
|
return ref
|
||||||
.watch(dbProvider)
|
.watch(dbProvider)
|
||||||
.assets
|
.assets
|
||||||
.where()
|
.where()
|
||||||
|
@ -275,12 +279,34 @@ final remoteAssetsProvider =
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(userId)
|
.ownerIdEqualTo(userId)
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
|
.stackParentIdIsNull()
|
||||||
.sortByFileCreatedAtDesc();
|
.sortByFileCreatedAtDesc();
|
||||||
final settings = ref.watch(appSettingsServiceProvider);
|
}
|
||||||
final groupBy =
|
|
||||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
QueryBuilder<Asset, Asset, QAfterSortBy>? getAssetStackSelectionQuery(
|
||||||
yield await RenderList.fromQuery(query, groupBy);
|
WidgetRef ref,
|
||||||
await for (final _ in query.watchLazy()) {
|
Asset parentAsset,
|
||||||
yield await RenderList.fromQuery(query, groupBy);
|
) {
|
||||||
|
final userId = ref.watch(currentUserProvider)?.isarId;
|
||||||
|
if (userId == null || !parentAsset.isRemote) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
});
|
return ref
|
||||||
|
.watch(dbProvider)
|
||||||
|
.assets
|
||||||
|
.where()
|
||||||
|
.remoteIdIsNotNull()
|
||||||
|
.filter()
|
||||||
|
.isArchivedEqualTo(false)
|
||||||
|
.ownerIdEqualTo(userId)
|
||||||
|
.not()
|
||||||
|
.remoteIdEqualTo(parentAsset.remoteId)
|
||||||
|
// Show existing stack children in selection page
|
||||||
|
.group(
|
||||||
|
(q) => q
|
||||||
|
.stackParentIdIsNull()
|
||||||
|
.or()
|
||||||
|
.stackParentIdEqualTo(parentAsset.remoteId),
|
||||||
|
)
|
||||||
|
.sortByFileCreatedAtDesc();
|
||||||
|
}
|
||||||
|
|
|
@ -133,6 +133,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
socket.on('on_asset_delete', _handleOnAssetDelete);
|
socket.on('on_asset_delete', _handleOnAssetDelete);
|
||||||
socket.on('on_asset_trash', _handleServerUpdates);
|
socket.on('on_asset_trash', _handleServerUpdates);
|
||||||
socket.on('on_asset_restore', _handleServerUpdates);
|
socket.on('on_asset_restore', _handleServerUpdates);
|
||||||
|
socket.on('on_asset_update', _handleServerUpdates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ class ApiService {
|
||||||
late PartnerApi partnerApi;
|
late PartnerApi partnerApi;
|
||||||
late PersonApi personApi;
|
late PersonApi personApi;
|
||||||
late AuditApi auditApi;
|
late AuditApi auditApi;
|
||||||
|
late SharedLinkApi sharedLinkApi;
|
||||||
|
|
||||||
ApiService() {
|
ApiService() {
|
||||||
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
|
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
|
||||||
|
@ -45,6 +46,7 @@ class ApiService {
|
||||||
partnerApi = PartnerApi(_apiClient);
|
partnerApi = PartnerApi(_apiClient);
|
||||||
personApi = PersonApi(_apiClient);
|
personApi = PersonApi(_apiClient);
|
||||||
auditApi = AuditApi(_apiClient);
|
auditApi = AuditApi(_apiClient);
|
||||||
|
sharedLinkApi = SharedLinkApi(_apiClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'api.service.dart';
|
import 'api.service.dart';
|
||||||
|
@ -13,32 +14,60 @@ final shareServiceProvider =
|
||||||
|
|
||||||
class ShareService {
|
class ShareService {
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
|
final Logger _log = Logger("ShareService");
|
||||||
|
|
||||||
ShareService(this._apiService);
|
ShareService(this._apiService);
|
||||||
|
|
||||||
Future<void> shareAsset(Asset asset) async {
|
Future<bool> shareAsset(Asset asset) async {
|
||||||
await shareAssets([asset]);
|
return await shareAssets([asset]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> shareAssets(List<Asset> assets) async {
|
Future<bool> shareAssets(List<Asset> assets) async {
|
||||||
final downloadedXFiles = assets.map<Future<XFile>>((asset) async {
|
try {
|
||||||
if (asset.isRemote) {
|
final downloadedXFiles = <XFile>[];
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
final fileName = asset.fileName;
|
|
||||||
final tempFile = await File('${tempDir.path}/$fileName').create();
|
|
||||||
final res = await _apiService.assetApi
|
|
||||||
.downloadFileWithHttpInfo(asset.remoteId!);
|
|
||||||
tempFile.writeAsBytesSync(res.bodyBytes);
|
|
||||||
return XFile(tempFile.path);
|
|
||||||
} else {
|
|
||||||
File? f = await asset.local!.file;
|
|
||||||
return XFile(f!.path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Share.shareXFiles(
|
for (var asset in assets) {
|
||||||
await Future.wait(downloadedXFiles),
|
if (asset.isRemote) {
|
||||||
sharePositionOrigin: Rect.zero,
|
final tempDir = await getTemporaryDirectory();
|
||||||
);
|
final fileName = asset.fileName;
|
||||||
|
final tempFile = await File('${tempDir.path}/$fileName').create();
|
||||||
|
final res = await _apiService.assetApi
|
||||||
|
.downloadFileWithHttpInfo(asset.remoteId!);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
_log.severe(
|
||||||
|
"Asset download failed with status - ${res.statusCode} and response - ${res.body}",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||||
|
downloadedXFiles.add(XFile(tempFile.path));
|
||||||
|
} else {
|
||||||
|
File? f = await asset.local!.file;
|
||||||
|
downloadedXFiles.add(XFile(f!.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadedXFiles.isEmpty) {
|
||||||
|
_log.warning("No asset can be retrieved for share");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadedXFiles.length != assets.length) {
|
||||||
|
_log.warning(
|
||||||
|
"Partial share - Requested: ${assets.length}, Sharing: ${downloadedXFiles.length}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Share.shareXFiles(
|
||||||
|
downloadedXFiles,
|
||||||
|
sharePositionOrigin: Rect.zero,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
_log.severe("Share failed with error $error");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,12 @@ ThemeData immichLightTheme = ThemeData(
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
scaffoldBackgroundColor: immichBackgroundColor,
|
scaffoldBackgroundColor: immichBackgroundColor,
|
||||||
snackBarTheme: const SnackBarThemeData(
|
snackBarTheme: const SnackBarThemeData(
|
||||||
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
|
contentTextStyle: TextStyle(
|
||||||
|
fontFamily: 'WorkSans',
|
||||||
|
color: Colors.indigo,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
titleTextStyle: const TextStyle(
|
titleTextStyle: const TextStyle(
|
||||||
|
@ -156,8 +161,13 @@ ThemeData immichDarkTheme = ThemeData(
|
||||||
scaffoldBackgroundColor: immichDarkBackgroundColor,
|
scaffoldBackgroundColor: immichDarkBackgroundColor,
|
||||||
hintColor: Colors.grey[600],
|
hintColor: Colors.grey[600],
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
snackBarTheme: const SnackBarThemeData(
|
snackBarTheme: SnackBarThemeData(
|
||||||
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
|
contentTextStyle: TextStyle(
|
||||||
|
fontFamily: 'WorkSans',
|
||||||
|
color: immichDarkThemePrimaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.grey[900],
|
||||||
),
|
),
|
||||||
textButtonTheme: TextButtonThemeData(
|
textButtonTheme: TextButtonThemeData(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
@ -15,10 +16,19 @@ void handleShareAssets(
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext buildContext) {
|
builder: (BuildContext buildContext) {
|
||||||
ref
|
ref.watch(shareServiceProvider).shareAssets(selection.toList()).then(
|
||||||
.watch(shareServiceProvider)
|
(bool status) {
|
||||||
.shareAssets(selection.toList())
|
if (!status) {
|
||||||
.then((_) => Navigator.of(buildContext).pop());
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'image_viewer_page_state_provider_share_error'.tr(),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Navigator.of(buildContext).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
return const ShareDialog();
|
return const ShareDialog();
|
||||||
},
|
},
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
|
||||||
String sanitizeUrl(String url) {
|
String sanitizeUrl(String url) {
|
||||||
// Add schema if none is set
|
// Add schema if none is set
|
||||||
final urlWithSchema =
|
final urlWithSchema =
|
||||||
|
@ -6,3 +8,15 @@ String sanitizeUrl(String url) {
|
||||||
// Remove trailing slash(es)
|
// Remove trailing slash(es)
|
||||||
return urlWithSchema.replaceFirst(RegExp(r"/+$"), "");
|
return urlWithSchema.replaceFirst(RegExp(r"/+$"), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? getServerUrl() {
|
||||||
|
final serverUrl = Store.tryGet(StoreKey.serverEndpoint);
|
||||||
|
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
|
||||||
|
if (serverUri == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverUri.hasPort
|
||||||
|
? "${serverUri.scheme}://${serverUri.host}:${serverUri.port}"
|
||||||
|
: "${serverUri.scheme}://${serverUri.host}";
|
||||||
|
}
|
||||||
|
|
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
|
@ -149,6 +149,7 @@ doc/TranscodePolicy.md
|
||||||
doc/UpdateAlbumDto.md
|
doc/UpdateAlbumDto.md
|
||||||
doc/UpdateAssetDto.md
|
doc/UpdateAssetDto.md
|
||||||
doc/UpdateLibraryDto.md
|
doc/UpdateLibraryDto.md
|
||||||
|
doc/UpdateStackParentDto.md
|
||||||
doc/UpdateTagDto.md
|
doc/UpdateTagDto.md
|
||||||
doc/UpdateUserDto.md
|
doc/UpdateUserDto.md
|
||||||
doc/UsageByUserDto.md
|
doc/UsageByUserDto.md
|
||||||
|
@ -314,6 +315,7 @@ lib/model/transcode_policy.dart
|
||||||
lib/model/update_album_dto.dart
|
lib/model/update_album_dto.dart
|
||||||
lib/model/update_asset_dto.dart
|
lib/model/update_asset_dto.dart
|
||||||
lib/model/update_library_dto.dart
|
lib/model/update_library_dto.dart
|
||||||
|
lib/model/update_stack_parent_dto.dart
|
||||||
lib/model/update_tag_dto.dart
|
lib/model/update_tag_dto.dart
|
||||||
lib/model/update_user_dto.dart
|
lib/model/update_user_dto.dart
|
||||||
lib/model/usage_by_user_dto.dart
|
lib/model/usage_by_user_dto.dart
|
||||||
|
@ -468,6 +470,7 @@ test/transcode_policy_test.dart
|
||||||
test/update_album_dto_test.dart
|
test/update_album_dto_test.dart
|
||||||
test/update_asset_dto_test.dart
|
test/update_asset_dto_test.dart
|
||||||
test/update_library_dto_test.dart
|
test/update_library_dto_test.dart
|
||||||
|
test/update_stack_parent_dto_test.dart
|
||||||
test/update_tag_dto_test.dart
|
test/update_tag_dto_test.dart
|
||||||
test/update_user_dto_test.dart
|
test/update_user_dto_test.dart
|
||||||
test/usage_by_user_dto_test.dart
|
test/usage_by_user_dto_test.dart
|
||||||
|
|
4
mobile/openapi/README.md
generated
4
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:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.82.0
|
- API version: 1.82.1
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
@ -116,6 +116,7 @@ Class | Method | HTTP request | Description
|
||||||
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
||||||
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
||||||
*AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset |
|
*AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset |
|
||||||
|
*AssetApi* | [**updateStackParent**](doc//AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent |
|
||||||
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
|
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
|
||||||
*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
|
*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
|
||||||
*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
|
*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
|
||||||
|
@ -330,6 +331,7 @@ Class | Method | HTTP request | Description
|
||||||
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
||||||
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||||
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
|
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
|
||||||
|
- [UpdateStackParentDto](doc//UpdateStackParentDto.md)
|
||||||
- [UpdateTagDto](doc//UpdateTagDto.md)
|
- [UpdateTagDto](doc//UpdateTagDto.md)
|
||||||
- [UpdateUserDto](doc//UpdateUserDto.md)
|
- [UpdateUserDto](doc//UpdateUserDto.md)
|
||||||
- [UsageByUserDto](doc//UsageByUserDto.md)
|
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||||
|
|
55
mobile/openapi/doc/AssetApi.md
generated
55
mobile/openapi/doc/AssetApi.md
generated
|
@ -38,6 +38,7 @@ Method | HTTP request | Description
|
||||||
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
||||||
[**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
[**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} |
|
||||||
[**updateAssets**](AssetApi.md#updateassets) | **PUT** /asset |
|
[**updateAssets**](AssetApi.md#updateassets) | **PUT** /asset |
|
||||||
|
[**updateStackParent**](AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent |
|
||||||
[**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |
|
[**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |
|
||||||
|
|
||||||
|
|
||||||
|
@ -1696,6 +1697,60 @@ 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)
|
[[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)
|
||||||
|
|
||||||
|
# **updateStackParent**
|
||||||
|
> updateStackParent(updateStackParentDto)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 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 updateStackParentDto = UpdateStackParentDto(); // UpdateStackParentDto |
|
||||||
|
|
||||||
|
try {
|
||||||
|
api_instance.updateStackParent(updateStackParentDto);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AssetApi->updateStackParent: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**updateStackParentDto** | [**UpdateStackParentDto**](UpdateStackParentDto.md)| |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
void (empty response body)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
- **Accept**: Not defined
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **uploadFile**
|
# **uploadFile**
|
||||||
> AssetFileUploadResponseDto uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, duration, isArchived, isExternal, isOffline, isReadOnly, isVisible, libraryId, livePhotoData, sidecarData)
|
> AssetFileUploadResponseDto uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, duration, isArchived, isExternal, isOffline, isReadOnly, isVisible, libraryId, livePhotoData, sidecarData)
|
||||||
|
|
||||||
|
|
2
mobile/openapi/doc/AssetBulkUpdateDto.md
generated
2
mobile/openapi/doc/AssetBulkUpdateDto.md
generated
|
@ -11,6 +11,8 @@ Name | Type | Description | Notes
|
||||||
**ids** | **List<String>** | | [default to const []]
|
**ids** | **List<String>** | | [default to const []]
|
||||||
**isArchived** | **bool** | | [optional]
|
**isArchived** | **bool** | | [optional]
|
||||||
**isFavorite** | **bool** | | [optional]
|
**isFavorite** | **bool** | | [optional]
|
||||||
|
**removeParent** | **bool** | | [optional]
|
||||||
|
**stackParentId** | **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)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
3
mobile/openapi/doc/AssetResponseDto.md
generated
3
mobile/openapi/doc/AssetResponseDto.md
generated
|
@ -33,6 +33,9 @@ Name | Type | Description | Notes
|
||||||
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
|
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
|
||||||
**resized** | **bool** | |
|
**resized** | **bool** | |
|
||||||
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
|
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
|
||||||
|
**stack** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [optional] [default to const []]
|
||||||
|
**stackCount** | **int** | |
|
||||||
|
**stackParentId** | **String** | | [optional]
|
||||||
**tags** | [**List<TagResponseDto>**](TagResponseDto.md) | | [optional] [default to const []]
|
**tags** | [**List<TagResponseDto>**](TagResponseDto.md) | | [optional] [default to const []]
|
||||||
**thumbhash** | **String** | |
|
**thumbhash** | **String** | |
|
||||||
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | |
|
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | |
|
||||||
|
|
1
mobile/openapi/doc/SharedLinkEditDto.md
generated
1
mobile/openapi/doc/SharedLinkEditDto.md
generated
|
@ -10,6 +10,7 @@ Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**allowDownload** | **bool** | | [optional]
|
**allowDownload** | **bool** | | [optional]
|
||||||
**allowUpload** | **bool** | | [optional]
|
**allowUpload** | **bool** | | [optional]
|
||||||
|
**changeExpiryTime** | **bool** | Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this. | [optional]
|
||||||
**description** | **String** | | [optional]
|
**description** | **String** | | [optional]
|
||||||
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
|
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
|
||||||
**showMetadata** | **bool** | | [optional]
|
**showMetadata** | **bool** | | [optional]
|
||||||
|
|
16
mobile/openapi/doc/UpdateStackParentDto.md
generated
Normal file
16
mobile/openapi/doc/UpdateStackParentDto.md
generated
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# openapi.model.UpdateStackParentDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**newParentId** | **String** | |
|
||||||
|
**oldParentId** | **String** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
|
@ -176,6 +176,7 @@ part 'model/transcode_policy.dart';
|
||||||
part 'model/update_album_dto.dart';
|
part 'model/update_album_dto.dart';
|
||||||
part 'model/update_asset_dto.dart';
|
part 'model/update_asset_dto.dart';
|
||||||
part 'model/update_library_dto.dart';
|
part 'model/update_library_dto.dart';
|
||||||
|
part 'model/update_stack_parent_dto.dart';
|
||||||
part 'model/update_tag_dto.dart';
|
part 'model/update_tag_dto.dart';
|
||||||
part 'model/update_user_dto.dart';
|
part 'model/update_user_dto.dart';
|
||||||
part 'model/usage_by_user_dto.dart';
|
part 'model/usage_by_user_dto.dart';
|
||||||
|
|
39
mobile/openapi/lib/api/asset_api.dart
generated
39
mobile/openapi/lib/api/asset_api.dart
generated
|
@ -1654,6 +1654,45 @@ class AssetApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'PUT /asset/stack/parent' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [UpdateStackParentDto] updateStackParentDto (required):
|
||||||
|
Future<Response> updateStackParentWithHttpInfo(UpdateStackParentDto updateStackParentDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/asset/stack/parent';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = updateStackParentDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'PUT',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [UpdateStackParentDto] updateStackParentDto (required):
|
||||||
|
Future<void> updateStackParent(UpdateStackParentDto updateStackParentDto,) async {
|
||||||
|
final response = await updateStackParentWithHttpInfo(updateStackParentDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /asset/upload' operation and returns the [Response].
|
/// Performs an HTTP 'POST /asset/upload' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
|
@ -443,6 +443,8 @@ class ApiClient {
|
||||||
return UpdateAssetDto.fromJson(value);
|
return UpdateAssetDto.fromJson(value);
|
||||||
case 'UpdateLibraryDto':
|
case 'UpdateLibraryDto':
|
||||||
return UpdateLibraryDto.fromJson(value);
|
return UpdateLibraryDto.fromJson(value);
|
||||||
|
case 'UpdateStackParentDto':
|
||||||
|
return UpdateStackParentDto.fromJson(value);
|
||||||
case 'UpdateTagDto':
|
case 'UpdateTagDto':
|
||||||
return UpdateTagDto.fromJson(value);
|
return UpdateTagDto.fromJson(value);
|
||||||
case 'UpdateUserDto':
|
case 'UpdateUserDto':
|
||||||
|
|
40
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
40
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
|
@ -16,6 +16,8 @@ class AssetBulkUpdateDto {
|
||||||
this.ids = const [],
|
this.ids = const [],
|
||||||
this.isArchived,
|
this.isArchived,
|
||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
|
this.removeParent,
|
||||||
|
this.stackParentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
List<String> ids;
|
List<String> ids;
|
||||||
|
@ -36,21 +38,41 @@ class AssetBulkUpdateDto {
|
||||||
///
|
///
|
||||||
bool? isFavorite;
|
bool? isFavorite;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
bool? removeParent;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? stackParentId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
|
||||||
other.ids == ids &&
|
other.ids == ids &&
|
||||||
other.isArchived == isArchived &&
|
other.isArchived == isArchived &&
|
||||||
other.isFavorite == isFavorite;
|
other.isFavorite == isFavorite &&
|
||||||
|
other.removeParent == removeParent &&
|
||||||
|
other.stackParentId == stackParentId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(ids.hashCode) +
|
(ids.hashCode) +
|
||||||
(isArchived == null ? 0 : isArchived!.hashCode) +
|
(isArchived == null ? 0 : isArchived!.hashCode) +
|
||||||
(isFavorite == null ? 0 : isFavorite!.hashCode);
|
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||||
|
(removeParent == null ? 0 : removeParent!.hashCode) +
|
||||||
|
(stackParentId == null ? 0 : stackParentId!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite]';
|
String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, removeParent=$removeParent, stackParentId=$stackParentId]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -65,6 +87,16 @@ class AssetBulkUpdateDto {
|
||||||
} else {
|
} else {
|
||||||
// json[r'isFavorite'] = null;
|
// json[r'isFavorite'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.removeParent != null) {
|
||||||
|
json[r'removeParent'] = this.removeParent;
|
||||||
|
} else {
|
||||||
|
// json[r'removeParent'] = null;
|
||||||
|
}
|
||||||
|
if (this.stackParentId != null) {
|
||||||
|
json[r'stackParentId'] = this.stackParentId;
|
||||||
|
} else {
|
||||||
|
// json[r'stackParentId'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +113,8 @@ class AssetBulkUpdateDto {
|
||||||
: const [],
|
: const [],
|
||||||
isArchived: mapValueOfType<bool>(json, r'isArchived'),
|
isArchived: mapValueOfType<bool>(json, r'isArchived'),
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||||
|
removeParent: mapValueOfType<bool>(json, r'removeParent'),
|
||||||
|
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
28
mobile/openapi/lib/model/asset_response_dto.dart
generated
28
mobile/openapi/lib/model/asset_response_dto.dart
generated
|
@ -38,6 +38,9 @@ class AssetResponseDto {
|
||||||
this.people = const [],
|
this.people = const [],
|
||||||
required this.resized,
|
required this.resized,
|
||||||
this.smartInfo,
|
this.smartInfo,
|
||||||
|
this.stack = const [],
|
||||||
|
required this.stackCount,
|
||||||
|
this.stackParentId,
|
||||||
this.tags = const [],
|
this.tags = const [],
|
||||||
required this.thumbhash,
|
required this.thumbhash,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
@ -113,6 +116,12 @@ class AssetResponseDto {
|
||||||
///
|
///
|
||||||
SmartInfoResponseDto? smartInfo;
|
SmartInfoResponseDto? smartInfo;
|
||||||
|
|
||||||
|
List<AssetResponseDto> stack;
|
||||||
|
|
||||||
|
int stackCount;
|
||||||
|
|
||||||
|
String? stackParentId;
|
||||||
|
|
||||||
List<TagResponseDto> tags;
|
List<TagResponseDto> tags;
|
||||||
|
|
||||||
String? thumbhash;
|
String? thumbhash;
|
||||||
|
@ -148,6 +157,9 @@ class AssetResponseDto {
|
||||||
other.people == people &&
|
other.people == people &&
|
||||||
other.resized == resized &&
|
other.resized == resized &&
|
||||||
other.smartInfo == smartInfo &&
|
other.smartInfo == smartInfo &&
|
||||||
|
other.stack == stack &&
|
||||||
|
other.stackCount == stackCount &&
|
||||||
|
other.stackParentId == stackParentId &&
|
||||||
other.tags == tags &&
|
other.tags == tags &&
|
||||||
other.thumbhash == thumbhash &&
|
other.thumbhash == thumbhash &&
|
||||||
other.type == type &&
|
other.type == type &&
|
||||||
|
@ -181,13 +193,16 @@ class AssetResponseDto {
|
||||||
(people.hashCode) +
|
(people.hashCode) +
|
||||||
(resized.hashCode) +
|
(resized.hashCode) +
|
||||||
(smartInfo == null ? 0 : smartInfo!.hashCode) +
|
(smartInfo == null ? 0 : smartInfo!.hashCode) +
|
||||||
|
(stack.hashCode) +
|
||||||
|
(stackCount.hashCode) +
|
||||||
|
(stackParentId == null ? 0 : stackParentId!.hashCode) +
|
||||||
(tags.hashCode) +
|
(tags.hashCode) +
|
||||||
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||||
(type.hashCode) +
|
(type.hashCode) +
|
||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
|
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -231,6 +246,13 @@ class AssetResponseDto {
|
||||||
json[r'smartInfo'] = this.smartInfo;
|
json[r'smartInfo'] = this.smartInfo;
|
||||||
} else {
|
} else {
|
||||||
// json[r'smartInfo'] = null;
|
// json[r'smartInfo'] = null;
|
||||||
|
}
|
||||||
|
json[r'stack'] = this.stack;
|
||||||
|
json[r'stackCount'] = this.stackCount;
|
||||||
|
if (this.stackParentId != null) {
|
||||||
|
json[r'stackParentId'] = this.stackParentId;
|
||||||
|
} else {
|
||||||
|
// json[r'stackParentId'] = null;
|
||||||
}
|
}
|
||||||
json[r'tags'] = this.tags;
|
json[r'tags'] = this.tags;
|
||||||
if (this.thumbhash != null) {
|
if (this.thumbhash != null) {
|
||||||
|
@ -276,6 +298,9 @@ class AssetResponseDto {
|
||||||
people: PersonResponseDto.listFromJson(json[r'people']),
|
people: PersonResponseDto.listFromJson(json[r'people']),
|
||||||
resized: mapValueOfType<bool>(json, r'resized')!,
|
resized: mapValueOfType<bool>(json, r'resized')!,
|
||||||
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
|
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
|
||||||
|
stack: AssetResponseDto.listFromJson(json[r'stack']),
|
||||||
|
stackCount: mapValueOfType<int>(json, r'stackCount')!,
|
||||||
|
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
|
||||||
tags: TagResponseDto.listFromJson(json[r'tags']),
|
tags: TagResponseDto.listFromJson(json[r'tags']),
|
||||||
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
||||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||||
|
@ -347,6 +372,7 @@ class AssetResponseDto {
|
||||||
'originalPath',
|
'originalPath',
|
||||||
'ownerId',
|
'ownerId',
|
||||||
'resized',
|
'resized',
|
||||||
|
'stackCount',
|
||||||
'thumbhash',
|
'thumbhash',
|
||||||
'type',
|
'type',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
|
|
20
mobile/openapi/lib/model/shared_link_edit_dto.dart
generated
20
mobile/openapi/lib/model/shared_link_edit_dto.dart
generated
|
@ -15,6 +15,7 @@ class SharedLinkEditDto {
|
||||||
SharedLinkEditDto({
|
SharedLinkEditDto({
|
||||||
this.allowDownload,
|
this.allowDownload,
|
||||||
this.allowUpload,
|
this.allowUpload,
|
||||||
|
this.changeExpiryTime,
|
||||||
this.description,
|
this.description,
|
||||||
this.expiresAt,
|
this.expiresAt,
|
||||||
this.showMetadata,
|
this.showMetadata,
|
||||||
|
@ -36,6 +37,15 @@ class SharedLinkEditDto {
|
||||||
///
|
///
|
||||||
bool? allowUpload;
|
bool? allowUpload;
|
||||||
|
|
||||||
|
/// Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
bool? changeExpiryTime;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
@ -58,6 +68,7 @@ class SharedLinkEditDto {
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto &&
|
||||||
other.allowDownload == allowDownload &&
|
other.allowDownload == allowDownload &&
|
||||||
other.allowUpload == allowUpload &&
|
other.allowUpload == allowUpload &&
|
||||||
|
other.changeExpiryTime == changeExpiryTime &&
|
||||||
other.description == description &&
|
other.description == description &&
|
||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.showMetadata == showMetadata;
|
other.showMetadata == showMetadata;
|
||||||
|
@ -67,12 +78,13 @@ class SharedLinkEditDto {
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(allowDownload == null ? 0 : allowDownload!.hashCode) +
|
(allowDownload == null ? 0 : allowDownload!.hashCode) +
|
||||||
(allowUpload == null ? 0 : allowUpload!.hashCode) +
|
(allowUpload == null ? 0 : allowUpload!.hashCode) +
|
||||||
|
(changeExpiryTime == null ? 0 : changeExpiryTime!.hashCode) +
|
||||||
(description == null ? 0 : description!.hashCode) +
|
(description == null ? 0 : description!.hashCode) +
|
||||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||||
(showMetadata == null ? 0 : showMetadata!.hashCode);
|
(showMetadata == null ? 0 : showMetadata!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, expiresAt=$expiresAt, showMetadata=$showMetadata]';
|
String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, changeExpiryTime=$changeExpiryTime, description=$description, expiresAt=$expiresAt, showMetadata=$showMetadata]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -86,6 +98,11 @@ class SharedLinkEditDto {
|
||||||
} else {
|
} else {
|
||||||
// json[r'allowUpload'] = null;
|
// json[r'allowUpload'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.changeExpiryTime != null) {
|
||||||
|
json[r'changeExpiryTime'] = this.changeExpiryTime;
|
||||||
|
} else {
|
||||||
|
// json[r'changeExpiryTime'] = null;
|
||||||
|
}
|
||||||
if (this.description != null) {
|
if (this.description != null) {
|
||||||
json[r'description'] = this.description;
|
json[r'description'] = this.description;
|
||||||
} else {
|
} else {
|
||||||
|
@ -114,6 +131,7 @@ class SharedLinkEditDto {
|
||||||
return SharedLinkEditDto(
|
return SharedLinkEditDto(
|
||||||
allowDownload: mapValueOfType<bool>(json, r'allowDownload'),
|
allowDownload: mapValueOfType<bool>(json, r'allowDownload'),
|
||||||
allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
|
allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
|
||||||
|
changeExpiryTime: mapValueOfType<bool>(json, r'changeExpiryTime'),
|
||||||
description: mapValueOfType<String>(json, r'description'),
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
expiresAt: mapDateTime(json, r'expiresAt', ''),
|
expiresAt: mapDateTime(json, r'expiresAt', ''),
|
||||||
showMetadata: mapValueOfType<bool>(json, r'showMetadata'),
|
showMetadata: mapValueOfType<bool>(json, r'showMetadata'),
|
||||||
|
|
106
mobile/openapi/lib/model/update_stack_parent_dto.dart
generated
Normal file
106
mobile/openapi/lib/model/update_stack_parent_dto.dart
generated
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
//
|
||||||
|
// 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 UpdateStackParentDto {
|
||||||
|
/// Returns a new [UpdateStackParentDto] instance.
|
||||||
|
UpdateStackParentDto({
|
||||||
|
required this.newParentId,
|
||||||
|
required this.oldParentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String newParentId;
|
||||||
|
|
||||||
|
String oldParentId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is UpdateStackParentDto &&
|
||||||
|
other.newParentId == newParentId &&
|
||||||
|
other.oldParentId == oldParentId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(newParentId.hashCode) +
|
||||||
|
(oldParentId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'UpdateStackParentDto[newParentId=$newParentId, oldParentId=$oldParentId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'newParentId'] = this.newParentId;
|
||||||
|
json[r'oldParentId'] = this.oldParentId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [UpdateStackParentDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static UpdateStackParentDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return UpdateStackParentDto(
|
||||||
|
newParentId: mapValueOfType<String>(json, r'newParentId')!,
|
||||||
|
oldParentId: mapValueOfType<String>(json, r'oldParentId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<UpdateStackParentDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <UpdateStackParentDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = UpdateStackParentDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, UpdateStackParentDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, UpdateStackParentDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = UpdateStackParentDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of UpdateStackParentDto-objects as value to a dart map
|
||||||
|
static Map<String, List<UpdateStackParentDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<UpdateStackParentDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = UpdateStackParentDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'newParentId',
|
||||||
|
'oldParentId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
5
mobile/openapi/test/asset_api_test.dart
generated
5
mobile/openapi/test/asset_api_test.dart
generated
|
@ -174,6 +174,11 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Future updateStackParent(UpdateStackParentDto updateStackParentDto) async
|
||||||
|
test('test updateStackParent', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
//Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String key, String duration, bool isArchived, bool isExternal, bool isOffline, bool isReadOnly, bool isVisible, String libraryId, MultipartFile livePhotoData, MultipartFile sidecarData }) async
|
//Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String key, String duration, bool isArchived, bool isExternal, bool isOffline, bool isReadOnly, bool isVisible, String libraryId, MultipartFile livePhotoData, MultipartFile sidecarData }) async
|
||||||
test('test uploadFile', () async {
|
test('test uploadFile', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
10
mobile/openapi/test/asset_bulk_update_dto_test.dart
generated
10
mobile/openapi/test/asset_bulk_update_dto_test.dart
generated
|
@ -31,6 +31,16 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// bool removeParent
|
||||||
|
test('to test the property `removeParent`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String stackParentId
|
||||||
|
test('to test the property `stackParentId`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
15
mobile/openapi/test/asset_response_dto_test.dart
generated
15
mobile/openapi/test/asset_response_dto_test.dart
generated
|
@ -142,6 +142,21 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// List<AssetResponseDto> stack (default value: const [])
|
||||||
|
test('to test the property `stack`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// int stackCount
|
||||||
|
test('to test the property `stackCount`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String stackParentId
|
||||||
|
test('to test the property `stackParentId`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// List<TagResponseDto> tags (default value: const [])
|
// List<TagResponseDto> tags (default value: const [])
|
||||||
test('to test the property `tags`', () async {
|
test('to test the property `tags`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
|
@ -26,6 +26,12 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.
|
||||||
|
// bool changeExpiryTime
|
||||||
|
test('to test the property `changeExpiryTime`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// String description
|
// String description
|
||||||
test('to test the property `description`', () async {
|
test('to test the property `description`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
32
mobile/openapi/test/update_stack_parent_dto_test.dart
generated
Normal file
32
mobile/openapi/test/update_stack_parent_dto_test.dart
generated
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for UpdateStackParentDto
|
||||||
|
void main() {
|
||||||
|
// final instance = UpdateStackParentDto();
|
||||||
|
|
||||||
|
group('test UpdateStackParentDto', () {
|
||||||
|
// String newParentId
|
||||||
|
test('to test the property `newParentId`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String oldParentId
|
||||||
|
test('to test the property `oldParentId`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ name: immich_mobile
|
||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.82.0+106
|
version: 1.82.1+106
|
||||||
isar_version: &isar_version 3.1.0+1
|
isar_version: &isar_version 3.1.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -25,6 +25,7 @@ void main() {
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isTrashed: false,
|
isTrashed: false,
|
||||||
|
stackCount: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ void main() {
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isTrashed: false,
|
isTrashed: false,
|
||||||
|
stackCount: 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1673,6 +1673,41 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/asset/stack/parent": {
|
||||||
|
"put": {
|
||||||
|
"operationId": "updateStackParent",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateStackParentDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Asset"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/asset/statistics": {
|
"/asset/statistics": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAssetStats",
|
"operationId": "getAssetStats",
|
||||||
|
@ -5379,7 +5414,7 @@
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.82.0",
|
"version": "1.82.1",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
@ -5696,6 +5731,13 @@
|
||||||
},
|
},
|
||||||
"isFavorite": {
|
"isFavorite": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"removeParent": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"stackParentId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -5941,6 +5983,19 @@
|
||||||
"smartInfo": {
|
"smartInfo": {
|
||||||
"$ref": "#/components/schemas/SmartInfoResponseDto"
|
"$ref": "#/components/schemas/SmartInfoResponseDto"
|
||||||
},
|
},
|
||||||
|
"stack": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AssetResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"stackCount": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"stackParentId": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/TagResponseDto"
|
"$ref": "#/components/schemas/TagResponseDto"
|
||||||
|
@ -5961,6 +6016,7 @@
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
|
"stackCount",
|
||||||
"deviceAssetId",
|
"deviceAssetId",
|
||||||
"deviceId",
|
"deviceId",
|
||||||
"ownerId",
|
"ownerId",
|
||||||
|
@ -7846,6 +7902,10 @@
|
||||||
"allowUpload": {
|
"allowUpload": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"changeExpiryTime": {
|
||||||
|
"description": "Few clients cannot send null to set the expiryTime to never.\nSetting this flag and not sending expiryAt is considered as null instead.\nClients that can send null values can ignore this.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -8521,6 +8581,23 @@
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"UpdateStackParentDto": {
|
||||||
|
"properties": {
|
||||||
|
"newParentId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"oldParentId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"oldParentId",
|
||||||
|
"newParentId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"UpdateTagDto": {
|
"UpdateTagDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
|
|
4
server/package-lock.json
generated
4
server/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.82.0",
|
"version": "1.82.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.82.0",
|
"version": "1.82.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.11",
|
"@babel/runtime": "^7.22.11",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.82.0",
|
"version": "1.82.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules --max_old_space_size=4096' jest --config test/e2e/jest-e2e.json --runInBand --forceExit",
|
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules --max_old_space_size=4096' jest --config test/e2e/jest-e2e.json --runInBand",
|
||||||
"typeorm": "typeorm",
|
"typeorm": "typeorm",
|
||||||
"typeorm:migrations:create": "typeorm migration:create",
|
"typeorm:migrations:create": "typeorm migration:create",
|
||||||
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js",
|
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js",
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
albumStub,
|
albumStub,
|
||||||
assetStub,
|
|
||||||
authStub,
|
authStub,
|
||||||
IAccessRepositoryMock,
|
IAccessRepositoryMock,
|
||||||
newAccessRepositoryMock,
|
newAccessRepositoryMock,
|
||||||
|
@ -225,7 +224,7 @@ describe(AlbumService.name, () => {
|
||||||
}),
|
}),
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
expect(albumMock.hasAsset).toHaveBeenCalledWith(albumStub.oneAsset.id, 'not-in-album');
|
expect(albumMock.hasAsset).toHaveBeenCalledWith({ albumId: 'album-4', assetId: 'not-in-album' });
|
||||||
expect(albumMock.update).not.toHaveBeenCalled();
|
expect(albumMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -461,6 +460,7 @@ describe(AlbumService.name, () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
albumMock.hasAsset.mockResolvedValue(false);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||||
|
@ -473,9 +473,12 @@ describe(AlbumService.name, () => {
|
||||||
expect(albumMock.update).toHaveBeenCalledWith({
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
|
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
|
expect(albumMock.addAssets).toHaveBeenCalledWith({
|
||||||
|
albumId: 'album-123',
|
||||||
|
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not set the thumbnail if the album has one already', async () => {
|
it('should not set the thumbnail if the album has one already', async () => {
|
||||||
|
@ -490,9 +493,9 @@ describe(AlbumService.name, () => {
|
||||||
expect(albumMock.update).toHaveBeenCalledWith({
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
assets: [{ id: 'asset-1' }],
|
|
||||||
albumThumbnailAssetId: 'asset-id',
|
albumThumbnailAssetId: 'asset-id',
|
||||||
});
|
});
|
||||||
|
expect(albumMock.addAssets).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow a shared user to add assets', async () => {
|
it('should allow a shared user to add assets', async () => {
|
||||||
|
@ -512,9 +515,12 @@ describe(AlbumService.name, () => {
|
||||||
expect(albumMock.update).toHaveBeenCalledWith({
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
assets: [{ id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
|
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
|
expect(albumMock.addAssets).toHaveBeenCalledWith({
|
||||||
|
albumId: 'album-123',
|
||||||
|
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow a shared link user to add assets', async () => {
|
it('should allow a shared link user to add assets', async () => {
|
||||||
|
@ -523,6 +529,7 @@ describe(AlbumService.name, () => {
|
||||||
accessMock.album.hasSharedLinkAccess.mockResolvedValue(true);
|
accessMock.album.hasSharedLinkAccess.mockResolvedValue(true);
|
||||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
albumMock.hasAsset.mockResolvedValue(false);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||||
|
@ -535,9 +542,12 @@ describe(AlbumService.name, () => {
|
||||||
expect(albumMock.update).toHaveBeenCalledWith({
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
|
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
|
expect(albumMock.addAssets).toHaveBeenCalledWith({
|
||||||
|
albumId: 'album-123',
|
||||||
|
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||||
|
});
|
||||||
|
|
||||||
expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith(
|
expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith(
|
||||||
authStub.adminSharedLink.sharedLinkId,
|
authStub.adminSharedLink.sharedLinkId,
|
||||||
|
@ -550,6 +560,7 @@ describe(AlbumService.name, () => {
|
||||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||||
accessMock.asset.hasPartnerAccess.mockResolvedValue(true);
|
accessMock.asset.hasPartnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
albumMock.hasAsset.mockResolvedValue(false);
|
||||||
|
|
||||||
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
|
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
|
||||||
{ success: true, id: 'asset-1' },
|
{ success: true, id: 'asset-1' },
|
||||||
|
@ -558,10 +569,8 @@ describe(AlbumService.name, () => {
|
||||||
expect(albumMock.update).toHaveBeenCalledWith({
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
assets: [assetStub.image, { id: 'asset-1' }],
|
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
|
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -569,6 +578,7 @@ describe(AlbumService.name, () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
albumMock.hasAsset.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
{ success: false, id: 'asset-id', error: BulkIdErrorReason.DUPLICATE },
|
{ success: false, id: 'asset-id', error: BulkIdErrorReason.DUPLICATE },
|
||||||
|
@ -620,17 +630,14 @@ describe(AlbumService.name, () => {
|
||||||
it('should allow the owner to remove assets', async () => {
|
it('should allow the owner to remove assets', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
albumMock.hasAsset.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
{ success: true, id: 'asset-id' },
|
{ success: true, id: 'asset-id' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(albumMock.update).toHaveBeenCalledWith({
|
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) });
|
||||||
id: 'album-123',
|
expect(albumMock.removeAssets).toHaveBeenCalledWith({ assetIds: ['asset-id'], albumId: 'album-123' });
|
||||||
updatedAt: expect.any(Date),
|
|
||||||
assets: [],
|
|
||||||
albumThumbnailAssetId: null,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip assets not in the album', async () => {
|
it('should skip assets not in the album', async () => {
|
||||||
|
@ -647,9 +654,14 @@ describe(AlbumService.name, () => {
|
||||||
it('should skip assets without user permission to remove', async () => {
|
it('should skip assets without user permission to remove', async () => {
|
||||||
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
|
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
albumMock.hasAsset.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
{ success: false, id: 'asset-id', error: BulkIdErrorReason.NO_PERMISSION },
|
{
|
||||||
|
success: false,
|
||||||
|
id: 'asset-id',
|
||||||
|
error: BulkIdErrorReason.NO_PERMISSION,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(albumMock.update).not.toHaveBeenCalled();
|
expect(albumMock.update).not.toHaveBeenCalled();
|
||||||
|
@ -658,6 +670,7 @@ describe(AlbumService.name, () => {
|
||||||
it('should reset the thumbnail if it is removed', async () => {
|
it('should reset the thumbnail if it is removed', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
|
||||||
|
albumMock.hasAsset.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
{ success: true, id: 'asset-id' },
|
{ success: true, id: 'asset-id' },
|
||||||
|
@ -666,9 +679,8 @@ describe(AlbumService.name, () => {
|
||||||
expect(albumMock.update).toHaveBeenCalledWith({
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
assets: [assetStub.withLocation],
|
|
||||||
albumThumbnailAssetId: assetStub.withLocation.id,
|
|
||||||
});
|
});
|
||||||
|
expect(albumMock.updateThumbnails).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ export class AlbumService {
|
||||||
const album = await this.findOrFail(id, { withAssets: true });
|
const album = await this.findOrFail(id, { withAssets: true });
|
||||||
|
|
||||||
if (dto.albumThumbnailAssetId) {
|
if (dto.albumThumbnailAssetId) {
|
||||||
const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId);
|
const valid = await this.albumRepository.hasAsset({ albumId: id, assetId: dto.albumThumbnailAssetId });
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new BadRequestException('Invalid album thumbnail');
|
throw new BadRequestException('Invalid album thumbnail');
|
||||||
}
|
}
|
||||||
|
@ -148,35 +148,34 @@ export class AlbumService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
const album = await this.findOrFail(id, { withAssets: true });
|
const album = await this.findOrFail(id, { withAssets: false });
|
||||||
|
|
||||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
||||||
|
|
||||||
const results: BulkIdResponseDto[] = [];
|
const results: BulkIdResponseDto[] = [];
|
||||||
for (const id of dto.ids) {
|
for (const assetId of dto.ids) {
|
||||||
const hasAsset = album.assets.find((asset) => asset.id === id);
|
const hasAsset = await this.albumRepository.hasAsset({ albumId: id, assetId });
|
||||||
if (hasAsset) {
|
if (hasAsset) {
|
||||||
results.push({ id, success: false, error: BulkIdErrorReason.DUPLICATE });
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.DUPLICATE });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, id);
|
const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId);
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push({ id, success: true });
|
results.push({ id: assetId, success: true });
|
||||||
album.assets.push({ id } as AssetEntity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAsset = results.find(({ success }) => success);
|
const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||||
if (newAsset) {
|
if (newAssetIds.length > 0) {
|
||||||
|
await this.albumRepository.addAssets({ albumId: id, assetIds: newAssetIds });
|
||||||
await this.albumRepository.update({
|
await this.albumRepository.update({
|
||||||
id,
|
id,
|
||||||
assets: album.assets,
|
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAsset.id,
|
albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAssetIds[0],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,42 +183,37 @@ export class AlbumService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
const album = await this.findOrFail(id, { withAssets: true });
|
const album = await this.findOrFail(id, { withAssets: false });
|
||||||
|
|
||||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
||||||
|
|
||||||
const results: BulkIdResponseDto[] = [];
|
const results: BulkIdResponseDto[] = [];
|
||||||
for (const id of dto.ids) {
|
for (const assetId of dto.ids) {
|
||||||
const hasAsset = album.assets.find((asset) => asset.id === id);
|
const hasAsset = await this.albumRepository.hasAsset({ albumId: id, assetId });
|
||||||
if (!hasAsset) {
|
if (!hasAsset) {
|
||||||
results.push({ id, success: false, error: BulkIdErrorReason.NOT_FOUND });
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NOT_FOUND });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess = await this.access.hasAny(authUser, [
|
const hasAccess = await this.access.hasAny(authUser, [
|
||||||
{ permission: Permission.ALBUM_REMOVE_ASSET, id },
|
{ permission: Permission.ALBUM_REMOVE_ASSET, id: assetId },
|
||||||
{ permission: Permission.ASSET_SHARE, id },
|
{ permission: Permission.ASSET_SHARE, id: assetId },
|
||||||
]);
|
]);
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push({ id, success: true });
|
results.push({ id: assetId, success: true });
|
||||||
album.assets = album.assets.filter((asset) => asset.id !== id);
|
|
||||||
if (album.albumThumbnailAssetId === id) {
|
|
||||||
album.albumThumbnailAssetId = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasSuccess = results.find(({ success }) => success);
|
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||||
if (hasSuccess) {
|
if (removedIds.length > 0) {
|
||||||
await this.albumRepository.update({
|
await this.albumRepository.removeAssets({ albumId: id, assetIds: removedIds });
|
||||||
id,
|
await this.albumRepository.update({ id, updatedAt: new Date() });
|
||||||
assets: album.assets,
|
if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
||||||
updatedAt: new Date(),
|
await this.albumRepository.updateThumbnails();
|
||||||
albumThumbnailAssetId: album.albumThumbnailAssetId || album.assets[0]?.id || null,
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { Readable } from 'stream';
|
||||||
import { JobName } from '../job';
|
import { JobName } from '../job';
|
||||||
import {
|
import {
|
||||||
AssetStats,
|
AssetStats,
|
||||||
|
CommunicationEvent,
|
||||||
IAssetRepository,
|
IAssetRepository,
|
||||||
ICommunicationRepository,
|
ICommunicationRepository,
|
||||||
ICryptoRepository,
|
ICryptoRepository,
|
||||||
|
@ -636,10 +637,89 @@ describe(AssetService.name, () => {
|
||||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
||||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Stack related
|
||||||
|
|
||||||
|
it('should require asset update access for parent', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'parent').mockResolvedValue(false);
|
||||||
|
await expect(
|
||||||
|
sut.updateAll(authStub.user1, {
|
||||||
|
ids: ['asset-1'],
|
||||||
|
stackParentId: 'parent',
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update parent asset when children are added', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
await sut.updateAll(authStub.user1, {
|
||||||
|
ids: [],
|
||||||
|
stackParentId: 'parent',
|
||||||
|
}),
|
||||||
|
expect(assetMock.updateAll).toHaveBeenCalledWith(['parent'], { stackParentId: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update parent asset when children are removed', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
assetMock.getByIds.mockResolvedValue([{ id: 'child-1', stackParentId: 'parent' } as AssetEntity]);
|
||||||
|
|
||||||
|
await sut.updateAll(authStub.user1, {
|
||||||
|
ids: ['child-1'],
|
||||||
|
removeParent: true,
|
||||||
|
}),
|
||||||
|
expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['parent']), { stackParentId: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update parentId for new children', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
await sut.updateAll(authStub.user1, {
|
||||||
|
stackParentId: 'parent',
|
||||||
|
ids: ['child-1', 'child-2'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stackParentId: 'parent' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nullify parentId for remove children', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
await sut.updateAll(authStub.user1, {
|
||||||
|
removeParent: true,
|
||||||
|
ids: ['child-1', 'child-2'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stackParentId: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merge stacks if new child has children', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
assetMock.getByIds.mockResolvedValue([
|
||||||
|
{ id: 'child-1', stack: [{ id: 'child-2' } as AssetEntity] } as AssetEntity,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.updateAll(authStub.user1, {
|
||||||
|
ids: ['child-1'],
|
||||||
|
stackParentId: 'parent',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stackParentId: 'parent' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send ws asset update event', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
await sut.updateAll(authStub.user1, {
|
||||||
|
ids: ['asset-1'],
|
||||||
|
stackParentId: 'parent',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(communicationMock.send).toHaveBeenCalledWith(CommunicationEvent.ASSET_UPDATE, authStub.user1.id, [
|
||||||
|
'asset-1',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteAll', () => {
|
describe('deleteAll', () => {
|
||||||
it('should required asset delete access for all ids', async () => {
|
it('should require asset delete access for all ids', async () => {
|
||||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||||
await expect(
|
await expect(
|
||||||
sut.deleteAll(authStub.user1, {
|
sut.deleteAll(authStub.user1, {
|
||||||
|
@ -677,7 +757,7 @@ describe(AssetService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('restoreAll', () => {
|
describe('restoreAll', () => {
|
||||||
it('should required asset restore access for all ids', async () => {
|
it('should require asset restore access for all ids', async () => {
|
||||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||||
await expect(
|
await expect(
|
||||||
sut.deleteAll(authStub.user1, {
|
sut.deleteAll(authStub.user1, {
|
||||||
|
@ -757,6 +837,21 @@ describe(AssetService.name, () => {
|
||||||
expect(assetMock.remove).toHaveBeenCalledWith(assetWithFace);
|
expect(assetMock.remove).toHaveBeenCalledWith(assetWithFace);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update stack parent if asset has stack children', async () => {
|
||||||
|
when(assetMock.getById)
|
||||||
|
.calledWith(assetStub.primaryImage.id)
|
||||||
|
.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
||||||
|
|
||||||
|
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id });
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toHaveBeenCalledWith(['stack-child-asset-2'], {
|
||||||
|
stackParentId: 'stack-child-asset-1',
|
||||||
|
});
|
||||||
|
expect(assetMock.updateAll).toHaveBeenCalledWith(['stack-child-asset-1'], {
|
||||||
|
stackParentId: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not schedule delete-files job for readonly assets', async () => {
|
it('should not schedule delete-files job for readonly assets', async () => {
|
||||||
when(assetMock.getById)
|
when(assetMock.getById)
|
||||||
.calledWith(assetStub.readOnly.id)
|
.calledWith(assetStub.readOnly.id)
|
||||||
|
@ -854,4 +949,70 @@ describe(AssetService.name, () => {
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } });
|
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateStackParent', () => {
|
||||||
|
it('should require asset update access for new parent', async () => {
|
||||||
|
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'old').mockResolvedValue(true);
|
||||||
|
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'new').mockResolvedValue(false);
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||||
|
await expect(
|
||||||
|
sut.updateStackParent(authStub.user1, {
|
||||||
|
oldParentId: 'old',
|
||||||
|
newParentId: 'new',
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require asset read access for old parent', async () => {
|
||||||
|
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'old').mockResolvedValue(false);
|
||||||
|
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'new').mockResolvedValue(true);
|
||||||
|
await expect(
|
||||||
|
sut.updateStackParent(authStub.user1, {
|
||||||
|
oldParentId: 'old',
|
||||||
|
newParentId: 'new',
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('make old parent the child of new parent', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
when(assetMock.getById)
|
||||||
|
.calledWith(assetStub.image.id)
|
||||||
|
.mockResolvedValue(assetStub.image as AssetEntity);
|
||||||
|
|
||||||
|
await sut.updateStackParent(authStub.user1, {
|
||||||
|
oldParentId: assetStub.image.id,
|
||||||
|
newParentId: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id], { stackParentId: 'new' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remove stackParentId of new parent', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
await sut.updateStackParent(authStub.user1, {
|
||||||
|
oldParentId: assetStub.primaryImage.id,
|
||||||
|
newParentId: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toBeCalledWith(['new'], { stackParentId: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update stackParentId of old parents children to new parent', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
when(assetMock.getById)
|
||||||
|
.calledWith(assetStub.primaryImage.id)
|
||||||
|
.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
||||||
|
|
||||||
|
await sut.updateStackParent(authStub.user1, {
|
||||||
|
oldParentId: assetStub.primaryImage.id,
|
||||||
|
newParentId: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toBeCalledWith(
|
||||||
|
[assetStub.primaryImage.id, 'stack-child-asset-1', 'stack-child-asset-2'],
|
||||||
|
{ stackParentId: 'new' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,6 +40,7 @@ import {
|
||||||
TimeBucketDto,
|
TimeBucketDto,
|
||||||
TrashAction,
|
TrashAction,
|
||||||
UpdateAssetDto,
|
UpdateAssetDto,
|
||||||
|
UpdateStackParentDto,
|
||||||
mapStats,
|
mapStats,
|
||||||
} from './dto';
|
} from './dto';
|
||||||
import {
|
import {
|
||||||
|
@ -208,7 +209,7 @@ export class AssetService {
|
||||||
if (authUser.isShowMetadata) {
|
if (authUser.isShowMetadata) {
|
||||||
return assets.map((asset) => mapAsset(asset));
|
return assets.map((asset) => mapAsset(asset));
|
||||||
} else {
|
} else {
|
||||||
return assets.map((asset) => mapAsset(asset, true));
|
return assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,10 +339,29 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
|
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||||
const { ids, ...options } = dto;
|
const { ids, removeParent, ...options } = dto;
|
||||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
||||||
|
|
||||||
|
if (removeParent) {
|
||||||
|
(options as Partial<AssetEntity>).stackParentId = null;
|
||||||
|
const assets = await this.assetRepository.getByIds(ids);
|
||||||
|
// This updates the updatedAt column of the parents to indicate that one of its children is removed
|
||||||
|
// All the unique parent's -> parent is set to null
|
||||||
|
ids.push(...new Set(assets.filter((a) => !!a.stackParentId).map((a) => a.stackParentId!)));
|
||||||
|
} else if (options.stackParentId) {
|
||||||
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, options.stackParentId);
|
||||||
|
// Merge stacks
|
||||||
|
const assets = await this.assetRepository.getByIds(ids);
|
||||||
|
const assetsWithChildren = assets.filter((a) => a.stack && a.stack.length > 0);
|
||||||
|
ids.push(...assetsWithChildren.flatMap((child) => child.stack!.map((gChild) => gChild.id)));
|
||||||
|
|
||||||
|
// This updates the updatedAt column of the parent to indicate that a new child has been added
|
||||||
|
await this.assetRepository.updateAll([options.stackParentId], { stackParentId: null });
|
||||||
|
}
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
||||||
await this.assetRepository.updateAll(ids, options);
|
await this.assetRepository.updateAll(ids, options);
|
||||||
|
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetDeletionCheck() {
|
async handleAssetDeletionCheck() {
|
||||||
|
@ -384,6 +404,14 @@ export class AssetService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace the parent of the stack children with a new asset
|
||||||
|
if (asset.stack && asset.stack.length != 0) {
|
||||||
|
const stackIds = asset.stack.map((a) => a.id);
|
||||||
|
const newParentId = stackIds[0];
|
||||||
|
await this.assetRepository.updateAll(stackIds.slice(1), { stackParentId: newParentId });
|
||||||
|
await this.assetRepository.updateAll([newParentId], { stackParentId: null });
|
||||||
|
}
|
||||||
|
|
||||||
await this.assetRepository.remove(asset);
|
await this.assetRepository.remove(asset);
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [asset.id] } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [asset.id] } });
|
||||||
this.communicationRepository.send(CommunicationEvent.ASSET_DELETE, asset.ownerId, id);
|
this.communicationRepository.send(CommunicationEvent.ASSET_DELETE, asset.ownerId, id);
|
||||||
|
@ -454,6 +482,25 @@ export class AssetService {
|
||||||
this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, authUser.id, ids);
|
this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, authUser.id, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateStackParent(authUser: AuthUserDto, dto: UpdateStackParentDto): Promise<void> {
|
||||||
|
const { oldParentId, newParentId } = dto;
|
||||||
|
await this.access.requirePermission(authUser, Permission.ASSET_READ, oldParentId);
|
||||||
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, newParentId);
|
||||||
|
|
||||||
|
const childIds: string[] = [];
|
||||||
|
const oldParent = await this.assetRepository.getById(oldParentId);
|
||||||
|
if (oldParent != null) {
|
||||||
|
childIds.push(oldParent.id);
|
||||||
|
// Get all children of old parent
|
||||||
|
childIds.push(...(oldParent.stack?.map((a) => a.id) ?? []));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, [...childIds, newParentId]);
|
||||||
|
await this.assetRepository.updateAll(childIds, { stackParentId: newParentId });
|
||||||
|
// Remove ParentId of new parent if this was previously a child of some other asset
|
||||||
|
return this.assetRepository.updateAll([newParentId], { stackParentId: null });
|
||||||
|
}
|
||||||
|
|
||||||
async run(authUser: AuthUserDto, dto: AssetJobsDto) {
|
async run(authUser: AuthUserDto, dto: AssetJobsDto) {
|
||||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, dto.assetIds);
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, dto.assetIds);
|
||||||
|
|
||||||
|
|
9
server/src/domain/asset/dto/asset-stack.dto.ts
Normal file
9
server/src/domain/asset/dto/asset-stack.dto.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
|
export class UpdateStackParentDto {
|
||||||
|
@ValidateUUID()
|
||||||
|
oldParentId!: string;
|
||||||
|
|
||||||
|
@ValidateUUID()
|
||||||
|
newParentId!: string;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsBoolean, IsInt, IsPositive, IsString } from 'class-validator';
|
import { IsBoolean, IsInt, IsPositive, IsString } from 'class-validator';
|
||||||
import { Optional } from '../../domain.util';
|
import { Optional, ValidateUUID } from '../../domain.util';
|
||||||
import { BulkIdsDto } from '../response-dto';
|
import { BulkIdsDto } from '../response-dto';
|
||||||
|
|
||||||
export class AssetBulkUpdateDto extends BulkIdsDto {
|
export class AssetBulkUpdateDto extends BulkIdsDto {
|
||||||
|
@ -11,6 +11,14 @@ export class AssetBulkUpdateDto extends BulkIdsDto {
|
||||||
@Optional()
|
@Optional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@ValidateUUID()
|
||||||
|
stackParentId?: string;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsBoolean()
|
||||||
|
removeParent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateAssetDto {
|
export class UpdateAssetDto {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './asset-ids.dto';
|
export * from './asset-ids.dto';
|
||||||
|
export * from './asset-stack.dto';
|
||||||
export * from './asset-statistics.dto';
|
export * from './asset-statistics.dto';
|
||||||
export * from './asset.dto';
|
export * from './asset.dto';
|
||||||
export * from './download.dto';
|
export * from './download.dto';
|
||||||
|
|
|
@ -42,9 +42,20 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||||
people?: PersonResponseDto[];
|
people?: PersonResponseDto[];
|
||||||
/**base64 encoded sha1 hash */
|
/**base64 encoded sha1 hash */
|
||||||
checksum!: string;
|
checksum!: string;
|
||||||
|
stackParentId?: string | null;
|
||||||
|
stack?: AssetResponseDto[];
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
stackCount!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapAsset(entity: AssetEntity, stripMetadata = false): AssetResponseDto {
|
export type AssetMapOptions = {
|
||||||
|
stripMetadata?: boolean;
|
||||||
|
withStack?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
|
||||||
|
const { stripMetadata = false, withStack = false } = options;
|
||||||
|
|
||||||
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
|
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
type: entity.type,
|
type: entity.type,
|
||||||
|
@ -87,6 +98,9 @@ export function mapAsset(entity: AssetEntity, stripMetadata = false): AssetRespo
|
||||||
tags: entity.tags?.map(mapTag),
|
tags: entity.tags?.map(mapTag),
|
||||||
people: entity.faces?.map(mapFace).filter((person) => !person.isHidden),
|
people: entity.faces?.map(mapFace).filter((person) => !person.isHidden),
|
||||||
checksum: entity.checksum.toString('base64'),
|
checksum: entity.checksum.toString('base64'),
|
||||||
|
stackParentId: entity.stackParentId,
|
||||||
|
stack: withStack ? entity.stack?.map((a) => mapAsset(a, { stripMetadata })) ?? undefined : undefined,
|
||||||
|
stackCount: entity.stack?.length ?? 0,
|
||||||
isExternal: entity.isExternal,
|
isExternal: entity.isExternal,
|
||||||
isOffline: entity.isOffline,
|
isOffline: entity.isOffline,
|
||||||
isReadOnly: entity.isReadOnly,
|
isReadOnly: entity.isReadOnly,
|
||||||
|
|
|
@ -409,6 +409,54 @@ describe(MetadataService.name, () => {
|
||||||
localDateTime: new Date('1970-01-01'),
|
localDateTime: new Date('1970-01-01'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle duration', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.getExifTags.mockResolvedValue({ Duration: 6.21 });
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
||||||
|
expect(assetMock.save).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: assetStub.image.id,
|
||||||
|
duration: '00:00:06.210',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle duration as an object without Scale', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.getExifTags.mockResolvedValue({ Duration: { Value: 6.2 } });
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
||||||
|
expect(assetMock.save).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: assetStub.image.id,
|
||||||
|
duration: '00:00:06.200',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle duration with scale', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.getExifTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } });
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
||||||
|
expect(assetMock.save).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: assetStub.image.id,
|
||||||
|
duration: '00:00:06.207',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleQueueSidecar', () => {
|
describe('handleQueueSidecar', () => {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Duration } from 'luxon';
|
||||||
import { usePagination } from '../domain.util';
|
import { usePagination } from '../domain.util';
|
||||||
import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
|
import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
|
||||||
import {
|
import {
|
||||||
|
ExifDuration,
|
||||||
IAlbumRepository,
|
IAlbumRepository,
|
||||||
IAssetRepository,
|
IAssetRepository,
|
||||||
ICryptoRepository,
|
ICryptoRepository,
|
||||||
|
@ -109,6 +110,10 @@ export class MetadataService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async teardown() {
|
||||||
|
await this.repository.teardown();
|
||||||
|
}
|
||||||
|
|
||||||
async handleLivePhotoLinking(job: IEntityJob) {
|
async handleLivePhotoLinking(job: IEntityJob) {
|
||||||
const { id } = job;
|
const { id } = job;
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
|
@ -394,7 +399,11 @@ export class MetadataService {
|
||||||
return bitsPerSample;
|
return bitsPerSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDuration(seconds?: number): string {
|
private getDuration(seconds?: number | ExifDuration): string {
|
||||||
return Duration.fromObject({ seconds }).toFormat('hh:mm:ss.SSS');
|
let _seconds = seconds as number;
|
||||||
|
if (typeof seconds === 'object') {
|
||||||
|
_seconds = seconds.Value * (seconds?.Scale || 1);
|
||||||
|
}
|
||||||
|
return Duration.fromObject({ seconds: _seconds }).toFormat('hh:mm:ss.SSS');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,13 +11,24 @@ export interface AlbumInfoOptions {
|
||||||
withAssets: boolean;
|
withAssets: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AlbumAsset {
|
||||||
|
albumId: string;
|
||||||
|
assetId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlbumAssets {
|
||||||
|
albumId: string;
|
||||||
|
assetIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAlbumRepository {
|
export interface IAlbumRepository {
|
||||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
|
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
|
||||||
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
||||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||||
hasAsset(id: string, assetId: string): Promise<boolean>;
|
addAssets(assets: AlbumAssets): Promise<void>;
|
||||||
/** Remove an asset from _all_ albums */
|
hasAsset(asset: AlbumAsset): Promise<boolean>;
|
||||||
removeAsset(id: string): Promise<void>;
|
removeAsset(assetId: string): Promise<void>;
|
||||||
|
removeAssets(assets: AlbumAssets): Promise<void>;
|
||||||
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||||
getInvalidThumbnail(): Promise<string[]>;
|
getInvalidThumbnail(): Promise<string[]>;
|
||||||
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
||||||
|
|
|
@ -4,6 +4,7 @@ export enum CommunicationEvent {
|
||||||
UPLOAD_SUCCESS = 'on_upload_success',
|
UPLOAD_SUCCESS = 'on_upload_success',
|
||||||
ASSET_DELETE = 'on_asset_delete',
|
ASSET_DELETE = 'on_asset_delete',
|
||||||
ASSET_TRASH = 'on_asset_trash',
|
ASSET_TRASH = 'on_asset_trash',
|
||||||
|
ASSET_UPDATE = 'on_asset_update',
|
||||||
ASSET_RESTORE = 'on_asset_restore',
|
ASSET_RESTORE = 'on_asset_restore',
|
||||||
PERSON_THUMBNAIL = 'on_person_thumbnail',
|
PERSON_THUMBNAIL = 'on_person_thumbnail',
|
||||||
SERVER_VERSION = 'on_server_version',
|
SERVER_VERSION = 'on_server_version',
|
||||||
|
|
|
@ -14,7 +14,12 @@ export interface ReverseGeocodeResult {
|
||||||
city: string | null;
|
city: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImmichTags extends Omit<Tags, 'FocalLength'> {
|
export interface ExifDuration {
|
||||||
|
Value: number;
|
||||||
|
Scale?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImmichTags extends Omit<Tags, 'FocalLength' | 'Duration'> {
|
||||||
ContentIdentifier?: string;
|
ContentIdentifier?: string;
|
||||||
MotionPhoto?: number;
|
MotionPhoto?: number;
|
||||||
MotionPhotoVersion?: number;
|
MotionPhotoVersion?: number;
|
||||||
|
@ -22,10 +27,12 @@ export interface ImmichTags extends Omit<Tags, 'FocalLength'> {
|
||||||
MediaGroupUUID?: string;
|
MediaGroupUUID?: string;
|
||||||
ImagePixelDepth?: string;
|
ImagePixelDepth?: string;
|
||||||
FocalLength?: number;
|
FocalLength?: number;
|
||||||
|
Duration?: number | ExifDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMetadataRepository {
|
export interface IMetadataRepository {
|
||||||
init(options: Partial<InitOptions>): Promise<void>;
|
init(options: Partial<InitOptions>): Promise<void>;
|
||||||
|
teardown(): Promise<void>;
|
||||||
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
||||||
deleteCache(): Promise<void>;
|
deleteCache(): Promise<void>;
|
||||||
getExifTags(path: string): Promise<ImmichTags | null>;
|
getExifTags(path: string): Promise<ImmichTags | null>;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue