Compare commits
26 Commits
refactor/e
...
feat/local
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eabf535246 | ||
|
|
b0947bf9b9 | ||
|
|
5ea3b5a5c1 | ||
|
|
ded18f3b6b | ||
|
|
3724dc465b | ||
|
|
91b07fbac8 | ||
|
|
fe84f1cc90 | ||
|
|
e748945b4f | ||
|
|
9f8a7e0bea | ||
|
|
a7719a94fc | ||
|
|
9a4a320cfb | ||
|
|
0cce7ebf25 | ||
|
|
b1cdf73a24 | ||
|
|
147747de32 | ||
|
|
9abfa6940c | ||
|
|
39ea73d654 | ||
|
|
7c1ea2dc73 | ||
|
|
14169d310a | ||
|
|
5a1a841365 | ||
|
|
af70111645 | ||
|
|
8cd3f6b884 | ||
|
|
124eb8251b | ||
|
|
529d49471f | ||
|
|
3868736799 | ||
|
|
94fc1f213a | ||
|
|
cfc575d89c |
@@ -25,6 +25,7 @@ server/upload/
|
||||
server/src/queries
|
||||
server/dist/
|
||||
server/www/
|
||||
server/resources/v1.pmtiles
|
||||
|
||||
web/node_modules/
|
||||
web/coverage/
|
||||
|
||||
@@ -24,6 +24,7 @@ services:
|
||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
||||
- /usr/src/app/node_modules
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- ../server/resources/v1.pmtiles:/usr/src/app/resources/v1.pmtiles
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -51,5 +51,4 @@ services:
|
||||
volumes:
|
||||
- /usr/lib/wsl:/usr/lib/wsl
|
||||
environment:
|
||||
- LD_LIBRARY_PATH=/usr/lib/wsl/lib
|
||||
- LIBVA_DRIVER_NAME=d3d12
|
||||
|
||||
@@ -64,3 +64,43 @@ Below is an example config for Apache2 site configuration.
|
||||
ProxyPreserveHost On
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### Traefik Proxy example config
|
||||
|
||||
The example below is for Traefik version 3.
|
||||
|
||||
The most important is to increase the `respondingTimeouts` of the entrypoint used by immich. In this example of entrypoint `websecure` for port `443`. Per default it's set to 60s which leeds to videos stop uploading after 1 minute (Error Code 499). With this config it will fail after 10 minutes which is in most cases enough. Increase it if needed.
|
||||
|
||||
`traefik.yaml`
|
||||
|
||||
```yaml
|
||||
[...]
|
||||
entryPoints:
|
||||
websecure:
|
||||
address: :443
|
||||
# this section needs to be added
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: 600s
|
||||
idleTimeout: 600s
|
||||
writeTimeout: 600s
|
||||
```
|
||||
|
||||
The second part is in the `docker-compose.yml` file where immich is in. Add the Traefik specific labels like in the example.
|
||||
|
||||
`docker-compose.yml`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
immich-server:
|
||||
[...]
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# increase readingTimeouts for the entrypoint used here
|
||||
traefik.http.routers.immich.entrypoints: websecure
|
||||
traefik.http.routers.immich.rule: Host(`immich.your-domain.com`)
|
||||
traefik.http.services.immich.loadbalancer.server.port: 3001
|
||||
```
|
||||
|
||||
Keep in mind, that Traefik needs to communicate with the network where immich is in, usually done
|
||||
by adding the Traefik network to the `immich-server`.
|
||||
|
||||
@@ -3,6 +3,7 @@ sidebar_position: 1
|
||||
---
|
||||
|
||||
import AppArchitecture from './img/app-architecture.png';
|
||||
import MobileArchitecture from './img/immich_mobile_architecture.svg';
|
||||
|
||||
# Architecture
|
||||
|
||||
@@ -28,7 +29,14 @@ All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for
|
||||
|
||||
### Mobile App
|
||||
|
||||
The mobile app is written in [Flutter](https://flutter.dev/). It uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management.
|
||||
The mobile app is written in [Dart](https://dart.dev/) using [Flutter](https://flutter.dev/). Below is an architecture overview:
|
||||
|
||||
<MobileArchitecture className="p-4 dark:bg-immich-dark-primary my-4" />
|
||||
|
||||
The diagrams shows the target architecture, the current state of the code-base is not always following the architecture yet. New code and contributions should follow this architecture.
|
||||
Currently, it uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management (providers).
|
||||
Entities and Models are the two types of data classes used. While entities are stored in the on-device database, models are ephemeral and only kept in memory.
|
||||
The Repositories should be the only place where other data classes are used internally (such as OpenAPI DTOs). However, their interfaces must not use foreign data classes!
|
||||
|
||||
### Web Client
|
||||
|
||||
|
||||
104
docs/docs/developer/img/immich_mobile_architecture.drawio
Normal file
104
docs/docs/developer/img/immich_mobile_architecture.drawio
Normal file
@@ -0,0 +1,104 @@
|
||||
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" version="24.7.16">
|
||||
<diagram name="Page-1" id="Bp2gX--FtC4sSMWxsLrs">
|
||||
<mxGraphModel dx="1728" dy="954" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="850" pageHeight="1100" background="none" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-1" value="" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.polygon;polyCoords=[[0.25,0],[0.75,0],[1,0.25],[1,0.75],[0.75,1],[0.25,1],[0,0.75],[0,0.25]];polyline=0;strokeWidth=4;rounded=1;fillColor=#4251B0;" vertex="1" parent="1">
|
||||
<mxGeometry x="280" y="217.5" width="465" height="465" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-2" value="<b><font style="font-size: 22px;">Mobile App</font></b>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;rounded=1;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="442.5" y="225" width="140" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-25" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-4" target="zHhczcy2-Jv_nqmJUiNH-5">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-4" value="Services" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFB400;" vertex="1" parent="1">
|
||||
<mxGeometry x="530" y="420" width="80" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-26" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-12">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-5" value="Repositories" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E83F7;" vertex="1" parent="1">
|
||||
<mxGeometry x="650" y="420" width="80" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-24" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-6" target="zHhczcy2-Jv_nqmJUiNH-4">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-6" value="Providers" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ED79B5;" vertex="1" parent="1">
|
||||
<mxGeometry x="410" y="420" width="80" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-29" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-7" target="zHhczcy2-Jv_nqmJUiNH-8">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-30" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-7" target="zHhczcy2-Jv_nqmJUiNH-6">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-7" value="Pages" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FA2921;" vertex="1" parent="1">
|
||||
<mxGeometry x="290" y="480" width="80" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-31" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-8" target="zHhczcy2-Jv_nqmJUiNH-6">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-8" value="Widgets" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FA2921;" vertex="1" parent="1">
|
||||
<mxGeometry x="290" y="360" width="80" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-11" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;rounded=1;fillColor=#4251B0;" vertex="1" parent="1">
|
||||
<mxGeometry x="180" y="368.5" width="81.5" height="163" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-12" value="platform<div>system</div>" style="rhombus;whiteSpace=wrap;html=1;rounded=1;fillColor=#ED79B5;" vertex="1" parent="1">
|
||||
<mxGeometry x="800" y="410" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-13" value="on-device<div>database</div>" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;fillColor=#FA2921;" vertex="1" parent="1">
|
||||
<mxGeometry x="810" y="310" width="60" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-14" value="server" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;rounded=1;fillColor=#FFB400;" vertex="1" parent="1">
|
||||
<mxGeometry x="780" y="500" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-16" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.07;entryY=0.4;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-14">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-39" value="OpenAPI" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];rounded=1;labelBackgroundColor=#1E83F7;" vertex="1" connectable="0" parent="zHhczcy2-Jv_nqmJUiNH-16">
|
||||
<mxGeometry x="0.0697" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="8" y="10" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-23" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-6" target="zHhczcy2-Jv_nqmJUiNH-6">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-27" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=1;entryDx=0;entryDy=-15;entryPerimeter=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-13">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-34" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;dashed=1;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-3">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="810" y="360" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-36" value="" style="endArrow=none;dashed=1;html=1;rounded=1;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-9">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="512.08" y="665" as="sourcePoint" />
|
||||
<mxPoint x="512.08" y="265" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-37" value="UI part" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;fontSize=14;fontColor=#FFFFFF;" vertex="1" parent="1">
|
||||
<mxGeometry x="387.5" y="640" width="70" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-38" value="non-UI part" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;fontSize=14;fontColor=#FFFFFF;" vertex="1" parent="1">
|
||||
<mxGeometry x="550" y="640" width="90" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-41" value="" style="endArrow=none;dashed=1;html=1;rounded=1;" edge="1" parent="1" target="zHhczcy2-Jv_nqmJUiNH-9">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="512.08" y="665" as="sourcePoint" />
|
||||
<mxPoint x="512.08" y="265" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-9" value="Models" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#18C249;" vertex="1" parent="1">
|
||||
<mxGeometry x="470" y="510" width="80" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zHhczcy2-Jv_nqmJUiNH-3" value="Entities" style="rounded=1;whiteSpace=wrap;html=1;gradientColor=none;fillColor=#18C249;" vertex="1" parent="1">
|
||||
<mxGeometry x="472.5" y="330" width="80" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
3
docs/docs/developer/img/immich_mobile_architecture.svg
Normal file
3
docs/docs/developer/img/immich_mobile_architecture.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 20 KiB |
@@ -22,7 +22,6 @@ services:
|
||||
- IMMICH_METRICS=true
|
||||
- IMMICH_ENV=testing
|
||||
volumes:
|
||||
- upload:/usr/src/app/upload
|
||||
- ./test-assets:/test-assets
|
||||
extra_hosts:
|
||||
- 'auth-server:host-gateway'
|
||||
@@ -44,7 +43,3 @@ services:
|
||||
POSTGRES_DB: immich
|
||||
ports:
|
||||
- 5435:5432
|
||||
|
||||
volumes:
|
||||
model-cache:
|
||||
upload:
|
||||
|
||||
6
e2e/package-lock.json
generated
6
e2e/package-lock.json
generated
@@ -5016,9 +5016,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz",
|
||||
"integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
||||
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
|
||||
@@ -53,8 +53,10 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'docker compose up --build -V --remove-orphans',
|
||||
command: 'docker compose up --build --renew-anon-volumes --force-recreate --remove-orphans',
|
||||
url: 'http://127.0.0.1:2285',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
||||
import { LoginResponseDto } from '@immich/sdk';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { basename, join } from 'node:path';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, testAssetDir, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
@@ -11,18 +10,13 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
describe('/map', () => {
|
||||
let websocket: Socket;
|
||||
let admin: LoginResponseDto;
|
||||
let nonAdmin: LoginResponseDto;
|
||||
let asset: AssetMediaResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup({ onboarding: false });
|
||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||
|
||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||
|
||||
asset = await utils.createAsset(admin.accessToken);
|
||||
|
||||
const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
|
||||
utils.resetEvents();
|
||||
const uploadFile = async (input: string) => {
|
||||
@@ -103,63 +97,6 @@ describe('/map', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /map/style.json', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/map/style.json');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should allow shared link access', async () => {
|
||||
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset.id],
|
||||
});
|
||||
const { status, body } = await request(app).get(`/map/style.json?key=${sharedLink.key}`).query({ theme: 'dark' });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||
});
|
||||
|
||||
it('should throw an error if a theme is not light or dark', async () => {
|
||||
for (const theme of ['dark1', true, 123, '', null, undefined]) {
|
||||
const { status, body } = await request(app)
|
||||
.get('/map/style.json')
|
||||
.query({ theme })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
|
||||
}
|
||||
});
|
||||
|
||||
it('should return the light style.json', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/map/style.json')
|
||||
.query({ theme: 'light' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
|
||||
});
|
||||
|
||||
it('should return the dark style.json', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/map/style.json')
|
||||
.query({ theme: 'dark' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||
});
|
||||
|
||||
it('should not require admin authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/map/style.json')
|
||||
.query({ theme: 'dark' })
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /map/reverse-geocode', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/map/reverse-geocode');
|
||||
|
||||
@@ -128,6 +128,8 @@ describe('/server-info', () => {
|
||||
isInitialized: true,
|
||||
externalDomain: '',
|
||||
isOnboarded: false,
|
||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,6 +134,8 @@ describe('/server', () => {
|
||||
isInitialized: true,
|
||||
externalDomain: '',
|
||||
isOnboarded: false,
|
||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,8 @@ const setup = async () => {
|
||||
|
||||
const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000);
|
||||
|
||||
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
||||
const command = 'compose up --build --renew-anon-volumes --force-recreate --remove-orphans';
|
||||
const child = spawn('docker', command.split(' '), { stdio: 'pipe' });
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const input = data.toString();
|
||||
|
||||
@@ -156,8 +156,7 @@ export const utils = {
|
||||
|
||||
for (const table of tables) {
|
||||
if (table === 'system_metadata') {
|
||||
// prevent reverse geocoder from being re-initialized
|
||||
sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`);
|
||||
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
|
||||
} else {
|
||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||
}
|
||||
|
||||
50
machine-learning/poetry.lock
generated
50
machine-learning/poetry.lock
generated
@@ -1237,13 +1237,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.24.6"
|
||||
version = "0.25.0"
|
||||
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "huggingface_hub-0.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970"},
|
||||
{file = "huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000"},
|
||||
{file = "huggingface_hub-0.25.0-py3-none-any.whl", hash = "sha256:e2f357b35d72d5012cfd127108c4e14abcd61ba4ebc90a5a374dc2456cb34e12"},
|
||||
{file = "huggingface_hub-0.25.0.tar.gz", hash = "sha256:fb5fbe6c12fcd99d187ec7db95db9110fb1a20505f23040a5449a717c1a0db4d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1531,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.31.5"
|
||||
version = "2.31.6"
|
||||
description = "Developer-friendly load testing framework"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "locust-2.31.5-py3-none-any.whl", hash = "sha256:2904ff6307d54d3202c9ebd776f9170214f6dfbe4059504dad9e3ffaca03f600"},
|
||||
{file = "locust-2.31.5.tar.gz", hash = "sha256:14b2fa6f95bf248668e6dc92d100a44f06c5dcb1c26f88a5442bcaaee18faceb"},
|
||||
{file = "locust-2.31.6-py3-none-any.whl", hash = "sha256:004c963c7a588dc15d57d710cdc6a262d85b57936d7fad3c38ac0657aa98fc3b"},
|
||||
{file = "locust-2.31.6.tar.gz", hash = "sha256:03b6da0491d6a0b905692d9ac128d9deec403f40dc605c481a90dbab5126318c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2834,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.4"
|
||||
version = "0.6.6"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258"},
|
||||
{file = "ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60"},
|
||||
{file = "ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f"},
|
||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc"},
|
||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617"},
|
||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408"},
|
||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e"},
|
||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818"},
|
||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6"},
|
||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa"},
|
||||
{file = "ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6"},
|
||||
{file = "ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d"},
|
||||
{file = "ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa"},
|
||||
{file = "ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1"},
|
||||
{file = "ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523"},
|
||||
{file = "ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58"},
|
||||
{file = "ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14"},
|
||||
{file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"},
|
||||
{file = "ruff-0.6.6-py3-none-linux_armv6l.whl", hash = "sha256:f5bc5398457484fc0374425b43b030e4668ed4d2da8ee7fdda0e926c9f11ccfb"},
|
||||
{file = "ruff-0.6.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:515a698254c9c47bb84335281a170213b3ee5eb47feebe903e1be10087a167ce"},
|
||||
{file = "ruff-0.6.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6bb1b4995775f1837ab70f26698dd73852bbb82e8f70b175d2713c0354fe9182"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c546f412dfae8bb9cc4f27f0e45cdd554e42fecbb34f03312b93368e1cd0a6"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59627e97364329e4eae7d86fa7980c10e2b129e2293d25c478ebcb861b3e3fd6"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94c3f78c3d32190aafbb6bc5410c96cfed0a88aadb49c3f852bbc2aa9783a7d8"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:704da526c1e137f38c8a067a4a975fe6834b9f8ba7dbc5fd7503d58148851b8f"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efeede5815a24104579a0f6320660536c5ffc1c91ae94f8c65659af915fb9de9"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e368aef0cc02ca3593eae2fb8186b81c9c2b3f39acaaa1108eb6b4d04617e61f"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2653fc3b2a9315bd809725c88dd2446550099728d077a04191febb5ea79a4f79"},
|
||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bb858cd9ce2d062503337c5b9784d7b583bcf9d1a43c4df6ccb5eab774fbafcb"},
|
||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:488f8e15c01ea9afb8c0ba35d55bd951f484d0c1b7c5fd746ce3c47ccdedce68"},
|
||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:aefb0bd15f1cfa4c9c227b6120573bb3d6c4ee3b29fb54a5ad58f03859bc43c6"},
|
||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a4c0698cc780bcb2c61496cbd56b6a3ac0ad858c966652f7dbf4ceb029252fbe"},
|
||||
{file = "ruff-0.6.6-py3-none-win32.whl", hash = "sha256:aadf81ddc8ab5b62da7aae78a91ec933cbae9f8f1663ec0325dae2c364e4ad84"},
|
||||
{file = "ruff-0.6.6-py3-none-win_amd64.whl", hash = "sha256:0adb801771bc1f1b8cf4e0a6fdc30776e7c1894810ff3b344e50da82ef50eeb1"},
|
||||
{file = "ruff-0.6.6-py3-none-win_arm64.whl", hash = "sha256:4b4d32c137bc781c298964dd4e52f07d6f7d57c03eae97a72d97856844aa510a"},
|
||||
{file = "ruff-0.6.6.tar.gz", hash = "sha256:0fc030b6fd14814d69ac0196396f6761921bd20831725c7361e1b8100b818034"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"flutter": "3.24.0"
|
||||
"flutter": "3.24.3"
|
||||
}
|
||||
|
||||
2
mobile/.vscode/settings.json
vendored
2
mobile/.vscode/settings.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.24.0",
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.24.3",
|
||||
"search.exclude": {
|
||||
"**/.fvm": true
|
||||
},
|
||||
|
||||
@@ -46,21 +46,66 @@ custom_lint:
|
||||
- avoid_public_notifier_properties: false
|
||||
- avoid_manual_providers_as_generated_provider_dependency: false
|
||||
- unsupported_provider_value: false
|
||||
- photo_manager:
|
||||
exclude:
|
||||
- import_rule_photo_manager:
|
||||
message: photo_manager must only be used in MediaRepositories
|
||||
restrict: package:photo_manager
|
||||
allowed:
|
||||
# required / wanted
|
||||
- album_media.repository.dart
|
||||
- asset_media.repository.dart
|
||||
- file_media.repository.dart
|
||||
# acceptable exceptions for the time being
|
||||
- asset.entity.dart # to provide local AssetEntity for now
|
||||
- immich_local_image_provider.dart # accesses thumbnails via PhotoManager
|
||||
- immich_local_thumbnail_provider.dart # accesses thumbnails via PhotoManager
|
||||
# refactor to make the providers and services testable
|
||||
- backup.provider.dart # uses only PMProgressHandler
|
||||
- manual_upload.provider.dart # uses only PMProgressHandler
|
||||
- background.service.dart # uses only PMProgressHandler
|
||||
- backup.service.dart # uses only PMProgressHandler
|
||||
- 'lib/repositories/{album,asset,file}_media.repository.dart'
|
||||
# acceptable exceptions for the time being
|
||||
- lib/entities/asset.entity.dart # to provide local AssetEntity for now
|
||||
- lib/providers/image/immich_local_{image,thumbnail}_provider.dart # accesses thumbnails via PhotoManager
|
||||
# refactor to make the providers and services testable
|
||||
- lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler
|
||||
- lib/services/{background,backup}.service.dart # uses only PMProgressHandler
|
||||
- import_rule_isar:
|
||||
message: isar must only be used in entities and repositories
|
||||
restrict: package:isar
|
||||
allowed:
|
||||
# required / wanted
|
||||
- lib/entities/*.entity.dart
|
||||
- lib/repositories/{album,asset,backup,user}.repository.dart
|
||||
# acceptable exceptions for the time being
|
||||
- integration_test/test_utils/general_helper.dart
|
||||
- lib/main.dart
|
||||
- lib/routing/router.dart
|
||||
- lib/utils/{db,image_url_builder,migration,renderlist_generator}.dart
|
||||
- test/**.dart
|
||||
# refactor to make the providers and services testable
|
||||
- lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart
|
||||
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
|
||||
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart
|
||||
- lib/services/{asset,asset_description,background,backup,backup_verification,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart
|
||||
- lib/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart
|
||||
|
||||
- import_rule_openapi:
|
||||
message: openapi must only be used through ApiRepositories
|
||||
restrict: package:openapi
|
||||
allowed:
|
||||
# requried / wanted
|
||||
- lib/repositories/album_api.repository.dart
|
||||
# acceptable exceptions for the time being
|
||||
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
|
||||
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
||||
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
||||
# refactor
|
||||
- lib/models/activities/activity.model.dart
|
||||
- lib/models/map/map_marker.model.dart
|
||||
- lib/models/search/search_filter.model.dart
|
||||
- lib/models/server_info/server_{config,disk_info,features,version}.model.dart
|
||||
- lib/models/shared_link/shared_link.model.dart
|
||||
- lib/pages/search/search_input.page.dart
|
||||
- lib/providers/asset_viewer/asset_people.provider.dart
|
||||
- lib/providers/authentication.provider.dart
|
||||
- lib/providers/image/immich_remote_{image,thumbnail}_provider.dart
|
||||
- lib/providers/map/map_state.provider.dart
|
||||
- lib/providers/search/{people,search,search_filter}.provider.dart
|
||||
- lib/providers/websocket.provider.dart
|
||||
- lib/routing/auth_guard.dart
|
||||
- lib/services/{activity,api,asset,asset_description,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart
|
||||
- lib/widgets/album/album_thumbnail_listtile.dart
|
||||
- lib/widgets/forms/login/login_form.dart
|
||||
- lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart
|
||||
|
||||
dart_code_metrics:
|
||||
metrics:
|
||||
|
||||
@@ -1,36 +1,61 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:analyzer/error/listener.dart';
|
||||
import 'package:analyzer/error/error.dart' show ErrorSeverity;
|
||||
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:glob/glob.dart';
|
||||
|
||||
PluginBase createPlugin() => ImmichLinter();
|
||||
|
||||
class ImmichLinter extends PluginBase {
|
||||
@override
|
||||
List<LintRule> getLintRules(CustomLintConfigs configs) => [
|
||||
PhotoManagerRule(configs.rules[PhotoManagerRule._code.name]),
|
||||
];
|
||||
}
|
||||
|
||||
class PhotoManagerRule extends DartLintRule {
|
||||
PhotoManagerRule(LintOptions? options) : super(code: _code) {
|
||||
final excludeOption = options?.json["exclude"];
|
||||
if (excludeOption is String) {
|
||||
_excludePaths.add(excludeOption);
|
||||
} else if (excludeOption is List) {
|
||||
_excludePaths.addAll(excludeOption.map((option) => option));
|
||||
List<LintRule> getLintRules(CustomLintConfigs configs) {
|
||||
final List<LintRule> rules = [];
|
||||
for (final entry in configs.rules.entries) {
|
||||
if (entry.value.enabled && entry.key.startsWith("import_rule_")) {
|
||||
final code = makeCode(entry.key, entry.value);
|
||||
final allowedPaths = getStrings(entry.value, "allowed");
|
||||
final forbiddenPaths = getStrings(entry.value, "forbidden");
|
||||
final restrict = getStrings(entry.value, "restrict");
|
||||
rules.add(ImportRule(code, buildGlob(allowedPaths),
|
||||
buildGlob(forbiddenPaths), restrict));
|
||||
}
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
final Set<String> _excludePaths = HashSet();
|
||||
static makeCode(String name, LintOptions options) => LintCode(
|
||||
name: name,
|
||||
problemMessage: options.json["message"] as String,
|
||||
errorSeverity: ErrorSeverity.WARNING,
|
||||
);
|
||||
|
||||
static const _code = LintCode(
|
||||
name: 'photo_manager',
|
||||
problemMessage:
|
||||
'photo_manager library must only be used in MediaRepository',
|
||||
errorSeverity: ErrorSeverity.WARNING,
|
||||
);
|
||||
static List<String> getStrings(LintOptions options, String field) {
|
||||
final List<String> result = [];
|
||||
final excludeOption = options.json[field];
|
||||
if (excludeOption is String) {
|
||||
result.add(excludeOption);
|
||||
} else if (excludeOption is List) {
|
||||
result.addAll(excludeOption.map((option) => option));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Glob? buildGlob(List<String> globs) {
|
||||
if (globs.isEmpty) return null;
|
||||
if (globs.length == 1) return Glob(globs[0], caseSensitive: true);
|
||||
return Glob("{${globs.join(",")}}", caseSensitive: true);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class ImportRule extends DartLintRule {
|
||||
ImportRule(LintCode code, this._allowed, this._forbidden, this._restrict)
|
||||
: super(code: code);
|
||||
|
||||
final Glob? _allowed;
|
||||
final Glob? _forbidden;
|
||||
final List<String> _restrict;
|
||||
int _rootOffset = -1;
|
||||
|
||||
@override
|
||||
void run(
|
||||
@@ -38,11 +63,23 @@ class PhotoManagerRule extends DartLintRule {
|
||||
ErrorReporter reporter,
|
||||
CustomLintContext context,
|
||||
) {
|
||||
if (_excludePaths.contains(resolver.source.shortName)) return;
|
||||
if (_rootOffset == -1) {
|
||||
const project = "/immich/mobile/";
|
||||
_rootOffset = resolver.path.indexOf(project) + project.length;
|
||||
}
|
||||
final path = resolver.path.substring(_rootOffset);
|
||||
|
||||
if ((_allowed != null && _allowed!.matches(path)) &&
|
||||
(_forbidden == null || !_forbidden!.matches(path))) return;
|
||||
|
||||
context.registry.addImportDirective((node) {
|
||||
if (node.uri.stringValue?.startsWith("package:photo_manager") == true) {
|
||||
reporter.atNode(node, code);
|
||||
final uri = node.uri.stringValue;
|
||||
if (uri == null) return;
|
||||
for (final restricted in _restrict) {
|
||||
if (uri.startsWith(restricted) == true) {
|
||||
reporter.atNode(node, code);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
glob:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: glob
|
||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||
|
||||
@@ -8,6 +8,7 @@ dependencies:
|
||||
analyzer: ^6.8.0
|
||||
analyzer_plugin: ^0.11.3
|
||||
custom_lint_builder: ^0.6.4
|
||||
glob: ^2.1.2
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^4.0.0
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:isar/src/common/isar_links_common.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
part 'album.entity.g.dart';
|
||||
@@ -23,6 +25,7 @@ class Album {
|
||||
required this.activityEnabled,
|
||||
});
|
||||
|
||||
// fields stored in DB
|
||||
Id id = Isar.autoIncrement;
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? remoteId;
|
||||
@@ -41,9 +44,17 @@ class Album {
|
||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
||||
|
||||
// transient fields
|
||||
@ignore
|
||||
bool isAll = false;
|
||||
|
||||
@ignore
|
||||
String? remoteThumbnailAssetId;
|
||||
|
||||
@ignore
|
||||
int remoteAssetCount = 0;
|
||||
|
||||
// getters
|
||||
@ignore
|
||||
bool get isRemote => remoteId != null;
|
||||
|
||||
@@ -74,6 +85,18 @@ class Album {
|
||||
@ignore
|
||||
String get eTagKeyAssetCount => "device-album-$localId-asset-count";
|
||||
|
||||
// the following getter are needed because Isar links do not make data
|
||||
// accessible in an object freshly created (not loaded from DB)
|
||||
|
||||
@ignore
|
||||
Iterable<User> get remoteUsers => sharedUsers.isEmpty
|
||||
? (sharedUsers as IsarLinksCommon<User>).addedObjects
|
||||
: sharedUsers;
|
||||
|
||||
@ignore
|
||||
Iterable<Asset> get remoteAssets =>
|
||||
assets.isEmpty ? (assets as IsarLinksCommon<Asset>).addedObjects : assets;
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! Album) return false;
|
||||
@@ -129,6 +152,7 @@ class Album {
|
||||
endDate: dto.endDate,
|
||||
activityEnabled: dto.isActivityEnabled,
|
||||
);
|
||||
a.remoteAssetCount = dto.assetCount;
|
||||
a.owner.value = await db.users.getById(dto.ownerId);
|
||||
if (dto.albumThumbnailAssetId != null) {
|
||||
a.thumbnail.value = await db.assets
|
||||
@@ -164,7 +188,3 @@ extension AssetsHelper on IsarCollection<Album> {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
extension AlbumResponseDtoHelper on AlbumResponseDto {
|
||||
List<Asset> getAssets() => assets.map(Asset.remote).toList();
|
||||
}
|
||||
|
||||
40
mobile/lib/interfaces/album_api.interface.dart
Normal file
40
mobile/lib/interfaces/album_api.interface.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
|
||||
abstract interface class IAlbumApiRepository {
|
||||
Future<Album> get(String id);
|
||||
|
||||
Future<List<Album>> getAll({bool? shared});
|
||||
|
||||
Future<Album> create(
|
||||
String name, {
|
||||
required Iterable<String> assetIds,
|
||||
Iterable<String> sharedUserIds = const [],
|
||||
});
|
||||
|
||||
Future<Album> update(
|
||||
String albumId, {
|
||||
String? name,
|
||||
String? thumbnailAssetId,
|
||||
String? description,
|
||||
bool? activityEnabled,
|
||||
});
|
||||
|
||||
Future<void> delete(String albumId);
|
||||
|
||||
Future<({List<String> added, List<String> duplicates})> addAssets(
|
||||
String albumId,
|
||||
Iterable<String> assetIds,
|
||||
);
|
||||
|
||||
Future<({List<String> removed, List<String> failed})> removeAssets(
|
||||
String albumId,
|
||||
Iterable<String> assetIds,
|
||||
);
|
||||
|
||||
Future<Album> addUsers(
|
||||
String albumId,
|
||||
Iterable<String> userIds,
|
||||
);
|
||||
|
||||
Future<void> removeUser(String albumId, {required String userId});
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
|
||||
abstract interface class IAssetRepository {
|
||||
Future<Asset?> getByRemoteId(String id);
|
||||
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids);
|
||||
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy});
|
||||
Future<void> deleteById(List<int> ids);
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ import 'package:immich_mobile/entities/user.entity.dart';
|
||||
|
||||
abstract interface class IUserRepository {
|
||||
Future<List<User>> getByIds(List<String> ids);
|
||||
Future<User?> get(String id);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@ class ServerConfig {
|
||||
final int trashDays;
|
||||
final String oauthButtonText;
|
||||
final String externalDomain;
|
||||
final String mapDarkStyleUrl;
|
||||
final String mapLightStyleUrl;
|
||||
|
||||
const ServerConfig({
|
||||
required this.trashDays,
|
||||
required this.oauthButtonText,
|
||||
required this.externalDomain,
|
||||
required this.mapDarkStyleUrl,
|
||||
required this.mapLightStyleUrl,
|
||||
});
|
||||
|
||||
ServerConfig copyWith({
|
||||
@@ -20,6 +24,8 @@ class ServerConfig {
|
||||
trashDays: trashDays ?? this.trashDays,
|
||||
oauthButtonText: oauthButtonText ?? this.oauthButtonText,
|
||||
externalDomain: externalDomain ?? this.externalDomain,
|
||||
mapDarkStyleUrl: mapDarkStyleUrl,
|
||||
mapLightStyleUrl: mapLightStyleUrl,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +36,9 @@ class ServerConfig {
|
||||
ServerConfig.fromDto(ServerConfigDto dto)
|
||||
: trashDays = dto.trashDays,
|
||||
oauthButtonText = dto.oauthButtonText,
|
||||
externalDomain = dto.externalDomain;
|
||||
externalDomain = dto.externalDomain,
|
||||
mapDarkStyleUrl = dto.mapDarkStyleUrl,
|
||||
mapLightStyleUrl = dto.mapLightStyleUrl;
|
||||
|
||||
@override
|
||||
bool operator ==(covariant ServerConfig other) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/favorite_provider.dart';
|
||||
import 'package:immich_mobile/providers/favorite.provider.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -7,27 +8,27 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
|
||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||
import 'package:immich_mobile/models/map/map_event.model.dart';
|
||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/map/map_marker.provider.dart';
|
||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/utils/map_utils.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -304,7 +305,7 @@ class MapPage extends HookConsumerWidget {
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 16,
|
||||
child: ElevatedButton(
|
||||
onPressed: onZoomToLocation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
||||
@@ -313,6 +313,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
/// Those assets are unique and are used as the total assets
|
||||
///
|
||||
Future<void> _updateBackupAssetCount() async {
|
||||
// Save to persistent storage
|
||||
await _updatePersistentAlbumsSelection();
|
||||
|
||||
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
|
||||
final Set<BackupCandidate> assetsFromSelectedAlbums = {};
|
||||
final Set<BackupCandidate> assetsFromExcludedAlbums = {};
|
||||
@@ -408,9 +411,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||
);
|
||||
}
|
||||
|
||||
// Save to persistent storage
|
||||
await _updatePersistentAlbumsSelection();
|
||||
}
|
||||
|
||||
/// Get all necessary information for calculating the available albums,
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||
import 'package:immich_mobile/models/map/map_state.model.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'map_state.provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class MapStateNotifier extends _$MapStateNotifier {
|
||||
final _log = Logger("MapStateNotifier");
|
||||
|
||||
@override
|
||||
MapState build() {
|
||||
final appSettingsProvider = ref.read(appSettingsServiceProvider);
|
||||
|
||||
// Fetch and save the Style JSONs
|
||||
loadStyles();
|
||||
final lightStyleUrl =
|
||||
ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl;
|
||||
final darkStyleUrl =
|
||||
ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl;
|
||||
|
||||
return MapState(
|
||||
themeMode: ThemeMode.values[
|
||||
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
|
||||
@@ -34,65 +29,11 @@ class MapStateNotifier extends _$MapStateNotifier {
|
||||
appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapwithPartners),
|
||||
relativeTime:
|
||||
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
|
||||
lightStyleFetched: AsyncData(lightStyleUrl),
|
||||
darkStyleFetched: AsyncData(darkStyleUrl),
|
||||
);
|
||||
}
|
||||
|
||||
void loadStyles() async {
|
||||
final documents = (await getApplicationDocumentsDirectory()).path;
|
||||
|
||||
// Set to loading
|
||||
state = state.copyWith(lightStyleFetched: const AsyncLoading());
|
||||
|
||||
// Fetch and save light theme
|
||||
final lightResponse = await ref
|
||||
.read(apiServiceProvider)
|
||||
.mapApi
|
||||
.getMapStyleWithHttpInfo(MapTheme.light);
|
||||
|
||||
if (lightResponse.statusCode >= HttpStatus.badRequest) {
|
||||
state = state.copyWith(
|
||||
lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current),
|
||||
);
|
||||
_log.severe(
|
||||
"Cannot fetch map light style",
|
||||
lightResponse.toLoggerString(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final lightJSON = lightResponse.body;
|
||||
final lightFile = await File("$documents/map-style-light.json")
|
||||
.writeAsString(lightJSON, flush: true);
|
||||
|
||||
// Update state with path
|
||||
state =
|
||||
state.copyWith(lightStyleFetched: AsyncData(lightFile.absolute.path));
|
||||
|
||||
// Set to loading
|
||||
state = state.copyWith(darkStyleFetched: const AsyncLoading());
|
||||
|
||||
// Fetch and save dark theme
|
||||
final darkResponse = await ref
|
||||
.read(apiServiceProvider)
|
||||
.mapApi
|
||||
.getMapStyleWithHttpInfo(MapTheme.dark);
|
||||
|
||||
if (darkResponse.statusCode >= HttpStatus.badRequest) {
|
||||
state = state.copyWith(
|
||||
darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current),
|
||||
);
|
||||
_log.severe("Cannot fetch map dark style", darkResponse.toLoggerString());
|
||||
return;
|
||||
}
|
||||
|
||||
final darkJSON = darkResponse.body;
|
||||
final darkFile = await File("$documents/map-style-dark.json")
|
||||
.writeAsString(darkJSON, flush: true);
|
||||
|
||||
// Update state with path
|
||||
state = state.copyWith(darkStyleFetched: AsyncData(darkFile.absolute.path));
|
||||
}
|
||||
|
||||
void switchTheme(ThemeMode mode) {
|
||||
ref.read(appSettingsServiceProvider).setSetting(
|
||||
AppSettingsEnum.mapThemeMode,
|
||||
|
||||
@@ -34,6 +34,9 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
trashDays: 30,
|
||||
oauthButtonText: '',
|
||||
externalDomain: '',
|
||||
mapLightStyleUrl:
|
||||
'https://tiles.immich.cloud/v1/style/light.json',
|
||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
),
|
||||
serverDiskInfo: const ServerDiskInfo(
|
||||
diskAvailable: "0",
|
||||
|
||||
172
mobile/lib/repositories/album_api.repository.dart
Normal file
172
mobile/lib/repositories/album_api.repository.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/errors.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final albumApiRepositoryProvider = Provider(
|
||||
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||
);
|
||||
|
||||
class AlbumApiRepository implements IAlbumApiRepository {
|
||||
final AlbumsApi _api;
|
||||
|
||||
AlbumApiRepository(this._api);
|
||||
|
||||
@override
|
||||
Future<Album> get(String id) async {
|
||||
final dto = await _checkNull(_api.getAlbumInfo(id));
|
||||
return _toAlbum(dto);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Album>> getAll({bool? shared}) async {
|
||||
final dtos = await _checkNull(_api.getAllAlbums(shared: shared));
|
||||
return dtos.map(_toAlbum).toList().cast();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> create(
|
||||
String name, {
|
||||
required Iterable<String> assetIds,
|
||||
Iterable<String> sharedUserIds = const [],
|
||||
}) async {
|
||||
final users = sharedUserIds.map(
|
||||
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
|
||||
);
|
||||
final responseDto = await _checkNull(
|
||||
_api.createAlbum(
|
||||
CreateAlbumDto(
|
||||
albumName: name,
|
||||
assetIds: assetIds.toList(),
|
||||
albumUsers: users.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
return _toAlbum(responseDto);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> update(
|
||||
String albumId, {
|
||||
String? name,
|
||||
String? thumbnailAssetId,
|
||||
String? description,
|
||||
bool? activityEnabled,
|
||||
}) async {
|
||||
final response = await _checkNull(
|
||||
_api.updateAlbumInfo(
|
||||
albumId,
|
||||
UpdateAlbumDto(
|
||||
albumName: name,
|
||||
albumThumbnailAssetId: thumbnailAssetId,
|
||||
description: description,
|
||||
isActivityEnabled: activityEnabled,
|
||||
),
|
||||
),
|
||||
);
|
||||
return _toAlbum(response);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String albumId) {
|
||||
return _api.deleteAlbum(albumId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<({List<String> added, List<String> duplicates})> addAssets(
|
||||
String albumId,
|
||||
Iterable<String> assetIds,
|
||||
) async {
|
||||
final response = await _checkNull(
|
||||
_api.addAssetsToAlbum(
|
||||
albumId,
|
||||
BulkIdsDto(ids: assetIds.toList()),
|
||||
),
|
||||
);
|
||||
|
||||
final List<String> added = [];
|
||||
final List<String> duplicates = [];
|
||||
|
||||
for (final result in response) {
|
||||
if (result.success) {
|
||||
added.add(result.id);
|
||||
} else if (result.error == BulkIdResponseDtoErrorEnum.duplicate) {
|
||||
duplicates.add(result.id);
|
||||
}
|
||||
}
|
||||
return (added: added, duplicates: duplicates);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<({List<String> removed, List<String> failed})> removeAssets(
|
||||
String albumId,
|
||||
Iterable<String> assetIds,
|
||||
) async {
|
||||
final response = await _checkNull(
|
||||
_api.removeAssetFromAlbum(
|
||||
albumId,
|
||||
BulkIdsDto(ids: assetIds.toList()),
|
||||
),
|
||||
);
|
||||
final List<String> removed = [], failed = [];
|
||||
for (final dto in response) {
|
||||
if (dto.success) {
|
||||
removed.add(dto.id);
|
||||
} else {
|
||||
failed.add(dto.id);
|
||||
}
|
||||
}
|
||||
return (removed: removed, failed: failed);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
|
||||
final albumUsers =
|
||||
userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
|
||||
final response = await _checkNull(
|
||||
_api.addUsersToAlbum(
|
||||
albumId,
|
||||
AddUsersDto(albumUsers: albumUsers),
|
||||
),
|
||||
);
|
||||
return _toAlbum(response);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeUser(String albumId, {required String userId}) {
|
||||
return _api.removeUserFromAlbum(albumId, userId);
|
||||
}
|
||||
|
||||
static Future<T> _checkNull<T>(Future<T?> future) async {
|
||||
final response = await future;
|
||||
if (response == null) throw NoResponseDtoError();
|
||||
return response;
|
||||
}
|
||||
|
||||
static Album _toAlbum(AlbumResponseDto dto) {
|
||||
final Album album = Album(
|
||||
remoteId: dto.id,
|
||||
name: dto.albumName,
|
||||
createdAt: dto.createdAt,
|
||||
modifiedAt: dto.updatedAt,
|
||||
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
||||
shared: dto.shared,
|
||||
startDate: dto.startDate,
|
||||
endDate: dto.endDate,
|
||||
activityEnabled: dto.isActivityEnabled,
|
||||
);
|
||||
album.remoteAssetCount = dto.assetCount;
|
||||
album.owner.value = User.fromSimpleUserDto(dto.owner);
|
||||
album.remoteThumbnailAssetId = dto.albumThumbnailAssetId;
|
||||
final users = dto.albumUsers
|
||||
.map((albumUser) => User.fromSimpleUserDto(albumUser.user));
|
||||
album.sharedUsers.addAll(users);
|
||||
final assets = dto.assets.map(Asset.remote).toList();
|
||||
album.assets.addAll(assets);
|
||||
return album;
|
||||
}
|
||||
}
|
||||
@@ -28,4 +28,11 @@ class AssetRepository implements IAssetRepository {
|
||||
@override
|
||||
Future<void> deleteById(List<int> ids) =>
|
||||
_db.writeTxn(() => _db.assets.deleteAll(ids));
|
||||
|
||||
@override
|
||||
Future<Asset?> getByRemoteId(String id) => _db.assets.getByRemoteId(id);
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
|
||||
_db.assets.getAllByRemoteId(ids);
|
||||
}
|
||||
|
||||
@@ -17,4 +17,7 @@ class UserRepository implements IUserRepository {
|
||||
@override
|
||||
Future<List<User>> getByIds(List<String> ids) async =>
|
||||
(await _db.users.getAllById(ids)).cast();
|
||||
|
||||
@override
|
||||
Future<User?> get(String id) => _db.users.getById(id);
|
||||
}
|
||||
|
||||
@@ -6,63 +6,61 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final albumServiceProvider = Provider(
|
||||
(ref) => AlbumService(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(entityServiceProvider),
|
||||
ref.watch(albumRepositoryProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(albumApiRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class AlbumService {
|
||||
final ApiService _apiService;
|
||||
final UserService _userService;
|
||||
final SyncService _syncService;
|
||||
final EntityService _entityService;
|
||||
final IAlbumRepository _albumRepository;
|
||||
final IAssetRepository _assetRepository;
|
||||
final IUserRepository _userRepository;
|
||||
final IBackupRepository _backupAlbumRepository;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IAlbumApiRepository _albumApiRepository;
|
||||
final Logger _log = Logger('AlbumService');
|
||||
Completer<bool> _localCompleter = Completer()..complete(false);
|
||||
Completer<bool> _remoteCompleter = Completer()..complete(false);
|
||||
|
||||
AlbumService(
|
||||
this._apiService,
|
||||
this._userService,
|
||||
this._syncService,
|
||||
this._entityService,
|
||||
this._albumRepository,
|
||||
this._assetRepository,
|
||||
this._userRepository,
|
||||
this._backupAlbumRepository,
|
||||
this._albumMediaRepository,
|
||||
this._albumApiRepository,
|
||||
);
|
||||
|
||||
/// Checks all selected device albums for changes of albums and their assets
|
||||
@@ -164,17 +162,11 @@ class AlbumService {
|
||||
bool changes = false;
|
||||
try {
|
||||
await _userService.refreshUsers();
|
||||
final List<AlbumResponseDto>? serverAlbums = await _apiService.albumsApi
|
||||
.getAllAlbums(shared: isShared ? true : null);
|
||||
if (serverAlbums == null) {
|
||||
return false;
|
||||
}
|
||||
final List<Album> serverAlbums =
|
||||
await _albumApiRepository.getAll(shared: isShared ? true : null);
|
||||
changes = await _syncService.syncRemoteAlbumsToDb(
|
||||
serverAlbums,
|
||||
isShared: isShared,
|
||||
loadDetails: (dto) async => dto.assetCount == dto.assets.length
|
||||
? dto
|
||||
: (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto,
|
||||
);
|
||||
} finally {
|
||||
_remoteCompleter.complete(changes);
|
||||
@@ -188,30 +180,13 @@ class AlbumService {
|
||||
Iterable<Asset> assets, [
|
||||
Iterable<User> sharedUsers = const [],
|
||||
]) async {
|
||||
try {
|
||||
AlbumResponseDto? remote = await _apiService.albumsApi.createAlbum(
|
||||
CreateAlbumDto(
|
||||
albumName: albumName,
|
||||
assetIds: assets.map((asset) => asset.remoteId!).toList(),
|
||||
albumUsers: sharedUsers
|
||||
.map(
|
||||
(e) => AlbumUserCreateDto(
|
||||
userId: e.id,
|
||||
role: AlbumUserRole.editor,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
if (remote != null) {
|
||||
final Album album = await Album.remote(remote);
|
||||
await _albumRepository.create(album);
|
||||
return album;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error createSharedAlbum ${e.toString()}");
|
||||
}
|
||||
return null;
|
||||
final Album album = await _albumApiRepository.create(
|
||||
albumName,
|
||||
assetIds: assets.map((asset) => asset.remoteId!),
|
||||
sharedUserIds: sharedUsers.map((user) => user.id),
|
||||
);
|
||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||
return _albumRepository.create(album);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -243,32 +218,21 @@ class AlbumService {
|
||||
Album album,
|
||||
) async {
|
||||
try {
|
||||
var response = await _apiService.albumsApi.addAssetsToAlbum(
|
||||
final result = await _albumApiRepository.addAssets(
|
||||
album.remoteId!,
|
||||
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
|
||||
assets.map((asset) => asset.remoteId!),
|
||||
);
|
||||
|
||||
if (response != null) {
|
||||
List<Asset> successAssets = [];
|
||||
List<String> duplicatedAssets = [];
|
||||
final List<Asset> addedAssets = result.added
|
||||
.map((id) => assets.firstWhere((asset) => asset.remoteId == id))
|
||||
.toList();
|
||||
|
||||
for (final result in response) {
|
||||
if (result.success) {
|
||||
successAssets
|
||||
.add(assets.firstWhere((asset) => asset.remoteId == result.id));
|
||||
} else if (!result.success &&
|
||||
result.error == BulkIdResponseDtoErrorEnum.duplicate) {
|
||||
duplicatedAssets.add(result.id);
|
||||
}
|
||||
}
|
||||
await _updateAssets(album.id, add: addedAssets);
|
||||
|
||||
await _updateAssets(album.id, add: successAssets);
|
||||
|
||||
return AlbumAddAssetsResponse(
|
||||
alreadyInAlbum: duplicatedAssets,
|
||||
successfullyAdded: successAssets.length,
|
||||
);
|
||||
}
|
||||
return AlbumAddAssetsResponse(
|
||||
alreadyInAlbum: result.duplicates,
|
||||
successfullyAdded: addedAssets.length,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||
}
|
||||
@@ -293,20 +257,11 @@ class AlbumService {
|
||||
Album album,
|
||||
) async {
|
||||
try {
|
||||
final List<AlbumUserAddDto> albumUsers = sharedUserIds
|
||||
.map((userId) => AlbumUserAddDto(userId: userId))
|
||||
.toList();
|
||||
|
||||
final result = await _apiService.albumsApi.addUsersToAlbum(
|
||||
album.remoteId!,
|
||||
AddUsersDto(albumUsers: albumUsers),
|
||||
);
|
||||
if (result != null) {
|
||||
album.sharedUsers.addAll(await _userRepository.getByIds(sharedUserIds));
|
||||
album.shared = result.shared;
|
||||
await _albumRepository.update(album);
|
||||
return true;
|
||||
}
|
||||
final updatedAlbum =
|
||||
await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds);
|
||||
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
||||
await _albumRepository.update(updatedAlbum);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
||||
}
|
||||
@@ -315,15 +270,13 @@ class AlbumService {
|
||||
|
||||
Future<bool> setActivityEnabled(Album album, bool enabled) async {
|
||||
try {
|
||||
final result = await _apiService.albumsApi.updateAlbumInfo(
|
||||
final updatedAlbum = await _albumApiRepository.update(
|
||||
album.remoteId!,
|
||||
UpdateAlbumDto(isActivityEnabled: enabled),
|
||||
activityEnabled: enabled,
|
||||
);
|
||||
if (result != null) {
|
||||
album.activityEnabled = enabled;
|
||||
await _albumRepository.update(album);
|
||||
return true;
|
||||
}
|
||||
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
||||
await _albumRepository.update(updatedAlbum);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error setActivityEnabled ${e.toString()}");
|
||||
}
|
||||
@@ -334,7 +287,7 @@ class AlbumService {
|
||||
try {
|
||||
final user = Store.get(StoreKey.currentUser);
|
||||
if (album.owner.value?.isarId == user.isarId) {
|
||||
await _apiService.albumsApi.deleteAlbum(album.remoteId!);
|
||||
await _albumApiRepository.delete(album.remoteId!);
|
||||
}
|
||||
if (album.shared) {
|
||||
final foreignAssets =
|
||||
@@ -365,7 +318,7 @@ class AlbumService {
|
||||
|
||||
Future<bool> leaveAlbum(Album album) async {
|
||||
try {
|
||||
await _apiService.albumsApi.removeUserFromAlbum(album.remoteId!, "me");
|
||||
await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error leaveAlbum ${e.toString()}");
|
||||
@@ -378,21 +331,14 @@ class AlbumService {
|
||||
Iterable<Asset> assets,
|
||||
) async {
|
||||
try {
|
||||
final response = await _apiService.albumsApi.removeAssetFromAlbum(
|
||||
final result = await _albumApiRepository.removeAssets(
|
||||
album.remoteId!,
|
||||
BulkIdsDto(
|
||||
ids: assets.map((asset) => asset.remoteId!).toList(),
|
||||
),
|
||||
assets.map((asset) => asset.remoteId!),
|
||||
);
|
||||
if (response != null) {
|
||||
final toRemove = response.every((e) => e.success)
|
||||
? assets
|
||||
: response
|
||||
.where((e) => e.success)
|
||||
.map((e) => assets.firstWhere((a) => a.remoteId == e.id));
|
||||
await _updateAssets(album.id, remove: toRemove.toList());
|
||||
return true;
|
||||
}
|
||||
final toRemove = result.removed
|
||||
.map((id) => assets.firstWhere((asset) => asset.remoteId == id));
|
||||
await _updateAssets(album.id, remove: toRemove.toList());
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
|
||||
}
|
||||
@@ -404,9 +350,9 @@ class AlbumService {
|
||||
User user,
|
||||
) async {
|
||||
try {
|
||||
await _apiService.albumsApi.removeUserFromAlbum(
|
||||
await _albumApiRepository.removeUser(
|
||||
album.remoteId!,
|
||||
user.id,
|
||||
userId: user.id,
|
||||
);
|
||||
|
||||
album.sharedUsers.remove(user);
|
||||
@@ -427,15 +373,12 @@ class AlbumService {
|
||||
String newAlbumTitle,
|
||||
) async {
|
||||
try {
|
||||
await _apiService.albumsApi.updateAlbumInfo(
|
||||
album = await _albumApiRepository.update(
|
||||
album.remoteId!,
|
||||
UpdateAlbumDto(
|
||||
albumName: newAlbumTitle,
|
||||
),
|
||||
name: newAlbumTitle,
|
||||
);
|
||||
album.name = newAlbumTitle;
|
||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||
await _albumRepository.update(album);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error changeTitleAlbum ${e.toString()}");
|
||||
@@ -456,12 +399,8 @@ class AlbumService {
|
||||
for (final albumName in albumNames) {
|
||||
Album? album = await getAlbumByName(albumName, true);
|
||||
album ??= await createAlbum(albumName, []);
|
||||
|
||||
if (album != null && album.remoteId != null) {
|
||||
await _apiService.albumsApi.addAssetsToAlbum(
|
||||
album.remoteId!,
|
||||
BulkIdsDto(ids: assetIds),
|
||||
);
|
||||
await _albumApiRepository.addAssets(album.remoteId!, assetIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ import 'package:immich_mobile/main.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
import 'package:immich_mobile/services/hash.service.dart';
|
||||
import 'package:immich_mobile/services/localization.service.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
@@ -363,23 +365,33 @@ class BackgroundService {
|
||||
PartnerService partnerService = PartnerService(apiService, db);
|
||||
AlbumRepository albumRepository = AlbumRepository(db);
|
||||
AssetRepository assetRepository = AssetRepository(db);
|
||||
UserRepository userRepository = UserRepository(db);
|
||||
BackupRepository backupAlbumRepository = BackupRepository(db);
|
||||
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
||||
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
||||
UserRepository userRepository = UserRepository(db);
|
||||
AlbumApiRepository albumApiRepository =
|
||||
AlbumApiRepository(apiService.albumsApi);
|
||||
HashService hashService = HashService(db, this, albumMediaRepository);
|
||||
SyncService syncSerive = SyncService(db, hashService, albumMediaRepository);
|
||||
EntityService entityService =
|
||||
EntityService(assetRepository, userRepository);
|
||||
SyncService syncSerive = SyncService(
|
||||
db,
|
||||
hashService,
|
||||
entityService,
|
||||
albumMediaRepository,
|
||||
albumApiRepository,
|
||||
);
|
||||
UserService userService =
|
||||
UserService(apiService, db, syncSerive, partnerService);
|
||||
AlbumService albumService = AlbumService(
|
||||
apiService,
|
||||
userService,
|
||||
syncSerive,
|
||||
entityService,
|
||||
albumRepository,
|
||||
assetRepository,
|
||||
userRepository,
|
||||
backupAlbumRepository,
|
||||
albumMediaRepository,
|
||||
albumApiRepository,
|
||||
);
|
||||
BackupService backupService = BackupService(
|
||||
apiService,
|
||||
|
||||
52
mobile/lib/services/entity.service.dart
Normal file
52
mobile/lib/services/entity.service.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||
|
||||
class EntityService {
|
||||
final IAssetRepository _assetRepository;
|
||||
final IUserRepository _userRepository;
|
||||
EntityService(
|
||||
this._assetRepository,
|
||||
this._userRepository,
|
||||
);
|
||||
|
||||
Future<Album> fillAlbumWithDatabaseEntities(Album album) async {
|
||||
final ownerId = album.ownerId;
|
||||
if (ownerId != null) {
|
||||
// replace owner with user from database
|
||||
album.owner.value = await _userRepository.get(ownerId);
|
||||
}
|
||||
final thumbnailAssetId =
|
||||
album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId;
|
||||
if (thumbnailAssetId != null) {
|
||||
// set thumbnail with asset from database
|
||||
album.thumbnail.value =
|
||||
await _assetRepository.getByRemoteId(thumbnailAssetId);
|
||||
}
|
||||
if (album.remoteUsers.isNotEmpty) {
|
||||
// replace all users with users from database
|
||||
final users = await _userRepository
|
||||
.getByIds(album.remoteUsers.map((user) => user.id).toList());
|
||||
album.sharedUsers.clear();
|
||||
album.sharedUsers.addAll(users);
|
||||
}
|
||||
if (album.remoteAssets.isNotEmpty) {
|
||||
// replace all assets with assets from database
|
||||
final assets = await _assetRepository
|
||||
.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!));
|
||||
album.assets.clear();
|
||||
album.assets.addAll(assets);
|
||||
}
|
||||
return album;
|
||||
}
|
||||
}
|
||||
|
||||
final entityServiceProvider = Provider(
|
||||
(ref) => EntityService(
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
),
|
||||
);
|
||||
@@ -65,7 +65,19 @@ class HashService {
|
||||
if (hashes[i] != null) {
|
||||
continue;
|
||||
}
|
||||
final file = await assets[i].local!.originFile;
|
||||
|
||||
File? file;
|
||||
|
||||
try {
|
||||
file = await assets[i].local!.originFile;
|
||||
} catch (error, stackTrace) {
|
||||
_log.warning(
|
||||
"Error getting file to hash for asset ${assets[i].localId}, name: ${assets[i].fileName}, created on: ${assets[i].fileCreatedAt}, skipping",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
final fileName = assets[i].fileName;
|
||||
|
||||
|
||||
@@ -8,9 +8,12 @@ import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
import 'package:immich_mobile/services/hash.service.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
@@ -18,24 +21,33 @@ import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final syncServiceProvider = Provider(
|
||||
(ref) => SyncService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(hashServiceProvider),
|
||||
ref.watch(entityServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(albumApiRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class SyncService {
|
||||
final Isar _db;
|
||||
final HashService _hashService;
|
||||
final EntityService _entityService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IAlbumApiRepository _albumApiRepository;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
|
||||
SyncService(this._db, this._hashService, this._albumMediaRepository);
|
||||
SyncService(
|
||||
this._db,
|
||||
this._hashService,
|
||||
this._entityService,
|
||||
this._albumMediaRepository,
|
||||
this._albumApiRepository,
|
||||
);
|
||||
|
||||
// public methods:
|
||||
|
||||
@@ -65,11 +77,10 @@ class SyncService {
|
||||
/// Syncs remote albums to the database
|
||||
/// returns `true` if there were any changes
|
||||
Future<bool> syncRemoteAlbumsToDb(
|
||||
List<AlbumResponseDto> remote, {
|
||||
List<Album> remote, {
|
||||
required bool isShared,
|
||||
required FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
}) =>
|
||||
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails));
|
||||
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared));
|
||||
|
||||
/// Syncs all device albums and their assets to the database
|
||||
/// Returns `true` if there were any changes
|
||||
@@ -289,11 +300,10 @@ class SyncService {
|
||||
/// Syncs remote albums to the database
|
||||
/// returns `true` if there were any changes
|
||||
Future<bool> _syncRemoteAlbumsToDb(
|
||||
List<AlbumResponseDto> remote,
|
||||
List<Album> remoteAlbums,
|
||||
bool isShared,
|
||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
) async {
|
||||
remote.sortBy((e) => e.id);
|
||||
remoteAlbums.sortBy((e) => e.remoteId!);
|
||||
|
||||
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
|
||||
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
|
||||
@@ -310,14 +320,14 @@ class SyncService {
|
||||
final List<Asset> existing = [];
|
||||
|
||||
final bool changes = await diffSortedLists(
|
||||
remote,
|
||||
remoteAlbums,
|
||||
dbAlbums,
|
||||
compare: (AlbumResponseDto a, Album b) => a.id.compareTo(b.remoteId!),
|
||||
both: (AlbumResponseDto a, Album b) =>
|
||||
_syncRemoteAlbum(a, b, toDelete, existing, loadDetails),
|
||||
onlyFirst: (AlbumResponseDto a) =>
|
||||
_addAlbumFromServer(a, existing, loadDetails),
|
||||
onlySecond: (Album a) => _removeAlbumFromDb(a, toDelete),
|
||||
compare: (remoteAlbum, dbAlbum) =>
|
||||
remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!),
|
||||
both: (remoteAlbum, dbAlbum) =>
|
||||
_syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing),
|
||||
onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing),
|
||||
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
|
||||
);
|
||||
|
||||
if (isShared && toDelete.isNotEmpty) {
|
||||
@@ -338,26 +348,22 @@ class SyncService {
|
||||
/// syncing changes from local back to server)
|
||||
/// accumulates
|
||||
Future<bool> _syncRemoteAlbum(
|
||||
AlbumResponseDto dto,
|
||||
Album dto,
|
||||
Album album,
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing,
|
||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
) async {
|
||||
if (!_hasAlbumResponseDtoChanged(dto, album)) {
|
||||
if (!_hasRemoteAlbumChanged(dto, album)) {
|
||||
return false;
|
||||
}
|
||||
// loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp,
|
||||
// i.e. it will always be null. Save it here.
|
||||
final originalDto = dto;
|
||||
dto = await loadDetails(dto);
|
||||
if (dto.assetCount != dto.assets.length) {
|
||||
return false;
|
||||
}
|
||||
dto = await _albumApiRepository.get(dto.remoteId!);
|
||||
final assetsInDb =
|
||||
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||
final List<Asset> assetsOnRemote = dto.getAssets();
|
||||
final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
|
||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
||||
assetsOnRemote,
|
||||
@@ -368,15 +374,16 @@ class SyncService {
|
||||
// update shared users
|
||||
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
||||
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
||||
dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id));
|
||||
final List<User> users = dto.remoteUsers.toList()
|
||||
..sort((a, b) => a.id.compareTo(b.id));
|
||||
final List<String> userIdsToAdd = [];
|
||||
final List<User> usersToUnlink = [];
|
||||
diffSortedListsSync(
|
||||
dto.albumUsers,
|
||||
users,
|
||||
sharedUsers,
|
||||
compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id),
|
||||
compare: (User a, User b) => a.id.compareTo(b.id),
|
||||
both: (a, b) => false,
|
||||
onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id),
|
||||
onlyFirst: (User a) => userIdsToAdd.add(a.id),
|
||||
onlySecond: (User a) => usersToUnlink.add(a),
|
||||
);
|
||||
|
||||
@@ -386,19 +393,19 @@ class SyncService {
|
||||
final assetsToLink = existingInDb + updated;
|
||||
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
||||
|
||||
album.name = dto.albumName;
|
||||
album.name = dto.name;
|
||||
album.shared = dto.shared;
|
||||
album.createdAt = dto.createdAt;
|
||||
album.modifiedAt = dto.updatedAt;
|
||||
album.modifiedAt = dto.modifiedAt;
|
||||
album.startDate = dto.startDate;
|
||||
album.endDate = dto.endDate;
|
||||
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
||||
album.shared = dto.shared;
|
||||
album.activityEnabled = dto.isActivityEnabled;
|
||||
if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
|
||||
album.activityEnabled = dto.activityEnabled;
|
||||
if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) {
|
||||
album.thumbnail.value = await _db.assets
|
||||
.where()
|
||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
||||
.remoteIdEqualTo(dto.remoteThumbnailAssetId)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@@ -434,27 +441,26 @@ class SyncService {
|
||||
/// (shared) assets to the database beforehand
|
||||
/// accumulates assets already existing in the database
|
||||
Future<void> _addAlbumFromServer(
|
||||
AlbumResponseDto dto,
|
||||
Album album,
|
||||
List<Asset> existing,
|
||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
) async {
|
||||
if (dto.assetCount != dto.assets.length) {
|
||||
dto = await loadDetails(dto);
|
||||
if (album.remoteAssetCount != album.remoteAssets.length) {
|
||||
album = await _albumApiRepository.get(album.remoteId!);
|
||||
}
|
||||
if (dto.assetCount == dto.assets.length) {
|
||||
if (album.remoteAssetCount == album.remoteAssets.length) {
|
||||
// in case an album contains assets not yet present in local DB:
|
||||
// put missing album assets into local DB
|
||||
final (existingInDb, updated) =
|
||||
await _linkWithExistingFromDb(dto.getAssets());
|
||||
await _linkWithExistingFromDb(album.remoteAssets.toList());
|
||||
existing.addAll(existingInDb);
|
||||
await upsertAssetsWithExif(updated);
|
||||
|
||||
final Album a = await Album.remote(dto);
|
||||
await _db.writeTxn(() => _db.albums.store(a));
|
||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||
await _db.writeTxn(() => _db.albums.store(album));
|
||||
} else {
|
||||
_log.warning(
|
||||
"Failed to add album from server: assetCount ${dto.assetCount} != "
|
||||
"asset array length ${dto.assets.length} for album ${dto.albumName}");
|
||||
"Failed to add album from server: assetCount ${album.remoteAssetCount} != "
|
||||
"asset array length ${album.remoteAssets.length} for album ${album.name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,17 +925,17 @@ class SyncService {
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
|
||||
return dto.assetCount != a.assetCount ||
|
||||
dto.albumName != a.name ||
|
||||
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
|
||||
dto.shared != a.shared ||
|
||||
dto.albumUsers.length != a.sharedUsers.length ||
|
||||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
|
||||
!isAtSameMomentAs(dto.startDate, a.startDate) ||
|
||||
!isAtSameMomentAs(dto.endDate, a.endDate) ||
|
||||
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
|
||||
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
|
||||
remoteAlbum.name != dbAlbum.name ||
|
||||
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
|
||||
remoteAlbum.shared != dbAlbum.shared ||
|
||||
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||
|
||||
!remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||
!isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) ||
|
||||
!isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) ||
|
||||
!isAtSameMomentAs(
|
||||
dto.lastModifiedAssetTimestamp,
|
||||
a.lastModifiedAssetTimestamp,
|
||||
remoteAlbum.lastModifiedAssetTimestamp,
|
||||
dbAlbum.lastModifiedAssetTimestamp,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,19 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
addDefault(value, 'tags', TagsResponse().toJson());
|
||||
}
|
||||
break;
|
||||
case 'ServerConfigDto':
|
||||
if (value is Map) {
|
||||
addDefault(
|
||||
value,
|
||||
'mapLightStyleUrl',
|
||||
'https://tiles.immich.cloud/v1/style/light.json',
|
||||
);
|
||||
addDefault(
|
||||
value,
|
||||
'mapDarkStyleUrl',
|
||||
'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
);
|
||||
}
|
||||
case 'UserResponseDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||
|
||||
5
mobile/openapi/README.md
generated
5
mobile/openapi/README.md
generated
@@ -116,6 +116,7 @@ Class | Method | HTTP request | Description
|
||||
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||
*DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets |
|
||||
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
|
||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
||||
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
|
||||
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates |
|
||||
@@ -137,7 +138,6 @@ Class | Method | HTTP request | Description
|
||||
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} |
|
||||
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate |
|
||||
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers |
|
||||
*MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json |
|
||||
*MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode |
|
||||
*MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |
|
||||
*MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories |
|
||||
@@ -172,6 +172,7 @@ Class | Method | HTTP request | Description
|
||||
*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata |
|
||||
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
|
||||
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
|
||||
*SearchApi* | [**searchRandom**](doc//SearchApi.md#searchrandom) | **POST** /search/random |
|
||||
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
|
||||
*ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license |
|
||||
*ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about |
|
||||
@@ -346,7 +347,6 @@ Class | Method | HTTP request | Description
|
||||
- [ManualJobName](doc//ManualJobName.md)
|
||||
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
||||
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
|
||||
- [MapTheme](doc//MapTheme.md)
|
||||
- [MemoriesResponse](doc//MemoriesResponse.md)
|
||||
- [MemoriesUpdate](doc//MemoriesUpdate.md)
|
||||
- [MemoryCreateDto](doc//MemoryCreateDto.md)
|
||||
@@ -379,6 +379,7 @@ Class | Method | HTTP request | Description
|
||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||
- [RandomSearchDto](doc//RandomSearchDto.md)
|
||||
- [RatingsResponse](doc//RatingsResponse.md)
|
||||
- [RatingsUpdate](doc//RatingsUpdate.md)
|
||||
- [ReactionLevel](doc//ReactionLevel.md)
|
||||
|
||||
2
mobile/openapi/lib/api.dart
generated
2
mobile/openapi/lib/api.dart
generated
@@ -159,7 +159,6 @@ part 'model/logout_response_dto.dart';
|
||||
part 'model/manual_job_name.dart';
|
||||
part 'model/map_marker_response_dto.dart';
|
||||
part 'model/map_reverse_geocode_response_dto.dart';
|
||||
part 'model/map_theme.dart';
|
||||
part 'model/memories_response.dart';
|
||||
part 'model/memories_update.dart';
|
||||
part 'model/memory_create_dto.dart';
|
||||
@@ -192,6 +191,7 @@ part 'model/places_response_dto.dart';
|
||||
part 'model/purchase_response.dart';
|
||||
part 'model/purchase_update.dart';
|
||||
part 'model/queue_status_dto.dart';
|
||||
part 'model/random_search_dto.dart';
|
||||
part 'model/ratings_response.dart';
|
||||
part 'model/ratings_update.dart';
|
||||
part 'model/reaction_level.dart';
|
||||
|
||||
7
mobile/openapi/lib/api/assets_api.dart
generated
7
mobile/openapi/lib/api/assets_api.dart
generated
@@ -449,7 +449,10 @@ class AssetsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /assets/random' operation and returns the [Response].
|
||||
/// This property was deprecated in v1.116.0
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
@@ -482,6 +485,8 @@ class AssetsApi {
|
||||
);
|
||||
}
|
||||
|
||||
/// This property was deprecated in v1.116.0
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
|
||||
59
mobile/openapi/lib/api/deprecated_api.dart
generated
59
mobile/openapi/lib/api/deprecated_api.dart
generated
@@ -71,4 +71,63 @@ class DeprecatedApi {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// This property was deprecated in v1.116.0
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/assets/random';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (count != null) {
|
||||
queryParams.addAll(_queryParams('', 'count', count));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// This property was deprecated in v1.116.0
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||
final response = await getRandomWithHttpInfo( count: count, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
||||
.cast<AssetResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
56
mobile/openapi/lib/api/map_api.dart
generated
56
mobile/openapi/lib/api/map_api.dart
generated
@@ -105,62 +105,6 @@ class MapApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /map/style.json' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [MapTheme] theme (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
Future<Response> getMapStyleWithHttpInfo(MapTheme theme, { String? key, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/map/style.json';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
queryParams.addAll(_queryParams('', 'theme', theme));
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [MapTheme] theme (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
Future<Object?> getMapStyle(MapTheme theme, { String? key, }) async {
|
||||
final response = await getMapStyleWithHttpInfo(theme, key: key, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'Object',) as Object;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /map/reverse-geocode' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
47
mobile/openapi/lib/api/search_api.dart
generated
47
mobile/openapi/lib/api/search_api.dart
generated
@@ -351,6 +351,53 @@ class SearchApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /search/random' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [RandomSearchDto] randomSearchDto (required):
|
||||
Future<Response> searchRandomWithHttpInfo(RandomSearchDto randomSearchDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/search/random';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = randomSearchDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [RandomSearchDto] randomSearchDto (required):
|
||||
Future<SearchResponseDto?> searchRandom(RandomSearchDto randomSearchDto,) async {
|
||||
final response = await searchRandomWithHttpInfo(randomSearchDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /search/smart' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
5
mobile/openapi/lib/api_client.dart
generated
5
mobile/openapi/lib/api_client.dart
generated
@@ -166,7 +166,6 @@ class ApiClient {
|
||||
|
||||
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
|
||||
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
|
||||
upgradeDto(value, targetType);
|
||||
try {
|
||||
switch (targetType) {
|
||||
case 'String':
|
||||
@@ -373,8 +372,6 @@ class ApiClient {
|
||||
return MapMarkerResponseDto.fromJson(value);
|
||||
case 'MapReverseGeocodeResponseDto':
|
||||
return MapReverseGeocodeResponseDto.fromJson(value);
|
||||
case 'MapTheme':
|
||||
return MapThemeTypeTransformer().decode(value);
|
||||
case 'MemoriesResponse':
|
||||
return MemoriesResponse.fromJson(value);
|
||||
case 'MemoriesUpdate':
|
||||
@@ -439,6 +436,8 @@ class ApiClient {
|
||||
return PurchaseUpdate.fromJson(value);
|
||||
case 'QueueStatusDto':
|
||||
return QueueStatusDto.fromJson(value);
|
||||
case 'RandomSearchDto':
|
||||
return RandomSearchDto.fromJson(value);
|
||||
case 'RatingsResponse':
|
||||
return RatingsResponse.fromJson(value);
|
||||
case 'RatingsUpdate':
|
||||
|
||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@@ -100,9 +100,6 @@ String parameterToString(dynamic value) {
|
||||
if (value is ManualJobName) {
|
||||
return ManualJobNameTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is MapTheme) {
|
||||
return MapThemeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is MemoryType) {
|
||||
return MemoryTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ class ActivityCreateDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ActivityCreateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "ActivityCreateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ class ActivityResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ActivityResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "ActivityResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class ActivityStatisticsResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ActivityStatisticsResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "ActivityStatisticsResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/add_users_dto.dart
generated
1
mobile/openapi/lib/model/add_users_dto.dart
generated
@@ -40,6 +40,7 @@ class AddUsersDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AddUsersDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AddUsersDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class AdminOnboardingUpdateDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AdminOnboardingUpdateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AdminOnboardingUpdateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/album_response_dto.dart
generated
1
mobile/openapi/lib/model/album_response_dto.dart
generated
@@ -186,6 +186,7 @@ class AlbumResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AlbumResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AlbumResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ class AlbumStatisticsResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AlbumStatisticsResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AlbumStatisticsResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/album_user_add_dto.dart
generated
1
mobile/openapi/lib/model/album_user_add_dto.dart
generated
@@ -56,6 +56,7 @@ class AlbumUserAddDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AlbumUserAddDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AlbumUserAddDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class AlbumUserCreateDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AlbumUserCreateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AlbumUserCreateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class AlbumUserResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AlbumUserResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AlbumUserResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ class AllJobStatusResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AllJobStatusResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AllJobStatusResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/api_key_create_dto.dart
generated
1
mobile/openapi/lib/model/api_key_create_dto.dart
generated
@@ -56,6 +56,7 @@ class APIKeyCreateDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static APIKeyCreateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "APIKeyCreateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class APIKeyCreateResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static APIKeyCreateResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "APIKeyCreateResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ class APIKeyResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static APIKeyResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "APIKeyResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/api_key_update_dto.dart
generated
1
mobile/openapi/lib/model/api_key_update_dto.dart
generated
@@ -40,6 +40,7 @@ class APIKeyUpdateDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static APIKeyUpdateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "APIKeyUpdateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ class AssetBulkDeleteDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetBulkDeleteDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetBulkDeleteDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ class AssetBulkUpdateDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetBulkUpdateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetBulkUpdateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class AssetBulkUploadCheckDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetBulkUploadCheckDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetBulkUploadCheckDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class AssetBulkUploadCheckItem {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetBulkUploadCheckItem? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetBulkUploadCheckItem");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class AssetBulkUploadCheckResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetBulkUploadCheckResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetBulkUploadCheckResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ class AssetBulkUploadCheckResult {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetBulkUploadCheckResult? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetBulkUploadCheckResult");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class AssetDeltaSyncDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetDeltaSyncDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetDeltaSyncDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ class AssetDeltaSyncResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetDeltaSyncResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetDeltaSyncResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ class AssetFaceResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetFaceResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class AssetFaceUpdateDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceUpdateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetFaceUpdateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class AssetFaceUpdateItem {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceUpdateItem? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetFaceUpdateItem");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ class AssetFaceWithoutPersonResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceWithoutPersonResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetFaceWithoutPersonResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ class AssetFullSyncDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFullSyncDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetFullSyncDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/asset_ids_dto.dart
generated
1
mobile/openapi/lib/model/asset_ids_dto.dart
generated
@@ -40,6 +40,7 @@ class AssetIdsDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetIdsDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetIdsDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ class AssetIdsResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetIdsResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetIdsResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/asset_jobs_dto.dart
generated
1
mobile/openapi/lib/model/asset_jobs_dto.dart
generated
@@ -46,6 +46,7 @@ class AssetJobsDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetJobsDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetJobsDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class AssetMediaResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetMediaResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetMediaResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/asset_response_dto.dart
generated
1
mobile/openapi/lib/model/asset_response_dto.dart
generated
@@ -293,6 +293,7 @@ class AssetResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ class AssetStackResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetStackResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetStackResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ class AssetStatsResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetStatsResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetStatsResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class AuditDeletesResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AuditDeletesResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AuditDeletesResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/avatar_response.dart
generated
1
mobile/openapi/lib/model/avatar_response.dart
generated
@@ -40,6 +40,7 @@ class AvatarResponse {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AvatarResponse? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AvatarResponse");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/avatar_update.dart
generated
1
mobile/openapi/lib/model/avatar_update.dart
generated
@@ -50,6 +50,7 @@ class AvatarUpdate {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AvatarUpdate? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AvatarUpdate");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ class BulkIdResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static BulkIdResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "BulkIdResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/bulk_ids_dto.dart
generated
1
mobile/openapi/lib/model/bulk_ids_dto.dart
generated
@@ -40,6 +40,7 @@ class BulkIdsDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static BulkIdsDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "BulkIdsDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class ChangePasswordDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ChangePasswordDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "ChangePasswordDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class CheckExistingAssetsDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CheckExistingAssetsDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "CheckExistingAssetsDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class CheckExistingAssetsResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CheckExistingAssetsResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "CheckExistingAssetsResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/clip_config.dart
generated
1
mobile/openapi/lib/model/clip_config.dart
generated
@@ -46,6 +46,7 @@ class CLIPConfig {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CLIPConfig? fromJson(dynamic value) {
|
||||
upgradeDto(value, "CLIPConfig");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/create_album_dto.dart
generated
1
mobile/openapi/lib/model/create_album_dto.dart
generated
@@ -68,6 +68,7 @@ class CreateAlbumDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CreateAlbumDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "CreateAlbumDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/create_library_dto.dart
generated
1
mobile/openapi/lib/model/create_library_dto.dart
generated
@@ -68,6 +68,7 @@ class CreateLibraryDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CreateLibraryDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "CreateLibraryDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ class CreateProfileImageResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CreateProfileImageResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "CreateProfileImageResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class DownloadArchiveInfo {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static DownloadArchiveInfo? fromJson(dynamic value) {
|
||||
upgradeDto(value, "DownloadArchiveInfo");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/download_info_dto.dart
generated
1
mobile/openapi/lib/model/download_info_dto.dart
generated
@@ -89,6 +89,7 @@ class DownloadInfoDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static DownloadInfoDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "DownloadInfoDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
1
mobile/openapi/lib/model/download_response.dart
generated
1
mobile/openapi/lib/model/download_response.dart
generated
@@ -46,6 +46,7 @@ class DownloadResponse {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static DownloadResponse? fromJson(dynamic value) {
|
||||
upgradeDto(value, "DownloadResponse");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user