forked from Cutlery/immich
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3c692aad8 | |||
| 2e99ce994b | |||
| 26c3635291 | |||
| 484f1256ea | |||
| d51a666692 | |||
| 73f20ef4e7 | |||
| e1f66ac4da | |||
| 2d95715ae8 | |||
| 692b8b189a | |||
| 7d51fba1c0 | |||
| 4c9ac82fdc |
@@ -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.
|
||||
|
||||
@@ -4,7 +4,6 @@ name: immich-e2e
|
||||
|
||||
x-server-build: &server-common
|
||||
image: immich-server:latest
|
||||
container_name: immich-e2e-server
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
|
||||
Generated
+39
-39
@@ -904,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",
|
||||
@@ -927,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": {
|
||||
@@ -945,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"
|
||||
},
|
||||
@@ -959,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",
|
||||
@@ -973,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"
|
||||
@@ -985,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",
|
||||
@@ -2551,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",
|
||||
@@ -2606,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",
|
||||
@@ -2642,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",
|
||||
@@ -2665,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": {
|
||||
@@ -2680,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": "*"
|
||||
},
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import {
|
||||
deleteAssets,
|
||||
getAuditFiles,
|
||||
updateAsset,
|
||||
type LoginResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/audit', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
await fileUtils.reset();
|
||||
|
||||
admin = await apiUtils.adminSetup();
|
||||
});
|
||||
|
||||
describe('GET :/file-report', () => {
|
||||
it('excludes assets without issues from report', async () => {
|
||||
const [trashedAsset, archivedAsset, _] = await Promise.all([
|
||||
apiUtils.createAsset(admin.accessToken),
|
||||
apiUtils.createAsset(admin.accessToken),
|
||||
apiUtils.createAsset(admin.accessToken),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
deleteAssets(
|
||||
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
),
|
||||
updateAsset(
|
||||
{
|
||||
id: archivedAsset.id,
|
||||
updateAssetDto: { isArchived: true },
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
),
|
||||
]);
|
||||
|
||||
const body = await getAuditFiles({
|
||||
headers: asBearerAuth(admin.accessToken),
|
||||
});
|
||||
|
||||
expect(body.orphans).toHaveLength(0);
|
||||
expect(body.extras).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-15
@@ -17,17 +17,14 @@ import {
|
||||
updatePerson,
|
||||
} from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
import { exec, spawn } from 'child_process';
|
||||
import { spawn } from 'child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { access } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
import pg from 'pg';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import request from 'supertest';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
export const app = 'http://127.0.0.1:2283/api';
|
||||
|
||||
const directoryExists = (directory: string) =>
|
||||
@@ -38,9 +35,6 @@ const directoryExists = (directory: string) =>
|
||||
// TODO move test assets into e2e/assets
|
||||
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
||||
|
||||
const serverContainerName = 'immich-e2e-server';
|
||||
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`
|
||||
@@ -56,14 +50,6 @@ export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||
|
||||
let client: pg.Client | null = null;
|
||||
|
||||
export const fileUtils = {
|
||||
reset: async () => {
|
||||
await execPromise(
|
||||
`docker exec -i "${serverContainerName}" rm -R "${uploadMediaDir}"`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const dbUtils = {
|
||||
createFace: async ({
|
||||
assetId,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -238,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()
|
||||
@@ -257,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)
|
||||
@@ -280,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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
3.13.6
|
||||
@@ -0,0 +1 @@
|
||||
3.13.6
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
C:/Users/alext/fvm/versions/3.13.6
|
||||
@@ -180,4 +180,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart' as store;
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
/// Provides the initialized video player controller
|
||||
/// If the asset is local, use the local file
|
||||
/// Otherwise, use a video player with a URL
|
||||
ChewieController? useChewieController(
|
||||
Asset asset, {
|
||||
EdgeInsets controlsSafeAreaMinimum = const EdgeInsets.only(
|
||||
bottom: 100,
|
||||
),
|
||||
bool showOptions = true,
|
||||
bool showControlsOnInitialize = false,
|
||||
bool autoPlay = true,
|
||||
bool autoInitialize = true,
|
||||
bool allowFullScreen = false,
|
||||
bool allowedScreenSleep = false,
|
||||
bool showControls = true,
|
||||
Widget? customControls,
|
||||
Widget? placeholder,
|
||||
Duration hideControlsTimer = const Duration(seconds: 1),
|
||||
VoidCallback? onPlaying,
|
||||
VoidCallback? onPaused,
|
||||
VoidCallback? onVideoEnded,
|
||||
}) {
|
||||
return use(
|
||||
_ChewieControllerHook(
|
||||
asset: asset,
|
||||
placeholder: placeholder,
|
||||
showOptions: showOptions,
|
||||
controlsSafeAreaMinimum: controlsSafeAreaMinimum,
|
||||
autoPlay: autoPlay,
|
||||
allowFullScreen: allowFullScreen,
|
||||
customControls: customControls,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showControlsOnInitialize: showControlsOnInitialize,
|
||||
showControls: showControls,
|
||||
autoInitialize: autoInitialize,
|
||||
allowedScreenSleep: allowedScreenSleep,
|
||||
onPlaying: onPlaying,
|
||||
onPaused: onPaused,
|
||||
onVideoEnded: onVideoEnded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ChewieControllerHook extends Hook<ChewieController?> {
|
||||
final Asset asset;
|
||||
final EdgeInsets controlsSafeAreaMinimum;
|
||||
final bool showOptions;
|
||||
final bool showControlsOnInitialize;
|
||||
final bool autoPlay;
|
||||
final bool autoInitialize;
|
||||
final bool allowFullScreen;
|
||||
final bool allowedScreenSleep;
|
||||
final bool showControls;
|
||||
final Widget? customControls;
|
||||
final Widget? placeholder;
|
||||
final Duration hideControlsTimer;
|
||||
final VoidCallback? onPlaying;
|
||||
final VoidCallback? onPaused;
|
||||
final VoidCallback? onVideoEnded;
|
||||
|
||||
const _ChewieControllerHook({
|
||||
required this.asset,
|
||||
this.controlsSafeAreaMinimum = const EdgeInsets.only(
|
||||
bottom: 100,
|
||||
),
|
||||
this.showOptions = true,
|
||||
this.showControlsOnInitialize = false,
|
||||
this.autoPlay = true,
|
||||
this.autoInitialize = true,
|
||||
this.allowFullScreen = false,
|
||||
this.allowedScreenSleep = false,
|
||||
this.showControls = true,
|
||||
this.customControls,
|
||||
this.placeholder,
|
||||
this.hideControlsTimer = const Duration(seconds: 3),
|
||||
this.onPlaying,
|
||||
this.onPaused,
|
||||
this.onVideoEnded,
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _ChewieControllerHookState();
|
||||
}
|
||||
|
||||
class _ChewieControllerHookState
|
||||
extends HookState<ChewieController?, _ChewieControllerHook> {
|
||||
ChewieController? chewieController;
|
||||
VideoPlayerController? videoPlayerController;
|
||||
|
||||
@override
|
||||
void initHook() async {
|
||||
super.initHook();
|
||||
unawaited(_initialize());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
chewieController?.dispose();
|
||||
videoPlayerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
ChewieController? build(BuildContext context) {
|
||||
return chewieController;
|
||||
}
|
||||
|
||||
/// Initializes the chewie controller and video player controller
|
||||
Future<void> _initialize() async {
|
||||
if (hook.asset.isLocal && hook.asset.livePhotoVideoId == null) {
|
||||
// Use a local file for the video player controller
|
||||
final file = await hook.asset.local!.file;
|
||||
if (file == null) {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
videoPlayerController = VideoPlayerController.file(file);
|
||||
} else {
|
||||
// Use a network URL for the video player controller
|
||||
final serverEndpoint = store.Store.get(store.StoreKey.serverEndpoint);
|
||||
final String videoUrl = hook.asset.livePhotoVideoId != null
|
||||
? '$serverEndpoint/asset/file/${hook.asset.livePhotoVideoId}'
|
||||
: '$serverEndpoint/asset/file/${hook.asset.remoteId}';
|
||||
|
||||
final url = Uri.parse(videoUrl);
|
||||
final accessToken = store.Store.get(StoreKey.accessToken);
|
||||
|
||||
videoPlayerController = VideoPlayerController.networkUrl(
|
||||
url,
|
||||
httpHeaders: {"x-immich-user-token": accessToken},
|
||||
);
|
||||
}
|
||||
|
||||
videoPlayerController!.addListener(() {
|
||||
final value = videoPlayerController!.value;
|
||||
if (value.isPlaying) {
|
||||
WakelockPlus.enable();
|
||||
hook.onPlaying?.call();
|
||||
} else if (!value.isPlaying) {
|
||||
WakelockPlus.disable();
|
||||
hook.onPaused?.call();
|
||||
}
|
||||
|
||||
if (value.position == value.duration) {
|
||||
WakelockPlus.disable();
|
||||
hook.onVideoEnded?.call();
|
||||
}
|
||||
});
|
||||
|
||||
await videoPlayerController!.initialize();
|
||||
|
||||
setState(() {
|
||||
chewieController = ChewieController(
|
||||
videoPlayerController: videoPlayerController!,
|
||||
controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum,
|
||||
showOptions: hook.showOptions,
|
||||
showControlsOnInitialize: hook.showControlsOnInitialize,
|
||||
autoPlay: hook.autoPlay,
|
||||
autoInitialize: hook.autoInitialize,
|
||||
allowFullScreen: hook.allowFullScreen,
|
||||
allowedScreenSleep: hook.allowedScreenSleep,
|
||||
showControls: hook.showControls,
|
||||
customControls: hook.customControls,
|
||||
placeholder: hook.placeholder,
|
||||
hideControlsTimer: hook.hideControlsTimer,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provi
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart';
|
||||
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class VideoPlayerControls extends ConsumerStatefulWidget {
|
||||
@@ -66,9 +66,7 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||
children: [
|
||||
if (_displayBufferingIndicator)
|
||||
const Center(
|
||||
child: DelayedLoadingIndicator(
|
||||
fadeInDuration: Duration(milliseconds: 400),
|
||||
),
|
||||
child: ImmichLoadingIndicator(),
|
||||
)
|
||||
else
|
||||
_buildHitArea(),
|
||||
@@ -81,7 +79,6 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||
@override
|
||||
void dispose() {
|
||||
_dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -95,7 +92,6 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||
final oldController = _chewieController;
|
||||
_chewieController = ChewieController.of(context);
|
||||
controller = chewieController.videoPlayerController;
|
||||
_latestValue = controller.value;
|
||||
|
||||
if (oldController != chewieController) {
|
||||
_dispose();
|
||||
@@ -110,10 +106,12 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (!_latestValue.isPlaying) {
|
||||
if (_latestValue.isPlaying) {
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
} else {
|
||||
_playPause();
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
}
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: Colors.black54,
|
||||
@@ -133,11 +131,10 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
_mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute)));
|
||||
|
||||
_latestValue = controller.value;
|
||||
controller.addListener(_updateState);
|
||||
_latestValue = controller.value;
|
||||
|
||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||
_startHideTimer();
|
||||
@@ -170,8 +167,9 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
final hideControlsTimer = chewieController.hideControlsTimer;
|
||||
_hideTimer?.cancel();
|
||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
||||
? ChewieController.defaultHideControlsTimer
|
||||
: chewieController.hideControlsTimer;
|
||||
_hideTimer = Timer(hideControlsTimer, () {
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
@@ -27,13 +26,13 @@ import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.da
|
||||
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';
|
||||
@@ -482,9 +481,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),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -699,18 +704,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (ref.read(showControlsProvider)) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
ref.listen(showControlsProvider, (_, show) {
|
||||
if (show) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
@@ -735,15 +728,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
|
||||
@@ -807,9 +794,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
minScale: 1.0,
|
||||
basePosition: Alignment.center,
|
||||
child: VideoViewerPage(
|
||||
onPlaying: () {
|
||||
isPlayingVideo.value = true;
|
||||
},
|
||||
onPlaying: () => isPlayingVideo.value = true,
|
||||
onPaused: () =>
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => isPlayingVideo.value = false,
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/hooks/chewiew_controller_hook.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
class VideoViewerPage extends HookWidget {
|
||||
class VideoViewerPage extends HookConsumerWidget {
|
||||
final Asset asset;
|
||||
final bool isMotionVideo;
|
||||
final Widget? placeholder;
|
||||
@@ -34,49 +42,211 @@ class VideoViewerPage extends HookWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = useChewieController(
|
||||
asset,
|
||||
controlsSafeAreaMinimum: const EdgeInsets.only(
|
||||
bottom: 100,
|
||||
),
|
||||
placeholder: placeholder,
|
||||
showControls: showControls && !isMotionVideo,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
customControls: const VideoPlayerControls(),
|
||||
onPlaying: onPlaying,
|
||||
onPaused: onPaused,
|
||||
onVideoEnded: onVideoEnded,
|
||||
);
|
||||
|
||||
// Loading
|
||||
return PopScope(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller == null) {
|
||||
return Stack(
|
||||
children: [
|
||||
if (placeholder != null) placeholder!,
|
||||
const DelayedLoadingIndicator(
|
||||
fadeInDuration: Duration(milliseconds: 500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final size = MediaQuery.of(context).size;
|
||||
return SizedBox(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
child: Chewie(
|
||||
controller: controller,
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (asset.isLocal && asset.livePhotoVideoId == null) {
|
||||
final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!));
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: videoFile.when(
|
||||
data: (data) => VideoPlayer(
|
||||
file: data,
|
||||
isMotionVideo: false,
|
||||
onVideoEnded: () {},
|
||||
),
|
||||
error: (error, stackTrace) => Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
loading: () => showDownloadingIndicator
|
||||
? const Center(child: ImmichLoadingIndicator())
|
||||
: Container(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final downloadAssetStatus =
|
||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||
final String videoUrl = isMotionVideo
|
||||
? '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.livePhotoVideoId}'
|
||||
: '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}';
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
VideoPlayer(
|
||||
url: videoUrl,
|
||||
accessToken: Store.get(StoreKey.accessToken),
|
||||
isMotionVideo: isMotionVideo,
|
||||
onVideoEnded: onVideoEnded,
|
||||
onPaused: onPaused,
|
||||
onPlaying: onPlaying,
|
||||
placeholder: placeholder,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showControls: showControls,
|
||||
showDownloadingIndicator: showDownloadingIndicator,
|
||||
),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
opacity: (downloadAssetStatus == DownloadAssetStatus.loading &&
|
||||
showDownloadingIndicator)
|
||||
? 1.0
|
||||
: 0.0,
|
||||
child: SizedBox(
|
||||
height: context.height,
|
||||
width: context.width,
|
||||
child: const Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _fileFamily =
|
||||
FutureProvider.family<File, AssetEntity>((ref, entity) async {
|
||||
final file = await entity.file;
|
||||
if (file == null) {
|
||||
throw Exception();
|
||||
}
|
||||
return file;
|
||||
});
|
||||
|
||||
class VideoPlayer extends StatefulWidget {
|
||||
final String? url;
|
||||
final String? accessToken;
|
||||
final File? file;
|
||||
final bool isMotionVideo;
|
||||
final VoidCallback? onVideoEnded;
|
||||
final Duration hideControlsTimer;
|
||||
final bool showControls;
|
||||
|
||||
final Function()? onPlaying;
|
||||
final Function()? onPaused;
|
||||
|
||||
/// The placeholder to show while the video is loading
|
||||
/// usually, a thumbnail of the video
|
||||
final Widget? placeholder;
|
||||
|
||||
final bool showDownloadingIndicator;
|
||||
|
||||
const VideoPlayer({
|
||||
super.key,
|
||||
this.url,
|
||||
this.accessToken,
|
||||
this.file,
|
||||
this.onVideoEnded,
|
||||
required this.isMotionVideo,
|
||||
this.onPlaying,
|
||||
this.onPaused,
|
||||
this.placeholder,
|
||||
this.hideControlsTimer = const Duration(
|
||||
seconds: 5,
|
||||
),
|
||||
this.showControls = true,
|
||||
this.showDownloadingIndicator = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VideoPlayer> createState() => _VideoPlayerState();
|
||||
}
|
||||
|
||||
class _VideoPlayerState extends State<VideoPlayer> {
|
||||
late VideoPlayerController videoPlayerController;
|
||||
ChewieController? chewieController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializePlayer();
|
||||
|
||||
videoPlayerController.addListener(() {
|
||||
if (videoPlayerController.value.isInitialized) {
|
||||
if (videoPlayerController.value.isPlaying) {
|
||||
WakelockPlus.enable();
|
||||
widget.onPlaying?.call();
|
||||
} else if (!videoPlayerController.value.isPlaying) {
|
||||
WakelockPlus.disable();
|
||||
widget.onPaused?.call();
|
||||
}
|
||||
|
||||
if (videoPlayerController.value.position ==
|
||||
videoPlayerController.value.duration) {
|
||||
WakelockPlus.disable();
|
||||
widget.onVideoEnded?.call();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initializePlayer() async {
|
||||
try {
|
||||
videoPlayerController = widget.file == null
|
||||
? VideoPlayerController.networkUrl(
|
||||
Uri.parse(widget.url!),
|
||||
httpHeaders: {"x-immich-user-token": widget.accessToken ?? ""},
|
||||
)
|
||||
: VideoPlayerController.file(widget.file!);
|
||||
|
||||
await videoPlayerController.initialize();
|
||||
_createChewieController();
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
debugPrint("ERROR initialize video player $e");
|
||||
}
|
||||
}
|
||||
|
||||
_createChewieController() {
|
||||
chewieController = ChewieController(
|
||||
controlsSafeAreaMinimum: const EdgeInsets.only(
|
||||
bottom: 100,
|
||||
),
|
||||
showOptions: true,
|
||||
showControlsOnInitialize: false,
|
||||
videoPlayerController: videoPlayerController,
|
||||
autoPlay: true,
|
||||
autoInitialize: true,
|
||||
allowFullScreen: false,
|
||||
allowedScreenSleep: false,
|
||||
showControls: widget.showControls && !widget.isMotionVideo,
|
||||
customControls: const VideoPlayerControls(),
|
||||
hideControlsTimer: widget.hideControlsTimer,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
videoPlayerController.pause();
|
||||
videoPlayerController.dispose();
|
||||
chewieController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (chewieController?.videoPlayerController.value.isInitialized == true) {
|
||||
return SizedBox(
|
||||
height: context.height,
|
||||
width: context.width,
|
||||
child: Chewie(
|
||||
controller: chewieController!,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: context.height,
|
||||
width: context.width,
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
if (widget.placeholder != null) widget.placeholder!,
|
||||
if (widget.showDownloadingIndicator)
|
||||
const Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||
|
||||
class BlurhashThumb extends HookWidget {
|
||||
final double height;
|
||||
final double width;
|
||||
final Asset asset;
|
||||
final EdgeInsets margin;
|
||||
|
||||
const BlurhashThumb({
|
||||
super.key,
|
||||
required this.height,
|
||||
required this.width,
|
||||
required this.asset,
|
||||
required this.margin,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final blurhash = useBlurHashRef(asset).value;
|
||||
if (blurhash == null) {
|
||||
return SizedBox(
|
||||
height: height,
|
||||
width: width,
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: margin,
|
||||
child: Image.memory(
|
||||
blurhash,
|
||||
gaplessPlayback: true,
|
||||
frameBuilder: (
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
int? frame,
|
||||
bool wasSynchronouslyLoaded,
|
||||
) {
|
||||
if (wasSynchronouslyLoaded) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: frame != null
|
||||
? child
|
||||
: SizedBox(
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
@@ -7,15 +6,12 @@ import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
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/blurhash_thumb.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/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import 'asset_grid_data_structure.dart';
|
||||
@@ -329,33 +325,32 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
}
|
||||
|
||||
/// A single row of all placeholder widgets
|
||||
class _PlaceholderRow extends HookWidget {
|
||||
class _PlaceholderRow extends StatelessWidget {
|
||||
final int number;
|
||||
final double width;
|
||||
final double height;
|
||||
final double margin;
|
||||
final List<Asset> assets;
|
||||
|
||||
const _PlaceholderRow({
|
||||
super.key,
|
||||
required this.number,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.margin,
|
||||
required this.assets,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
for (int i = 0; i < assets.length; i++)
|
||||
BlurhashThumb(
|
||||
for (int i = 0; i < number; i++)
|
||||
ThumbnailPlaceholder(
|
||||
key: ValueKey(i),
|
||||
asset: assets[i],
|
||||
width: width,
|
||||
height: height,
|
||||
margin: EdgeInsets.only(
|
||||
bottom: margin,
|
||||
right: i + 1 == assets.length ? 0.0 : margin,
|
||||
right: i + 1 == number ? 0.0 : margin,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -406,9 +401,9 @@ class _Section extends StatelessWidget {
|
||||
final width = constraints.maxWidth / assetsPerRow -
|
||||
margin * (assetsPerRow - 1) / assetsPerRow;
|
||||
final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow;
|
||||
final List<Asset> assetsToRender = //scrolling
|
||||
//? []
|
||||
renderList.loadAssets(section.offset, section.count);
|
||||
final List<Asset> assetsToRender = scrolling
|
||||
? []
|
||||
: renderList.loadAssets(section.offset, section.count);
|
||||
return Column(
|
||||
key: ValueKey(section.offset),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -427,22 +422,18 @@ class _Section extends StatelessWidget {
|
||||
selectAssets: selectAssets,
|
||||
deselectAssets: deselectAssets,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
for (int i = 0; i < rows; i++)
|
||||
_PlaceholderRow(
|
||||
key: ValueKey('placeholder-$i'),
|
||||
assets: assetsToRender.nestedSlice(
|
||||
i * assetsPerRow,
|
||||
min((i + 1) * assetsPerRow, section.count),
|
||||
),
|
||||
width: width,
|
||||
height: width,
|
||||
margin: margin,
|
||||
),
|
||||
if (!scrolling)
|
||||
for (int i = 0; i < rows; i++)
|
||||
_AssetRow(
|
||||
for (int i = 0; i < rows; i++)
|
||||
scrolling
|
||||
? _PlaceholderRow(
|
||||
key: ValueKey(i),
|
||||
number: i + 1 == rows
|
||||
? section.count - i * assetsPerRow
|
||||
: assetsPerRow,
|
||||
width: width,
|
||||
height: width,
|
||||
margin: margin,
|
||||
)
|
||||
: _AssetRow(
|
||||
key: ValueKey(i),
|
||||
assets: assetsToRender.nestedSlice(
|
||||
i * assetsPerRow,
|
||||
@@ -463,8 +454,6 @@ class _Section extends StatelessWidget {
|
||||
onSelect: (asset) => selectAssets([asset]),
|
||||
onDeselect: (asset) => deselectAssets([asset]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ 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';
|
||||
|
||||
@@ -134,10 +134,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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ 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/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
|
||||
class MemoryCard extends StatelessWidget {
|
||||
final Asset asset;
|
||||
@@ -43,8 +42,9 @@ class MemoryCard extends StatelessWidget {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: ImmichThumbnail.imageProvider(
|
||||
image: ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
isThumbnail: true,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
@@ -55,9 +55,9 @@ class MemoryCard extends StatelessWidget {
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Determine the fit using the aspect ratio
|
||||
BoxFit fit = BoxFit.contain;
|
||||
BoxFit fit = BoxFit.fitWidth;
|
||||
if (asset.width != null && asset.height != null) {
|
||||
final aspectRatio = asset.width! / asset.height!;
|
||||
final aspectRatio = asset.height! / asset.width!;
|
||||
final phoneAspectRatio =
|
||||
constraints.maxWidth / constraints.maxHeight;
|
||||
// Look for a 25% difference in either direction
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart';
|
||||
import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MemoryPage extends HookConsumerWidget {
|
||||
@@ -121,8 +120,9 @@ class MemoryPage extends HookConsumerWidget {
|
||||
context,
|
||||
),
|
||||
precacheImage(
|
||||
ImmichThumbnail.imageProvider(
|
||||
ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
isThumbnail: true,
|
||||
),
|
||||
context,
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1393,7 +1393,7 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||
void Function()? onPaused,
|
||||
Widget? placeholder,
|
||||
bool showControls = true,
|
||||
Duration hideControlsTimer = const Duration(milliseconds: 1500),
|
||||
Duration hideControlsTimer = const Duration(seconds: 5),
|
||||
bool showDownloadingIndicator = true,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
@@ -1429,7 +1429,7 @@ class VideoViewerRouteArgs {
|
||||
this.onPaused,
|
||||
this.placeholder,
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(milliseconds: 1500),
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.showDownloadingIndicator = true,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -175,11 +171,6 @@ class Asset {
|
||||
|
||||
int? stackCount;
|
||||
|
||||
/// Aspect ratio of the asset
|
||||
@ignore
|
||||
double? get aspectRatio =>
|
||||
width == null || height == null ? 0 : width! / height!;
|
||||
|
||||
/// `true` if this [Asset] is present on the device
|
||||
@ignore
|
||||
bool get isLocal => localId != null;
|
||||
@@ -283,7 +274,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 +338,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 +374,6 @@ class Asset {
|
||||
ExifInfo? exifInfo,
|
||||
String? stackParentId,
|
||||
int? stackCount,
|
||||
String? thumbhash,
|
||||
}) =>
|
||||
Asset(
|
||||
id: id ?? this.id,
|
||||
@@ -410,7 +398,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');
|
||||
|
||||
@@ -58,6 +58,18 @@ class AssetService {
|
||||
final assetDto = await _apiService.assetApi
|
||||
.getAllAssets(userId: user.id, updatedAfter: since);
|
||||
if (assetDto == null) return (null, null);
|
||||
|
||||
print("AssetDto length: ${assetDto.length} ");
|
||||
for (final e in assetDto) {
|
||||
print("AssetDto: ${e.stackParentId}");
|
||||
var b = Asset.remote(e);
|
||||
print("e.stackParentId ${e.stackParentId}");
|
||||
print("e.id ${e.id}");
|
||||
print(
|
||||
"e.stackParentId == e.id ? null : e.stackParentId, ${e.stackParentId == e.id ? null : e.stackParentId}",
|
||||
);
|
||||
print("Mapped asset ${b.stackParentId}");
|
||||
}
|
||||
return (assetDto.map(Asset.remote).toList(), deleted.ids);
|
||||
}
|
||||
|
||||
@@ -82,6 +94,7 @@ class AssetService {
|
||||
if (assets == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
allAssets.addAll(assets.map(Asset.remote));
|
||||
if (assets.length < chunkSize) {
|
||||
break;
|
||||
@@ -90,7 +103,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 +130,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,40 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
|
||||
class DelayedLoadingIndicator extends StatelessWidget {
|
||||
/// The delay to avoid showing the loading indicator
|
||||
final Duration delay;
|
||||
|
||||
/// Defaults to using the [ImmichLoadingIndicator]
|
||||
final Widget? child;
|
||||
|
||||
/// An optional fade in duration to animate the loading
|
||||
final Duration? fadeInDuration;
|
||||
|
||||
const DelayedLoadingIndicator({
|
||||
super.key,
|
||||
this.delay = const Duration(seconds: 3),
|
||||
this.child,
|
||||
this.fadeInDuration,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSwitcher(
|
||||
duration: fadeInDuration ?? Duration.zero,
|
||||
child: FutureBuilder(
|
||||
future: Future.delayed(delay),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return child ??
|
||||
const ImmichLoadingIndicator(
|
||||
key: ValueKey('loading'),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(key: const ValueKey('hiding'));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,47 +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()
|
||||
: Image(
|
||||
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],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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/shared/ui/delayed_loading_indicator.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
|
||||
final _loadingEntry = OverlayEntry(
|
||||
builder: (context) => SizedBox.square(
|
||||
@@ -9,12 +9,7 @@ final _loadingEntry = OverlayEntry(
|
||||
child: DecoratedBox(
|
||||
decoration:
|
||||
BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
|
||||
child: const Center(
|
||||
child: DelayedLoadingIndicator(
|
||||
delay: Duration(seconds: 1),
|
||||
fadeInDuration: Duration(milliseconds: 400),
|
||||
),
|
||||
),
|
||||
child: const Center(child: ImmichLoadingIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -32,19 +27,19 @@ class _LoadingOverlay extends Hook<ValueNotifier<bool>> {
|
||||
|
||||
class _LoadingOverlayState
|
||||
extends HookState<ValueNotifier<bool>, _LoadingOverlay> {
|
||||
late final _isLoading = ValueNotifier(false)..addListener(_listener);
|
||||
OverlayEntry? _loadingOverlay;
|
||||
late final _isProcessing = ValueNotifier(false)..addListener(_listener);
|
||||
OverlayEntry? overlayEntry;
|
||||
|
||||
void _listener() {
|
||||
setState(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_isLoading.value) {
|
||||
_loadingOverlay?.remove();
|
||||
_loadingOverlay = _loadingEntry;
|
||||
if (_isProcessing.value) {
|
||||
overlayEntry?.remove();
|
||||
overlayEntry = _loadingEntry;
|
||||
Overlay.of(context).insert(_loadingEntry);
|
||||
} else {
|
||||
_loadingOverlay?.remove();
|
||||
_loadingOverlay = null;
|
||||
overlayEntry?.remove();
|
||||
overlayEntry = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -52,17 +47,17 @@ class _LoadingOverlayState
|
||||
|
||||
@override
|
||||
ValueNotifier<bool> build(BuildContext context) {
|
||||
return _isLoading;
|
||||
return _isProcessing;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isLoading.dispose();
|
||||
_isProcessing.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Object? get debugValue => _isLoading.value;
|
||||
Object? get debugValue => _isProcessing.value;
|
||||
|
||||
@override
|
||||
String get debugLabel => 'useProcessingOverlay<>';
|
||||
|
||||
@@ -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
-2
@@ -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
|
||||
|
||||
+10
-18
@@ -569,10 +569,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_udid
|
||||
sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07"
|
||||
sha256: "666412097b86d9a6f9803073d0f0ba70de9b198fe6493d89d352a1f8cd6c5c84"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "2.1.1"
|
||||
flutter_web_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -619,10 +619,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: geolocator
|
||||
sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd"
|
||||
sha256: e946395fc608842bb2f6c914807e9183f86f3cb787f6b8f832753e5251036f02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.0"
|
||||
version: "10.1.0"
|
||||
geolocator_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -651,10 +651,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_web
|
||||
sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed"
|
||||
sha256: "59083f7e0871b78299918d92bf930a14377f711d2d1156c558cd5ebae6c20d58"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "2.2.0"
|
||||
geolocator_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1298,10 +1298,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.3.0"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1322,10 +1322,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.3.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1467,14 +1467,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:
|
||||
|
||||
+2
-3
@@ -32,8 +32,8 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/maplibre/flutter-maplibre-gl.git
|
||||
ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
|
||||
geolocator: ^11.0.0 # used to move to current location in map view
|
||||
flutter_udid: ^3.0.0
|
||||
geolocator: ^10.1.0 # used to move to current location in map view
|
||||
flutter_udid: ^2.1.1
|
||||
package_info_plus: ^5.0.1
|
||||
url_launcher: ^6.2.4
|
||||
http: 0.13.5
|
||||
@@ -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
|
||||
|
||||
@@ -4691,50 +4691,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/search/places": {
|
||||
"get": {
|
||||
"operationId": "searchPlaces",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PlacesResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Search"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/search/smart": {
|
||||
"post": {
|
||||
"operationId": "searchSmart",
|
||||
@@ -8800,31 +8756,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PlacesResponseDto": {
|
||||
"properties": {
|
||||
"admin1name": {
|
||||
"type": "string"
|
||||
},
|
||||
"admin2name": {
|
||||
"type": "string"
|
||||
},
|
||||
"latitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"latitude",
|
||||
"longitude",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueueStatusDto": {
|
||||
"properties": {
|
||||
"isActive": {
|
||||
|
||||
-128
@@ -2994,43 +2994,6 @@ export interface PersonWithFacesResponseDto {
|
||||
*/
|
||||
'thumbnailPath': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PlacesResponseDto
|
||||
*/
|
||||
export interface PlacesResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'admin1name'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'admin2name'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'latitude': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'longitude': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'name': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -15484,51 +15447,6 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
searchPlaces: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('searchPlaces', 'name', name)
|
||||
const localVarPath = `/search/places`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (name !== undefined) {
|
||||
localVarQueryParameter['name'] = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
@@ -15666,18 +15584,6 @@ export const SearchApiFp = function(configuration?: Configuration) {
|
||||
const operationBasePath = operationServerMap['SearchApi.searchPerson']?.[index]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async searchPlaces(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PlacesResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.searchPlaces(name, options);
|
||||
const index = configuration?.serverIndex ?? 0;
|
||||
const operationBasePath = operationServerMap['SearchApi.searchPlaces']?.[index]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SmartSearchDto} smartSearchDto
|
||||
@@ -15745,15 +15651,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
|
||||
searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
|
||||
return localVarFp.searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchApiSearchPlacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
searchPlaces(requestParameters: SearchApiSearchPlacesRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<PlacesResponseDto>> {
|
||||
return localVarFp.searchPlaces(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchApiSearchSmartRequest} requestParameters Request parameters.
|
||||
@@ -15920,20 +15817,6 @@ export interface SearchApiSearchPersonRequest {
|
||||
readonly withHidden?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for searchPlaces operation in SearchApi.
|
||||
* @export
|
||||
* @interface SearchApiSearchPlacesRequest
|
||||
*/
|
||||
export interface SearchApiSearchPlacesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SearchApiSearchPlaces
|
||||
*/
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for searchSmart operation in SearchApi.
|
||||
* @export
|
||||
@@ -16010,17 +15893,6 @@ export class SearchApi extends BaseAPI {
|
||||
return SearchApiFp(this.configuration).searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SearchApiSearchPlacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof SearchApi
|
||||
*/
|
||||
public searchPlaces(requestParameters: SearchApiSearchPlacesRequest, options?: RawAxiosRequestConfig) {
|
||||
return SearchApiFp(this.configuration).searchPlaces(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SearchApiSearchSmartRequest} requestParameters Request parameters.
|
||||
|
||||
Generated
-19
@@ -646,13 +646,6 @@ export type MetadataSearchDto = {
|
||||
withPeople?: boolean;
|
||||
withStacked?: boolean;
|
||||
};
|
||||
export type PlacesResponseDto = {
|
||||
admin1name?: string;
|
||||
admin2name?: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
};
|
||||
export type SmartSearchDto = {
|
||||
city?: string;
|
||||
country?: string;
|
||||
@@ -2205,18 +2198,6 @@ export function searchPerson({ name, withHidden }: {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function searchPlaces({ name }: {
|
||||
name: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PlacesResponseDto[];
|
||||
}>(`/search/places${QS.query(QS.explode({
|
||||
name
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function searchSmart({ smartSearchDto }: {
|
||||
smartSearchDto: SmartSearchDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { HttpError } from '@oazapfts/runtime';
|
||||
|
||||
export interface ApiExceptionResponse {
|
||||
message: string;
|
||||
error?: string;
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
export interface ApiHttpError extends HttpError {
|
||||
data: ApiExceptionResponse;
|
||||
}
|
||||
|
||||
export function isHttpError(error: unknown): error is ApiHttpError {
|
||||
return error instanceof HttpError;
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
export * from './fetch-client';
|
||||
export * from './fetch-errors';
|
||||
|
||||
@@ -167,7 +167,7 @@ export class AuditService {
|
||||
`Found ${libraryFiles.size} original files, ${thumbFiles.size} thumbnails, ${videoFiles.size} encoded videos, ${profileFiles.size} profile files`,
|
||||
);
|
||||
const pagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (options) =>
|
||||
this.assetRepository.getAll(options, { withDeleted: true, withArchived: true }),
|
||||
this.assetRepository.getAll(options, { withDeleted: true }),
|
||||
);
|
||||
|
||||
let assetCount = 0;
|
||||
|
||||
@@ -91,7 +91,7 @@ export const citiesFile = 'cities500.txt';
|
||||
export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt');
|
||||
export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt');
|
||||
export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt');
|
||||
export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
|
||||
export const geodataCitites500Path = join(GEODATA_ROOT_PATH, citiesFile);
|
||||
|
||||
const image: Record<string, string[]> = {
|
||||
'.3fr': ['image/3fr', 'image/x-hasselblad-3fr'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssetEntity, AssetFaceEntity, AssetType, GeodataPlacesEntity, SmartInfoEntity } from '@app/infra/entities';
|
||||
import { AssetEntity, AssetFaceEntity, AssetType, SmartInfoEntity } from '@app/infra/entities';
|
||||
import { Paginated } from '../domain.util';
|
||||
|
||||
export const ISearchRepository = 'ISearchRepository';
|
||||
@@ -157,7 +157,9 @@ type BaseAssetSearchOptions = SearchDateOptions &
|
||||
|
||||
export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions;
|
||||
|
||||
export type AssetSearchOneToOneRelationOptions = BaseAssetSearchOptions & SearchOneToOneRelationOptions;
|
||||
export type AssetSearchOneToOneRelationOptions = BaseAssetSearchOptions &
|
||||
SearchOneToOneRelationOptions &
|
||||
SearchRelationOptions;
|
||||
|
||||
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
|
||||
|
||||
@@ -186,5 +188,4 @@ export interface ISearchRepository {
|
||||
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity>;
|
||||
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
||||
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
|
||||
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AssetOrder } from '@app/domain/asset/dto/asset.dto';
|
||||
import { AssetType, GeodataPlacesEntity } from '@app/infra/entities';
|
||||
import { AssetType } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
@@ -241,12 +241,6 @@ export class SearchDto {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export class SearchPlacesDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
}
|
||||
|
||||
export class SearchPeopleDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@@ -257,21 +251,3 @@ export class SearchPeopleDto {
|
||||
@Optional()
|
||||
withHidden?: boolean;
|
||||
}
|
||||
|
||||
export class PlacesResponseDto {
|
||||
name!: string;
|
||||
latitude!: number;
|
||||
longitude!: number;
|
||||
admin1name?: string;
|
||||
admin2name?: string;
|
||||
}
|
||||
|
||||
export function mapPlaces(place: GeodataPlacesEntity): PlacesResponseDto {
|
||||
return {
|
||||
name: place.name,
|
||||
latitude: place.latitude,
|
||||
longitude: place.longitude,
|
||||
admin1name: place.admin1Name,
|
||||
admin2name: place.admin2Name,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,15 +16,7 @@ import {
|
||||
SearchStrategy,
|
||||
} from '../repositories';
|
||||
import { FeatureFlag, SystemConfigCore } from '../system-config';
|
||||
import {
|
||||
MetadataSearchDto,
|
||||
PlacesResponseDto,
|
||||
SearchDto,
|
||||
SearchPeopleDto,
|
||||
SearchPlacesDto,
|
||||
SmartSearchDto,
|
||||
mapPlaces,
|
||||
} from './dto';
|
||||
import { MetadataSearchDto, SearchDto, SearchPeopleDto, SmartSearchDto } from './dto';
|
||||
import { SearchSuggestionRequestDto, SearchSuggestionType } from './dto/search-suggestion.dto';
|
||||
import { SearchResponseDto } from './response-dto';
|
||||
|
||||
@@ -49,11 +41,6 @@ export class SearchService {
|
||||
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
|
||||
}
|
||||
|
||||
async searchPlaces(dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
|
||||
const places = await this.searchRepository.searchPlaces(dto.name);
|
||||
return places.map((place) => mapPlaces(place));
|
||||
}
|
||||
|
||||
async getExploreData(auth: AuthDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
|
||||
await this.configCore.requireFeature(FeatureFlag.SEARCH);
|
||||
const options = { maxFields: 12, minAssetsPerField: 5 };
|
||||
@@ -195,22 +182,26 @@ export class SearchService {
|
||||
}
|
||||
|
||||
async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise<string[]> {
|
||||
switch (dto.type) {
|
||||
case SearchSuggestionType.COUNTRY: {
|
||||
return this.metadataRepository.getCountries(auth.user.id);
|
||||
}
|
||||
case SearchSuggestionType.STATE: {
|
||||
return this.metadataRepository.getStates(auth.user.id, dto.country);
|
||||
}
|
||||
case SearchSuggestionType.CITY: {
|
||||
return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state);
|
||||
}
|
||||
case SearchSuggestionType.CAMERA_MAKE: {
|
||||
return this.metadataRepository.getCameraMakes(auth.user.id, dto.model);
|
||||
}
|
||||
case SearchSuggestionType.CAMERA_MODEL: {
|
||||
return this.metadataRepository.getCameraModels(auth.user.id, dto.make);
|
||||
}
|
||||
if (dto.type === SearchSuggestionType.COUNTRY) {
|
||||
return this.metadataRepository.getCountries(auth.user.id);
|
||||
}
|
||||
|
||||
if (dto.type === SearchSuggestionType.STATE) {
|
||||
return this.metadataRepository.getStates(auth.user.id, dto.country);
|
||||
}
|
||||
|
||||
if (dto.type === SearchSuggestionType.CITY) {
|
||||
return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state);
|
||||
}
|
||||
|
||||
if (dto.type === SearchSuggestionType.CAMERA_MAKE) {
|
||||
return this.metadataRepository.getCameraMakes(auth.user.id, dto.model);
|
||||
}
|
||||
|
||||
if (dto.type === SearchSuggestionType.CAMERA_MODEL) {
|
||||
return this.metadataRepository.getCameraModels(auth.user.id, dto.make);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export class StorageTemplateService {
|
||||
return true;
|
||||
}
|
||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||
this.assetRepository.getAll(pagination, { withExif: true }),
|
||||
this.assetRepository.getAll(pagination),
|
||||
);
|
||||
const users = await this.userRepository.getList();
|
||||
|
||||
|
||||
@@ -116,9 +116,17 @@ export class AssetService {
|
||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
|
||||
const assets = await this.assetRepository.getAllByFileCreationDate(
|
||||
{ take: dto.take ?? 1000, skip: dto.skip },
|
||||
{ ...dto, userIds: [userId], withDeleted: true, orderDirection: 'DESC', withExif: true, isVisible: true },
|
||||
{
|
||||
...dto,
|
||||
userIds: [userId],
|
||||
withDeleted: true,
|
||||
orderDirection: 'DESC',
|
||||
withExif: true,
|
||||
isVisible: true,
|
||||
withStacked: true,
|
||||
},
|
||||
);
|
||||
return assets.items.map((asset) => mapAsset(asset));
|
||||
return assets.items.map((asset) => mapAsset(asset, { withStack: true }));
|
||||
}
|
||||
|
||||
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {
|
||||
|
||||
@@ -2,11 +2,9 @@ import {
|
||||
AuthDto,
|
||||
MetadataSearchDto,
|
||||
PersonResponseDto,
|
||||
PlacesResponseDto,
|
||||
SearchDto,
|
||||
SearchExploreResponseDto,
|
||||
SearchPeopleDto,
|
||||
SearchPlacesDto,
|
||||
SearchResponseDto,
|
||||
SearchService,
|
||||
SmartSearchDto,
|
||||
@@ -50,11 +48,6 @@ export class SearchController {
|
||||
return this.service.searchPerson(auth, dto);
|
||||
}
|
||||
|
||||
@Get('places')
|
||||
searchPlaces(@Query() dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
|
||||
return this.service.searchPlaces(dto);
|
||||
}
|
||||
|
||||
@Get('suggestions')
|
||||
getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> {
|
||||
return this.service.getSearchSuggestions(auth, dto);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('geodata_admin1')
|
||||
export class GeodataAdmin1Entity {
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
key!: string;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
name!: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('geodata_admin2')
|
||||
export class GeodataAdmin2Entity {
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
key!: string;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
name!: string;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { GeodataAdmin1Entity } from '@app/infra/entities/geodata-admin1.entity';
|
||||
import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity';
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('geodata_places', { synchronize: false })
|
||||
export class GeodataPlacesEntity {
|
||||
@@ -19,7 +21,7 @@ export class GeodataPlacesEntity {
|
||||
// asExpression: 'll_to_earth((latitude)::double precision, (longitude)::double precision)',
|
||||
// type: 'earth',
|
||||
// })
|
||||
// earthCoord!: unknown;
|
||||
earthCoord!: unknown;
|
||||
|
||||
@Column({ type: 'char', length: 2 })
|
||||
countryCode!: string;
|
||||
@@ -30,14 +32,27 @@ export class GeodataPlacesEntity {
|
||||
@Column({ type: 'varchar', length: 80, nullable: true })
|
||||
admin2Code!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
admin1Name!: string;
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
generatedType: 'STORED',
|
||||
asExpression: `"countryCode" || '.' || "admin1Code"`,
|
||||
nullable: true,
|
||||
})
|
||||
admin1Key!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
admin2Name!: string;
|
||||
@ManyToOne(() => GeodataAdmin1Entity, { eager: true, nullable: true, createForeignKeyConstraints: false })
|
||||
admin1!: GeodataAdmin1Entity;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
alternateNames!: string;
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
generatedType: 'STORED',
|
||||
asExpression: `"countryCode" || '.' || "admin1Code" || '.' || "admin2Code"`,
|
||||
nullable: true,
|
||||
})
|
||||
admin2Key!: string;
|
||||
|
||||
@ManyToOne(() => GeodataAdmin2Entity, { eager: true, nullable: true, createForeignKeyConstraints: false })
|
||||
admin2!: GeodataAdmin2Entity;
|
||||
|
||||
@Column({ type: 'date' })
|
||||
modificationDate!: Date;
|
||||
|
||||
@@ -7,6 +7,8 @@ import { AssetStackEntity } from './asset-stack.entity';
|
||||
import { AssetEntity } from './asset.entity';
|
||||
import { AuditEntity } from './audit.entity';
|
||||
import { ExifEntity } from './exif.entity';
|
||||
import { GeodataAdmin1Entity } from './geodata-admin1.entity';
|
||||
import { GeodataAdmin2Entity } from './geodata-admin2.entity';
|
||||
import { GeodataPlacesEntity } from './geodata-places.entity';
|
||||
import { LibraryEntity } from './library.entity';
|
||||
import { MoveEntity } from './move.entity';
|
||||
@@ -30,6 +32,8 @@ export * from './asset-stack.entity';
|
||||
export * from './asset.entity';
|
||||
export * from './audit.entity';
|
||||
export * from './exif.entity';
|
||||
export * from './geodata-admin1.entity';
|
||||
export * from './geodata-admin2.entity';
|
||||
export * from './geodata-places.entity';
|
||||
export * from './library.entity';
|
||||
export * from './move.entity';
|
||||
@@ -55,6 +59,8 @@ export const databaseEntities = [
|
||||
AuditEntity,
|
||||
ExifEntity,
|
||||
GeodataPlacesEntity,
|
||||
GeodataAdmin1Entity,
|
||||
GeodataAdmin2Entity,
|
||||
MoveEntity,
|
||||
PartnerEntity,
|
||||
PersonEntity,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user