forked from Cutlery/immich
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76abdc3c82 | |||
| 8b79b31ae5 | |||
| a7b7d7b417 | |||
| 0ab6a26871 | |||
| ed1294a0ea |
@@ -58,7 +58,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.1.0
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.1.0
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
# Workaround to fix error:
|
||||
# failed to push: failed to copy: io: read/write on closed pipe
|
||||
# See https://github.com/docker/build-push-action/issues/761
|
||||
|
||||
@@ -12,7 +12,7 @@ concurrency:
|
||||
jobs:
|
||||
server-e2e-api:
|
||||
name: Server (e2e-api)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: mich
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./server
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
server-e2e-jobs:
|
||||
name: Server (e2e-jobs)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: mich
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
Generated
+404
-414
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,12 @@ services:
|
||||
- /dev/dri:/dev/dri
|
||||
- /dev/dma_heap:/dev/dma_heap
|
||||
- /dev/mpp_service:/dev/mpp_service
|
||||
volumes:
|
||||
- /usr/bin/ffmpeg:/usr/bin/ffmpeg_mpp:ro
|
||||
- /lib/aarch64-linux-gnu:/lib/ffmpeg-mpp:ro
|
||||
- /lib/aarch64-linux-gnu/libblas.so.3:/lib/ffmpeg-mpp/libblas.so.3:ro # symlink is resolved by mounting
|
||||
- /lib/aarch64-linux-gnu/liblapack.so.3:/lib/ffmpeg-mpp/liblapack.so.3:ro # symlink is resolved by mounting
|
||||
- /lib/aarch64-linux-gnu/pulseaudio/libpulsecommon-15.99.so:/lib/ffmpeg-mpp/libpulsecommon-15.99.so:ro
|
||||
|
||||
vaapi:
|
||||
devices:
|
||||
|
||||
@@ -44,13 +44,22 @@ Below is an example config for Apache2 site configuration.
|
||||
|
||||
```
|
||||
<VirtualHost *:80>
|
||||
ServerName <snip>
|
||||
ProxyRequests Off
|
||||
ProxyPass / http://127.0.0.1:2283/ timeout=600 upgrade=websocket
|
||||
ProxyPassReverse / http://127.0.0.1:2283/
|
||||
ProxyPreserveHost On
|
||||
ServerName <snip>
|
||||
|
||||
ProxyRequests off
|
||||
ProxyVia on
|
||||
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_URI} ^/api/socket.io [NC]
|
||||
RewriteCond %{QUERY_STRING} transport=websocket [NC]
|
||||
RewriteRule /(.*) ws://localhost:2283/$1 [P,L]
|
||||
|
||||
ProxyPass /api/socket.io ws://localhost:2283/api/socket.io
|
||||
ProxyPassReverse /api/socket.io ws://localhost:2283/api/socket.io
|
||||
|
||||
<Location />
|
||||
ProxyPass http://localhost:2283/
|
||||
ProxyPassReverse http://localhost:2283/
|
||||
</Location>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
**timeout:** is measured in seconds, and it is particularly useful when long operations are triggered (i.e. Repair), so the server doesn't return an error.
|
||||
|
||||
@@ -50,22 +50,12 @@ import {
|
||||
mdiVectorCombine,
|
||||
mdiVideo,
|
||||
mdiWeb,
|
||||
mdiScaleBalance,
|
||||
} from '@mdi/js';
|
||||
import Layout from '@theme/Layout';
|
||||
import React from 'react';
|
||||
import Timeline, { DateType, Item } from '../components/timeline';
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
icon: mdiScaleBalance,
|
||||
description: 'Immich switches to AGPLv3 license',
|
||||
title: 'AGPL License',
|
||||
release: 'v1.95.0',
|
||||
tag: 'v1.95.0',
|
||||
date: new Date(2024, 1, 20),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiEyeRefreshOutline,
|
||||
description: 'Automatically import files in external libraries when the operating system detects changes.',
|
||||
|
||||
@@ -4,6 +4,7 @@ name: immich-e2e
|
||||
|
||||
x-server-build: &server-common
|
||||
image: immich-server:latest
|
||||
container_name: immich-e2e-server
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
@@ -22,16 +23,14 @@ x-server-build: &server-common
|
||||
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich-e2e-server
|
||||
command: [ "./start.sh", "immich" ]
|
||||
<<: *server-common
|
||||
ports:
|
||||
- 2283:3001
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich-e2e-microservices
|
||||
command: [ "./start.sh", "microservices" ]
|
||||
<<: *server-common
|
||||
# immich-microservices:
|
||||
# command: [ "./start.sh", "microservices" ]
|
||||
# <<: *server-common
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||
|
||||
Generated
+42
-146
@@ -12,14 +12,11 @@
|
||||
"@immich/cli": "file:../cli",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@vitest/coverage-v8": "^1.3.0",
|
||||
"luxon": "^3.4.4",
|
||||
"pg": "^8.11.3",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^6.3.4",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.3.0"
|
||||
@@ -784,12 +781,6 @@
|
||||
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/cookiejar": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
||||
@@ -808,12 +799,6 @@
|
||||
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
@@ -821,9 +806,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
|
||||
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
|
||||
"version": "20.11.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
|
||||
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
@@ -919,9 +904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz",
|
||||
"integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.0.tgz",
|
||||
"integrity": "sha512-e5Y5uK5NNoQMQaNitGQQjo9FoA5ZNcu7Bn6pH+dxUf48u6po1cX38kFBYUHZ9GNVkF4JLbncE0WeWwTw+nLrxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
@@ -942,17 +927,17 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "1.3.1"
|
||||
"vitest": "1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz",
|
||||
"integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.0.tgz",
|
||||
"integrity": "sha512-7bWt0vBTZj08B+Ikv70AnLRicohYwFgzNjFqo9SxxqHHxSlUJGSXmCRORhOnRMisiUryKMdvsi1n27Bc6jL9DQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/spy": "1.3.1",
|
||||
"@vitest/utils": "1.3.1",
|
||||
"@vitest/spy": "1.3.0",
|
||||
"@vitest/utils": "1.3.0",
|
||||
"chai": "^4.3.10"
|
||||
},
|
||||
"funding": {
|
||||
@@ -960,12 +945,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz",
|
||||
"integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.0.tgz",
|
||||
"integrity": "sha512-1Jb15Vo/Oy7mwZ5bXi7zbgszsdIBNjc4IqP8Jpr/8RdBC4nF1CTzIAn2dxYvpF1nGSseeL39lfLQ2uvs5u1Y9A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "1.3.1",
|
||||
"@vitest/utils": "1.3.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"pathe": "^1.1.1"
|
||||
},
|
||||
@@ -974,9 +959,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz",
|
||||
"integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.0.tgz",
|
||||
"integrity": "sha512-swmktcviVVPYx9U4SEQXLV6AEY51Y6bZ14jA2yo6TgMxQ3h+ZYiO0YhAHGJNp0ohCFbPAis1R9kK0cvN6lDPQA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -988,9 +973,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz",
|
||||
"integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.0.tgz",
|
||||
"integrity": "sha512-AkCU0ThZunMvblDpPKgjIi025UxR8V7MZ/g/EwmAGpjIujLVV2X6rGYGmxE2D4FJbAy0/ijdROHMWa2M/6JVMw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tinyspy": "^2.2.0"
|
||||
@@ -1000,9 +985,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz",
|
||||
"integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.0.tgz",
|
||||
"integrity": "sha512-/LibEY/fkaXQufi4GDlQZhikQsPO2entBKtfuyIpr1jV4DpaeasqkeHjhdOhU24vSHshcSuEyVlWdzvv2XmYCw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"diff-sequences": "^29.6.3",
|
||||
@@ -1278,28 +1263,6 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
|
||||
"integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
@@ -1741,15 +1704,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.7",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
|
||||
@@ -2392,34 +2346,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
|
||||
"integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -2625,9 +2551,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
|
||||
"integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
|
||||
"integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.19.3",
|
||||
@@ -2680,9 +2606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz",
|
||||
"integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.0.tgz",
|
||||
"integrity": "sha512-D/oiDVBw75XMnjAXne/4feCkCEwcbr2SU1bjAhCcfI5Bq3VoOHji8/wCPAfUkDIeohJ5nSZ39fNxM3dNZ6OBOA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
@@ -2716,16 +2642,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz",
|
||||
"integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.0.tgz",
|
||||
"integrity": "sha512-V9qb276J1jjSx9xb75T2VoYXdO1UKi+qfflY7V7w93jzX7oA/+RtYE6TcifxksxsZvygSSMwu2Uw6di7yqDMwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "1.3.1",
|
||||
"@vitest/runner": "1.3.1",
|
||||
"@vitest/snapshot": "1.3.1",
|
||||
"@vitest/spy": "1.3.1",
|
||||
"@vitest/utils": "1.3.1",
|
||||
"@vitest/expect": "1.3.0",
|
||||
"@vitest/runner": "1.3.0",
|
||||
"@vitest/snapshot": "1.3.0",
|
||||
"@vitest/spy": "1.3.0",
|
||||
"@vitest/utils": "1.3.0",
|
||||
"acorn-walk": "^8.3.2",
|
||||
"chai": "^4.3.10",
|
||||
"debug": "^4.3.4",
|
||||
@@ -2739,7 +2665,7 @@
|
||||
"tinybench": "^2.5.1",
|
||||
"tinypool": "^0.8.2",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "1.3.1",
|
||||
"vite-node": "1.3.0",
|
||||
"why-is-node-running": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -2754,8 +2680,8 @@
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"@vitest/browser": "1.3.1",
|
||||
"@vitest/ui": "1.3.1",
|
||||
"@vitest/browser": "1.3.0",
|
||||
"@vitest/ui": "1.3.0",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -2817,36 +2743,6 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -16,14 +16,11 @@
|
||||
"@immich/cli": "file:../cli",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@vitest/coverage-v8": "^1.3.0",
|
||||
"luxon": "^3.4.4",
|
||||
"pg": "^8.11.3",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^6.3.4",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.3.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
ActivityCreateDto,
|
||||
AlbumResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
AssetResponseDto,
|
||||
LoginResponseDto,
|
||||
ReactionType,
|
||||
createActivity as create,
|
||||
@@ -16,13 +16,13 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
describe('/activity', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let nonOwner: LoginResponseDto;
|
||||
let asset: AssetFileUploadResponseDto;
|
||||
let asset: AssetResponseDto;
|
||||
let album: AlbumResponseDto;
|
||||
|
||||
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
||||
create(
|
||||
{ activityCreateDto: dto },
|
||||
{ headers: asBearerAuth(accessToken || admin.accessToken) },
|
||||
{ headers: asBearerAuth(accessToken || admin.accessToken) }
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -40,7 +40,7 @@ describe('/activity', () => {
|
||||
sharedWithUserIds: [nonOwner.userId],
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('/activity', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
|
||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
|
||||
);
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('/activity', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
|
||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
|
||||
);
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('/activity', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])),
|
||||
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID']))
|
||||
);
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('/activity', () => {
|
||||
assetIds: [asset.id],
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
|
||||
const [reaction] = await Promise.all([
|
||||
@@ -216,7 +216,7 @@ describe('/activity', () => {
|
||||
.send({ albumId: uuidDto.invalid });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
|
||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
|
||||
);
|
||||
});
|
||||
|
||||
@@ -230,7 +230,7 @@ describe('/activity', () => {
|
||||
errorDto.badRequest([
|
||||
'comment must be a string',
|
||||
'comment should not be empty',
|
||||
]),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -357,7 +357,7 @@ describe('/activity', () => {
|
||||
describe('DELETE /activity/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(
|
||||
`/activity/${uuidDto.notFound}`,
|
||||
`/activity/${uuidDto.notFound}`
|
||||
);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -421,7 +421,7 @@ describe('/activity', () => {
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest('Not found or no activity.delete access'),
|
||||
errorDto.badRequest('Not found or no activity.delete access')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -432,7 +432,7 @@ describe('/activity', () => {
|
||||
type: ReactionType.Comment,
|
||||
comment: 'This is a test comment',
|
||||
},
|
||||
nonOwner.accessToken,
|
||||
nonOwner.accessToken
|
||||
);
|
||||
|
||||
const { status } = await request(app)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
AlbumResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
AssetResponseDto,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
deleteUser,
|
||||
@@ -21,8 +21,8 @@ const user2NotShared = 'user2NotShared';
|
||||
describe('/album', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user1: LoginResponseDto;
|
||||
let user1Asset1: AssetFileUploadResponseDto;
|
||||
let user1Asset2: AssetFileUploadResponseDto;
|
||||
let user1Asset1: AssetResponseDto;
|
||||
let user1Asset2: AssetResponseDto;
|
||||
let user1Albums: AlbumResponseDto[];
|
||||
let user2: LoginResponseDto;
|
||||
let user2Albums: AlbumResponseDto[];
|
||||
@@ -95,7 +95,7 @@ describe('/album', () => {
|
||||
|
||||
await deleteUser(
|
||||
{ id: user3.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
});
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('/album', () => {
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['shared must be a boolean value']),
|
||||
errorDto.badRequest(['shared must be a boolean value'])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('/album', () => {
|
||||
albumName: user2SharedUser,
|
||||
shared: true,
|
||||
}),
|
||||
]),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -175,7 +175,7 @@ describe('/album', () => {
|
||||
albumName: user1NotShared,
|
||||
shared: false,
|
||||
}),
|
||||
]),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('/album', () => {
|
||||
albumName: user2SharedUser,
|
||||
shared: true,
|
||||
}),
|
||||
]),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -219,7 +219,7 @@ describe('/album', () => {
|
||||
albumName: user1NotShared,
|
||||
shared: false,
|
||||
}),
|
||||
]),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -251,7 +251,7 @@ describe('/album', () => {
|
||||
describe('GET /album/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
`/album/${user1Albums[0].id}`,
|
||||
`/album/${user1Albums[0].id}`
|
||||
);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -361,7 +361,7 @@ describe('/album', () => {
|
||||
describe('PUT /album/:id/assets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(
|
||||
`/album/${user1Albums[0].id}/assets`,
|
||||
`/album/${user1Albums[0].id}/assets`
|
||||
);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -519,7 +519,7 @@ describe('/album', () => {
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
sharedUsers: [expect.objectContaining({ id: user2.userId })],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
import {
|
||||
AssetFileUploadResponseDto,
|
||||
AssetResponseDto,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
} from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const today = DateTime.fromObject({
|
||||
year: 2023,
|
||||
month: 11,
|
||||
day: 3,
|
||||
}) as DateTime<true>;
|
||||
const yesterday = today.minus({ days: 1 });
|
||||
|
||||
describe('/asset', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user1: LoginResponseDto;
|
||||
let user2: LoginResponseDto;
|
||||
let userStats: LoginResponseDto;
|
||||
let asset1: AssetFileUploadResponseDto;
|
||||
let asset2: AssetFileUploadResponseDto;
|
||||
let asset3: AssetFileUploadResponseDto;
|
||||
let asset4: AssetFileUploadResponseDto; // user2 asset
|
||||
let asset5: AssetFileUploadResponseDto;
|
||||
let asset6: AssetFileUploadResponseDto;
|
||||
let ws: Socket;
|
||||
|
||||
beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
admin = await apiUtils.adminSetup({ onboarding: false });
|
||||
[user1, user2, userStats] = await Promise.all([
|
||||
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
|
||||
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
|
||||
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
||||
]);
|
||||
|
||||
[asset1, asset2, asset3, asset4, asset5, asset6] = await Promise.all([
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(
|
||||
user1.accessToken,
|
||||
{
|
||||
isFavorite: true,
|
||||
isExternal: true,
|
||||
isReadOnly: true,
|
||||
fileCreatedAt: yesterday.toISO(),
|
||||
fileModifiedAt: yesterday.toISO(),
|
||||
},
|
||||
{ filename: 'example.mp4' },
|
||||
),
|
||||
apiUtils.createAsset(user2.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
|
||||
// stats
|
||||
apiUtils.createAsset(userStats.accessToken),
|
||||
apiUtils.createAsset(userStats.accessToken, { isFavorite: true }),
|
||||
apiUtils.createAsset(userStats.accessToken, { isArchived: true }),
|
||||
apiUtils.createAsset(
|
||||
userStats.accessToken,
|
||||
{
|
||||
isArchived: true,
|
||||
isFavorite: true,
|
||||
},
|
||||
{ filename: 'example.mp4' },
|
||||
),
|
||||
]);
|
||||
|
||||
const person1 = await apiUtils.createPerson(user1.accessToken, {
|
||||
name: 'Test Person',
|
||||
});
|
||||
await dbUtils.createFace({ assetId: asset1.id, personId: person1.id });
|
||||
});
|
||||
|
||||
describe('GET /asset/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
`/asset/${uuidDto.notFound}`,
|
||||
);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
expect(status).toBe(401);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/asset/${uuidDto.invalid}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/asset/${asset4.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should get the asset info', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/asset/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ id: asset1.id });
|
||||
});
|
||||
|
||||
it('should work with a shared link', async () => {
|
||||
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset1.id],
|
||||
});
|
||||
|
||||
const { status, body } = await request(app).get(
|
||||
`/asset/${asset1.id}?key=${sharedLink.key}`,
|
||||
);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ id: asset1.id });
|
||||
});
|
||||
|
||||
it('should not send people data for shared links for un-authenticated users', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/asset/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(body).toMatchObject({
|
||||
id: asset1.id,
|
||||
isFavorite: false,
|
||||
people: [
|
||||
{
|
||||
birthDate: null,
|
||||
id: expect.any(String),
|
||||
isHidden: false,
|
||||
name: 'Test Person',
|
||||
thumbnailPath: '/my/awesome/thumbnail.jpg',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset1.id],
|
||||
});
|
||||
|
||||
const data = await request(app).get(
|
||||
`/asset/${asset1.id}?key=${sharedLink.key}`,
|
||||
);
|
||||
expect(data.status).toBe(200);
|
||||
expect(data.body).toMatchObject({ people: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /asset/statistics', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/asset/statistics');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should return stats of all assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/statistics')
|
||||
.set('Authorization', `Bearer ${userStats.accessToken}`);
|
||||
|
||||
expect(body).toEqual({ images: 3, videos: 1, total: 4 });
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return stats of all favored assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/statistics')
|
||||
.set('Authorization', `Bearer ${userStats.accessToken}`)
|
||||
.query({ isFavorite: true });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ images: 1, videos: 1, total: 2 });
|
||||
});
|
||||
|
||||
it('should return stats of all archived assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/statistics')
|
||||
.set('Authorization', `Bearer ${userStats.accessToken}`)
|
||||
.query({ isArchived: true });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ images: 1, videos: 1, total: 2 });
|
||||
});
|
||||
|
||||
it('should return stats of all favored and archived assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/statistics')
|
||||
.set('Authorization', `Bearer ${userStats.accessToken}`)
|
||||
.query({ isFavorite: true, isArchived: true });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ images: 0, videos: 1, total: 1 });
|
||||
});
|
||||
|
||||
it('should return stats of all assets neither favored nor archived', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/statistics')
|
||||
.set('Authorization', `Bearer ${userStats.accessToken}`)
|
||||
.query({ isFavorite: false, isArchived: false });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ images: 1, videos: 0, total: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /asset/random', () => {
|
||||
beforeAll(async () => {
|
||||
await Promise.all([
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/asset/random');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it.each(Array(10))('should return 1 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/random')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const assets: AssetResponseDto[] = body;
|
||||
expect(assets.length).toBe(1);
|
||||
expect(assets[0].ownerId).toBe(user1.userId);
|
||||
//
|
||||
// assets owned by user2
|
||||
expect(assets[0].id).not.toBe(asset4.id);
|
||||
// assets owned by user1
|
||||
expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id);
|
||||
});
|
||||
|
||||
it.each(Array(10))('should return 2 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/random?count=2')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const assets: AssetResponseDto[] = body;
|
||||
expect(assets.length).toBe(2);
|
||||
|
||||
for (const asset of assets) {
|
||||
expect(asset.ownerId).toBe(user1.userId);
|
||||
// assets owned by user1
|
||||
expect([asset1.id, asset2.id, asset3.id]).toContain(asset.id);
|
||||
// assets owned by user2
|
||||
expect(asset.id).not.toBe(asset4.id);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(Array(10))(
|
||||
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
|
||||
async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/[]asset/random')
|
||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([expect.objectContaining({ id: asset4.id })]);
|
||||
},
|
||||
);
|
||||
|
||||
it('should return error', async () => {
|
||||
const { status } = await request(app)
|
||||
.get('/asset/random?count=ABC')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /asset/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(
|
||||
`/asset/:${uuidDto.notFound}`,
|
||||
);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/asset/${uuidDto.invalid}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/asset/${asset4.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should favorite an asset', async () => {
|
||||
const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(before.isFavorite).toBe(false);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/asset/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ isFavorite: true });
|
||||
expect(body).toMatchObject({ id: asset1.id, isFavorite: true });
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should archive an asset', async () => {
|
||||
const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(before.isArchived).toBe(false);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/asset/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ isArchived: true });
|
||||
expect(body).toMatchObject({ id: asset1.id, isArchived: true });
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should update date time original', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/asset/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
||||
|
||||
expect(body).toMatchObject({
|
||||
id: asset1.id,
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2023-11-20T01:11:00.000Z',
|
||||
}),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should reject invalid gps coordinates', async () => {
|
||||
for (const test of [
|
||||
{ latitude: 12 },
|
||||
{ longitude: 12 },
|
||||
{ latitude: 12, longitude: 'abc' },
|
||||
{ latitude: 'abc', longitude: 12 },
|
||||
{ latitude: null, longitude: 12 },
|
||||
{ latitude: 12, longitude: null },
|
||||
{ latitude: 91, longitude: 12 },
|
||||
{ latitude: -91, longitude: 12 },
|
||||
{ latitude: 12, longitude: -181 },
|
||||
{ latitude: 12, longitude: 181 },
|
||||
]) {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/asset/${asset1.id}`)
|
||||
.send(test)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
}
|
||||
});
|
||||
|
||||
it('should update gps data', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/asset/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ latitude: 12, longitude: 12 });
|
||||
|
||||
expect(body).toMatchObject({
|
||||
id: asset1.id,
|
||||
exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should set the description', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/asset/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ description: 'Test asset description' });
|
||||
expect(body).toMatchObject({
|
||||
id: asset1.id,
|
||||
exifInfo: expect.objectContaining({
|
||||
description: 'Test asset description',
|
||||
}),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should return tagged people', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/asset/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ isFavorite: true });
|
||||
expect(status).toEqual(200);
|
||||
expect(body).toMatchObject({
|
||||
id: asset1.id,
|
||||
isFavorite: true,
|
||||
people: [
|
||||
{
|
||||
birthDate: null,
|
||||
id: expect.any(String),
|
||||
isHidden: false,
|
||||
name: 'Test Person',
|
||||
thumbnailPath: '/my/awesome/thumbnail.jpg',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /asset', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/asset`)
|
||||
.send({ ids: [uuidDto.notFound] });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/asset`)
|
||||
.send({ ids: [uuidDto.invalid] })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['each value in ids must be a UUID']),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when the id is not found', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/asset`)
|
||||
.send({ ids: [uuidDto.notFound] })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest('Not found or no asset.delete access'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should move an asset to the trash', async () => {
|
||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
||||
|
||||
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||
expect(before.isTrashed).toBe(false);
|
||||
|
||||
const { status } = await request(app)
|
||||
.delete('/asset')
|
||||
.send({ ids: [assetId] })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||
expect(after.isTrashed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
|
||||
import { AssetResponseDto, LoginResponseDto } from '@immich/sdk';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
@@ -6,7 +6,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/download', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let asset1: AssetFileUploadResponseDto;
|
||||
let asset1: AssetResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
@@ -35,7 +35,7 @@ describe('/download', () => {
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
archives: [expect.objectContaining({ assetIds: [asset1.id] })],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe('/download', () => {
|
||||
describe('POST /download/asset/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post(
|
||||
`/download/asset/${asset1.id}`,
|
||||
`/download/asset/${asset1.id}`
|
||||
);
|
||||
|
||||
expect(status).toBe(401);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
AlbumResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
AssetResponseDto,
|
||||
LoginResponseDto,
|
||||
SharedLinkCreateDto,
|
||||
SharedLinkResponseDto,
|
||||
SharedLinkType,
|
||||
createSharedLink as create,
|
||||
createAlbum,
|
||||
deleteUser,
|
||||
} from '@immich/sdk';
|
||||
@@ -15,8 +17,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/shared-link', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let asset1: AssetFileUploadResponseDto;
|
||||
let asset2: AssetFileUploadResponseDto;
|
||||
let asset1: AssetResponseDto;
|
||||
let asset2: AssetResponseDto;
|
||||
let user1: LoginResponseDto;
|
||||
let user2: LoginResponseDto;
|
||||
let album: AlbumResponseDto;
|
||||
@@ -48,11 +50,11 @@ describe('/shared-link', () => {
|
||||
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
||||
createAlbum(
|
||||
{ createAlbumDto: { albumName: 'album' } },
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
{ headers: asBearerAuth(user1.accessToken) }
|
||||
),
|
||||
createAlbum(
|
||||
{ createAlbumDto: { albumName: 'deleted album' } },
|
||||
{ headers: asBearerAuth(user2.accessToken) },
|
||||
{ headers: asBearerAuth(user2.accessToken) }
|
||||
),
|
||||
createAlbum(
|
||||
{
|
||||
@@ -61,7 +63,7 @@ describe('/shared-link', () => {
|
||||
assetIds: [asset1.id],
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
{ headers: asBearerAuth(user1.accessToken) }
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -104,7 +106,7 @@ describe('/shared-link', () => {
|
||||
|
||||
await deleteUser(
|
||||
{ id: user2.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
});
|
||||
|
||||
@@ -130,7 +132,7 @@ describe('/shared-link', () => {
|
||||
expect.objectContaining({ id: linkWithPassword.id }),
|
||||
expect.objectContaining({ id: linkWithMetadata.id }),
|
||||
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
||||
]),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -164,7 +166,7 @@ describe('/shared-link', () => {
|
||||
album,
|
||||
userId: user1.userId,
|
||||
type: SharedLinkType.Album,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -206,7 +208,7 @@ describe('/shared-link', () => {
|
||||
album,
|
||||
userId: user1.userId,
|
||||
type: SharedLinkType.Album,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -223,7 +225,7 @@ describe('/shared-link', () => {
|
||||
localDateTime: expect.any(String),
|
||||
fileCreatedAt: expect.any(String),
|
||||
exifInfo: expect.any(Object),
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(body.album).toBeDefined();
|
||||
});
|
||||
@@ -248,7 +250,7 @@ describe('/shared-link', () => {
|
||||
describe('GET /shared-link/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
`/shared-link/${linkWithAlbum.id}`,
|
||||
`/shared-link/${linkWithAlbum.id}`
|
||||
);
|
||||
|
||||
expect(status).toBe(401);
|
||||
@@ -266,7 +268,7 @@ describe('/shared-link', () => {
|
||||
album,
|
||||
userId: user1.userId,
|
||||
type: SharedLinkType.Album,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -277,7 +279,7 @@ describe('/shared-link', () => {
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Shared link not found' }),
|
||||
expect.objectContaining({ message: 'Shared link not found' })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -309,7 +311,7 @@ describe('/shared-link', () => {
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Invalid albumId' }),
|
||||
expect.objectContaining({ message: 'Invalid albumId' })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -321,7 +323,7 @@ describe('/shared-link', () => {
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Invalid assetIds' }),
|
||||
expect.objectContaining({ message: 'Invalid assetIds' })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -336,7 +338,7 @@ describe('/shared-link', () => {
|
||||
expect.objectContaining({
|
||||
type: SharedLinkType.Album,
|
||||
userId: user1.userId,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -373,7 +375,7 @@ describe('/shared-link', () => {
|
||||
type: SharedLinkType.Album,
|
||||
userId: user1.userId,
|
||||
description: 'foo',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -425,7 +427,7 @@ describe('/shared-link', () => {
|
||||
describe('DELETE /shared-link/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(
|
||||
`/shared-link/${linkWithAlbum.id}`,
|
||||
`/shared-link/${linkWithAlbum.id}`
|
||||
);
|
||||
|
||||
expect(status).toBe(401);
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { LoginResponseDto, getAllAssets } from '@immich/sdk';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { apiUtils, app, asBearerAuth, dbUtils, wsUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/trash', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let ws: Socket;
|
||||
|
||||
beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
admin = await apiUtils.adminSetup({ onboarding: false });
|
||||
ws = await wsUtils.connect(admin.accessToken);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
wsUtils.disconnect(ws);
|
||||
});
|
||||
|
||||
describe('POST /trash/empty', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post('/trash/empty');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should empty the trash', async () => {
|
||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
||||
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
|
||||
|
||||
const before = await getAllAssets(
|
||||
{},
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
|
||||
expect(before.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post('/trash/empty')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
await wsUtils.once(ws, 'on_asset_delete');
|
||||
|
||||
const after = await getAllAssets(
|
||||
{},
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
expect(after.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /trash/restore', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post('/trash/restore');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should restore all trashed assets', async () => {
|
||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
||||
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
|
||||
|
||||
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||
expect(before.isTrashed).toBe(true);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post('/trash/restore')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||
expect(after.isTrashed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /trash/restore/assets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post('/trash/restore/assets');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should restore a trashed asset by id', async () => {
|
||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
||||
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
|
||||
|
||||
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||
expect(before.isTrashed).toBe(true);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post('/trash/restore/assets')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ ids: [assetId] });
|
||||
expect(status).toBe(204);
|
||||
|
||||
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||
expect(after.isTrashed).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,12 @@
|
||||
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`immich server-info`, () => {
|
||||
beforeAll(async () => {
|
||||
beforeAll(() => {
|
||||
apiUtils.setup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await dbUtils.reset();
|
||||
await cliUtils.login();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
||||
import {
|
||||
apiUtils,
|
||||
asKeyAuth,
|
||||
@@ -9,18 +8,18 @@ import {
|
||||
testAssetDir,
|
||||
} from 'src/utils';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
||||
|
||||
describe(`immich upload`, () => {
|
||||
let key: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeAll(() => {
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
key = await cliUtils.login();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await dbUtils.reset(['assets', 'albums']);
|
||||
await dbUtils.reset();
|
||||
key = await cliUtils.login();
|
||||
});
|
||||
|
||||
describe('immich upload --recursive', () => {
|
||||
@@ -34,7 +33,7 @@ describe(`immich upload`, () => {
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
]),
|
||||
])
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
@@ -56,7 +55,7 @@ describe(`immich upload`, () => {
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
expect.stringContaining('Successfully created 1 new album'),
|
||||
expect.stringContaining('Successfully updated 9 assets'),
|
||||
]),
|
||||
])
|
||||
);
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
@@ -78,7 +77,7 @@ describe(`immich upload`, () => {
|
||||
expect(response1.stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
]),
|
||||
])
|
||||
);
|
||||
expect(response1.stderr).toBe('');
|
||||
expect(response1.exitCode).toBe(0);
|
||||
@@ -98,10 +97,10 @@ describe(`immich upload`, () => {
|
||||
expect(response2.stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'All assets were already uploaded, nothing to do.',
|
||||
'All assets were already uploaded, nothing to do.'
|
||||
),
|
||||
expect.stringContaining('Successfully updated 9 assets'),
|
||||
]),
|
||||
])
|
||||
);
|
||||
expect(response2.stderr).toBe('');
|
||||
expect(response2.exitCode).toBe(0);
|
||||
@@ -128,7 +127,7 @@ describe(`immich upload`, () => {
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
expect.stringContaining('Successfully created 1 new album'),
|
||||
expect.stringContaining('Successfully updated 9 assets'),
|
||||
]),
|
||||
])
|
||||
);
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
@@ -149,7 +148,7 @@ describe(`immich upload`, () => {
|
||||
for (const file of filesToLink) {
|
||||
await symlink(
|
||||
`${testAssetDir}/albums/nature/${file}`,
|
||||
`/tmp/albums/nature/${file}`,
|
||||
`/tmp/albums/nature/${file}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,7 +166,7 @@ describe(`immich upload`, () => {
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
expect.stringContaining('Deleting assets that have been uploaded'),
|
||||
]),
|
||||
])
|
||||
);
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
+27
-92
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
AssetFileUploadResponseDto,
|
||||
AssetResponseDto,
|
||||
CreateAlbumDto,
|
||||
CreateAssetDto,
|
||||
CreateUserDto,
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
createSharedLink,
|
||||
createUser,
|
||||
defaults,
|
||||
deleteAssets,
|
||||
getAssetInfo,
|
||||
login,
|
||||
setAdminOnboarding,
|
||||
signUpAdmin,
|
||||
@@ -25,7 +23,6 @@ import { access } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
import pg from 'pg';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import request from 'supertest';
|
||||
|
||||
@@ -42,19 +39,15 @@ const directoryExists = (directory: string) =>
|
||||
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
||||
|
||||
const serverContainerName = 'immich-e2e-server';
|
||||
const mediaDir = '/usr/src/app/upload';
|
||||
const dirs = [
|
||||
`"${mediaDir}/thumbs"`,
|
||||
`"${mediaDir}/upload"`,
|
||||
`"${mediaDir}/library"`,
|
||||
].join(' ');
|
||||
const uploadMediaDir = '/usr/src/app/upload/upload';
|
||||
|
||||
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
||||
throw new Error(
|
||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`,
|
||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`
|
||||
);
|
||||
}
|
||||
|
||||
const setBaseUrl = () => (defaults.baseUrl = app);
|
||||
export const asBearerAuth = (accessToken: string) => ({
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
});
|
||||
@@ -66,7 +59,7 @@ let client: pg.Client | null = null;
|
||||
export const fileUtils = {
|
||||
reset: async () => {
|
||||
await execPromise(
|
||||
`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`,
|
||||
`docker exec -i "${serverContainerName}" rm -R "${uploadMediaDir}"`
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -88,7 +81,7 @@ export const dbUtils = {
|
||||
|
||||
await client.query(
|
||||
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
|
||||
[assetId, personId, embedding],
|
||||
[assetId, personId, embedding]
|
||||
);
|
||||
},
|
||||
setPersonThumbnail: async (personId: string) => {
|
||||
@@ -98,14 +91,14 @@ export const dbUtils = {
|
||||
|
||||
await client.query(
|
||||
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
|
||||
[personId],
|
||||
[personId]
|
||||
);
|
||||
},
|
||||
reset: async (tables?: string[]) => {
|
||||
try {
|
||||
if (!client) {
|
||||
client = new pg.Client(
|
||||
'postgres://postgres:postgres@127.0.0.1:5433/immich',
|
||||
'postgres://postgres:postgres@127.0.0.1:5433/immich'
|
||||
);
|
||||
await client.connect();
|
||||
}
|
||||
@@ -177,42 +170,10 @@ export interface AdminSetupOptions {
|
||||
onboarding?: boolean;
|
||||
}
|
||||
|
||||
export const wsUtils = {
|
||||
connect: async (accessToken: string) => {
|
||||
const websocket = io('http://127.0.0.1:2283', {
|
||||
path: '/api/socket.io',
|
||||
transports: ['websocket'],
|
||||
extraHeaders: { Authorization: `Bearer ${accessToken}` },
|
||||
autoConnect: false,
|
||||
forceNew: true,
|
||||
});
|
||||
|
||||
return new Promise<Socket>((resolve) => {
|
||||
websocket.on('connect', () => resolve(websocket));
|
||||
websocket.connect();
|
||||
});
|
||||
},
|
||||
disconnect: (ws: Socket) => {
|
||||
if (ws?.connected) {
|
||||
ws.disconnect();
|
||||
}
|
||||
},
|
||||
once: <T = any>(ws: Socket, event: string): Promise<T> => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Timeout')), 4000);
|
||||
ws.once(event, (data: T) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const apiUtils = {
|
||||
setup: () => {
|
||||
defaults.baseUrl = app;
|
||||
setBaseUrl();
|
||||
},
|
||||
|
||||
adminSetup: async (options?: AdminSetupOptions) => {
|
||||
options = options || { onboarding: true };
|
||||
|
||||
@@ -226,7 +187,7 @@ export const apiUtils = {
|
||||
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
||||
await createUser(
|
||||
{ createUserDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
{ headers: asBearerAuth(accessToken) }
|
||||
);
|
||||
return login({
|
||||
loginCredentialDto: { email: dto.email, password: dto.password },
|
||||
@@ -235,74 +196,48 @@ export const apiUtils = {
|
||||
createApiKey: (accessToken: string) => {
|
||||
return createApiKey(
|
||||
{ apiKeyCreateDto: { name: 'e2e' } },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
{ headers: asBearerAuth(accessToken) }
|
||||
);
|
||||
},
|
||||
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
||||
createAlbum(
|
||||
{ createAlbumDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
{ headers: asBearerAuth(accessToken) }
|
||||
),
|
||||
createAsset: async (
|
||||
accessToken: string,
|
||||
dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
|
||||
data?: {
|
||||
bytes?: Buffer;
|
||||
filename?: string;
|
||||
},
|
||||
dto?: Omit<CreateAssetDto, 'assetData'>
|
||||
) => {
|
||||
const _dto = {
|
||||
dto = dto || {
|
||||
deviceAssetId: 'test-1',
|
||||
deviceId: 'test',
|
||||
fileCreatedAt: new Date().toISOString(),
|
||||
fileModifiedAt: new Date().toISOString(),
|
||||
...(dto || {}),
|
||||
};
|
||||
|
||||
const _assetData = {
|
||||
bytes: randomBytes(32),
|
||||
filename: 'example.jpg',
|
||||
...(data || {}),
|
||||
};
|
||||
|
||||
const builder = request(app)
|
||||
const { body } = await request(app)
|
||||
.post(`/asset/upload`)
|
||||
.attach('assetData', _assetData.bytes, _assetData.filename)
|
||||
.field('deviceAssetId', dto.deviceAssetId)
|
||||
.field('deviceId', dto.deviceId)
|
||||
.field('fileCreatedAt', dto.fileCreatedAt)
|
||||
.field('fileModifiedAt', dto.fileModifiedAt)
|
||||
.attach('assetData', randomBytes(32), 'example.jpg')
|
||||
.set('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
for (const [key, value] of Object.entries(_dto)) {
|
||||
builder.field(key, String(value));
|
||||
}
|
||||
|
||||
const { body } = await builder;
|
||||
|
||||
return body as AssetFileUploadResponseDto;
|
||||
return body as AssetResponseDto;
|
||||
},
|
||||
getAssetInfo: (accessToken: string, id: string) =>
|
||||
getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||
deleteAssets: (accessToken: string, ids: string[]) =>
|
||||
deleteAssets(
|
||||
{ assetBulkDeleteDto: { ids } },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
),
|
||||
createPerson: async (accessToken: string, dto?: PersonUpdateDto) => {
|
||||
createPerson: async (accessToken: string, dto: PersonUpdateDto) => {
|
||||
// TODO fix createPerson to accept a body
|
||||
let person = await createPerson({ headers: asBearerAuth(accessToken) });
|
||||
await dbUtils.setPersonThumbnail(person.id);
|
||||
|
||||
if (!dto) {
|
||||
return person;
|
||||
}
|
||||
|
||||
const { id } = await createPerson({ headers: asBearerAuth(accessToken) });
|
||||
await dbUtils.setPersonThumbnail(id);
|
||||
return updatePerson(
|
||||
{ id: person.id, personUpdateDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
{ id, personUpdateDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) }
|
||||
);
|
||||
},
|
||||
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
||||
createSharedLink(
|
||||
{ sharedLinkCreateDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
{ headers: asBearerAuth(accessToken) }
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ test.describe('Shared Links', () => {
|
||||
let asset: AssetResponseDto;
|
||||
let album: AlbumResponseDto;
|
||||
let sharedLink: SharedLinkResponseDto;
|
||||
let sharedLinkPassword: SharedLinkResponseDto;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
@@ -30,16 +29,17 @@ test.describe('Shared Links', () => {
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
// { headers: asBearerAuth(admin.accessToken)},
|
||||
);
|
||||
sharedLink = await createSharedLink(
|
||||
{
|
||||
sharedLinkCreateDto: {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
sharedLink = await apiUtils.createSharedLink(admin.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
});
|
||||
sharedLinkPassword = await apiUtils.createSharedLink(admin.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
password: 'test-password',
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
@@ -55,16 +55,4 @@ test.describe('Shared Links', () => {
|
||||
await page.getByRole('button', { name: 'Download' }).click();
|
||||
await page.getByText('DOWNLOADING').waitFor();
|
||||
});
|
||||
|
||||
test('enter password for a shared link', async ({ page }) => {
|
||||
await page.goto(`/share/${sharedLinkPassword.key}`);
|
||||
await page.getByPlaceholder('Password').fill('test-password');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||
});
|
||||
|
||||
test('show error for invalid shared link', async ({ page }) => {
|
||||
await page.goto('/share/invalid');
|
||||
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .ann import Ann, is_available
|
||||
|
||||
@@ -32,7 +32,8 @@ T = TypeVar("T", covariant=True)
|
||||
|
||||
|
||||
class Newable(Protocol[T]):
|
||||
def new(self) -> None: ...
|
||||
def new(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
class _Singleton(type, Newable[T]):
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from typing import Any
|
||||
|
||||
import onnx
|
||||
import onnxruntime as ort
|
||||
from huggingface_hub import snapshot_download
|
||||
from onnx.shape_inference import infer_shapes
|
||||
from onnx.tools.update_model_dims import update_inputs_outputs_dims
|
||||
|
||||
import ann.ann
|
||||
from app.models.constants import SUPPORTED_PROVIDERS
|
||||
from app.models.constants import STATIC_INPUT_PROVIDERS, SUPPORTED_PROVIDERS
|
||||
|
||||
from ..config import get_cache_dir, get_hf_model_name, log, settings
|
||||
from ..schemas import ModelRuntime, ModelType
|
||||
@@ -111,25 +113,63 @@ class InferenceModel(ABC):
|
||||
)
|
||||
model_path = onnx_path
|
||||
|
||||
if any(provider in STATIC_INPUT_PROVIDERS for provider in self.providers):
|
||||
static_path = model_path.parent / "static_1" / "model.onnx"
|
||||
static_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not static_path.is_file():
|
||||
self._convert_to_static(model_path, static_path)
|
||||
model_path = static_path
|
||||
|
||||
match model_path.suffix:
|
||||
case ".armnn":
|
||||
session = AnnSession(model_path)
|
||||
case ".onnx":
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(model_path.parent)
|
||||
session = ort.InferenceSession(
|
||||
model_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
session = ort.InferenceSession(
|
||||
model_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported model file type: {model_path.suffix}")
|
||||
return session
|
||||
|
||||
def _convert_to_static(self, source_path: Path, target_path: Path) -> None:
|
||||
inferred = infer_shapes(onnx.load(source_path))
|
||||
inputs = self._get_static_dims(inferred.graph.input)
|
||||
outputs = self._get_static_dims(inferred.graph.output)
|
||||
|
||||
# check_model gets called in update_inputs_outputs_dims and doesn't work for large models
|
||||
check_model = onnx.checker.check_model
|
||||
try:
|
||||
|
||||
def check_model_stub(*args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
onnx.checker.check_model = check_model_stub
|
||||
updated_model = update_inputs_outputs_dims(inferred, inputs, outputs)
|
||||
finally:
|
||||
onnx.checker.check_model = check_model
|
||||
|
||||
onnx.save(
|
||||
updated_model,
|
||||
target_path,
|
||||
save_as_external_data=True,
|
||||
all_tensors_to_one_file=False,
|
||||
size_threshold=1048576,
|
||||
)
|
||||
|
||||
def _get_static_dims(self, graph_io: Any, dim_size: int = 1) -> dict[str, list[int]]:
|
||||
return {
|
||||
field.name: [
|
||||
d.dim_value if d.HasField("dim_value") else dim_size
|
||||
for shape in field.type.ListFields()
|
||||
if (dim := shape[1].shape.dim)
|
||||
for d in dim
|
||||
]
|
||||
for field in graph_io
|
||||
}
|
||||
|
||||
@property
|
||||
def model_type(self) -> ModelType:
|
||||
return self._model_type
|
||||
@@ -165,14 +205,6 @@ class InferenceModel(ABC):
|
||||
def providers_default(self) -> list[str]:
|
||||
available_providers = set(ort.get_available_providers())
|
||||
log.debug(f"Available ORT providers: {available_providers}")
|
||||
if (openvino := "OpenVINOExecutionProvider") in available_providers:
|
||||
device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids()
|
||||
log.debug(f"Available OpenVINO devices: {device_ids}")
|
||||
|
||||
gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")]
|
||||
if not gpu_devices:
|
||||
log.warning("No GPU device found in OpenVINO. Falling back to CPU.")
|
||||
available_providers.remove(openvino)
|
||||
return [provider for provider in SUPPORTED_PROVIDERS if provider in available_providers]
|
||||
|
||||
@property
|
||||
@@ -192,7 +224,15 @@ class InferenceModel(ABC):
|
||||
case "CPUExecutionProvider" | "CUDAExecutionProvider":
|
||||
option = {"arena_extend_strategy": "kSameAsRequested"}
|
||||
case "OpenVINOExecutionProvider":
|
||||
option = {"device_type": "GPU_FP32"}
|
||||
try:
|
||||
device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids()
|
||||
log.debug(f"Available OpenVINO devices: {device_ids}")
|
||||
gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")]
|
||||
option = {"device_id": gpu_devices[0]} if gpu_devices else {}
|
||||
except AttributeError as e:
|
||||
log.warning("Failed to get OpenVINO device IDs. Using default options.")
|
||||
log.error(e)
|
||||
option = {}
|
||||
case _:
|
||||
option = {}
|
||||
options.append(option)
|
||||
|
||||
@@ -54,6 +54,9 @@ _INSIGHTFACE_MODELS = {
|
||||
SUPPORTED_PROVIDERS = ["CUDAExecutionProvider", "OpenVINOExecutionProvider", "CPUExecutionProvider"]
|
||||
|
||||
|
||||
STATIC_INPUT_PROVIDERS = ["OpenVINOExecutionProvider"]
|
||||
|
||||
|
||||
def is_openclip(model_name: str) -> bool:
|
||||
return clean_name(model_name) in _OPENCLIP_MODELS
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from random import randint
|
||||
@@ -45,23 +44,11 @@ class TestBase:
|
||||
assert encoder.providers == self.CUDA_EP
|
||||
|
||||
@pytest.mark.providers(OV_EP)
|
||||
def test_sets_openvino_provider_if_available(self, providers: list[str], mocker: MockerFixture) -> None:
|
||||
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
||||
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
|
||||
|
||||
def test_sets_openvino_provider_if_available(self, providers: list[str]) -> None:
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
|
||||
assert encoder.providers == self.OV_EP
|
||||
|
||||
@pytest.mark.providers(OV_EP)
|
||||
def test_avoids_openvino_if_gpu_not_available(self, providers: list[str], mocker: MockerFixture) -> None:
|
||||
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
||||
mocked.get_available_openvino_device_ids.return_value = ["CPU"]
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
|
||||
assert encoder.providers == self.CPU_EP
|
||||
|
||||
@pytest.mark.providers(CUDA_EP_OUT_OF_ORDER)
|
||||
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
@@ -80,14 +67,22 @@ class TestBase:
|
||||
|
||||
assert encoder.providers == providers
|
||||
|
||||
def test_sets_default_provider_options(self, mocker: MockerFixture) -> None:
|
||||
def test_sets_default_provider_options(self) -> None:
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
|
||||
|
||||
assert encoder.provider_options == [
|
||||
{},
|
||||
{"arena_extend_strategy": "kSameAsRequested"},
|
||||
]
|
||||
|
||||
def test_sets_openvino_device_id_if_possible(self, mocker: MockerFixture) -> None:
|
||||
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
||||
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
|
||||
|
||||
assert encoder.provider_options == [
|
||||
{"device_type": "GPU_FP32"},
|
||||
{"device_id": "GPU.0"},
|
||||
{"arena_extend_strategy": "kSameAsRequested"},
|
||||
]
|
||||
|
||||
@@ -242,12 +237,12 @@ class TestBase:
|
||||
mock_model_path.is_file.return_value = True
|
||||
mock_model_path.suffix = ".armnn"
|
||||
mock_model_path.with_suffix.return_value = mock_model_path
|
||||
mock_ann = mocker.patch("app.models.base.AnnSession")
|
||||
mock_session = mocker.patch("app.models.base.AnnSession")
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
encoder._make_session(mock_model_path)
|
||||
|
||||
mock_ann.assert_called_once()
|
||||
mock_session.assert_called_once()
|
||||
|
||||
def test_make_session_return_ort_if_available_and_ann_is_not(self, mocker: MockerFixture) -> None:
|
||||
mock_armnn_path = mocker.Mock()
|
||||
@@ -261,7 +256,6 @@ class TestBase:
|
||||
|
||||
mock_ann = mocker.patch("app.models.base.AnnSession")
|
||||
mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
|
||||
mocker.patch("app.models.base.os.chdir")
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
encoder._make_session(mock_armnn_path)
|
||||
@@ -284,26 +278,6 @@ class TestBase:
|
||||
mock_ann.assert_not_called()
|
||||
mock_ort.assert_not_called()
|
||||
|
||||
def test_make_session_changes_cwd(self, mocker: MockerFixture) -> None:
|
||||
mock_model_path = mocker.Mock()
|
||||
mock_model_path.is_file.return_value = True
|
||||
mock_model_path.suffix = ".onnx"
|
||||
mock_model_path.parent = "model_parent"
|
||||
mock_model_path.with_suffix.return_value = mock_model_path
|
||||
mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
|
||||
mock_chdir = mocker.patch("app.models.base.os.chdir")
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
encoder._make_session(mock_model_path)
|
||||
|
||||
mock_chdir.assert_has_calls(
|
||||
[
|
||||
mock.call(mock_model_path.parent),
|
||||
mock.call(os.getcwd()),
|
||||
]
|
||||
)
|
||||
mock_ort.assert_called_once()
|
||||
|
||||
def test_download(self, mocker: MockerFixture) -> None:
|
||||
mock_snapshot_download = mocker.patch("app.models.base.snapshot_download")
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:6038b89363c9181215f3d9e8ce2720c880e224537f4028a854482e43a9b4998a as builder
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:926cac38640709f90f3fef2a3f730733b5c350be612f0d14706be8833b79ad8c as builder
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
|
||||
Generated
+75
-75
@@ -1250,13 +1250,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.0"
|
||||
version = "0.26.0"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
||||
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
|
||||
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2101,61 +2101,61 @@ numpy = [
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.9.15"
|
||||
version = "3.9.14"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "orjson-3.9.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d61f7ce4727a9fa7680cd6f3986b0e2c732639f46a5e0156e550e35258aa313a"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"},
|
||||
{file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"},
|
||||
{file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c8e8fe01e435005d4421f183038fc70ca85d2c1e490f51fb972db92af6e047c2"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"},
|
||||
{file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"},
|
||||
{file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:82425dd5c7bd3adfe4e94c78e27e2fa02971750c2b7ffba648b0f5d5cc016a73"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"},
|
||||
{file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b17f0f14a9c0ba55ff6279a922d1932e24b13fc218a3e968ecdbf791b3682b25"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"},
|
||||
{file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"},
|
||||
{file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6fc2fe4647927070df3d93f561d7e588a38865ea0040027662e3e541d592811e"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"},
|
||||
{file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"},
|
||||
{file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"},
|
||||
{file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"},
|
||||
{file = "orjson-3.9.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:793f6c9448ab6eb7d4974b4dde3f230345c08ca6c7995330fbceeb43a5c8aa5e"},
|
||||
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bc7928d161840096adc956703494b5c0193ede887346f028216cac0af87500"},
|
||||
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b36f54da759602d8e2f7dad958752d453dfe2c7122767bc7f765e17dc59959"},
|
||||
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abcda41ecdc950399c05eff761c3de91485d9a70d8227cb599ad3a66afe93bcc"},
|
||||
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df76ecd17b1b3627bddfd689faaf206380a1a38cc9f6c4075bd884eaedcf46c2"},
|
||||
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d450a8e0656efb5d0fcb062157b918ab02dcca73278975b4ee9ea49e2fcf5bd5"},
|
||||
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95c03137b0cf66517c8baa65770507a756d3a89489d8ecf864ea92348e1beabe"},
|
||||
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20837e10835c98973673406d6798e10f821e7744520633811a5a3d809762d8cc"},
|
||||
{file = "orjson-3.9.14-cp310-none-win32.whl", hash = "sha256:1f7b6f3ef10ae8e3558abb729873d033dbb5843507c66b1c0767e32502ba96bb"},
|
||||
{file = "orjson-3.9.14-cp310-none-win_amd64.whl", hash = "sha256:ea890e6dc1711aeec0a33b8520e395c2f3d59ead5b4351a788e06bf95fc7ba81"},
|
||||
{file = "orjson-3.9.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c19009ff37f033c70acd04b636380379499dac2cba27ae7dfc24f304deabbc81"},
|
||||
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19cdea0664aec0b7f385be84986d4defd3334e9c3c799407686ee1c26f7b8251"},
|
||||
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135d518f73787ce323b1a5e21fb854fe22258d7a8ae562b81a49d6c7f826f2a3"},
|
||||
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2cf1d0557c61c75e18cf7d69fb689b77896e95553e212c0cc64cf2087944b84"},
|
||||
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7c11667421df2d8b18b021223505dcc3ee51be518d54e4dc49161ac88ac2b87"},
|
||||
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eefc41ba42e75ed88bc396d8fe997beb20477f3e7efa000cd7a47eda452fbb2"},
|
||||
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:917311d6a64d1c327c0dfda1e41f3966a7fb72b11ca7aa2e7a68fcccc7db35d9"},
|
||||
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4dc1c132259b38d12c6587d190cd09cd76e3b5273ce71fe1372437b4cbc65f6f"},
|
||||
{file = "orjson-3.9.14-cp311-none-win32.whl", hash = "sha256:6f39a10408478f4c05736a74da63727a1ae0e83e3533d07b19443400fe8591ca"},
|
||||
{file = "orjson-3.9.14-cp311-none-win_amd64.whl", hash = "sha256:26280a7fcb62d8257f634c16acebc3bec626454f9ab13558bbf7883b9140760e"},
|
||||
{file = "orjson-3.9.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:08e722a8d06b13b67a51f247a24938d1a94b4b3862e40e0eef3b2e98c99cd04c"},
|
||||
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2591faa0c031cf3f57e5bce1461cfbd6160f3f66b5a72609a130924917cb07d"},
|
||||
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2450d87dd7b4f277f4c5598faa8b49a0c197b91186c47a2c0b88e15531e4e3e"},
|
||||
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90903d2908158a2c9077a06f11e27545de610af690fb178fd3ba6b32492d4d1c"},
|
||||
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6f095eef0026eae76fc212f20f786011ecf482fc7df2f4c272a8ae6dd7b1ef"},
|
||||
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751250a31fef2bac05a2da2449aae7142075ea26139271f169af60456d8ad27a"},
|
||||
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a1af21160a38ee8be3f4fcf24ee4b99e6184cadc7f915d599f073f478a94d2c"},
|
||||
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:449bf090b2aa4e019371d7511a6ea8a5a248139205c27d1834bb4b1e3c44d936"},
|
||||
{file = "orjson-3.9.14-cp312-none-win_amd64.whl", hash = "sha256:a603161318ff699784943e71f53899983b7dee571b4dd07c336437c9c5a272b0"},
|
||||
{file = "orjson-3.9.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:814f288c011efdf8f115c5ebcc1ab94b11da64b207722917e0ceb42f52ef30a3"},
|
||||
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88cafb100af68af3b9b29b5ccd09fdf7a48c63327916c8c923a94c336d38dd3"},
|
||||
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba3518b999f88882ade6686f1b71e207b52e23546e180499be5bbb63a2f9c6e6"},
|
||||
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978f416bbff9da8d2091e3cf011c92da68b13f2c453dcc2e8109099b2a19d234"},
|
||||
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fc593cf836f631153d0e21beaeb8d26e144445c73645889335c2247fcd71a0"},
|
||||
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d1528db3c7554f9d6eeb09df23cb80dd5177ec56eeb55cc5318826928de506"},
|
||||
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7183cc68ee2113b19b0b8714221e5e3b07b3ba10ca2bb108d78fd49cefaae101"},
|
||||
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df3266d54246cb56b8bb17fa908660d2a0f2e3f63fbc32451ffc1b1505051d07"},
|
||||
{file = "orjson-3.9.14-cp38-none-win32.whl", hash = "sha256:7913079b029e1b3501854c9a78ad938ed40d61fe09bebab3c93e60ff1301b189"},
|
||||
{file = "orjson-3.9.14-cp38-none-win_amd64.whl", hash = "sha256:29512eb925b620e5da2fd7585814485c67cc6ba4fe739a0a700c50467a8a8065"},
|
||||
{file = "orjson-3.9.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5bf597530544db27a8d76aced49cfc817ee9503e0a4ebf0109cd70331e7bbe0c"},
|
||||
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac650d49366fa41fe702e054cb560171a8634e2865537e91f09a8d05ea5b1d37"},
|
||||
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:236230433a9a4968ab895140514c308fdf9f607cb8bee178a04372b771123860"},
|
||||
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3014ccbda9be0b1b5f8ea895121df7e6524496b3908f4397ff02e923bcd8f6dd"},
|
||||
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac0c7eae7ad3a223bde690565442f8a3d620056bd01196f191af8be58a5248e1"},
|
||||
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca33fdd0b38839b01912c57546d4f412ba7bfa0faf9bf7453432219aec2df07"},
|
||||
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f75823cc1674a840a151e999a7dfa0d86c911150dd6f951d0736ee9d383bf415"},
|
||||
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f52ac2eb49e99e7373f62e2a68428c6946cda52ce89aa8fe9f890c7278e2d3a"},
|
||||
{file = "orjson-3.9.14-cp39-none-win32.whl", hash = "sha256:0572f174f50b673b7df78680fb52cd0087a8585a6d06d295a5f790568e1064c6"},
|
||||
{file = "orjson-3.9.14-cp39-none-win_amd64.whl", hash = "sha256:ab90c02cb264250b8a58cedcc72ed78a4a257d956c8d3c8bebe9751b818dfad8"},
|
||||
{file = "orjson-3.9.14.tar.gz", hash = "sha256:06fb40f8e49088ecaa02f1162581d39e2cf3fd9dbbfe411eb2284147c99bad79"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2465,13 +2465,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.0.2"
|
||||
version = "8.0.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"},
|
||||
{file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"},
|
||||
{file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"},
|
||||
{file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2836,28 +2836,28 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.2.2"
|
||||
version = "0.2.1"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"},
|
||||
{file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"},
|
||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"},
|
||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"},
|
||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"},
|
||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"},
|
||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"},
|
||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"},
|
||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"},
|
||||
{file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"},
|
||||
{file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"},
|
||||
{file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"},
|
||||
{file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"},
|
||||
{file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"},
|
||||
{file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"},
|
||||
{file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"},
|
||||
{file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
|
||||
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"},
|
||||
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"},
|
||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"},
|
||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"},
|
||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"},
|
||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"},
|
||||
{file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"},
|
||||
{file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"},
|
||||
{file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"},
|
||||
{file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.96.0"
|
||||
version = "1.95.1"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
@@ -82,10 +82,10 @@ warn_untyped_fields = true
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
per-file-ignores = { "test_main.py" = ["F403"] }
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"test_main.py" = ["F403"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 124,
|
||||
"android.injected.version.name" => "1.96.0",
|
||||
"android.injected.version.code" => 123,
|
||||
"android.injected.version.name" => "1.95.1",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000271">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000232">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="74.334294">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="78.881681">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.507669">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.080999">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 140;
|
||||
CURRENT_PROJECT_VERSION = 139;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -515,7 +515,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 140;
|
||||
CURRENT_PROJECT_VERSION = 139;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -543,7 +543,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 140;
|
||||
CURRENT_PROJECT_VERSION = 139;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -55,11 +55,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.96.0</string>
|
||||
<string>1.95.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>140</string>
|
||||
<string>139</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.96.0"
|
||||
version_number: "1.95.1"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000316">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000255">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.190055">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.157832">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.109364">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.825919">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.15926">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.18815">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="80.90681">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="110.912709">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="71.634559">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.396901">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ extension LogOnError<T> on AsyncValue<T> {
|
||||
}
|
||||
|
||||
if (hasError && !hasValue) {
|
||||
_asyncErrorLogger.severe('Could not load value', error, stackTrace);
|
||||
_asyncErrorLogger.severe("$error", error, stackTrace);
|
||||
return onError?.call(error, stackTrace) ??
|
||||
ScaffoldErrorBody(errorMsg: error?.toString());
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import 'package:http/http.dart';
|
||||
|
||||
extension LoggerExtension on Response {
|
||||
String toLoggerString() => "Status: $statusCode $reasonPhrase\n\n$body";
|
||||
}
|
||||
@@ -73,14 +73,15 @@ Future<void> initApp() async {
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
log.severe(
|
||||
'FlutterError - Catch all',
|
||||
"${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}",
|
||||
'FlutterError - Catch all error: ${details.toString()} - ${details.exception} - ${details.library} - ${details.context} - ${details.stack}',
|
||||
details,
|
||||
details.stack,
|
||||
);
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
log.severe('PlatformDispatcher - Catch all', error, stack);
|
||||
log.severe('PlatformDispatcher - Catch all error: $error', error, stack);
|
||||
debugPrint("PlatformDispatcher - Catch all error: $error $stack");
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,14 +10,13 @@ mixin ErrorLoggerMixin {
|
||||
/// Else, logs the error to the overrided logger and returns an AsyncError<>
|
||||
AsyncFuture<T> guardError<T>(
|
||||
Future<T> Function() fn, {
|
||||
required String errorMessage,
|
||||
Level logLevel = Level.SEVERE,
|
||||
}) async {
|
||||
try {
|
||||
final result = await fn();
|
||||
return AsyncData(result);
|
||||
} catch (error, stackTrace) {
|
||||
logger.log(logLevel, errorMessage, error, stackTrace);
|
||||
logger.log(logLevel, "$error", error, stackTrace);
|
||||
return AsyncError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
@@ -27,13 +26,12 @@ mixin ErrorLoggerMixin {
|
||||
Future<T> logError<T>(
|
||||
Future<T> Function() fn, {
|
||||
required T defaultValue,
|
||||
required String errorMessage,
|
||||
Level logLevel = Level.SEVERE,
|
||||
}) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error, stackTrace) {
|
||||
logger.log(logLevel, errorMessage, error, stackTrace);
|
||||
logger.log(logLevel, "$error", error, stackTrace);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ class ActivityService with ErrorLoggerMixin {
|
||||
return list != null ? list.map(Activity.fromDto).toList() : [];
|
||||
},
|
||||
defaultValue: [],
|
||||
errorMessage: "Failed to get all activities for album $albumId",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +35,6 @@ class ActivityService with ErrorLoggerMixin {
|
||||
return dto?.comments ?? 0;
|
||||
},
|
||||
defaultValue: 0,
|
||||
errorMessage: "Failed to statistics for album $albumId",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +45,6 @@ class ActivityService with ErrorLoggerMixin {
|
||||
return true;
|
||||
},
|
||||
defaultValue: false,
|
||||
errorMessage: "Failed to delete activity",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,24 +54,21 @@ class ActivityService with ErrorLoggerMixin {
|
||||
String? assetId,
|
||||
String? comment,
|
||||
}) async {
|
||||
return guardError(
|
||||
() async {
|
||||
final dto = await _apiService.activityApi.createActivity(
|
||||
ActivityCreateDto(
|
||||
albumId: albumId,
|
||||
type: type == ActivityType.comment
|
||||
? ReactionType.comment
|
||||
: ReactionType.like,
|
||||
assetId: assetId,
|
||||
comment: comment,
|
||||
),
|
||||
);
|
||||
if (dto != null) {
|
||||
return Activity.fromDto(dto);
|
||||
}
|
||||
throw NoResponseDtoError();
|
||||
},
|
||||
errorMessage: "Failed to create $type for album $albumId",
|
||||
);
|
||||
return guardError(() async {
|
||||
final dto = await _apiService.activityApi.createActivity(
|
||||
ActivityCreateDto(
|
||||
albumId: albumId,
|
||||
type: type == ActivityType.comment
|
||||
? ReactionType.comment
|
||||
: ReactionType.like,
|
||||
assetId: assetId,
|
||||
comment: comment,
|
||||
),
|
||||
);
|
||||
if (dto != null) {
|
||||
return Activity.fromDto(dto);
|
||||
}
|
||||
throw NoResponseDtoError();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
class AlbumThumbnailCard extends StatelessWidget {
|
||||
final Function()? onTap;
|
||||
@@ -45,8 +45,8 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
buildAlbumThumbnail() => ImmichThumbnail(
|
||||
asset: album.thumbnail.value,
|
||||
buildAlbumThumbnail() => ImmichImage.thumbnail(
|
||||
album.thumbnail.value,
|
||||
width: cardSize,
|
||||
height: cardSize,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
||||
final Asset asset;
|
||||
@@ -16,8 +16,8 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
ImmichThumbnail(
|
||||
asset: asset,
|
||||
ImmichImage.thumbnail(
|
||||
asset,
|
||||
width: 500,
|
||||
height: 500,
|
||||
),
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:immich_mobile/modules/partner/ui/partner_list.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
@RoutePage()
|
||||
class SharingPage extends HookConsumerWidget {
|
||||
@@ -72,8 +72,8 @@ class SharingPage extends HookConsumerWidget {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ImmichThumbnail(
|
||||
asset: album.thumbnail.value,
|
||||
child: ImmichImage.thumbnail(
|
||||
album.thumbnail.value,
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
/// The local image provider for an asset
|
||||
/// Only viable
|
||||
class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
|
||||
class ImmichLocalImageProvider extends ImageProvider<Asset> {
|
||||
final Asset asset;
|
||||
|
||||
ImmichLocalImageProvider({
|
||||
@@ -21,18 +21,15 @@ class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
|
||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||
/// that describes the precise image to load.
|
||||
@override
|
||||
Future<ImmichLocalImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(this);
|
||||
Future<Asset> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(asset);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
ImmichLocalImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
ImageStreamCompleter loadImage(Asset key, ImageDecoderCallback decode) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key.asset, decode, chunkEvents),
|
||||
codec: _codec(key, decode, chunkEvents),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkEvents.stream,
|
||||
informationCollector: () sync* {
|
||||
@@ -85,6 +82,11 @@ class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
|
||||
yield codec;
|
||||
} catch (error) {
|
||||
throw StateError("Loading asset ${asset.fileName} failed");
|
||||
} finally {
|
||||
if (Platform.isIOS) {
|
||||
// Clean up this file
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
/// The local image provider for an asset
|
||||
/// Only viable
|
||||
class ImmichLocalThumbnailProvider extends ImageProvider<Asset> {
|
||||
final Asset asset;
|
||||
final int height;
|
||||
final int width;
|
||||
|
||||
ImmichLocalThumbnailProvider({
|
||||
required this.asset,
|
||||
this.height = 256,
|
||||
this.width = 256,
|
||||
}) : assert(asset.local != null, 'Only usable when asset.local is set');
|
||||
|
||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||
/// that describes the precise image to load.
|
||||
@override
|
||||
Future<Asset> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(asset);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(Asset key, ImageDecoderCallback decode) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key, decode, chunkEvents),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkEvents.stream,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription(asset.fileName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<ui.Codec> _codec(
|
||||
Asset key,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
) async* {
|
||||
// Load a small thumbnail
|
||||
final thumbBytes = await asset.local?.thumbnailDataWithSize(
|
||||
const ThumbnailSize.square(32),
|
||||
quality: 75,
|
||||
);
|
||||
if (thumbBytes != null) {
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
||||
final codec = await decode(buffer);
|
||||
yield codec;
|
||||
} else {
|
||||
debugPrint("Loading thumb for ${asset.fileName} failed");
|
||||
}
|
||||
|
||||
final normalThumbBytes =
|
||||
await asset.local?.thumbnailDataWithSize(ThumbnailSize(width, height));
|
||||
if (normalThumbBytes == null) {
|
||||
throw StateError(
|
||||
"Loading thumb for local photo ${asset.fileName} failed",
|
||||
);
|
||||
}
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes);
|
||||
final codec = await decode(buffer);
|
||||
yield codec;
|
||||
|
||||
chunkEvents.close();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! ImmichLocalThumbnailProvider) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return asset == other.asset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => asset.hashCode;
|
||||
}
|
||||
@@ -13,13 +13,10 @@ import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
/// Our Image Provider HTTP client to make the request
|
||||
final _httpClient = HttpClient()
|
||||
..autoUncompress = false
|
||||
..maxConnectionsPerHost = 10;
|
||||
final _httpClient = HttpClient()..autoUncompress = false;
|
||||
|
||||
/// The remote image provider
|
||||
class ImmichRemoteImageProvider
|
||||
extends ImageProvider<ImmichRemoteImageProvider> {
|
||||
class ImmichRemoteImageProvider extends ImageProvider<String> {
|
||||
/// The [Asset.remoteId] of the asset to fetch
|
||||
final String assetId;
|
||||
|
||||
@@ -35,20 +32,16 @@ class ImmichRemoteImageProvider
|
||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||
/// that describes the precise image to load.
|
||||
@override
|
||||
Future<ImmichRemoteImageProvider> obtainKey(
|
||||
ImageConfiguration configuration,
|
||||
) {
|
||||
return SynchronousFuture(this);
|
||||
Future<String> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture('$assetId,$isThumbnail');
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
ImmichRemoteImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) {
|
||||
final id = key.split(',').first;
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key, decode, chunkEvents),
|
||||
codec: _codec(id, decode, chunkEvents),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkEvents.stream,
|
||||
);
|
||||
@@ -68,14 +61,14 @@ class ImmichRemoteImageProvider
|
||||
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<ui.Codec> _codec(
|
||||
ImmichRemoteImageProvider key,
|
||||
String key,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
) async* {
|
||||
// Load a preview to the chunk events
|
||||
if (_loadPreview || key.isThumbnail) {
|
||||
if (_loadPreview || isThumbnail) {
|
||||
final preview = getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
assetId,
|
||||
type: api.ThumbnailFormat.WEBP,
|
||||
);
|
||||
|
||||
@@ -87,14 +80,14 @@ class ImmichRemoteImageProvider
|
||||
}
|
||||
|
||||
// Guard thumnbail rendering
|
||||
if (key.isThumbnail) {
|
||||
if (isThumbnail) {
|
||||
await chunkEvents.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the higher resolution version of the image
|
||||
final url = getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
assetId,
|
||||
type: api.ThumbnailFormat.JPEG,
|
||||
);
|
||||
final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents);
|
||||
@@ -103,7 +96,7 @@ class ImmichRemoteImageProvider
|
||||
// Load the final remote image
|
||||
if (_useOriginal) {
|
||||
// Load the original image
|
||||
final url = getImageUrlFromId(key.assetId);
|
||||
final url = getImageUrlFromId(assetId);
|
||||
final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents);
|
||||
yield codec;
|
||||
}
|
||||
@@ -144,7 +137,7 @@ class ImmichRemoteImageProvider
|
||||
bool operator ==(Object other) {
|
||||
if (other is! ImmichRemoteImageProvider) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return assetId == other.assetId && isThumbnail == other.isThumbnail;
|
||||
return assetId == other.assetId;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+9
-17
@@ -12,17 +12,14 @@ import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
/// Our HTTP client to make the request
|
||||
final _httpClient = HttpClient()
|
||||
..autoUncompress = false
|
||||
..maxConnectionsPerHost = 100;
|
||||
|
||||
/// The remote image provider
|
||||
class ImmichRemoteThumbnailProvider
|
||||
extends ImageProvider<ImmichRemoteThumbnailProvider> {
|
||||
class ImmichRemoteThumbnailProvider extends ImageProvider<String> {
|
||||
/// The [Asset.remoteId] of the asset to fetch
|
||||
final String assetId;
|
||||
|
||||
/// Our HTTP client to make the request
|
||||
final _httpClient = HttpClient()..autoUncompress = false;
|
||||
|
||||
ImmichRemoteThumbnailProvider({
|
||||
required this.assetId,
|
||||
});
|
||||
@@ -30,17 +27,12 @@ class ImmichRemoteThumbnailProvider
|
||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||
/// that describes the precise image to load.
|
||||
@override
|
||||
Future<ImmichRemoteThumbnailProvider> obtainKey(
|
||||
ImageConfiguration configuration,
|
||||
) {
|
||||
return SynchronousFuture(this);
|
||||
Future<String> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(assetId);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
ImmichRemoteThumbnailProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key, decode, chunkEvents),
|
||||
@@ -51,13 +43,13 @@ class ImmichRemoteThumbnailProvider
|
||||
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<ui.Codec> _codec(
|
||||
ImmichRemoteThumbnailProvider key,
|
||||
String key,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
) async* {
|
||||
// Load a preview to the chunk events
|
||||
final preview = getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
assetId,
|
||||
type: api.ThumbnailFormat.WEBP,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
@@ -40,8 +39,7 @@ class ImageViewerService {
|
||||
final failedResponse =
|
||||
imageResponse.statusCode != 200 ? imageResponse : motionReponse;
|
||||
_log.severe(
|
||||
"Motion asset download failed",
|
||||
failedResponse.toLoggerString(),
|
||||
"Motion asset download failed with status - ${failedResponse.statusCode} and response - ${failedResponse.body}",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -77,7 +75,9 @@ class ImageViewerService {
|
||||
.downloadFileWithHttpInfo(asset.remoteId!);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
_log.severe("Asset download failed", res.toLoggerString());
|
||||
_log.severe(
|
||||
"Asset download failed with status - ${res.statusCode} and response - ${res.body}",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class ImageViewerService {
|
||||
return entity != null;
|
||||
}
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error saving downloaded asset", error, stack);
|
||||
_log.severe("Error saving file ${error.toString()}", error, stack);
|
||||
return false;
|
||||
} finally {
|
||||
// Clear temp files
|
||||
|
||||
@@ -48,7 +48,7 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
);
|
||||
} catch (error, stack) {
|
||||
hasError.value = true;
|
||||
_log.severe("Error updating description", error, stack);
|
||||
_log.severe("Error updating description $error", error, stack);
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "description_input_submit_error".tr(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
@@ -10,7 +10,6 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
|
||||
@@ -24,16 +23,17 @@ import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
|
||||
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart';
|
||||
@@ -47,17 +47,15 @@ import 'package:openapi/api.dart' show ThumbnailFormat;
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
class GalleryViewerPage extends HookConsumerWidget {
|
||||
final Asset Function(int index) loadAsset;
|
||||
final int totalAssets;
|
||||
final int initialIndex;
|
||||
final int heroOffset;
|
||||
final bool showStack;
|
||||
final RenderList renderList;
|
||||
|
||||
GalleryViewerPage({
|
||||
super.key,
|
||||
required this.initialIndex,
|
||||
required this.loadAsset,
|
||||
required this.totalAssets,
|
||||
required this.renderList,
|
||||
this.initialIndex = 0,
|
||||
this.heroOffset = 0,
|
||||
this.showStack = false,
|
||||
}) : controller = PageController(initialPage: initialIndex);
|
||||
@@ -70,6 +68,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final loadAsset = renderList.loadAsset;
|
||||
final totalAssets = useState(renderList.totalAssets);
|
||||
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
|
||||
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
||||
final isZoomed = useState<bool>(false);
|
||||
@@ -138,7 +138,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
debugPrint('Error precaching next image: $exception, $stackTrace');
|
||||
}
|
||||
|
||||
if (index < totalAssets && index >= 0) {
|
||||
if (index < totalAssets.value && index >= 0) {
|
||||
final asset = loadAsset(index);
|
||||
precacheImage(
|
||||
ImmichImage.imageProvider(asset: asset),
|
||||
@@ -200,16 +200,14 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
force: force,
|
||||
);
|
||||
if (isDeleted && isParent) {
|
||||
if (totalAssets == 1) {
|
||||
// Workaround for asset remaining in the gallery
|
||||
renderList.deleteAsset(deleteAsset);
|
||||
if (totalAssets.value == 1) {
|
||||
// Handle only one asset
|
||||
context.popRoute();
|
||||
} else {
|
||||
// Go to next page otherwise
|
||||
controller.nextPage(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
);
|
||||
}
|
||||
|
||||
totalAssets.value -= 1;
|
||||
}
|
||||
return isDeleted;
|
||||
}
|
||||
@@ -482,9 +480,15 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Image(
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
image: ImmichRemoteImageProvider(assetId: assetId!),
|
||||
imageUrl:
|
||||
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$assetId',
|
||||
httpHeaders: {
|
||||
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||
},
|
||||
errorWidget: (context, url, error) =>
|
||||
const Icon(Icons.image_not_supported_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -735,15 +739,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
|
||||
},
|
||||
loadingBuilder: (context, event, index) => ImageFiltered(
|
||||
imageFilter: ui.ImageFilter.blur(
|
||||
sigmaX: 1,
|
||||
sigmaY: 1,
|
||||
),
|
||||
child: ImmichThumbnail(
|
||||
asset: asset(),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
loadingBuilder: (context, event, index) => ImmichImage.thumbnail(
|
||||
asset(),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
pageController: controller,
|
||||
scrollPhysics: isZoomed.value
|
||||
@@ -752,7 +750,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
? const ScrollPhysics() // Use bouncing physics for iOS
|
||||
: const ClampingScrollPhysics() // Use heavy physics for Android
|
||||
),
|
||||
itemCount: totalAssets,
|
||||
itemCount: totalAssets.value,
|
||||
scrollDirection: Axis.horizontal,
|
||||
onPageChanged: (value) {
|
||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||
|
||||
@@ -40,7 +40,7 @@ class VideoViewerPage extends HookWidget {
|
||||
controlsSafeAreaMinimum: const EdgeInsets.only(
|
||||
bottom: 100,
|
||||
),
|
||||
placeholder: SizedBox.expand(child: placeholder),
|
||||
placeholder: placeholder,
|
||||
showControls: showControls && !isMotionVideo,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
customControls: const VideoPlayerControls(),
|
||||
@@ -58,7 +58,7 @@ class VideoViewerPage extends HookWidget {
|
||||
if (controller == null) {
|
||||
return Stack(
|
||||
children: [
|
||||
if (placeholder != null) SizedBox.expand(child: placeholder!),
|
||||
if (placeholder != null) placeholder!,
|
||||
const DelayedLoadingIndicator(
|
||||
fadeInDuration: Duration(milliseconds: 500),
|
||||
),
|
||||
|
||||
@@ -245,7 +245,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
} catch (e, stack) {
|
||||
log.severe(
|
||||
"Failed to get thumbnail for album ${album.name}",
|
||||
e,
|
||||
e.toString(),
|
||||
stack,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -311,4 +311,12 @@ class RenderList {
|
||||
GroupAssetsBy groupBy,
|
||||
) =>
|
||||
_buildRenderList(assets, null, groupBy);
|
||||
|
||||
/// Deletes an asset from the render list and clears the buffer
|
||||
/// This is only a workaround for deleted images still appearing in the gallery
|
||||
void deleteAsset(Asset deleteAsset) {
|
||||
allAssets?.remove(deleteAsset);
|
||||
_buf.clear();
|
||||
_bufOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:collection';
|
||||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -11,6 +12,7 @@ import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
@@ -587,6 +589,7 @@ class _AssetRow extends StatelessWidget {
|
||||
key: key,
|
||||
children: assets.mapIndexed((int index, Asset asset) {
|
||||
final bool last = index + 1 == assetsPerRow;
|
||||
final isSelected = isSelectionActive && selectedAssets.contains(asset);
|
||||
return Container(
|
||||
width: width * widthDistribution[index],
|
||||
height: width,
|
||||
@@ -594,18 +597,37 @@ class _AssetRow extends StatelessWidget {
|
||||
bottom: margin,
|
||||
right: last ? 0.0 : margin,
|
||||
),
|
||||
child: ThumbnailImage(
|
||||
asset: asset,
|
||||
index: absoluteOffset + index,
|
||||
loadAsset: renderList.loadAsset,
|
||||
totalAssets: renderList.totalAssets,
|
||||
multiselectEnabled: selectionActive,
|
||||
isSelected: isSelectionActive && selectedAssets.contains(asset),
|
||||
onSelect: () => onSelect?.call(asset),
|
||||
onDeselect: () => onDeselect?.call(asset),
|
||||
showStorageIndicator: showStorageIndicator,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (selectionActive) {
|
||||
if (isSelected) {
|
||||
onDeselect?.call(asset);
|
||||
} else {
|
||||
onSelect?.call(asset);
|
||||
}
|
||||
} else {
|
||||
context.pushRoute(
|
||||
GalleryViewerRoute(
|
||||
renderList: renderList,
|
||||
initialIndex: absoluteOffset + index,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
onSelect?.call(asset);
|
||||
HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: ThumbnailImage(
|
||||
asset: asset,
|
||||
multiselectEnabled: selectionActive,
|
||||
isSelected: isSelected,
|
||||
showStorageIndicator: showStorageIndicator,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:immich_mobile/utils/storage_indicator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
/// Shows the thumbnail images in the asset grid view
|
||||
class ThumbnailImage extends StatelessWidget {
|
||||
/// The asset to show the thumbnail image for
|
||||
final Asset asset;
|
||||
final int index;
|
||||
final Asset Function(int index) loadAsset;
|
||||
final int totalAssets;
|
||||
|
||||
/// Whether to show the storage indicator icont over the image or not
|
||||
final bool showStorageIndicator;
|
||||
|
||||
/// Whether to show the show stack icon over the image or not
|
||||
final bool showStack;
|
||||
|
||||
/// Whether to show the checkmark indicating that this image is selected
|
||||
final bool isSelected;
|
||||
|
||||
/// Can override [isSelected] and never show the selection indicator
|
||||
final bool multiselectEnabled;
|
||||
final Function? onSelect;
|
||||
final Function? onDeselect;
|
||||
|
||||
/// If we are allowed to deselect this image
|
||||
final bool canDeselect;
|
||||
|
||||
/// The offset index to apply to this hero tag for animation
|
||||
final int heroOffset;
|
||||
|
||||
const ThumbnailImage({
|
||||
super.key,
|
||||
required this.asset,
|
||||
required this.index,
|
||||
required this.loadAsset,
|
||||
required this.totalAssets,
|
||||
this.showStorageIndicator = true,
|
||||
this.showStack = false,
|
||||
this.isSelected = false,
|
||||
this.multiselectEnabled = false,
|
||||
this.onDeselect,
|
||||
this.onSelect,
|
||||
this.heroOffset = 0,
|
||||
this.canDeselect = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -134,10 +137,10 @@ class ThumbnailImage extends StatelessWidget {
|
||||
tag: isFromDto
|
||||
? '${asset.remoteId}-$heroOffset'
|
||||
: asset.id + heroOffset,
|
||||
child: ImmichThumbnail(
|
||||
asset: asset,
|
||||
height: 250,
|
||||
width: 250,
|
||||
child: ImmichImage.thumbnail(
|
||||
asset,
|
||||
height: 300,
|
||||
width: 300,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -146,11 +149,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 0,
|
||||
color: onDeselect == null ? Colors.grey : assetContainerColor,
|
||||
),
|
||||
color: onDeselect == null ? Colors.grey : assetContainerColor,
|
||||
color: canDeselect ? assetContainerColor : Colors.grey,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
@@ -164,79 +163,52 @@ class ThumbnailImage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (multiselectEnabled) {
|
||||
if (isSelected) {
|
||||
onDeselect?.call();
|
||||
} else {
|
||||
onSelect?.call();
|
||||
}
|
||||
} else {
|
||||
context.pushRoute(
|
||||
GalleryViewerRoute(
|
||||
initialIndex: index,
|
||||
loadAsset: loadAsset,
|
||||
totalAssets: totalAssets,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
onSelect?.call();
|
||||
HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.decelerate,
|
||||
decoration: BoxDecoration(
|
||||
border: multiselectEnabled && isSelected
|
||||
? Border.all(
|
||||
color: onDeselect == null
|
||||
? Colors.grey
|
||||
: assetContainerColor,
|
||||
width: 8,
|
||||
)
|
||||
: const Border(),
|
||||
),
|
||||
child: buildImage(),
|
||||
return Stack(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.decelerate,
|
||||
decoration: BoxDecoration(
|
||||
border: multiselectEnabled && isSelected
|
||||
? Border.all(
|
||||
color: canDeselect ? assetContainerColor : Colors.grey,
|
||||
width: 8,
|
||||
)
|
||||
: const Border(),
|
||||
),
|
||||
if (multiselectEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: buildSelectionIcon(asset),
|
||||
),
|
||||
child: buildImage(),
|
||||
),
|
||||
if (multiselectEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: buildSelectionIcon(asset),
|
||||
),
|
||||
if (showStorageIndicator)
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
storageIcon(asset),
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
if (showStorageIndicator)
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
storageIcon(asset),
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
if (asset.isFavorite)
|
||||
const Positioned(
|
||||
left: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
if (asset.isFavorite)
|
||||
const Positioned(
|
||||
left: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
if (!asset.isImage) buildVideoIcon(),
|
||||
if (asset.stackChildrenCount > 0) buildStackIcon(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!asset.isImage) buildVideoIcon(),
|
||||
if (asset.stackChildrenCount > 0) buildStackIcon(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
.then((_) => log.info("Logout was successful for $userEmail"))
|
||||
.onError(
|
||||
(error, stackTrace) =>
|
||||
log.severe("Logout failed for $userEmail", error, stackTrace),
|
||||
log.severe("Error logging out $userEmail", error, stackTrace),
|
||||
);
|
||||
|
||||
await Future.wait([
|
||||
@@ -129,8 +129,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
shouldChangePassword: false,
|
||||
isAuthenticated: false,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
log.severe('Logout failed', e, stack);
|
||||
} catch (e) {
|
||||
log.severe("Error logging out $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class OAuthService {
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
log.severe("OAuth login failed", e, stack);
|
||||
log.severe("Error performing oAuthLogin: ${e.toString()}", e, stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||
import 'package:immich_mobile/modules/map/models/map_state.model.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
@@ -52,8 +51,7 @@ class MapStateNotifier extends _$MapStateNotifier {
|
||||
lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current),
|
||||
);
|
||||
_log.severe(
|
||||
"Cannot fetch map light style",
|
||||
lightResponse.toLoggerString(),
|
||||
"Cannot fetch map light style with status - ${lightResponse.statusCode} and response - ${lightResponse.body}",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -79,7 +77,9 @@ class MapStateNotifier extends _$MapStateNotifier {
|
||||
state = state.copyWith(
|
||||
darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current),
|
||||
);
|
||||
_log.severe("Cannot fetch map dark style", darkResponse.toLoggerString());
|
||||
_log.severe(
|
||||
"Cannot fetch map dark style with status - ${darkResponse.statusCode} and response - ${darkResponse.body}",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ class MapSerivce with ErrorLoggerMixin {
|
||||
return markers?.map(MapMarker.fromDto) ?? [];
|
||||
},
|
||||
defaultValue: [],
|
||||
errorMessage: "Failed to get map markers",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,8 +105,10 @@ class MapUtils {
|
||||
timeLimit: const Duration(seconds: 5),
|
||||
);
|
||||
return (currentUserLocation, null);
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot get user's current location", error, stack);
|
||||
} catch (error) {
|
||||
_log.severe(
|
||||
"Cannot get user's current location due to ${error.toString()}",
|
||||
);
|
||||
return (null, LocationPermission.unableToDetermine);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/map/models/map_event.model.dart';
|
||||
import 'package:immich_mobile/modules/map/models/map_marker.dart';
|
||||
import 'package:immich_mobile/modules/map/providers/map_marker.provider.dart';
|
||||
@@ -178,11 +179,16 @@ class MapPage extends HookConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we only have a single asset, we can just show GroupAssetBy.none
|
||||
final renderList = await RenderList.fromAssets(
|
||||
[asset],
|
||||
GroupAssetsBy.none,
|
||||
);
|
||||
|
||||
context.pushRoute(
|
||||
GalleryViewerRoute(
|
||||
initialIndex: 0,
|
||||
loadAsset: (index) => asset,
|
||||
totalAssets: 1,
|
||||
renderList: renderList,
|
||||
heroOffset: 0,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -147,7 +147,7 @@ class MapAssetGrid extends HookConsumerWidget {
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
log.warning(
|
||||
"Cannot get assets in the current map bounds",
|
||||
"Cannot get assets in the current map bounds $error",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ class MemoryService {
|
||||
|
||||
return memories.isNotEmpty ? memories : null;
|
||||
} catch (error, stack) {
|
||||
log.severe("Cannot get memories", error, stack);
|
||||
log.severe("Cannot get memories ${error.toString()}", error, stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
class MemoryCard extends StatelessWidget {
|
||||
@@ -22,6 +21,8 @@ class MemoryCard extends StatelessWidget {
|
||||
super.key,
|
||||
});
|
||||
|
||||
String get accessToken => Store.get(StoreKey.accessToken);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
@@ -36,8 +37,20 @@ class MemoryCard extends StatelessWidget {
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox.expand(
|
||||
child: _BlurredBackdrop(asset: asset),
|
||||
ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
isThumbnail: true,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: Container(color: Colors.black.withOpacity(0.2)),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
@@ -100,50 +113,3 @@ class MemoryCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BlurredBackdrop extends HookWidget {
|
||||
final Asset asset;
|
||||
|
||||
const _BlurredBackdrop({required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final blurhash = useBlurHashRef(asset).value;
|
||||
if (blurhash != null) {
|
||||
// Use a nice cheap blur hash image decoration
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: MemoryImage(
|
||||
blurhash,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Fall back to using a more expensive image filtered
|
||||
// Since the ImmichImage is already precached, we can
|
||||
// safely use that as the image provider
|
||||
return ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,13 +109,25 @@ class MemoryPage extends HookConsumerWidget {
|
||||
asset = memories[nextMemoryIndex].assets.first;
|
||||
}
|
||||
|
||||
// Precache the asset
|
||||
await precacheImage(
|
||||
ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
// Gets the thumbnail url and precaches it
|
||||
final precaches = <Future<dynamic>>[];
|
||||
|
||||
precaches.addAll([
|
||||
precacheImage(
|
||||
ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
),
|
||||
context,
|
||||
),
|
||||
context,
|
||||
);
|
||||
precacheImage(
|
||||
ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
isThumbnail: true,
|
||||
),
|
||||
context,
|
||||
),
|
||||
]);
|
||||
await Future.wait(precaches);
|
||||
}
|
||||
|
||||
// Precache the next page right away if we are on the first page
|
||||
|
||||
@@ -40,7 +40,7 @@ class PartnerService {
|
||||
return userDtos.map((u) => User.fromPartnerDto(u)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
_log.warning("Failed to get partners for direction $direction", e);
|
||||
_log.warning("failed to get partners for direction $direction:\n$e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class PartnerService {
|
||||
partner.isPartnerSharedBy = false;
|
||||
await _db.writeTxn(() => _db.users.put(partner));
|
||||
} catch (e) {
|
||||
_log.warning("Failed to remove partner ${partner.id}", e);
|
||||
_log.warning("failed to remove partner ${partner.id}:\n$e");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -66,7 +66,7 @@ class PartnerService {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
_log.warning("Failed to add partner ${partner.id}", e);
|
||||
_log.warning("failed to add partner ${partner.id}:\n$e");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -81,7 +81,7 @@ class PartnerService {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
_log.warning("Failed to update partner ${partner.id}", e);
|
||||
_log.warning("failed to update partner ${partner.id}:\n$e");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class SharedLinkService {
|
||||
? AsyncData(list.map(SharedLink.fromDto).toList())
|
||||
: const AsyncData([]);
|
||||
} catch (e, stack) {
|
||||
_log.severe("Failed to fetch shared links", e, stack);
|
||||
_log.severe("failed to fetch shared links - $e");
|
||||
return AsyncError(e, stack);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class SharedLinkService {
|
||||
try {
|
||||
return await _apiService.sharedLinkApi.removeSharedLink(id);
|
||||
} catch (e) {
|
||||
_log.severe("Failed to delete shared link id - $id", e);
|
||||
_log.severe("failed to delete shared link id - $id with error - $e");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ class SharedLinkService {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("Failed to create shared link", e);
|
||||
_log.severe("failed to create shared link with error - $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -113,7 +113,7 @@ class SharedLinkService {
|
||||
return SharedLink.fromDto(responseDto);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("Failed to update shared link id - $id", e);
|
||||
_log.severe("failed to update shared link id - $id with error - $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class TrashNotifier extends StateNotifier<bool> {
|
||||
.read(syncServiceProvider)
|
||||
.handleRemoteAssetRemoval(idsToRemove.cast<String>().toList());
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot empty trash", error, stack);
|
||||
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class TrashNotifier extends StateNotifier<bool> {
|
||||
|
||||
return isRemoved;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot remove assets", error, stack);
|
||||
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -93,7 +93,7 @@ class TrashNotifier extends StateNotifier<bool> {
|
||||
return true;
|
||||
}
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore assets", error, stack);
|
||||
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ class TrashNotifier extends StateNotifier<bool> {
|
||||
await _db.assets.putAll(updatedAssets);
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore trash", error, stack);
|
||||
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class TrashService {
|
||||
await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteIds));
|
||||
return true;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore assets", error, stack);
|
||||
_log.severe("Cannot restore assets ${error.toString()}", error, stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class TrashService {
|
||||
try {
|
||||
await _apiService.trashApi.emptyTrash();
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot empty trash", error, stack);
|
||||
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class TrashService {
|
||||
try {
|
||||
await _apiService.trashApi.restoreTrash();
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore trash", error, stack);
|
||||
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@@ -16,31 +16,28 @@ class AuthGuard extends AutoRouteGuard {
|
||||
resolver.next(true);
|
||||
|
||||
try {
|
||||
// Look in the store for an access token
|
||||
Store.get(StoreKey.accessToken);
|
||||
|
||||
// Validate the access token with the server
|
||||
final res = await _apiService.authenticationApi.validateAccessToken();
|
||||
var res = await _apiService.authenticationApi.validateAccessToken();
|
||||
if (res == null || res.authStatus != true) {
|
||||
// If the access token is invalid, take user back to login
|
||||
_log.fine('User token is invalid. Redirecting to login');
|
||||
_log.fine("User token is invalid. Redirecting to login");
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
}
|
||||
} on StoreKeyNotFoundException catch (_) {
|
||||
// If there is no access token, take us to the login page
|
||||
_log.warning('No access token in the store.');
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
return;
|
||||
} on ApiException catch (e) {
|
||||
// On an unauthorized request, take us to the login page
|
||||
if (e.code == HttpStatus.unauthorized) {
|
||||
_log.warning("Unauthorized access token.");
|
||||
if (e.code == HttpStatus.badRequest &&
|
||||
e.innerException is SocketException) {
|
||||
// offline?
|
||||
_log.fine(
|
||||
"Unable to validate user token. User may be offline and offline browsing is allowed.",
|
||||
);
|
||||
} else {
|
||||
debugPrint("Error [onNavigation] ${e.toString()}");
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Otherwise, this is not fatal, but we still log the warning
|
||||
_log.warning('Error validating access token from server: $e');
|
||||
debugPrint("Error [onNavigation] ${e.toString()}");
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/library_page.dart';
|
||||
import 'package:immich_mobile/modules/backup/views/backup_options_page.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/map/views/map_location_picker_page.dart';
|
||||
import 'package:immich_mobile/modules/map/views/map_page.dart';
|
||||
import 'package:immich_mobile/modules/memories/models/memory.dart';
|
||||
|
||||
@@ -162,9 +162,8 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
routeData: routeData,
|
||||
child: GalleryViewerPage(
|
||||
key: args.key,
|
||||
renderList: args.renderList,
|
||||
initialIndex: args.initialIndex,
|
||||
loadAsset: args.loadAsset,
|
||||
totalAssets: args.totalAssets,
|
||||
heroOffset: args.heroOffset,
|
||||
showStack: args.showStack,
|
||||
),
|
||||
@@ -793,9 +792,8 @@ class FavoritesRoute extends PageRouteInfo<void> {
|
||||
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||
GalleryViewerRoute({
|
||||
Key? key,
|
||||
required int initialIndex,
|
||||
required Asset Function(int) loadAsset,
|
||||
required int totalAssets,
|
||||
required RenderList renderList,
|
||||
int initialIndex = 0,
|
||||
int heroOffset = 0,
|
||||
bool showStack = false,
|
||||
List<PageRouteInfo>? children,
|
||||
@@ -803,9 +801,8 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||
GalleryViewerRoute.name,
|
||||
args: GalleryViewerRouteArgs(
|
||||
key: key,
|
||||
renderList: renderList,
|
||||
initialIndex: initialIndex,
|
||||
loadAsset: loadAsset,
|
||||
totalAssets: totalAssets,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
),
|
||||
@@ -821,28 +818,25 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||
class GalleryViewerRouteArgs {
|
||||
const GalleryViewerRouteArgs({
|
||||
this.key,
|
||||
required this.initialIndex,
|
||||
required this.loadAsset,
|
||||
required this.totalAssets,
|
||||
required this.renderList,
|
||||
this.initialIndex = 0,
|
||||
this.heroOffset = 0,
|
||||
this.showStack = false,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final RenderList renderList;
|
||||
|
||||
final int initialIndex;
|
||||
|
||||
final Asset Function(int) loadAsset;
|
||||
|
||||
final int totalAssets;
|
||||
|
||||
final int heroOffset;
|
||||
|
||||
final bool showStack;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset, showStack: $showStack}';
|
||||
return 'GalleryViewerRouteArgs{key: $key, renderList: $renderList, initialIndex: $initialIndex, heroOffset: $heroOffset, showStack: $showStack}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@ class Asset {
|
||||
// stack handling to properly handle it
|
||||
stackParentId =
|
||||
remote.stackParentId == remote.id ? null : remote.stackParentId,
|
||||
stackCount = remote.stackCount,
|
||||
thumbhash = remote.thumbhash;
|
||||
stackCount = remote.stackCount;
|
||||
|
||||
Asset.local(AssetEntity local, List<int> hash)
|
||||
: localId = local.id,
|
||||
@@ -92,7 +91,6 @@ class Asset {
|
||||
this.stackCount = 0,
|
||||
this.isReadOnly = false,
|
||||
this.isOffline = false,
|
||||
this.thumbhash,
|
||||
});
|
||||
|
||||
@ignore
|
||||
@@ -121,8 +119,6 @@ class Asset {
|
||||
/// because Isar cannot sort lists of byte arrays
|
||||
String checksum;
|
||||
|
||||
String? thumbhash;
|
||||
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? remoteId;
|
||||
|
||||
@@ -283,7 +279,6 @@ class Asset {
|
||||
a.exifInfo?.latitude != exifInfo?.latitude ||
|
||||
a.exifInfo?.longitude != exifInfo?.longitude ||
|
||||
// no local stack count or different count from remote
|
||||
a.thumbhash != thumbhash ||
|
||||
((stackCount == null && a.stackCount != null) ||
|
||||
(stackCount != null &&
|
||||
a.stackCount != null &&
|
||||
@@ -348,7 +343,6 @@ class Asset {
|
||||
isReadOnly: a.isReadOnly,
|
||||
isOffline: a.isOffline,
|
||||
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
|
||||
thumbhash: a.thumbhash,
|
||||
);
|
||||
} else {
|
||||
// add only missing values (and set isLocal to true)
|
||||
@@ -385,7 +379,6 @@ class Asset {
|
||||
ExifInfo? exifInfo,
|
||||
String? stackParentId,
|
||||
int? stackCount,
|
||||
String? thumbhash,
|
||||
}) =>
|
||||
Asset(
|
||||
id: id ?? this.id,
|
||||
@@ -410,7 +403,6 @@ class Asset {
|
||||
exifInfo: exifInfo ?? this.exifInfo,
|
||||
stackParentId: stackParentId ?? this.stackParentId,
|
||||
stackCount: stackCount ?? this.stackCount,
|
||||
thumbhash: thumbhash ?? this.thumbhash,
|
||||
);
|
||||
|
||||
Future<void> put(Isar db) async {
|
||||
|
||||
Generated
+11
-209
@@ -102,24 +102,19 @@ const AssetSchema = CollectionSchema(
|
||||
name: r'stackParentId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'thumbhash': PropertySchema(
|
||||
id: 17,
|
||||
name: r'thumbhash',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'type': PropertySchema(
|
||||
id: 18,
|
||||
id: 17,
|
||||
name: r'type',
|
||||
type: IsarType.byte,
|
||||
enumMap: _AssettypeEnumValueMap,
|
||||
),
|
||||
r'updatedAt': PropertySchema(
|
||||
id: 19,
|
||||
id: 18,
|
||||
name: r'updatedAt',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'width': PropertySchema(
|
||||
id: 20,
|
||||
id: 19,
|
||||
name: r'width',
|
||||
type: IsarType.int,
|
||||
)
|
||||
@@ -215,12 +210,6 @@ int _assetEstimateSize(
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.thumbhash;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
@@ -247,10 +236,9 @@ void _assetSerialize(
|
||||
writer.writeString(offsets[14], object.remoteId);
|
||||
writer.writeLong(offsets[15], object.stackCount);
|
||||
writer.writeString(offsets[16], object.stackParentId);
|
||||
writer.writeString(offsets[17], object.thumbhash);
|
||||
writer.writeByte(offsets[18], object.type.index);
|
||||
writer.writeDateTime(offsets[19], object.updatedAt);
|
||||
writer.writeInt(offsets[20], object.width);
|
||||
writer.writeByte(offsets[17], object.type.index);
|
||||
writer.writeDateTime(offsets[18], object.updatedAt);
|
||||
writer.writeInt(offsets[19], object.width);
|
||||
}
|
||||
|
||||
Asset _assetDeserialize(
|
||||
@@ -278,11 +266,10 @@ Asset _assetDeserialize(
|
||||
remoteId: reader.readStringOrNull(offsets[14]),
|
||||
stackCount: reader.readLongOrNull(offsets[15]),
|
||||
stackParentId: reader.readStringOrNull(offsets[16]),
|
||||
thumbhash: reader.readStringOrNull(offsets[17]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ??
|
||||
AssetType.other,
|
||||
updatedAt: reader.readDateTime(offsets[19]),
|
||||
width: reader.readIntOrNull(offsets[20]),
|
||||
updatedAt: reader.readDateTime(offsets[18]),
|
||||
width: reader.readIntOrNull(offsets[19]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
@@ -329,13 +316,11 @@ P _assetDeserializeProp<P>(
|
||||
case 16:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 17:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 18:
|
||||
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
AssetType.other) as P;
|
||||
case 19:
|
||||
case 18:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 20:
|
||||
case 19:
|
||||
return (reader.readIntOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
@@ -2093,152 +2078,6 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'thumbhash',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'thumbhash',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashEqualTo(
|
||||
String? value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'thumbhash',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashGreaterThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'thumbhash',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashLessThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'thumbhash',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashBetween(
|
||||
String? lower,
|
||||
String? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'thumbhash',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'thumbhash',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'thumbhash',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashContains(
|
||||
String value,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'thumbhash',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashMatches(
|
||||
String pattern,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'thumbhash',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'thumbhash',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'thumbhash',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> typeEqualTo(
|
||||
AssetType value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -2623,18 +2462,6 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByThumbhash() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'thumbhash', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByThumbhashDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'thumbhash', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByType() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'type', Sort.asc);
|
||||
@@ -2889,18 +2716,6 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByThumbhash() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'thumbhash', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByThumbhashDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'thumbhash', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByType() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'type', Sort.asc);
|
||||
@@ -3049,13 +2864,6 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByThumbhash(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'thumbhash', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByType() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'type');
|
||||
@@ -3184,12 +2992,6 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, String?, QQueryOperations> thumbhashProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'thumbhash');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, AssetType, QQueryOperations> typeProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'type');
|
||||
|
||||
@@ -9,7 +9,6 @@ part 'logger_message.model.g.dart';
|
||||
class LoggerMessage {
|
||||
Id id = Isar.autoIncrement;
|
||||
String message;
|
||||
String? details;
|
||||
@Enumerated(EnumType.ordinal)
|
||||
LogLevel level = LogLevel.INFO;
|
||||
DateTime createdAt;
|
||||
@@ -18,7 +17,6 @@ class LoggerMessage {
|
||||
|
||||
LoggerMessage({
|
||||
required this.message,
|
||||
required this.details,
|
||||
required this.level,
|
||||
required this.createdAt,
|
||||
required this.context1,
|
||||
|
||||
+7
-213
@@ -32,19 +32,14 @@ const LoggerMessageSchema = CollectionSchema(
|
||||
name: r'createdAt',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'details': PropertySchema(
|
||||
id: 3,
|
||||
name: r'details',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'level': PropertySchema(
|
||||
id: 4,
|
||||
id: 3,
|
||||
name: r'level',
|
||||
type: IsarType.byte,
|
||||
enumMap: _LoggerMessagelevelEnumValueMap,
|
||||
),
|
||||
r'message': PropertySchema(
|
||||
id: 5,
|
||||
id: 4,
|
||||
name: r'message',
|
||||
type: IsarType.string,
|
||||
)
|
||||
@@ -81,12 +76,6 @@ int _loggerMessageEstimateSize(
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.details;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
bytesCount += 3 + object.message.length * 3;
|
||||
return bytesCount;
|
||||
}
|
||||
@@ -100,9 +89,8 @@ void _loggerMessageSerialize(
|
||||
writer.writeString(offsets[0], object.context1);
|
||||
writer.writeString(offsets[1], object.context2);
|
||||
writer.writeDateTime(offsets[2], object.createdAt);
|
||||
writer.writeString(offsets[3], object.details);
|
||||
writer.writeByte(offsets[4], object.level.index);
|
||||
writer.writeString(offsets[5], object.message);
|
||||
writer.writeByte(offsets[3], object.level.index);
|
||||
writer.writeString(offsets[4], object.message);
|
||||
}
|
||||
|
||||
LoggerMessage _loggerMessageDeserialize(
|
||||
@@ -115,10 +103,9 @@ LoggerMessage _loggerMessageDeserialize(
|
||||
context1: reader.readStringOrNull(offsets[0]),
|
||||
context2: reader.readStringOrNull(offsets[1]),
|
||||
createdAt: reader.readDateTime(offsets[2]),
|
||||
details: reader.readStringOrNull(offsets[3]),
|
||||
level: _LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offsets[4])] ??
|
||||
level: _LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offsets[3])] ??
|
||||
LogLevel.ALL,
|
||||
message: reader.readString(offsets[5]),
|
||||
message: reader.readString(offsets[4]),
|
||||
);
|
||||
object.id = id;
|
||||
return object;
|
||||
@@ -138,11 +125,9 @@ P _loggerMessageDeserializeProp<P>(
|
||||
case 2:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 3:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 4:
|
||||
return (_LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
LogLevel.ALL) as P;
|
||||
case 5:
|
||||
case 4:
|
||||
return (reader.readString(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
@@ -634,160 +619,6 @@ extension LoggerMessageQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'details',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'details',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsEqualTo(
|
||||
String? value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'details',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsGreaterThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'details',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsLessThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'details',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsBetween(
|
||||
String? lower,
|
||||
String? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'details',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'details',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'details',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'details',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsMatches(String pattern, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'details',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'details',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
|
||||
detailsIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'details',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition> idEqualTo(
|
||||
Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -1082,18 +913,6 @@ extension LoggerMessageQuerySortBy
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByDetails() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'details', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByDetailsDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'details', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByLevel() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'level', Sort.asc);
|
||||
@@ -1160,18 +979,6 @@ extension LoggerMessageQuerySortThenBy
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByDetails() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'details', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByDetailsDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'details', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
@@ -1231,13 +1038,6 @@ extension LoggerMessageQueryWhereDistinct
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QDistinct> distinctByDetails(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'details', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LoggerMessage, QDistinct> distinctByLevel() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'level');
|
||||
@@ -1278,12 +1078,6 @@ extension LoggerMessageQueryProperty
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, String?, QQueryOperations> detailsProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'details');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<LoggerMessage, LogLevel, QQueryOperations> levelProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'level');
|
||||
|
||||
@@ -90,7 +90,7 @@ class AssetService {
|
||||
return allAssets;
|
||||
} catch (error, stack) {
|
||||
log.severe(
|
||||
'Error while getting remote assets',
|
||||
'Error while getting remote assets: ${error.toString()}',
|
||||
error,
|
||||
stack,
|
||||
);
|
||||
@@ -117,7 +117,7 @@ class AssetService {
|
||||
);
|
||||
return true;
|
||||
} catch (error, stack) {
|
||||
log.severe("Error while deleting assets", error, stack);
|
||||
log.severe("Error deleteAssets ${error.toString()}", error, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:share_plus/share_plus.dart';
|
||||
/// [ImmichLogger] is a custom logger that is built on top of the [logging] package.
|
||||
/// The logs are written to the database and onto console, using `debugPrint` method.
|
||||
///
|
||||
/// The logs are deleted when exceeding the `maxLogEntries` (default 500) property
|
||||
/// The logs are deleted when exceeding the `maxLogEntries` (default 200) property
|
||||
/// in the class.
|
||||
///
|
||||
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
||||
@@ -58,7 +58,6 @@ class ImmichLogger {
|
||||
debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
|
||||
final lm = LoggerMessage(
|
||||
message: record.message,
|
||||
details: record.error?.toString(),
|
||||
level: record.level.toLogLevel(),
|
||||
createdAt: record.time,
|
||||
context1: record.loggerName,
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -42,8 +41,7 @@ class ShareService {
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
_log.severe(
|
||||
"Asset download for ${asset.fileName} failed",
|
||||
res.toLoggerString(),
|
||||
"Asset download failed with status - ${res.statusCode} and response - ${res.body}",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -70,7 +68,7 @@ class ShareService {
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
_log.severe("Share failed", error);
|
||||
_log.severe("Share failed with error $error");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class SyncService {
|
||||
try {
|
||||
await _db.writeTxn(() => a.put(_db));
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to put new asset into db", e);
|
||||
_log.severe("Failed to put new asset into db: $e");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -173,7 +173,7 @@ class SyncService {
|
||||
}
|
||||
return false;
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
_log.severe("Failed to sync remote assets to db: $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -232,7 +232,7 @@ class SyncService {
|
||||
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
|
||||
await upsertAssetsWithExif(toAdd + toUpdate);
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
_log.severe("Failed to sync remote assets to db: $e");
|
||||
}
|
||||
await _updateUserAssetsETag(user, now);
|
||||
return true;
|
||||
@@ -364,7 +364,7 @@ class SyncService {
|
||||
});
|
||||
_log.info("Synced changes of remote album ${album.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote album to database", e);
|
||||
_log.severe("Failed to sync remote album to database $e");
|
||||
}
|
||||
|
||||
if (album.shared || dto.shared) {
|
||||
@@ -441,7 +441,7 @@ class SyncService {
|
||||
assert(ok);
|
||||
_log.info("Removed local album $album from DB");
|
||||
} catch (e) {
|
||||
_log.severe("Failed to remove local album $album from DB", e);
|
||||
_log.severe("Failed to remove local album $album from DB");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,7 +577,7 @@ class SyncService {
|
||||
});
|
||||
_log.info("Synced changes of local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to update synced album ${ape.name} in DB", e);
|
||||
_log.severe("Failed to update synced album ${ape.name} in DB: $e");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -623,7 +623,7 @@ class SyncService {
|
||||
});
|
||||
_log.info("Fast synced local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to fast sync local album ${ape.name} to DB", e);
|
||||
_log.severe("Failed to fast sync local album ${ape.name} to DB: $e");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -656,7 +656,7 @@ class SyncService {
|
||||
await _db.writeTxn(() => _db.albums.store(a));
|
||||
_log.info("Added a new local album to DB: ${ape.name}");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to add new local album ${ape.name} to DB", e);
|
||||
_log.severe("Failed to add new local album ${ape.name} to DB: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,7 +706,9 @@ class SyncService {
|
||||
});
|
||||
_log.info("Upserted ${assets.length} assets into the DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to upsert ${assets.length} assets into the DB", e);
|
||||
_log.severe(
|
||||
"Failed to upsert ${assets.length} assets into the DB: ${e.toString()}",
|
||||
);
|
||||
// give details on the errors
|
||||
assets.sort(Asset.compareByOwnerChecksum);
|
||||
final inDb = await _db.assets.getAllByOwnerIdChecksum(
|
||||
@@ -774,7 +776,7 @@ class SyncService {
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
_log.severe("Failed to remove all local albums and assets", e);
|
||||
_log.severe("Failed to remove all local albums and assets: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class UserService {
|
||||
final dto = await _apiService.userApi.getAllUsers(isAll);
|
||||
return dto?.map(User.fromUserDto).toList();
|
||||
} catch (e) {
|
||||
_log.warning("Failed get all users", e);
|
||||
_log.warning("Failed get all users:\n$e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class UserService {
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
_log.warning("Failed to upload profile image", e);
|
||||
_log.warning("Failed to upload profile image:\n$e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/shared/ui/transparent_image.dart';
|
||||
|
||||
class FadeInPlaceholderImage extends StatelessWidget {
|
||||
final Widget placeholder;
|
||||
final ImageProvider image;
|
||||
final Duration duration;
|
||||
final BoxFit fit;
|
||||
|
||||
const FadeInPlaceholderImage({
|
||||
super.key,
|
||||
required this.placeholder,
|
||||
required this.image,
|
||||
this.duration = const Duration(milliseconds: 100),
|
||||
this.fit = BoxFit.cover,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.expand(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
placeholder,
|
||||
FadeInImage(
|
||||
fadeInDuration: duration,
|
||||
image: image,
|
||||
fit: fit,
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:thumbhash/thumbhash.dart' as thumbhash;
|
||||
|
||||
ObjectRef<Uint8List?> useBlurHashRef(Asset? asset) {
|
||||
if (asset?.thumbhash == null) {
|
||||
return useRef(null);
|
||||
}
|
||||
|
||||
final rbga = thumbhash.thumbHashToRGBA(
|
||||
base64Decode(asset!.thumbhash!),
|
||||
);
|
||||
|
||||
return useRef(thumbhash.rgbaToBmp(rbga));
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
@@ -7,6 +9,8 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.d
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:octo_image/octo_image.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
|
||||
|
||||
class ImmichImage extends StatelessWidget {
|
||||
const ImmichImage(
|
||||
@@ -15,6 +19,8 @@ class ImmichImage extends StatelessWidget {
|
||||
this.height,
|
||||
this.fit = BoxFit.cover,
|
||||
this.placeholder = const ThumbnailPlaceholder(),
|
||||
this.isThumbnail = false,
|
||||
this.thumbnailSize = 250,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -23,6 +29,32 @@ class ImmichImage extends StatelessWidget {
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit fit;
|
||||
final bool isThumbnail;
|
||||
final int thumbnailSize;
|
||||
|
||||
/// Factory constructor to use the thumbnail variant
|
||||
factory ImmichImage.thumbnail(
|
||||
Asset? asset, {
|
||||
BoxFit fit = BoxFit.cover,
|
||||
double? width,
|
||||
double? height,
|
||||
}) {
|
||||
// Use the width and height to derive thumbnail size
|
||||
final thumbnailSize = max(width ?? 250, height ?? 250).toInt();
|
||||
|
||||
return ImmichImage(
|
||||
asset,
|
||||
isThumbnail: true,
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
placeholder: ThumbnailPlaceholder(
|
||||
height: thumbnailSize.toDouble(),
|
||||
width: thumbnailSize.toDouble(),
|
||||
),
|
||||
thumbnailSize: thumbnailSize,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to return the image provider for the asset
|
||||
// either by using the asset ID or the asset itself
|
||||
@@ -34,6 +66,8 @@ class ImmichImage extends StatelessWidget {
|
||||
static ImageProvider imageProvider({
|
||||
Asset? asset,
|
||||
String? assetId,
|
||||
bool isThumbnail = false,
|
||||
int thumbnailSize = 250,
|
||||
}) {
|
||||
if (asset == null && assetId == null) {
|
||||
throw Exception('Must supply either asset or assetId');
|
||||
@@ -42,18 +76,24 @@ class ImmichImage extends StatelessWidget {
|
||||
if (asset == null) {
|
||||
return ImmichRemoteImageProvider(
|
||||
assetId: assetId!,
|
||||
isThumbnail: false,
|
||||
isThumbnail: isThumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
if (useLocal(asset)) {
|
||||
if (useLocal(asset) && isThumbnail) {
|
||||
return AssetEntityImageProvider(
|
||||
asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: ThumbnailSize.square(thumbnailSize),
|
||||
);
|
||||
} else if (useLocal(asset) && !isThumbnail) {
|
||||
return ImmichLocalImageProvider(
|
||||
asset: asset,
|
||||
);
|
||||
} else {
|
||||
return ImmichRemoteImageProvider(
|
||||
assetId: asset.remoteId!,
|
||||
isThumbnail: false,
|
||||
isThumbnail: isThumbnail,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -65,11 +105,15 @@ class ImmichImage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
if (asset == null) {
|
||||
return Container(
|
||||
color: Colors.grey,
|
||||
width: width,
|
||||
height: height,
|
||||
child: const Center(
|
||||
child: Icon(Icons.no_photography),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.grey,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: const Center(
|
||||
child: Icon(Icons.no_photography),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -87,6 +131,7 @@ class ImmichImage extends StatelessWidget {
|
||||
},
|
||||
image: ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
isThumbnail: isThumbnail,
|
||||
),
|
||||
width: width,
|
||||
height: height,
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_thumbnail_provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||
import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart';
|
||||
import 'package:octo_image/octo_image.dart';
|
||||
|
||||
class ImmichThumbnail extends HookWidget {
|
||||
const ImmichThumbnail({
|
||||
this.asset,
|
||||
this.width = 250,
|
||||
this.height = 250,
|
||||
this.fit = BoxFit.cover,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Asset? asset;
|
||||
final double width;
|
||||
final double height;
|
||||
final BoxFit fit;
|
||||
|
||||
/// Helper function to return the image provider for the asset thumbnail
|
||||
/// either by using the asset ID or the asset itself
|
||||
/// [asset] is the Asset to request, or else use [assetId] to get a remote
|
||||
/// image provider
|
||||
static ImageProvider imageProvider({
|
||||
Asset? asset,
|
||||
String? assetId,
|
||||
int thumbnailSize = 256,
|
||||
}) {
|
||||
if (asset == null && assetId == null) {
|
||||
throw Exception('Must supply either asset or assetId');
|
||||
}
|
||||
|
||||
if (asset == null) {
|
||||
return ImmichRemoteImageProvider(
|
||||
assetId: assetId!,
|
||||
isThumbnail: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (useLocal(asset)) {
|
||||
return ImmichLocalThumbnailProvider(
|
||||
asset: asset,
|
||||
height: thumbnailSize,
|
||||
width: thumbnailSize,
|
||||
);
|
||||
} else {
|
||||
return ImmichRemoteImageProvider(
|
||||
assetId: asset.remoteId!,
|
||||
isThumbnail: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static bool useLocal(Asset asset) => !asset.isRemote || asset.isLocal;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Uint8List? blurhash = useBlurHashRef(asset).value;
|
||||
if (asset == null) {
|
||||
return Container(
|
||||
color: Colors.grey,
|
||||
width: width,
|
||||
height: height,
|
||||
child: const Center(
|
||||
child: Icon(Icons.no_photography),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return OctoImage.fromSet(
|
||||
placeholderFadeInDuration: Duration.zero,
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: const Duration(milliseconds: 100),
|
||||
octoSet: blurHashOrPlaceholder(blurhash),
|
||||
image: ImmichThumbnail.imageProvider(
|
||||
asset: asset,
|
||||
),
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/shared/ui/fade_in_placeholder_image.dart';
|
||||
import 'package:octo_image/octo_image.dart';
|
||||
|
||||
/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as
|
||||
/// placeholder and [OctoError.icon] as error.
|
||||
OctoSet blurHashOrPlaceholder(
|
||||
Uint8List? blurhash, {
|
||||
BoxFit? fit,
|
||||
Text? errorMessage,
|
||||
}) {
|
||||
return OctoSet(
|
||||
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
|
||||
errorBuilder: blurHashErrorBuilder(blurhash, fit: fit),
|
||||
);
|
||||
}
|
||||
|
||||
OctoPlaceholderBuilder blurHashPlaceholderBuilder(
|
||||
Uint8List? blurhash, {
|
||||
BoxFit? fit,
|
||||
}) {
|
||||
return (context) => blurhash == null
|
||||
? const ThumbnailPlaceholder()
|
||||
: FadeInPlaceholderImage(
|
||||
placeholder: const ThumbnailPlaceholder(),
|
||||
image: MemoryImage(blurhash),
|
||||
fit: fit ?? BoxFit.cover,
|
||||
);
|
||||
}
|
||||
|
||||
OctoErrorBuilder blurHashErrorBuilder(
|
||||
Uint8List? blurhash, {
|
||||
BoxFit? fit,
|
||||
Text? message,
|
||||
IconData? icon,
|
||||
Color? iconColor,
|
||||
double? iconSize,
|
||||
}) {
|
||||
return OctoError.placeholderWithErrorIcon(
|
||||
blurHashPlaceholderBuilder(blurhash, fit: fit),
|
||||
message: message,
|
||||
icon: icon,
|
||||
iconColor: iconColor,
|
||||
iconSize: iconSize,
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var isDarkTheme = context.isDarkTheme;
|
||||
|
||||
buildTextWithCopyButton(String header, String text) {
|
||||
buildStackMessage(String stackTrace) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
@@ -28,7 +28,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
header,
|
||||
"STACK TRACES",
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: context.primaryColor,
|
||||
@@ -38,7 +38,8 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: text)).then((_) {
|
||||
Clipboard.setData(ClipboardData(text: stackTrace))
|
||||
.then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
@@ -67,7 +68,73 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SelectableText(
|
||||
text,
|
||||
stackTrace,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: "Inconsolata",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildLogMessage(String message) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"MESSAGE",
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: message)).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Copied to clipboard",
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.copy,
|
||||
size: 16.0,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkTheme ? Colors.grey[900] : Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SelectableText(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -127,16 +194,11 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
buildTextWithCopyButton("MESSAGE", logMessage.message),
|
||||
if (logMessage.details != null)
|
||||
buildTextWithCopyButton("DETAILS", logMessage.details.toString()),
|
||||
buildLogMessage(logMessage.message),
|
||||
if (logMessage.context1 != null)
|
||||
buildLogContext1(logMessage.context1.toString()),
|
||||
if (logMessage.context2 != null)
|
||||
buildTextWithCopyButton(
|
||||
"STACK TRACE",
|
||||
logMessage.context2.toString(),
|
||||
),
|
||||
buildStackMessage(logMessage.context2.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -69,9 +69,9 @@ class AppLogPage extends HookConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"Logs",
|
||||
style: TextStyle(
|
||||
title: Text(
|
||||
"Logs - ${logMessages.value.length}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
@@ -135,15 +135,29 @@ class AppLogPage extends HookConsumerWidget {
|
||||
dense: true,
|
||||
tileColor: getTileColor(logMessage.level),
|
||||
minLeadingWidth: 10,
|
||||
title: Text(
|
||||
truncateLogMessage(logMessage.message, 4),
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontFamily: "Inconsolata",
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "#$index ",
|
||||
style: TextStyle(
|
||||
color: isDarkTheme ? Colors.white70 : Colors.grey[600],
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: truncateLogMessage(logMessage.message, 4),
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: const TextStyle(fontSize: 14.0, fontFamily: "Inconsolata"),
|
||||
),
|
||||
subtitle: Text(
|
||||
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}",
|
||||
"[${logMessage.context1}] Logged on ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}",
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: Colors.grey[600],
|
||||
|
||||
@@ -35,10 +35,10 @@ class SplashScreenPage extends HookConsumerWidget {
|
||||
deviceIsOffline = true;
|
||||
log.fine("Device seems to be offline upon launch");
|
||||
} else {
|
||||
log.severe("Failed to resolve endpoint", e);
|
||||
log.severe(e);
|
||||
}
|
||||
} catch (e) {
|
||||
log.severe("Failed to resolve endpoint", e);
|
||||
log.severe(e);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -53,7 +53,7 @@ class SplashScreenPage extends HookConsumerWidget {
|
||||
ref.read(authenticationProvider.notifier).logout();
|
||||
|
||||
log.severe(
|
||||
'Cannot set success login info',
|
||||
'Cannot set success login info: $error',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
|
||||
Generated
-3
@@ -108,7 +108,6 @@ doc/PersonResponseDto.md
|
||||
doc/PersonStatisticsResponseDto.md
|
||||
doc/PersonUpdateDto.md
|
||||
doc/PersonWithFacesResponseDto.md
|
||||
doc/PlacesResponseDto.md
|
||||
doc/QueueStatusDto.md
|
||||
doc/ReactionLevel.md
|
||||
doc/ReactionType.md
|
||||
@@ -309,7 +308,6 @@ lib/model/person_response_dto.dart
|
||||
lib/model/person_statistics_response_dto.dart
|
||||
lib/model/person_update_dto.dart
|
||||
lib/model/person_with_faces_response_dto.dart
|
||||
lib/model/places_response_dto.dart
|
||||
lib/model/queue_status_dto.dart
|
||||
lib/model/reaction_level.dart
|
||||
lib/model/reaction_type.dart
|
||||
@@ -487,7 +485,6 @@ test/person_response_dto_test.dart
|
||||
test/person_statistics_response_dto_test.dart
|
||||
test/person_update_dto_test.dart
|
||||
test/person_with_faces_response_dto_test.dart
|
||||
test/places_response_dto_test.dart
|
||||
test/queue_status_dto_test.dart
|
||||
test/reaction_level_test.dart
|
||||
test/reaction_type_test.dart
|
||||
|
||||
Generated
+1
-3
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.96.0
|
||||
- API version: 1.95.1
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
@@ -166,7 +166,6 @@ Class | Method | HTTP request | Description
|
||||
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
|
||||
*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* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
|
||||
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
|
||||
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
|
||||
@@ -307,7 +306,6 @@ Class | Method | HTTP request | Description
|
||||
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
|
||||
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
||||
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
|
||||
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||
- [ReactionLevel](doc//ReactionLevel.md)
|
||||
- [ReactionType](doc//ReactionType.md)
|
||||
|
||||
Generated
-19
@@ -1,19 +0,0 @@
|
||||
# openapi.model.PlacesResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**admin1name** | **String** | | [optional]
|
||||
**admin2name** | **String** | | [optional]
|
||||
**latitude** | **num** | |
|
||||
**longitude** | **num** | |
|
||||
**name** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
Generated
-56
@@ -14,7 +14,6 @@ Method | HTTP request | Description
|
||||
[**search**](SearchApi.md#search) | **GET** /search |
|
||||
[**searchMetadata**](SearchApi.md#searchmetadata) | **POST** /search/metadata |
|
||||
[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person |
|
||||
[**searchPlaces**](SearchApi.md#searchplaces) | **GET** /search/places |
|
||||
[**searchSmart**](SearchApi.md#searchsmart) | **POST** /search/smart |
|
||||
|
||||
|
||||
@@ -317,61 +316,6 @@ Name | Type | Description | Notes
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **searchPlaces**
|
||||
> List<PlacesResponseDto> searchPlaces(name)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = SearchApi();
|
||||
final name = name_example; // String |
|
||||
|
||||
try {
|
||||
final result = api_instance.searchPlaces(name);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling SearchApi->searchPlaces: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**name** | **String**| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<PlacesResponseDto>**](PlacesResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **searchSmart**
|
||||
> SearchResponseDto searchSmart(smartSearchDto)
|
||||
|
||||
|
||||
Generated
-1
@@ -142,7 +142,6 @@ part 'model/person_response_dto.dart';
|
||||
part 'model/person_statistics_response_dto.dart';
|
||||
part 'model/person_update_dto.dart';
|
||||
part 'model/person_with_faces_response_dto.dart';
|
||||
part 'model/places_response_dto.dart';
|
||||
part 'model/queue_status_dto.dart';
|
||||
part 'model/reaction_level.dart';
|
||||
part 'model/reaction_type.dart';
|
||||
|
||||
Generated
-52
@@ -360,58 +360,6 @@ class SearchApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /search/places' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] name (required):
|
||||
Future<Response> searchPlacesWithHttpInfo(String name,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/search/places';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
queryParams.addAll(_queryParams('', 'name', name));
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] name (required):
|
||||
Future<List<PlacesResponseDto>?> searchPlaces(String name,) async {
|
||||
final response = await searchPlacesWithHttpInfo(name,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<PlacesResponseDto>') as List)
|
||||
.cast<PlacesResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /search/smart' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
Generated
-2
@@ -366,8 +366,6 @@ class ApiClient {
|
||||
return PersonUpdateDto.fromJson(value);
|
||||
case 'PersonWithFacesResponseDto':
|
||||
return PersonWithFacesResponseDto.fromJson(value);
|
||||
case 'PlacesResponseDto':
|
||||
return PlacesResponseDto.fromJson(value);
|
||||
case 'QueueStatusDto':
|
||||
return QueueStatusDto.fromJson(value);
|
||||
case 'ReactionLevel':
|
||||
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class PlacesResponseDto {
|
||||
/// Returns a new [PlacesResponseDto] instance.
|
||||
PlacesResponseDto({
|
||||
this.admin1name,
|
||||
this.admin2name,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? admin1name;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? admin2name;
|
||||
|
||||
num latitude;
|
||||
|
||||
num longitude;
|
||||
|
||||
String name;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PlacesResponseDto &&
|
||||
other.admin1name == admin1name &&
|
||||
other.admin2name == admin2name &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude &&
|
||||
other.name == name;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(admin1name == null ? 0 : admin1name!.hashCode) +
|
||||
(admin2name == null ? 0 : admin2name!.hashCode) +
|
||||
(latitude.hashCode) +
|
||||
(longitude.hashCode) +
|
||||
(name.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PlacesResponseDto[admin1name=$admin1name, admin2name=$admin2name, latitude=$latitude, longitude=$longitude, name=$name]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.admin1name != null) {
|
||||
json[r'admin1name'] = this.admin1name;
|
||||
} else {
|
||||
// json[r'admin1name'] = null;
|
||||
}
|
||||
if (this.admin2name != null) {
|
||||
json[r'admin2name'] = this.admin2name;
|
||||
} else {
|
||||
// json[r'admin2name'] = null;
|
||||
}
|
||||
json[r'latitude'] = this.latitude;
|
||||
json[r'longitude'] = this.longitude;
|
||||
json[r'name'] = this.name;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PlacesResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PlacesResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PlacesResponseDto(
|
||||
admin1name: mapValueOfType<String>(json, r'admin1name'),
|
||||
admin2name: mapValueOfType<String>(json, r'admin2name'),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PlacesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PlacesResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PlacesResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PlacesResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PlacesResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PlacesResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PlacesResponseDto-objects as value to a dart map
|
||||
static Map<String, List<PlacesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PlacesResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PlacesResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'latitude',
|
||||
'longitude',
|
||||
'name',
|
||||
};
|
||||
}
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for PlacesResponseDto
|
||||
void main() {
|
||||
// final instance = PlacesResponseDto();
|
||||
|
||||
group('test PlacesResponseDto', () {
|
||||
// String admin1name
|
||||
test('to test the property `admin1name`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String admin2name
|
||||
test('to test the property `admin2name`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// num latitude
|
||||
test('to test the property `latitude`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// num longitude
|
||||
test('to test the property `longitude`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String name
|
||||
test('to test the property `name`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
Generated
-5
@@ -42,11 +42,6 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<PlacesResponseDto>> searchPlaces(String name) async
|
||||
test('test searchPlaces', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<SearchResponseDto> searchSmart(SmartSearchDto smartSearchDto) async
|
||||
test('test searchSmart', () async {
|
||||
// TODO
|
||||
|
||||
+40
-24
@@ -413,10 +413,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "7.0.0"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -860,6 +860,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -907,18 +931,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
@@ -1002,10 +1026,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
version: "1.9.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1138,10 +1162,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "3.1.4"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1170,10 +1194,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
|
||||
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
version: "5.0.2"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1467,14 +1491,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
thumbhash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: thumbhash
|
||||
sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0+1"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1639,10 +1655,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.10.0"
|
||||
version: "13.0.0"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1687,10 +1703,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49"
|
||||
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+1
-2
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.96.0+124
|
||||
version: 1.95.1+123
|
||||
isar_version: &isar_version 3.1.0+1
|
||||
|
||||
environment:
|
||||
@@ -57,7 +57,6 @@ dependencies:
|
||||
flutter_local_notifications: ^16.3.2
|
||||
timezone: ^0.9.2
|
||||
octo_image: ^2.0.0
|
||||
thumbhash: 0.1.0+1
|
||||
|
||||
openapi:
|
||||
path: openapi
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user