mirror of
https://github.com/immich-app/immich.git
synced 2026-03-17 07:38:39 -07:00
Compare commits
3 Commits
postgres-s
...
feat/edit-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a2649a59c | ||
|
|
54e6dd1697 | ||
|
|
22317b0360 |
@@ -2,7 +2,6 @@
|
||||
"name": "Immich - Backend, Frontend and ML",
|
||||
"service": "immich-server",
|
||||
"runServices": [
|
||||
"immich-init",
|
||||
"immich-server",
|
||||
"redis",
|
||||
"database",
|
||||
@@ -32,8 +31,29 @@
|
||||
"tasks": {
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Fix Permissions, Install Dependencies",
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich API Server (Nest)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
@@ -54,6 +74,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Immich Web Server (Vite)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
@@ -109,8 +130,8 @@
|
||||
}
|
||||
},
|
||||
"overrideCommand": true,
|
||||
"workspaceFolder": "/usr/src/app",
|
||||
"remoteUser": "root",
|
||||
"workspaceFolder": "/workspaces/immich",
|
||||
"remoteUser": "node",
|
||||
"userEnvProbe": "loginInteractiveShell",
|
||||
"remoteEnv": {
|
||||
// The location where your uploaded files are stored
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
services:
|
||||
immich-app-base:
|
||||
image: busybox
|
||||
immich-server:
|
||||
extends:
|
||||
service: immich-app-base
|
||||
profiles: !reset []
|
||||
image: immich-server-dev:latest
|
||||
build:
|
||||
target: dev-container-mobile
|
||||
environment:
|
||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||
volumes:
|
||||
volumes: !override # bind mount host to /workspaces/immich
|
||||
- ..:/workspaces/immich
|
||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||
- pnpm-store:/usr/src/app/.pnpm-store
|
||||
- server-node_modules:/usr/src/app/server/node_modules
|
||||
- web-node_modules:/usr/src/app/web/node_modules
|
||||
- github-node_modules:/usr/src/app/.github/node_modules
|
||||
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||
- docs-node_modules:/usr/src/app/docs/node_modules
|
||||
- e2e-node_modules:/usr/src/app/e2e/node_modules
|
||||
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
|
||||
- app-node_modules:/usr/src/app/node_modules
|
||||
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||
- coverage:/usr/src/app/web/coverage
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
immich-web:
|
||||
env_file: !reset []
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"name": "Immich - Mobile",
|
||||
"service": "immich-server",
|
||||
"runServices": [
|
||||
"immich-init",
|
||||
"immich-server",
|
||||
"redis",
|
||||
"database",
|
||||
@@ -36,7 +35,7 @@
|
||||
},
|
||||
"forwardPorts": [],
|
||||
"overrideCommand": true,
|
||||
"workspaceFolder": "/usr/src/app",
|
||||
"workspaceFolder": "/workspaces/immich",
|
||||
"remoteUser": "node",
|
||||
"userEnvProbe": "loginInteractiveShell",
|
||||
"remoteEnv": {
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
|
||||
export DEV_PORT="${DEV_PORT:-3000}"
|
||||
|
||||
# search for immich directory inside workspace.
|
||||
# /workspaces/immich is the bind mount, but other directories can be mounted if runing
|
||||
# Devcontainer: Clone [repository|pull request] in container volumne
|
||||
WORKSPACES_DIR="/workspaces"
|
||||
IMMICH_DIR="$WORKSPACES_DIR/immich"
|
||||
IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log"
|
||||
|
||||
log() {
|
||||
@@ -25,8 +30,52 @@ run_cmd() {
|
||||
return "${PIPESTATUS[0]}"
|
||||
}
|
||||
|
||||
export IMMICH_WORKSPACE="/usr/src/app"
|
||||
# Find directories excluding /workspaces/immich
|
||||
mapfile -t other_dirs < <(find "$WORKSPACES_DIR" -mindepth 1 -maxdepth 1 -type d ! -path "$IMMICH_DIR" ! -name ".*")
|
||||
|
||||
if [ ${#other_dirs[@]} -gt 1 ]; then
|
||||
log "Error: More than one directory found in $WORKSPACES_DIR other than $IMMICH_DIR."
|
||||
exit 1
|
||||
elif [ ${#other_dirs[@]} -eq 1 ]; then
|
||||
export IMMICH_WORKSPACE="${other_dirs[0]}"
|
||||
else
|
||||
export IMMICH_WORKSPACE="$IMMICH_DIR"
|
||||
fi
|
||||
|
||||
log "Found immich workspace in $IMMICH_WORKSPACE"
|
||||
log ""
|
||||
|
||||
fix_permissions() {
|
||||
|
||||
log "Fixing permissions for ${IMMICH_WORKSPACE}"
|
||||
|
||||
# Change ownership for directories that exist
|
||||
for dir in "${IMMICH_WORKSPACE}/.vscode" \
|
||||
"${IMMICH_WORKSPACE}/server/upload" \
|
||||
"${IMMICH_WORKSPACE}/.pnpm-store" \
|
||||
"${IMMICH_WORKSPACE}/.github/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/cli/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/e2e/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/server/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/server/dist" \
|
||||
"${IMMICH_WORKSPACE}/web/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/web/dist"; do
|
||||
if [ -d "$dir" ]; then
|
||||
run_cmd sudo chown node -R "$dir"
|
||||
fi
|
||||
done
|
||||
|
||||
log ""
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
|
||||
log "Installing dependencies"
|
||||
(
|
||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||
export CI=1 FROZEN=1 OFFLINE=1
|
||||
run_cmd make setup-web-dev setup-server-dev
|
||||
)
|
||||
log ""
|
||||
}
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
services:
|
||||
immich-app-base:
|
||||
image: busybox
|
||||
immich-server:
|
||||
extends:
|
||||
service: immich-app-base
|
||||
profiles: !reset []
|
||||
image: immich-server-dev:latest
|
||||
build:
|
||||
target: dev-container-server
|
||||
env_file: !reset []
|
||||
hostname: immich-dev
|
||||
environment:
|
||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||
volumes:
|
||||
volumes: !override
|
||||
- ..:/workspaces/immich
|
||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- pnpm_store_server:/buildcache/pnpm-store
|
||||
- pnpm-store:/usr/src/app/.pnpm-store
|
||||
- server-node_modules:/usr/src/app/server/node_modules
|
||||
- web-node_modules:/usr/src/app/web/node_modules
|
||||
- github-node_modules:/usr/src/app/.github/node_modules
|
||||
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||
- docs-node_modules:/usr/src/app/docs/node_modules
|
||||
- e2e-node_modules:/usr/src/app/e2e/node_modules
|
||||
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
|
||||
- app-node_modules:/usr/src/app/node_modules
|
||||
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||
- coverage:/usr/src/app/web/coverage
|
||||
- ../plugins:/build/corePlugin
|
||||
immich-web:
|
||||
env_file: !reset []
|
||||
|
||||
17
.devcontainer/server/container-start.sh
Executable file
17
.devcontainer/server/container-start.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# shellcheck source=common.sh
|
||||
# shellcheck disable=SC1091
|
||||
source /immich-devcontainer/container-common.sh
|
||||
|
||||
log "Setting up Immich dev container..."
|
||||
fix_permissions
|
||||
|
||||
log "Setup complete, please wait while backend and frontend services automatically start"
|
||||
log
|
||||
log "If necessary, the services may be manually started using"
|
||||
log
|
||||
log "$ /immich-devcontainer/container-start-backend.sh"
|
||||
log "$ /immich-devcontainer/container-start-frontend.sh"
|
||||
log
|
||||
log "From different terminal windows, as these scripts automatically restart the server"
|
||||
log "on error, and will continuously run in a loop"
|
||||
7
.github/workflows/check-openapi.yml
vendored
7
.github/workflows/check-openapi.yml
vendored
@@ -5,6 +5,11 @@ on:
|
||||
paths:
|
||||
- 'open-api/**'
|
||||
- '.github/workflows/check-openapi.yml'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'open-api/**'
|
||||
- '.github/workflows/check-openapi.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -19,7 +24,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
8
.github/workflows/static_analysis.yml
vendored
8
.github/workflows/static_analysis.yml
vendored
@@ -69,14 +69,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: dart pub get
|
||||
|
||||
- name: Install dependencies for UI package
|
||||
run: dart pub get
|
||||
working-directory: ./mobile/packages/ui
|
||||
|
||||
- name: Install dependencies for UI Showcase
|
||||
run: dart pub get
|
||||
working-directory: ./mobile/packages/ui/showcase
|
||||
|
||||
- name: Install DCM
|
||||
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
|
||||
with:
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -511,7 +511,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install chromium --only-shell
|
||||
run: npx playwright install chromium --only-shell
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Docker build
|
||||
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
|
||||
@@ -4,18 +4,12 @@ module.exports = {
|
||||
if (!pkg.name) {
|
||||
return pkg;
|
||||
}
|
||||
// make exiftool-vendored.pl a regular dependency since Docker prod
|
||||
// images build with --no-optional to reduce image size
|
||||
if (pkg.name === "exiftool-vendored") {
|
||||
const binaryPackage =
|
||||
process.platform === "win32"
|
||||
? "exiftool-vendored.exe"
|
||||
: "exiftool-vendored.pl";
|
||||
|
||||
if (pkg.optionalDependencies[binaryPackage]) {
|
||||
pkg.dependencies[binaryPackage] =
|
||||
pkg.optionalDependencies[binaryPackage];
|
||||
delete pkg.optionalDependencies[binaryPackage];
|
||||
if (pkg.optionalDependencies["exiftool-vendored.pl"]) {
|
||||
// make exiftool-vendored.pl a regular dependency
|
||||
pkg.dependencies["exiftool-vendored.pl"] =
|
||||
pkg.optionalDependencies["exiftool-vendored.pl"];
|
||||
delete pkg.optionalDependencies["exiftool-vendored.pl"];
|
||||
}
|
||||
}
|
||||
return pkg;
|
||||
|
||||
2
Makefile
2
Makefile
@@ -52,7 +52,7 @@ attach-server:
|
||||
docker exec -it docker_immich-server_1 sh
|
||||
|
||||
renovate:
|
||||
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset
|
||||
LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
|
||||
|
||||
# Directories that need to be created for volumes or build output
|
||||
VOLUME_DIRS = \
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"cli"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
@@ -25,11 +25,11 @@
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^12.0.0",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^63.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
@@ -45,8 +45,8 @@
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --sourcemap true",
|
||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
"prepack": "pnpm run build",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"prepack": "npm run build",
|
||||
"test": "vitest",
|
||||
"test:cov": "vitest --coverage",
|
||||
"format": "prettier --check .",
|
||||
|
||||
@@ -7,15 +7,7 @@ import { describe, expect, it, MockedFunction, vi } from 'vitest';
|
||||
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
|
||||
import createFetchMock from 'vitest-fetch-mock';
|
||||
|
||||
import {
|
||||
checkForDuplicates,
|
||||
deleteFiles,
|
||||
findSidecar,
|
||||
getAlbumName,
|
||||
startWatch,
|
||||
uploadFiles,
|
||||
UploadOptionsDto,
|
||||
} from 'src/commands/asset';
|
||||
import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset';
|
||||
|
||||
vi.mock('@immich/sdk');
|
||||
|
||||
@@ -317,85 +309,3 @@ describe('startWatch', () => {
|
||||
await fs.promises.rm(testFolder, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('findSidecar', () => {
|
||||
let testDir: string;
|
||||
let testFilePath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-sidecar-'));
|
||||
testFilePath = path.join(testDir, 'test.jpg');
|
||||
fs.writeFileSync(testFilePath, 'test');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should find sidecar file with photo.xmp naming convention', () => {
|
||||
const sidecarPath = path.join(testDir, 'test.xmp');
|
||||
fs.writeFileSync(sidecarPath, 'xmp data');
|
||||
|
||||
const result = findSidecar(testFilePath);
|
||||
expect(result).toBe(sidecarPath);
|
||||
});
|
||||
|
||||
it('should find sidecar file with photo.ext.xmp naming convention', () => {
|
||||
const sidecarPath = path.join(testDir, 'test.jpg.xmp');
|
||||
fs.writeFileSync(sidecarPath, 'xmp data');
|
||||
|
||||
const result = findSidecar(testFilePath);
|
||||
expect(result).toBe(sidecarPath);
|
||||
});
|
||||
|
||||
it('should prefer photo.ext.xmp over photo.xmp when both exist', () => {
|
||||
const sidecarPath1 = path.join(testDir, 'test.xmp');
|
||||
const sidecarPath2 = path.join(testDir, 'test.jpg.xmp');
|
||||
fs.writeFileSync(sidecarPath1, 'xmp data 1');
|
||||
fs.writeFileSync(sidecarPath2, 'xmp data 2');
|
||||
|
||||
const result = findSidecar(testFilePath);
|
||||
// Should return the first one found (photo.xmp) based on the order in the code
|
||||
expect(result).toBe(sidecarPath1);
|
||||
});
|
||||
|
||||
it('should return undefined when no sidecar file exists', () => {
|
||||
const result = findSidecar(testFilePath);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFiles', () => {
|
||||
let testDir: string;
|
||||
let testFilePath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-delete-'));
|
||||
testFilePath = path.join(testDir, 'test.jpg');
|
||||
fs.writeFileSync(testFilePath, 'test');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should delete asset and sidecar file when main file is deleted', async () => {
|
||||
const sidecarPath = path.join(testDir, 'test.xmp');
|
||||
fs.writeFileSync(sidecarPath, 'xmp data');
|
||||
|
||||
await deleteFiles([{ id: 'test-id', filepath: testFilePath }], [], { delete: true, concurrency: 1 });
|
||||
|
||||
expect(fs.existsSync(testFilePath)).toBe(false);
|
||||
expect(fs.existsSync(sidecarPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not delete sidecar file when delete option is false', async () => {
|
||||
const sidecarPath = path.join(testDir, 'test.xmp');
|
||||
fs.writeFileSync(sidecarPath, 'xmp data');
|
||||
|
||||
await deleteFiles([{ id: 'test-id', filepath: testFilePath }], [], { delete: false, concurrency: 1 });
|
||||
|
||||
expect(fs.existsSync(testFilePath)).toBe(true);
|
||||
expect(fs.existsSync(sidecarPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Matcher, watch as watchFs } from 'chokidar';
|
||||
import { MultiBar, Presets, SingleBar } from 'cli-progress';
|
||||
import { chunk } from 'lodash-es';
|
||||
import micromatch from 'micromatch';
|
||||
import { Stats, createReadStream, existsSync } from 'node:fs';
|
||||
import { Stats, createReadStream } from 'node:fs';
|
||||
import { stat, unlink } from 'node:fs/promises';
|
||||
import path, { basename } from 'node:path';
|
||||
import { Queue } from 'src/queue';
|
||||
@@ -403,6 +403,23 @@ export const uploadFiles = async (
|
||||
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
|
||||
const { baseUrl, headers } = defaults;
|
||||
|
||||
const assetPath = path.parse(input);
|
||||
const noExtension = path.join(assetPath.dir, assetPath.name);
|
||||
|
||||
const sidecarsFiles = await Promise.all(
|
||||
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
|
||||
[`${noExtension}.xmp`, `${input}.xmp`].map(async (sidecarPath) => {
|
||||
try {
|
||||
const stats = await stat(sidecarPath);
|
||||
return new UploadFile(sidecarPath, stats.size);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const sidecarData = sidecarsFiles.find((file): file is UploadFile => file !== false);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
|
||||
formData.append('deviceId', 'CLI');
|
||||
@@ -412,15 +429,8 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
|
||||
formData.append('isFavorite', 'false');
|
||||
formData.append('assetData', new UploadFile(input, stats.size));
|
||||
|
||||
const sidecarPath = findSidecar(input);
|
||||
if (sidecarPath) {
|
||||
try {
|
||||
const stats = await stat(sidecarPath);
|
||||
const sidecarData = new UploadFile(sidecarPath, stats.size);
|
||||
formData.append('sidecarData', sidecarData);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
if (sidecarData) {
|
||||
formData.append('sidecarData', sidecarData);
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/assets`, {
|
||||
@@ -436,19 +446,7 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const findSidecar = (filepath: string): string | undefined => {
|
||||
const assetPath = path.parse(filepath);
|
||||
const noExtension = path.join(assetPath.dir, assetPath.name);
|
||||
|
||||
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
|
||||
for (const sidecarPath of [`${noExtension}.xmp`, `${filepath}.xmp`]) {
|
||||
if (existsSync(sidecarPath)) {
|
||||
return sidecarPath;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: UploadOptionsDto): Promise<void> => {
|
||||
const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: UploadOptionsDto): Promise<void> => {
|
||||
let fileCount = 0;
|
||||
if (options.delete) {
|
||||
fileCount += uploaded.length;
|
||||
@@ -476,15 +474,7 @@ export const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], option
|
||||
|
||||
const chunkDelete = async (files: Asset[]) => {
|
||||
for (const assetBatch of chunk(files, options.concurrency)) {
|
||||
await Promise.all(
|
||||
assetBatch.map(async (input: Asset) => {
|
||||
await unlink(input.filepath);
|
||||
const sidecarPath = findSidecar(input.filepath);
|
||||
if (sidecarPath) {
|
||||
await unlink(sidecarPath);
|
||||
}
|
||||
}),
|
||||
);
|
||||
await Promise.all(assetBatch.map((input: Asset) => unlink(input.filepath)));
|
||||
deletionProgress.update(assetBatch.length);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ Immich has three main clients:
|
||||
3. CLI - Command-line utility for bulk upload
|
||||
|
||||
:::info
|
||||
All three clients use [OpenAPI](/api.md) to auto-generate rest clients for easy integration. For more information about this process, see [OpenAPI](/api.md).
|
||||
All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for easy integration. For more information about this process, see [OpenAPI](./open-api.md).
|
||||
:::
|
||||
|
||||
### Mobile App
|
||||
@@ -71,7 +71,7 @@ An incoming HTTP request is mapped to a controller (`src/controllers`). Controll
|
||||
|
||||
### Domain Transfer Objects (DTOs)
|
||||
|
||||
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](/api.md) schemas and control the generated code used by each client.
|
||||
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client.
|
||||
|
||||
### Background Jobs
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# API
|
||||
# OpenAPI
|
||||
|
||||
Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generate API documentation. To view the published docs see [here](https://api.immich.app/).
|
||||
|
||||
@@ -53,7 +53,7 @@ You can use `dart fix --apply` and `dcm fix lib` to potentially correct some iss
|
||||
|
||||
## OpenAPI
|
||||
|
||||
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/api.md) for more details.
|
||||
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/developer/open-api.md) for more details.
|
||||
|
||||
## Database Migrations
|
||||
|
||||
|
||||
@@ -80,10 +80,6 @@ There is an automatic scan job that is scheduled to run once a day. Its schedule
|
||||
|
||||
This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page.
|
||||
|
||||
### Deleting a Library
|
||||
|
||||
When deleting an external library, all assets inside are immediately deleted along with the library. Note that while a library can take a long time to fully delete in the background, it is immediately removed from the library list. If the deletion process is interrupted (for example, due to server restart), it will be cleaned up in the next nightly cron job. The cleanup process can also be manually initiated by clicking the "Scan All Libraries" button in the library list.
|
||||
|
||||
## Usage
|
||||
|
||||
Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add:
|
||||
|
||||
@@ -50,7 +50,6 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
|
||||
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
|
||||
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
|
||||
- MIGraphX is a new backend for AMD cards, which compiles models at runtime. As such, the first few inferences will be slow.
|
||||
|
||||
#### OpenVINO
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
||||
| `MP2T` | `.mts` `.m2ts` `.m2t` | :white_check_mark: | |
|
||||
| `MP4` | `.mp4` `.insv` | :white_check_mark: | |
|
||||
| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | |
|
||||
| `MXF` | `.mxf` | :white_check_mark: | |
|
||||
| `QUICKTIME` | `.mov` | :white_check_mark: | |
|
||||
| `WEBM` | `.webm` | :white_check_mark: | |
|
||||
| `WMV` | `.wmv` | :white_check_mark: | |
|
||||
|
||||
@@ -6,7 +6,7 @@ const prism = require('prism-react-renderer');
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'Immich',
|
||||
tagline: 'Self-hosted photo and video management solution',
|
||||
tagline: 'High performance self-hosted photo and video backup solution directly from your mobile phone',
|
||||
url: 'https://docs.immich.app',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'throw',
|
||||
@@ -93,15 +93,35 @@ const config = {
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://immich.app/',
|
||||
to: '/overview/quick-start',
|
||||
position: 'right',
|
||||
label: 'Home',
|
||||
label: 'Docs',
|
||||
},
|
||||
{
|
||||
href: 'https://immich.app/roadmap',
|
||||
position: 'right',
|
||||
label: 'Roadmap',
|
||||
},
|
||||
{
|
||||
href: 'https://api.immich.app/',
|
||||
position: 'right',
|
||||
label: 'API',
|
||||
},
|
||||
{
|
||||
href: 'https://immich.store',
|
||||
position: 'right',
|
||||
label: 'Merch',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/immich-app/immich',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://discord.immich.app',
|
||||
label: 'Discord',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
type: 'html',
|
||||
position: 'right',
|
||||
@@ -114,78 +134,19 @@ const config = {
|
||||
style: 'light',
|
||||
links: [
|
||||
{
|
||||
title: 'Download',
|
||||
title: 'Overview',
|
||||
items: [
|
||||
{
|
||||
label: 'Android',
|
||||
href: 'https://get.immich.app/android',
|
||||
label: 'Quick start',
|
||||
to: '/overview/quick-start',
|
||||
},
|
||||
{
|
||||
label: 'iOS',
|
||||
href: 'https://get.immich.app/ios',
|
||||
label: 'Installation',
|
||||
to: '/install/requirements',
|
||||
},
|
||||
{
|
||||
label: 'Server',
|
||||
href: 'https://immich.app/download',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
items: [
|
||||
{
|
||||
label: 'FUTO',
|
||||
href: 'https://futo.tech/',
|
||||
},
|
||||
{
|
||||
label: 'Purchase',
|
||||
href: 'https://buy.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Merch',
|
||||
href: 'https://immich.store/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Sites',
|
||||
items: [
|
||||
{
|
||||
label: 'Home',
|
||||
href: 'https://immich.app',
|
||||
},
|
||||
{
|
||||
label: 'My Immich',
|
||||
href: 'https://my.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Awesome Immich',
|
||||
href: 'https://awesome.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Immich API',
|
||||
href: 'https://api.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Immich Data',
|
||||
href: 'https://data.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Immich Datasets',
|
||||
href: 'https://datasets.immich.app/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Miscellaneous',
|
||||
items: [
|
||||
{
|
||||
label: 'Roadmap',
|
||||
href: 'https://immich.app/roadmap',
|
||||
},
|
||||
{
|
||||
label: 'Cursed Knowledge',
|
||||
href: 'https://immich.app/cursed-knowledge',
|
||||
label: 'Contributing',
|
||||
to: '/overview/support-the-project',
|
||||
},
|
||||
{
|
||||
label: 'Privacy Policy',
|
||||
@@ -194,7 +155,24 @@ const config = {
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Social',
|
||||
title: 'Documentation',
|
||||
items: [
|
||||
{
|
||||
label: 'Roadmap',
|
||||
href: 'https://immich.app/roadmap',
|
||||
},
|
||||
{
|
||||
label: 'API',
|
||||
href: 'https://api.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Cursed Knowledge',
|
||||
href: 'https://immich.app/cursed-knowledge',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Links',
|
||||
items: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"format:fix": "prettier --write .",
|
||||
"start": "docusaurus start --port 3005",
|
||||
"copy:openapi": "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
|
||||
"build": "pnpm run copy:openapi && docusaurus build",
|
||||
"build": "npm run copy:openapi && docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
|
||||
1
docs/static/_redirects
vendored
1
docs/static/_redirects
vendored
@@ -23,7 +23,6 @@
|
||||
/features/storage-template /administration/storage-template 307
|
||||
/features/user-management /administration/user-management 307
|
||||
/developer/contributing /developer/pr-checklist 307
|
||||
/developer/open-api /api 307
|
||||
/guides/machine-learning /guides/remote-machine-learning 307
|
||||
/administration/password-login /administration/system-settings 307
|
||||
/features/search /features/searching 307
|
||||
|
||||
@@ -11,7 +11,6 @@ services:
|
||||
immich-server:
|
||||
container_name: immich-e2e-server
|
||||
image: immich-server:latest
|
||||
shm_size: 128mb
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
|
||||
@@ -8,23 +8,23 @@
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest",
|
||||
"test:maintenance": "vitest --run --config vitest.maintenance.config.ts",
|
||||
"test:web": "pnpm exec playwright test --project=web",
|
||||
"test:web:maintenance": "pnpm exec playwright test --project=maintenance",
|
||||
"test:web:ui": "pnpm exec playwright test --project=ui",
|
||||
"start:web": "pnpm exec playwright test --ui --project=web",
|
||||
"start:web:maintenance": "pnpm exec playwright test --ui --project=maintenance",
|
||||
"start:web:ui": "pnpm exec playwright test --ui --project=ui",
|
||||
"test:web": "npx playwright test --project=web",
|
||||
"test:web:maintenance": "npx playwright test --project=maintenance",
|
||||
"test:web:ui": "npx playwright test --project=ui",
|
||||
"start:web": "npx playwright test --ui --project=web",
|
||||
"start:web:maintenance": "npx playwright test --ui --project=maintenance",
|
||||
"start:web:ui": "npx playwright test --ui --project=ui",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write .",
|
||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@immich/cli": "workspace:*",
|
||||
"@immich/e2e-auth-server": "workspace:*",
|
||||
@@ -37,12 +37,12 @@
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^63.0.0",
|
||||
"exiftool-vendored": "^35.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"exiftool-vendored": "^34.3.0",
|
||||
"globals": "^16.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
"pg": "^8.11.3",
|
||||
"pngjs": "^7.0.0",
|
||||
|
||||
@@ -253,8 +253,7 @@ describe('/asset', () => {
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.id).toEqual(facesAsset.id);
|
||||
const sortedPeople = body.people.toSorted((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
expect(sortedPeople).toMatchObject(expectedFaces);
|
||||
expect(body.people).toMatchObject(expectedFaces);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ test.describe('Shared Links', () => {
|
||||
await page.goto(`/share/${sharedLink.key}`);
|
||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||
await page.locator(`[data-asset-id="${asset.id}"]`).hover();
|
||||
await page.waitForSelector(`[data-asset-id="${asset.id}"] [role="checkbox"]`);
|
||||
await page.waitForSelector('[data-group] svg');
|
||||
await page.getByRole('checkbox').click();
|
||||
await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]);
|
||||
});
|
||||
|
||||
|
||||
@@ -438,7 +438,7 @@ test.describe('Timeline', () => {
|
||||
const asset = getAsset(timelineRestData, album.assetIds[0])!;
|
||||
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
||||
await thumbnailUtils.expectInViewport(page, asset.id);
|
||||
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
|
||||
await thumbnailUtils.expectSelectedReadonly(page, asset.id);
|
||||
});
|
||||
test('Add photos to album', async ({ page }) => {
|
||||
const album = timelineRestData.album;
|
||||
@@ -447,7 +447,7 @@ test.describe('Timeline', () => {
|
||||
const asset = getAsset(timelineRestData, album.assetIds[0])!;
|
||||
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
||||
await thumbnailUtils.expectInViewport(page, asset.id);
|
||||
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
|
||||
await thumbnailUtils.expectSelectedReadonly(page, asset.id);
|
||||
await pageUtils.selectDay(page, 'Tue, Feb 27, 2024');
|
||||
const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => {
|
||||
const requestJson = request.postDataJSON();
|
||||
|
||||
@@ -65,7 +65,7 @@ export const thumbnailUtils = {
|
||||
return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`);
|
||||
},
|
||||
selectedAsset(page: Page) {
|
||||
return page.locator('[data-thumbnail-focus-container][data-selected]');
|
||||
return page.locator('[data-thumbnail-focus-container]:has(button[aria-checked])');
|
||||
},
|
||||
async clickAssetId(page: Page, assetId: string) {
|
||||
await thumbnailUtils.withAssetId(page, assetId).click();
|
||||
@@ -102,9 +102,12 @@ export const thumbnailUtils = {
|
||||
async expectThumbnailIsNotArchive(page: Page, assetId: string) {
|
||||
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
|
||||
},
|
||||
async expectSelectedDisabled(page: Page, assetId: string) {
|
||||
async expectSelectedReadonly(page: Page, assetId: string) {
|
||||
// todo - need a data attribute for selected
|
||||
await expect(
|
||||
page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected][data-disabled]`),
|
||||
page.locator(
|
||||
`[data-thumbnail-focus-container][data-asset="${assetId}"] > .group.cursor-not-allowed > .rounded-xl`,
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
async expectTimelineHasOnScreenAssets(page: Page) {
|
||||
|
||||
19
i18n/en.json
19
i18n/en.json
@@ -561,6 +561,8 @@
|
||||
"asset_adding_to_album": "Adding to album…",
|
||||
"asset_created": "Asset created",
|
||||
"asset_description_updated": "Asset description has been updated",
|
||||
"asset_edit_failed": "Asset edit failed",
|
||||
"asset_edit_success": "Asset edited successfully",
|
||||
"asset_filename_is_offline": "Asset {filename} is offline",
|
||||
"asset_has_unassigned_faces": "Asset has unassigned faces",
|
||||
"asset_hashing": "Hashing…",
|
||||
@@ -1007,9 +1009,12 @@
|
||||
"editor_discard_edits_title": "Discard edits?",
|
||||
"editor_edits_applied_error": "Failed to apply edits",
|
||||
"editor_edits_applied_success": "Edits applied successfully",
|
||||
"editor_filters": "Filters",
|
||||
"editor_flip_horizontal": "Flip horizontal",
|
||||
"editor_flip_vertical": "Flip vertical",
|
||||
"editor_orientation": "Orientation",
|
||||
"editor_panel_filter": "Filter",
|
||||
"editor_panel_transform": "Transform",
|
||||
"editor_reset_all_changes": "Reset changes",
|
||||
"editor_rotate_left": "Rotate 90° counterclockwise",
|
||||
"editor_rotate_right": "Rotate 90° clockwise",
|
||||
@@ -1074,7 +1079,6 @@
|
||||
"failed_to_update_notification_status": "Failed to update notification status",
|
||||
"incorrect_email_or_password": "Incorrect email or password",
|
||||
"library_folder_already_exists": "This import path already exists.",
|
||||
"page_not_found": "Page not found :/",
|
||||
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
|
||||
"profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.",
|
||||
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
|
||||
@@ -1810,8 +1814,9 @@
|
||||
"rate_asset": "Rate Asset",
|
||||
"rating": "Star rating",
|
||||
"rating_clear": "Clear rating",
|
||||
"rating_count": "{count, plural, =0 {Unrated} one {# star} other {# stars}}",
|
||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
||||
"rating_description": "Display the EXIF rating in the info panel",
|
||||
"rating_set": "Rating set to {rating, plural, one {# star} other {# stars}}",
|
||||
"reaction_options": "Reaction options",
|
||||
"read_changelog": "Read Changelog",
|
||||
"readonly_mode_disabled": "Read-only mode disabled",
|
||||
@@ -1883,10 +1888,7 @@
|
||||
"reset_pin_code_success": "Successfully reset PIN code",
|
||||
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
|
||||
"reset_sqlite": "Reset SQLite Database",
|
||||
"reset_sqlite_clear_app_data": "Clear Data",
|
||||
"reset_sqlite_confirmation": "Are you sure you want to clear the app data? This will remove all settings and sign you out.",
|
||||
"reset_sqlite_confirmation_note": "Note: You will need to restart the app after clearing.",
|
||||
"reset_sqlite_done": "App data has been cleared. Please restart Immich and log in again.",
|
||||
"reset_sqlite_confirmation": "Are you sure you want to reset the SQLite database? You will need to log out and log in again to resync the data",
|
||||
"reset_sqlite_success": "Successfully reset the SQLite database",
|
||||
"reset_to_default": "Reset to default",
|
||||
"resolution": "Resolution",
|
||||
@@ -1914,7 +1916,6 @@
|
||||
"saved_settings": "Saved settings",
|
||||
"say_something": "Say something",
|
||||
"scaffold_body_error_occurred": "Error occurred",
|
||||
"scaffold_body_error_unrecoverable": "An unrecoverable error has occurred. Please share the error and stack trace on Discord or GitHub so we can help. If advised, you can clear the app data below.",
|
||||
"scan": "Scan",
|
||||
"scan_all_libraries": "Scan All Libraries",
|
||||
"scan_library": "Scan",
|
||||
@@ -2030,9 +2031,6 @@
|
||||
"set_profile_picture": "Set profile picture",
|
||||
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
|
||||
"set_stack_primary_asset": "Set as primary asset",
|
||||
"setting_image_navigation_enable_subtitle": "If enabled, you can navigate to the previous/next image by tapping the leftmost/rightmost quarter of the screen.",
|
||||
"setting_image_navigation_enable_title": "Tap to Navigate",
|
||||
"setting_image_navigation_title": "Image Navigation",
|
||||
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
|
||||
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
|
||||
"setting_image_viewer_original_title": "Load original image",
|
||||
@@ -2311,7 +2309,6 @@
|
||||
"unstack_action_prompt": "{count} unstacked",
|
||||
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
||||
"unsupported_field_type": "Unsupported field type",
|
||||
"unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.",
|
||||
"untagged": "Untagged",
|
||||
"untitled_workflow": "Untitled workflow",
|
||||
"up_next": "Up next",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.13
|
||||
@@ -22,7 +22,48 @@ FROM builder-cpu AS builder-rknn
|
||||
|
||||
# Warning: 25GiB+ disk space required to pull this image
|
||||
# TODO: find a way to reduce the image size
|
||||
FROM rocm/dev-ubuntu-24.04:7.2-complete@sha256:86e11093b4a7ec2a79b1b6701d10e840a6994f21c7e05929b51eb9be361c683a AS builder-rocm
|
||||
FROM rocm/dev-ubuntu-24.04:6.4.4-complete@sha256:31418ac10a3769a71eaef330c07280d1d999d7074621339b8f93c484c35f6078 AS builder-rocm
|
||||
|
||||
# renovate: datasource=github-releases depName=Microsoft/onnxruntime
|
||||
ARG ONNXRUNTIME_VERSION="v1.22.1"
|
||||
WORKDIR /code
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends wget git
|
||||
RUN wget -nv https://github.com/Kitware/CMake/releases/download/v3.31.9/cmake-3.31.9-linux-x86_64.sh && \
|
||||
chmod +x cmake-3.31.9-linux-x86_64.sh && \
|
||||
mkdir -p /code/cmake-3.31.9-linux-x86_64 && \
|
||||
./cmake-3.31.9-linux-x86_64.sh --skip-license --prefix=/code/cmake-3.31.9-linux-x86_64 && \
|
||||
rm cmake-3.31.9-linux-x86_64.sh
|
||||
|
||||
RUN git clone --single-branch --branch "${ONNXRUNTIME_VERSION}" --recursive "https://github.com/Microsoft/onnxruntime" onnxruntime
|
||||
WORKDIR /code/onnxruntime
|
||||
# Fix for multi-threading based on comments in https://github.com/microsoft/onnxruntime/pull/19567
|
||||
# TODO: find a way to fix this without disabling algo caching
|
||||
COPY ./patches/* /tmp/
|
||||
RUN git apply /tmp/*.patch
|
||||
|
||||
RUN /bin/sh ./dockerfiles/scripts/install_common_deps.sh
|
||||
|
||||
ENV PATH=/opt/rocm-venv/bin:/code/cmake-3.31.9-linux-x86_64/bin:${PATH}
|
||||
ENV CCACHE_DIR="/ccache"
|
||||
# Note: the `parallel` setting uses a substantial amount of RAM
|
||||
RUN --mount=type=cache,target=/ccache \
|
||||
./build.sh \
|
||||
--allow_running_as_root \
|
||||
--config Release \
|
||||
--build_wheel \
|
||||
--update \
|
||||
--build \
|
||||
--parallel 48 \
|
||||
--cmake_extra_defines \
|
||||
ONNXRUNTIME_VERSION="${ONNXRUNTIME_VERSION}" \
|
||||
CMAKE_HIP_ARCHITECTURES="gfx900;gfx906;gfx908;gfx90a;gfx940;gfx941;gfx942;gfx1030;gfx1100;gfx1101;gfx1102;gfx1200;gfx1201" \
|
||||
--skip_tests \
|
||||
--use_rocm \
|
||||
--rocm_home=/opt/rocm \
|
||||
--use_cache \
|
||||
--compile_no_warning_as_error
|
||||
RUN mv /code/onnxruntime/build/Linux/Release/dist/*.whl /opt/
|
||||
|
||||
FROM builder-${DEVICE} AS builder
|
||||
|
||||
@@ -38,6 +79,9 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
|
||||
RUN if [ "$DEVICE" = "rocm" ]; then \
|
||||
uv pip install /opt/onnxruntime_rocm-*.whl; \
|
||||
fi
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:04cd27899595a99dfe77709d96f08876bf2ee99139ee2f0fe9ac948005034e5b AS prod-cpu
|
||||
|
||||
@@ -48,14 +92,14 @@ FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c3
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-core-2_2.28.4+20760_amd64.deb && \
|
||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-opencl-2_2.28.4+20760_amd64.deb && \
|
||||
wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/intel-opencl-icd_26.05.37020.3-0_amd64.deb && \
|
||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-core-2_2.27.10+20617_amd64.deb && \
|
||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-opencl-2_2.27.10+20617_amd64.deb && \
|
||||
wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/intel-opencl-icd_26.01.36711.4-0_amd64.deb && \
|
||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \
|
||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \
|
||||
wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \
|
||||
# TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file
|
||||
wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/libigdgmm12_22.9.0_amd64.deb && \
|
||||
wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/libigdgmm12_22.9.0_amd64.deb && \
|
||||
dpkg -i *.deb && \
|
||||
rm *.deb && \
|
||||
apt-get remove wget -yqq && \
|
||||
@@ -76,11 +120,7 @@ COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
|
||||
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
||||
COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so
|
||||
|
||||
FROM rocm/dev-ubuntu-24.04:7.2-complete@sha256:86e11093b4a7ec2a79b1b6701d10e840a6994f21c7e05929b51eb9be361c683a AS prod-rocm
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -yqq migraphx miopen-hip && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
FROM rocm/dev-ubuntu-24.04:6.4.4-complete@sha256:31418ac10a3769a71eaef330c07280d1d999d7074621339b8f93c484c35f6078 AS prod-rocm
|
||||
|
||||
FROM prod-cpu AS prod-armnn
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ class Settings(BaseSettings):
|
||||
preload: PreloadModelData | None = None
|
||||
max_batch_size: MaxBatchSize | None = None
|
||||
openvino_precision: ModelPrecision = ModelPrecision.FP32
|
||||
rocm_precision: ModelPrecision = ModelPrecision.FP32
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
|
||||
@@ -90,7 +90,7 @@ _PADDLE_MODELS = {
|
||||
|
||||
SUPPORTED_PROVIDERS = [
|
||||
"CUDAExecutionProvider",
|
||||
"MIGraphXExecutionProvider",
|
||||
"ROCMExecutionProvider",
|
||||
"OpenVINOExecutionProvider",
|
||||
"CoreMLExecutionProvider",
|
||||
"CPUExecutionProvider",
|
||||
|
||||
@@ -8,7 +8,7 @@ import onnxruntime as ort
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from immich_ml.models.constants import SUPPORTED_PROVIDERS
|
||||
from immich_ml.schemas import ModelPrecision, SessionNode
|
||||
from immich_ml.schemas import SessionNode
|
||||
|
||||
from ..config import log, settings
|
||||
|
||||
@@ -90,17 +90,8 @@ class OrtSession:
|
||||
match provider:
|
||||
case "CPUExecutionProvider":
|
||||
options = {"arena_extend_strategy": "kSameAsRequested"}
|
||||
case "CUDAExecutionProvider":
|
||||
case "CUDAExecutionProvider" | "ROCMExecutionProvider":
|
||||
options = {"arena_extend_strategy": "kSameAsRequested", "device_id": settings.device_id}
|
||||
case "MIGraphXExecutionProvider":
|
||||
migraphx_dir = self.model_path.parent / "migraphx"
|
||||
# MIGraphX does not create the underlying folder and will crash if it does not exist
|
||||
migraphx_dir.mkdir(parents=True, exist_ok=True)
|
||||
options = {
|
||||
"device_id": settings.device_id,
|
||||
"migraphx_model_cache_dir": migraphx_dir.as_posix(),
|
||||
"migraphx_fp16_enable": "1" if settings.rocm_precision == ModelPrecision.FP16 else "0",
|
||||
}
|
||||
case "OpenVINOExecutionProvider":
|
||||
openvino_dir = self.model_path.parent / "openvino"
|
||||
device = f"GPU.{settings.device_id}"
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
commit 16839b58d9b3c3162a67ce5d776b36d4d24e801f
|
||||
Author: mertalev <101130780+mertalev@users.noreply.github.com>
|
||||
Date: Wed Mar 5 11:25:38 2025 -0500
|
||||
|
||||
disable algo caching (attributed to @dmnieto in https://github.com/microsoft/onnxruntime/pull/19567)
|
||||
|
||||
diff --git a/onnxruntime/core/providers/rocm/nn/conv.cc b/onnxruntime/core/providers/rocm/nn/conv.cc
|
||||
index d7f47d07a8..4060a2af52 100644
|
||||
--- a/onnxruntime/core/providers/rocm/nn/conv.cc
|
||||
+++ b/onnxruntime/core/providers/rocm/nn/conv.cc
|
||||
@@ -127,7 +127,6 @@ Status Conv<T, NHWC>::UpdateState(OpKernelContext* context, bool bias_expected)
|
||||
|
||||
if (w_dims_changed) {
|
||||
s_.last_w_dims = gsl::make_span(w_dims);
|
||||
- s_.cached_benchmark_fwd_results.clear();
|
||||
}
|
||||
|
||||
ORT_RETURN_IF_ERROR(conv_attrs_.ValidateInputShape(X->Shape(), W->Shape(), channels_last, channels_last));
|
||||
@@ -277,35 +276,6 @@ Status Conv<T, NHWC>::UpdateState(OpKernelContext* context, bool bias_expected)
|
||||
HIP_CALL_THROW(hipMalloc(&s_.b_zero, malloc_size));
|
||||
HIP_CALL_THROW(hipMemsetAsync(s_.b_zero, 0, malloc_size, Stream(context)));
|
||||
}
|
||||
-
|
||||
- if (!s_.cached_benchmark_fwd_results.contains(x_dims_miopen)) {
|
||||
- miopenConvAlgoPerf_t perf;
|
||||
- int algo_count = 1;
|
||||
- const ROCMExecutionProvider* rocm_ep = static_cast<const ROCMExecutionProvider*>(this->Info().GetExecutionProvider());
|
||||
- static constexpr int num_algos = MIOPEN_CONVOLUTION_FWD_ALGO_COUNT;
|
||||
- size_t max_ws_size = rocm_ep->GetMiopenConvUseMaxWorkspace() ? GetMaxWorkspaceSize(GetMiopenHandle(context), s_, kAllAlgos, num_algos, rocm_ep->GetDeviceId())
|
||||
- : AlgoSearchWorkspaceSize;
|
||||
- IAllocatorUniquePtr<void> algo_search_workspace = GetTransientScratchBuffer<void>(max_ws_size);
|
||||
- MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionForwardAlgorithm(
|
||||
- GetMiopenHandle(context),
|
||||
- s_.x_tensor,
|
||||
- s_.x_data,
|
||||
- s_.w_desc,
|
||||
- s_.w_data,
|
||||
- s_.conv_desc,
|
||||
- s_.y_tensor,
|
||||
- s_.y_data,
|
||||
- 1, // requestedAlgoCount
|
||||
- &algo_count, // returnedAlgoCount
|
||||
- &perf,
|
||||
- algo_search_workspace.get(),
|
||||
- max_ws_size,
|
||||
- false)); // Do not do exhaustive algo search.
|
||||
- s_.cached_benchmark_fwd_results.insert(x_dims_miopen, {perf.fwd_algo, perf.memory});
|
||||
- }
|
||||
- const auto& perf = s_.cached_benchmark_fwd_results.at(x_dims_miopen);
|
||||
- s_.fwd_algo = perf.fwd_algo;
|
||||
- s_.workspace_bytes = perf.memory;
|
||||
} else {
|
||||
// set Y
|
||||
s_.Y = context->Output(0, TensorShape(s_.y_dims));
|
||||
@@ -319,6 +289,31 @@ Status Conv<T, NHWC>::UpdateState(OpKernelContext* context, bool bias_expected)
|
||||
s_.y_data = reinterpret_cast<HipT*>(s_.Y->MutableData<T>());
|
||||
}
|
||||
}
|
||||
+
|
||||
+ miopenConvAlgoPerf_t perf;
|
||||
+ int algo_count = 1;
|
||||
+ const ROCMExecutionProvider* rocm_ep = static_cast<const ROCMExecutionProvider*>(this->Info().GetExecutionProvider());
|
||||
+ static constexpr int num_algos = MIOPEN_CONVOLUTION_FWD_ALGO_COUNT;
|
||||
+ size_t max_ws_size = rocm_ep->GetMiopenConvUseMaxWorkspace() ? GetMaxWorkspaceSize(GetMiopenHandle(context), s_, kAllAlgos, num_algos, rocm_ep->GetDeviceId())
|
||||
+ : AlgoSearchWorkspaceSize;
|
||||
+ IAllocatorUniquePtr<void> algo_search_workspace = GetTransientScratchBuffer<void>(max_ws_size);
|
||||
+ MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionForwardAlgorithm(
|
||||
+ GetMiopenHandle(context),
|
||||
+ s_.x_tensor,
|
||||
+ s_.x_data,
|
||||
+ s_.w_desc,
|
||||
+ s_.w_data,
|
||||
+ s_.conv_desc,
|
||||
+ s_.y_tensor,
|
||||
+ s_.y_data,
|
||||
+ 1, // requestedAlgoCount
|
||||
+ &algo_count, // returnedAlgoCount
|
||||
+ &perf,
|
||||
+ algo_search_workspace.get(),
|
||||
+ max_ws_size,
|
||||
+ false)); // Do not do exhaustive algo search.
|
||||
+ s_.fwd_algo = perf.fwd_algo;
|
||||
+ s_.workspace_bytes = perf.memory;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
diff --git a/onnxruntime/core/providers/rocm/nn/conv.h b/onnxruntime/core/providers/rocm/nn/conv.h
|
||||
index bc9846203e..d54218f258 100644
|
||||
--- a/onnxruntime/core/providers/rocm/nn/conv.h
|
||||
+++ b/onnxruntime/core/providers/rocm/nn/conv.h
|
||||
@@ -108,9 +108,6 @@ class lru_unordered_map {
|
||||
list_type lru_list_;
|
||||
};
|
||||
|
||||
-// cached miopen descriptors
|
||||
-constexpr size_t MAX_CACHED_ALGO_PERF_RESULTS = 10000;
|
||||
-
|
||||
template <typename AlgoPerfType>
|
||||
struct MiopenConvState {
|
||||
// if x/w dims changed, update algo and miopenTensors
|
||||
@@ -148,9 +145,6 @@ struct MiopenConvState {
|
||||
decltype(AlgoPerfType().memory) memory;
|
||||
};
|
||||
|
||||
- lru_unordered_map<TensorShapeVector, PerfFwdResultParams, vector_hash> cached_benchmark_fwd_results{MAX_CACHED_ALGO_PERF_RESULTS};
|
||||
- lru_unordered_map<TensorShapeVector, PerfBwdResultParams, vector_hash> cached_benchmark_bwd_results{MAX_CACHED_ALGO_PERF_RESULTS};
|
||||
-
|
||||
// Some properties needed to support asymmetric padded Conv nodes
|
||||
bool post_slicing_required;
|
||||
TensorShapeVector slice_starts;
|
||||
diff --git a/onnxruntime/core/providers/rocm/nn/conv_transpose.cc b/onnxruntime/core/providers/rocm/nn/conv_transpose.cc
|
||||
index 7447113fdf..a662e35b2e 100644
|
||||
--- a/onnxruntime/core/providers/rocm/nn/conv_transpose.cc
|
||||
+++ b/onnxruntime/core/providers/rocm/nn/conv_transpose.cc
|
||||
@@ -76,7 +76,6 @@ Status ConvTranspose<T, NHWC>::DoConvTranspose(OpKernelContext* context, bool dy
|
||||
|
||||
if (w_dims_changed) {
|
||||
s_.last_w_dims = gsl::make_span(w_dims);
|
||||
- s_.cached_benchmark_bwd_results.clear();
|
||||
}
|
||||
|
||||
ConvTransposeAttributes::Prepare p;
|
||||
@@ -126,35 +125,29 @@ Status ConvTranspose<T, NHWC>::DoConvTranspose(OpKernelContext* context, bool dy
|
||||
}
|
||||
|
||||
y_data = reinterpret_cast<HipT*>(p.Y->MutableData<T>());
|
||||
-
|
||||
- if (!s_.cached_benchmark_bwd_results.contains(x_dims)) {
|
||||
- IAllocatorUniquePtr<void> algo_search_workspace = GetScratchBuffer<void>(AlgoSearchWorkspaceSize, context->GetComputeStream());
|
||||
-
|
||||
- miopenConvAlgoPerf_t perf;
|
||||
- int algo_count = 1;
|
||||
- MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionBackwardDataAlgorithm(
|
||||
- GetMiopenHandle(context),
|
||||
- s_.x_tensor,
|
||||
- x_data,
|
||||
- s_.w_desc,
|
||||
- w_data,
|
||||
- s_.conv_desc,
|
||||
- s_.y_tensor,
|
||||
- y_data,
|
||||
- 1,
|
||||
- &algo_count,
|
||||
- &perf,
|
||||
- algo_search_workspace.get(),
|
||||
- AlgoSearchWorkspaceSize,
|
||||
- false));
|
||||
- s_.cached_benchmark_bwd_results.insert(x_dims, {perf.bwd_data_algo, perf.memory});
|
||||
- }
|
||||
-
|
||||
- const auto& perf = s_.cached_benchmark_bwd_results.at(x_dims);
|
||||
- s_.bwd_data_algo = perf.bwd_data_algo;
|
||||
- s_.workspace_bytes = perf.memory;
|
||||
}
|
||||
|
||||
+ IAllocatorUniquePtr<void> algo_search_workspace = GetScratchBuffer<void>(AlgoSearchWorkspaceSize, context->GetComputeStream());
|
||||
+ miopenConvAlgoPerf_t perf;
|
||||
+ int algo_count = 1;
|
||||
+ MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionBackwardDataAlgorithm(
|
||||
+ GetMiopenHandle(context),
|
||||
+ s_.x_tensor,
|
||||
+ x_data,
|
||||
+ s_.w_desc,
|
||||
+ w_data,
|
||||
+ s_.conv_desc,
|
||||
+ s_.y_tensor,
|
||||
+ y_data,
|
||||
+ 1,
|
||||
+ &algo_count,
|
||||
+ &perf,
|
||||
+ algo_search_workspace.get(),
|
||||
+ AlgoSearchWorkspaceSize,
|
||||
+ false));
|
||||
+ s_.bwd_data_algo = perf.bwd_data_algo;
|
||||
+ s_.workspace_bytes = perf.memory;
|
||||
+
|
||||
// The following block will be executed in case there has been no change in the shapes of the
|
||||
// input and the filter compared to the previous run
|
||||
if (!y_data) {
|
||||
33
machine-learning/patches/0002-install-system-deps.patch
Normal file
33
machine-learning/patches/0002-install-system-deps.patch
Normal file
@@ -0,0 +1,33 @@
|
||||
diff --git a/dockerfiles/scripts/install_common_deps.sh b/dockerfiles/scripts/install_common_deps.sh
|
||||
index bbb672a99e..0dc652fbda 100644
|
||||
--- a/dockerfiles/scripts/install_common_deps.sh
|
||||
+++ b/dockerfiles/scripts/install_common_deps.sh
|
||||
@@ -8,16 +8,23 @@ apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
libcurl4-openssl-dev \
|
||||
libssl-dev \
|
||||
- python3-dev
|
||||
+ python3-dev \
|
||||
+ ccache
|
||||
|
||||
# Dependencies: conda
|
||||
-wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-4.5.11-Linux-x86_64.sh -O ~/miniconda.sh --no-check-certificate && /bin/bash ~/miniconda.sh -b -p /opt/miniconda
|
||||
+wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-py312_25.9.1-1-Linux-x86_64.sh -O ~/miniconda.sh && /bin/bash ~/miniconda.sh -b -p /opt/miniconda
|
||||
rm ~/miniconda.sh
|
||||
/opt/miniconda/bin/conda clean -ya
|
||||
|
||||
-pip install numpy
|
||||
-pip install packaging
|
||||
-pip install "wheel>=0.35.1"
|
||||
+# Dependencies: venv and packages
|
||||
+/opt/miniconda/bin/python3 -m venv /opt/rocm-venv
|
||||
+/opt/rocm-venv/bin/pip install --no-cache-dir --upgrade pip
|
||||
+/opt/rocm-venv/bin/pip install --no-cache-dir \
|
||||
+ "numpy==2.3.4" \
|
||||
+ "packaging==25.0" \
|
||||
+ "wheel==0.45.1" \
|
||||
+ "setuptools==80.9.0"
|
||||
+
|
||||
rm -rf /opt/miniconda/pkgs
|
||||
|
||||
# Dependencies: cmake
|
||||
@@ -8,6 +8,7 @@ readme = "README.md"
|
||||
dependencies = [
|
||||
"aiocache>=0.12.1,<1.0",
|
||||
"fastapi>=0.95.2,<1.0",
|
||||
"ftfy>=6.1.1",
|
||||
"gunicorn>=21.1.0",
|
||||
"huggingface-hub>=0.20.1,<1.0",
|
||||
"insightface>=0.7.3,<1.0",
|
||||
@@ -49,10 +50,10 @@ dev = ["locust>=2.15.1", { include-group = "test" }, { include-group = "lint" }]
|
||||
[project.optional-dependencies]
|
||||
cpu = ["onnxruntime>=1.23.2,<2"]
|
||||
cuda = ["onnxruntime-gpu>=1.23.2,<2"]
|
||||
openvino = ["onnxruntime-openvino>=1.24.1,<2"]
|
||||
openvino = ["onnxruntime-openvino>=1.23.0,<2"]
|
||||
armnn = ["onnxruntime>=1.23.2,<2"]
|
||||
rknn = ["onnxruntime>=1.23.2,<2", "rknn-toolkit-lite2>=2.3.0,<3"]
|
||||
rocm = ["onnxruntime-migraphx>=1.23.2,<2"]
|
||||
rocm = []
|
||||
|
||||
[tool.uv]
|
||||
compile-bytecode = true
|
||||
|
||||
@@ -179,7 +179,7 @@ class TestOrtSession:
|
||||
OV_EP = ["OpenVINOExecutionProvider", "CPUExecutionProvider"]
|
||||
CUDA_EP_OUT_OF_ORDER = ["CPUExecutionProvider", "CUDAExecutionProvider"]
|
||||
TRT_EP = ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"]
|
||||
ROCM_EP = ["MIGraphXExecutionProvider", "CPUExecutionProvider"]
|
||||
ROCM_EP = ["ROCMExecutionProvider", "CPUExecutionProvider"]
|
||||
COREML_EP = ["CoreMLExecutionProvider", "CPUExecutionProvider"]
|
||||
|
||||
@pytest.mark.providers(CPU_EP)
|
||||
@@ -289,38 +289,12 @@ class TestOrtSession:
|
||||
|
||||
assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}]
|
||||
|
||||
def test_sets_provider_options_for_rocm(self, mocker: MockerFixture) -> None:
|
||||
model_path = "/cache/ViT-B-32__openai/textual/model.onnx"
|
||||
def test_sets_provider_options_for_rocm(self) -> None:
|
||||
os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
|
||||
mkdir = mocker.patch("immich_ml.sessions.ort.Path.mkdir")
|
||||
|
||||
session = OrtSession(model_path, providers=["MIGraphXExecutionProvider"])
|
||||
session = OrtSession("ViT-B-32__openai", providers=["ROCMExecutionProvider"])
|
||||
|
||||
assert session.provider_options == [
|
||||
{
|
||||
"device_id": "1",
|
||||
"migraphx_model_cache_dir": "/cache/ViT-B-32__openai/textual/migraphx",
|
||||
"migraphx_fp16_enable": "0",
|
||||
}
|
||||
]
|
||||
mkdir.assert_called_once_with(parents=True, exist_ok=True)
|
||||
|
||||
def test_sets_rocm_to_fp16_if_enabled(self, path: mock.Mock, mocker: MockerFixture) -> None:
|
||||
model_path = "/cache/ViT-B-32__openai/textual/model.onnx"
|
||||
os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
|
||||
mocker.patch.object(settings, "rocm_precision", ModelPrecision.FP16)
|
||||
mkdir = mocker.patch("immich_ml.sessions.ort.Path.mkdir")
|
||||
|
||||
session = OrtSession(model_path, providers=["MIGraphXExecutionProvider"])
|
||||
|
||||
assert session.provider_options == [
|
||||
{
|
||||
"device_id": "1",
|
||||
"migraphx_model_cache_dir": "/cache/ViT-B-32__openai/textual/migraphx",
|
||||
"migraphx_fp16_enable": "1",
|
||||
}
|
||||
]
|
||||
mkdir.assert_called_once_with(parents=True, exist_ok=True)
|
||||
assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}]
|
||||
|
||||
def test_sets_provider_options_kwarg(self) -> None:
|
||||
session = OrtSession(
|
||||
|
||||
163
machine-learning/uv.lock
generated
163
machine-learning/uv.lock
generated
@@ -262,6 +262,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coloredlogs"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "humanfriendly" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorlog"
|
||||
version = "6.9.0"
|
||||
@@ -642,6 +654,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/25/fab23259a52ece5670dcb8452e1af34b89e6135ecc17cd4b54b4b479eac6/fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960", size = 168979, upload-time = "2023-12-11T21:19:52.446Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ftfy"
|
||||
version = "6.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gevent"
|
||||
version = "24.10.3"
|
||||
@@ -764,14 +788,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "25.1.0"
|
||||
version = "23.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -874,6 +898,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humanfriendly"
|
||||
version = "10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
@@ -903,6 +939,7 @@ source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "ftfy" },
|
||||
{ name = "gunicorn" },
|
||||
{ name = "huggingface-hub" },
|
||||
{ name = "insightface" },
|
||||
@@ -936,9 +973,6 @@ rknn = [
|
||||
{ name = "onnxruntime" },
|
||||
{ name = "rknn-toolkit-lite2" },
|
||||
]
|
||||
rocm = [
|
||||
{ name = "onnxruntime-migraphx" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
@@ -984,6 +1018,7 @@ types = [
|
||||
requires-dist = [
|
||||
{ name = "aiocache", specifier = ">=0.12.1,<1.0" },
|
||||
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
|
||||
{ name = "ftfy", specifier = ">=6.1.1" },
|
||||
{ name = "gunicorn", specifier = ">=21.1.0" },
|
||||
{ name = "huggingface-hub", specifier = ">=0.20.1,<1.0" },
|
||||
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
|
||||
@@ -992,8 +1027,7 @@ requires-dist = [
|
||||
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
|
||||
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" },
|
||||
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.23.2,<2" },
|
||||
{ name = "onnxruntime-migraphx", marker = "extra == 'rocm'", specifier = ">=1.23.2,<2" },
|
||||
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" },
|
||||
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.23.0,<2" },
|
||||
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
||||
{ name = "orjson", specifier = ">=3.9.5" },
|
||||
{ name = "pillow", specifier = ">=12.1.1,<12.2" },
|
||||
@@ -1409,55 +1443,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "msgpack"
|
||||
version = "1.1.2"
|
||||
version = "1.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/d5/5662032db1571110b5b51647aed4b56dfbd01bfae789fa566a2be1f385d1/msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", size = 166311, upload-time = "2023-09-28T13:20:36.726Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b3/309de40dc7406b7f3492332c5ee2b492a593c2a9bb97ea48ebf2f5279999/msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", size = 305096, upload-time = "2023-09-28T13:18:49.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/56/a677cd761a2cefb2e3ffe7e684633294dccb161d78e8ea6da9277e45b4a2/msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", size = 235210, upload-time = "2023-09-28T13:18:51.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/4e/1ab4a982cbd90f988e49f849fc1212f2c04a59870c59daabf8950617e2aa/msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", size = 231952, upload-time = "2023-09-28T13:18:52.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/74/bd02044eb628c7361ad2bd8c1a6147af5c6c2bbceb77b3b1da20f4a8a9c5/msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", size = 549511, upload-time = "2023-09-28T13:18:54.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/09/dee50913ba5cc047f7fd7162f09453a676e7935c84b3bf3a398e12108677/msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", size = 557980, upload-time = "2023-09-28T13:18:56.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/a5/78a7d87f5f8ffe4c32167afa15d4957db649bab4822f909d8d765339bbab/msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", size = 545547, upload-time = "2023-09-28T13:18:57.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/53/698c10913947f97f6fe7faad86a34e6aa1b66cea2df6f99105856bd346d9/msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", size = 554669, upload-time = "2023-09-28T13:18:58.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/3f/9730c6cb574b15d349b80cd8523a7df4b82058528339f952ea1c32ac8a10/msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", size = 583353, upload-time = "2023-09-28T13:19:01.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/bc/dc184d943692671149848438fb3bed3a3de288ce7998cb91bc98f40f201b/msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", size = 557455, upload-time = "2023-09-28T13:19:03.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/7b/1bc69d4a56c8d2f4f2dfbe4722d40344af9a85b6fb3b09cfb350ba6a42f6/msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", size = 216367, upload-time = "2023-09-28T13:19:04.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/3d/c8dd23050eefa3d9b9c5b8329ed3308c2f2f80f65825e9ea4b7fa621cdab/msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", size = 222860, upload-time = "2023-09-28T13:19:06.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/47/20dff6b4512cf3575550c8801bc53fe7d540f4efef9c5c37af51760fcdcf/msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", size = 305759, upload-time = "2023-09-28T13:19:08.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/8a/34f1726d2c9feccec3d946776e9bce8f20ae09d8b91899fc20b296c942af/msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", size = 235330, upload-time = "2023-09-28T13:19:09.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/f6/e64c72577d6953789c3cb051b059a4b56317056b3c65013952338ed8a34e/msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", size = 232537, upload-time = "2023-09-28T13:19:10.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/75/1ed3a96e12941873fd957e016cc40c0c178861a872bd45e75b9a188eb422/msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", size = 546561, upload-time = "2023-09-28T13:19:12.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/0a/c6a1390f9c6a31da0fecbbfdb86b1cb39ad302d9e24f9cca3d9e14c364f0/msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", size = 559009, upload-time = "2023-09-28T13:19:14.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/74/99f6077754665613ea1f37b3d91c10129f6976b7721ab4d0973023808e5a/msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", size = 543882, upload-time = "2023-09-28T13:19:16.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/7e/dc0dc8de2bf27743b31691149258f9b1bd4bf3c44c105df3df9b97081cd1/msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", size = 546949, upload-time = "2023-09-28T13:19:18.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/61/91bae9474def032f6c333d62889bbeda9e1554c6b123375ceeb1767efd78/msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", size = 579836, upload-time = "2023-09-28T13:19:19.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/4d/d98592099d4f18945f89cf3e634dc0cb128bb33b1b93f85a84173d35e181/msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", size = 556587, upload-time = "2023-09-28T13:19:21.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/44/6556ffe169bf2c0e974e2ea25fb82a7e55ebcf52a81b03a5e01820de5f84/msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", size = 216509, upload-time = "2023-09-28T13:19:23.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/c1/63903f30d51d165e132e5221a2a4a1bbfab7508b68131c871d70bffac78a/msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", size = 223287, upload-time = "2023-09-28T13:19:25.097Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1704,29 +1715,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/94/a3b20276261f5e64dbd72bda656af988282cff01f18c2685953600e2f810/onnxruntime_gpu-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2cee7e12b0f4813c62f9a48df83fd01d066cc970400c832252cf3c155a6957", size = 252633096, upload-time = "2026-02-05T17:24:53.248Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime-migraphx"
|
||||
version = "1.24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flatbuffers" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "sympy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/da/ca7ebc1a8d1193c97ceb9a05fad50f675eb955dc51beb7eb9ba89c8e7db0/onnxruntime_migraphx-1.24.2-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:a2b434fb8880cac2b268950bdf279f33741d29c1f1c5461d27af835e8e288043", size = 20339710, upload-time = "2026-02-21T07:25:13.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/2e/8c83ec45a9365b4256495ca55eea30da7f03b02177b6da423c7da1ff5f6a/onnxruntime_migraphx-1.24.2-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ec814818da952bda3062e26f56c88bb713c00491ef91f86716c8d7346f9bc31b", size = 20341883, upload-time = "2026-02-21T07:25:17.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/52/4776ac68dbc46ca02c9a14cc9e5c496017f47a18cedf606cc38f4911b96a/onnxruntime_migraphx-1.24.2-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:20e497538362170af639b03a40249d7ed61b873ac354f20d732b90252206e320", size = 20342422, upload-time = "2026-02-21T07:25:22.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/44/db9035204a3363f9c0a4822c68e9a7520c13ef8d261f96b89b1375106dab/onnxruntime_migraphx-1.24.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:9d7f1b1a2b9651143a2080b4f42ee99eead02023de1855d1b8a02199a9c179aa", size = 20343783, upload-time = "2026-02-21T07:25:29.155Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime-openvino"
|
||||
version = "1.24.1"
|
||||
version = "1.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coloredlogs" },
|
||||
{ name = "flatbuffers" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
@@ -1734,12 +1728,12 @@ dependencies = [
|
||||
{ name = "sympy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/16/69ca742f0b65c40d4de3ff44bb6abc23c47b23e932bc901116176ae69922/onnxruntime_openvino-1.24.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3007c803634cc69c6d52af1dea7ce729d9bb62b9a11070fd2f959119199007a8", size = 84430935, upload-time = "2026-02-26T13:44:32.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/73/619bb416bbfc40aebdd493fd6800d2637359294fe683d8a6bae3ff8d869a/onnxruntime_openvino-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:8042698232bf67f1f6b219c2b07728d7ae7ddff17d8524588de3675480609aef", size = 13655357, upload-time = "2026-02-26T13:44:35.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cf/17ba72de2df0fcba349937d2788f154397bbc2d1a2d67772a97e26f6bc5f/onnxruntime_openvino-1.24.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d617fac2f59a6ab5ea59a788c3e1592240a129642519aaeaa774761dfe35150e", size = 84433207, upload-time = "2026-02-26T13:44:41.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/37/d301f2c68b19a9485ed5db3047e0fb52478f3e73eb08c7d2a7c61be7cc1c/onnxruntime_openvino-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:f186335a9c9b255633275290da7521d3d4d14c7773fee3127bfa040234d3fa5a", size = 13658075, upload-time = "2026-02-26T13:44:44.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/07/f225999919f56506b603aaa3ff837ad563ab26f86906ed7fa7e5abcd849e/onnxruntime_openvino-1.24.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2c3bb73e68ac27f4891af8a595c1faf574ec68b772e6583c90a0b997a1822782", size = 84433183, upload-time = "2026-02-26T13:44:50.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/92/46ae2cd565961a89189900f385bb2f13a9fa731ea4674001d23720fbb1e0/onnxruntime_openvino-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:434bf49aa71393c577a456c9d76c98e6d6958a833fa0876793e3d5437b5a511a", size = 13658485, upload-time = "2026-02-26T13:44:53.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/10/adcd4ac68ffc8dee003553125ef5c091be822e2d7c1077d0bb85690baa9c/onnxruntime_openvino-1.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:91938837e6e92e30c63d12fad68a8a4959c40d2eade2bd60f38bdd5b6392f8d3", size = 70481480, upload-time = "2025-10-14T15:19:45.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/95/25f28d6fecf300aa0af393e96af9e00cc676e5dab650ab84f2122610df50/onnxruntime_openvino-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f05d2d6a804fb70d3f4329d777ac62439773dcc2df827dd5f42644b10bf1fea", size = 13117353, upload-time = "2025-10-14T15:19:49.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/0c/8d97419dfeedf419c5fe5293f3dbc59284855a63ad22e71f46c0010c9dc4/onnxruntime_openvino-1.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b963ea19bf9856f3d6b2f719d451f2eeae482a8f69c729906465aa4f27f4d39c", size = 70483359, upload-time = "2025-10-14T15:19:52.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/30/ff6111b16ffb4187c462824aa4e95acc20fdd90f856d44a339d56c6dacd6/onnxruntime_openvino-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:937e52657f94c56990a6e5bd4c3705bd6e970834c7c94e23d300dde6848f2889", size = 13117933, upload-time = "2025-10-14T15:19:58.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/48/e42f618a8ec5fcf825fed4fdc8125f7105256cc6020b84567ecb88d5e2b7/onnxruntime_openvino-1.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2e93b9a8323e196b7433866054a59260f2206ab6fb0e7223dda91da71f1db8c5", size = 70483088, upload-time = "2025-10-14T15:20:02.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/f9/a531dc497dc113dc14df9a9de5aacb1676cadebc3ec6cc7cd3ca65cb3db0/onnxruntime_openvino-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:0ebbf70929de4ce269371cb255536bbedef588932d744da0b40e66c38a620f35", size = 13118206, upload-time = "2025-10-14T15:20:05.587Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2179,6 +2173,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/92/8486ede85fcc088f1b3dba4ce92dd29d126fd96b0008ea213167940a2475/pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", size = 103139, upload-time = "2023-07-30T15:06:59.829Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyreadline3"
|
||||
version = "3.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/86/3d61a61f36a0067874a00cb4dceb9028d34b6060e47828f7fc86fb9f7ee9/pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae", size = 86465, upload-time = "2022-01-24T20:05:11.66Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/fc/a3c13ded7b3057680c8ae95a9b6cc83e63657c38e0005c400a5d018a33a7/pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb", size = 95203, upload-time = "2022-01-24T20:05:10.442Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
|
||||
@@ -16,7 +16,7 @@ config_roots = [
|
||||
[tools]
|
||||
node = "24.13.1"
|
||||
flutter = "3.35.7"
|
||||
pnpm = "10.30.0"
|
||||
pnpm = "10.29.3"
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.11.4"
|
||||
java = "21.0.2"
|
||||
@@ -37,12 +37,13 @@ run = "pnpm install --filter @immich/sdk --frozen-lockfile"
|
||||
|
||||
[tasks."sdk:build"]
|
||||
dir = "open-api/typescript-sdk"
|
||||
run = "pnpm run build"
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "tsc"
|
||||
|
||||
# i18n tasks
|
||||
[tasks."i18n:format"]
|
||||
dir = "i18n"
|
||||
run = "pnpm run format"
|
||||
run = { task = ":i18n:format-fix" }
|
||||
|
||||
[tasks."i18n:format-fix"]
|
||||
dir = "i18n"
|
||||
|
||||
@@ -59,7 +59,7 @@ private open class LocalImagesPigeonCodec : StandardMessageCodec() {
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface LocalImageApi {
|
||||
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, preferEncoded: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
|
||||
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
|
||||
fun cancelRequest(requestId: Long)
|
||||
fun getThumbhash(thumbhash: String, callback: (Result<Map<String, Long>>) -> Unit)
|
||||
|
||||
@@ -82,8 +82,7 @@ interface LocalImageApi {
|
||||
val widthArg = args[2] as Long
|
||||
val heightArg = args[3] as Long
|
||||
val isVideoArg = args[4] as Boolean
|
||||
val preferEncodedArg = args[5] as Boolean
|
||||
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg, preferEncodedArg) { result: Result<Map<String, Long>?> ->
|
||||
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg) { result: Result<Map<String, Long>?> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(LocalImagesPigeonUtils.wrapError(error))
|
||||
|
||||
@@ -14,7 +14,6 @@ import android.util.Size
|
||||
import androidx.annotation.RequiresApi
|
||||
import app.alextran.immich.NativeBuffer
|
||||
import kotlin.math.*
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.Priority
|
||||
@@ -49,6 +48,7 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
||||
try {
|
||||
val buffer = NativeBuffer.wrap(pointer, size)
|
||||
copyPixelsToBuffer(buffer)
|
||||
recycle()
|
||||
return mapOf(
|
||||
"pointer" to pointer,
|
||||
"width" to width.toLong(),
|
||||
@@ -57,9 +57,8 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
NativeBuffer.free(pointer)
|
||||
throw e
|
||||
} finally {
|
||||
recycle()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,17 +99,12 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
||||
width: Long,
|
||||
height: Long,
|
||||
isVideo: Boolean,
|
||||
preferEncoded: Boolean,
|
||||
callback: (Result<Map<String, Long>?>) -> Unit
|
||||
) {
|
||||
val signal = CancellationSignal()
|
||||
val task = threadPool.submit {
|
||||
try {
|
||||
if (preferEncoded) {
|
||||
getEncodedImageInternal(assetId, callback, signal)
|
||||
} else {
|
||||
getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal)
|
||||
}
|
||||
getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is OperationCanceledException -> callback(CANCELLED)
|
||||
@@ -139,35 +133,6 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEncodedImageInternal(
|
||||
assetId: String,
|
||||
callback: (Result<Map<String, Long>?>) -> Unit,
|
||||
signal: CancellationSignal
|
||||
) {
|
||||
signal.throwIfCanceled()
|
||||
val id = assetId.toLong()
|
||||
val uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||
|
||||
signal.throwIfCanceled()
|
||||
val bytes = resolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
?: throw IOException("Could not read image data for $assetId")
|
||||
|
||||
signal.throwIfCanceled()
|
||||
val pointer = NativeBuffer.allocate(bytes.size)
|
||||
try {
|
||||
val buffer = NativeBuffer.wrap(pointer, bytes.size)
|
||||
buffer.put(bytes)
|
||||
signal.throwIfCanceled()
|
||||
callback(Result.success(mapOf(
|
||||
"pointer" to pointer,
|
||||
"length" to bytes.size.toLong()
|
||||
)))
|
||||
} catch (e: Exception) {
|
||||
NativeBuffer.free(pointer)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThumbnailBufferInternal(
|
||||
assetId: String,
|
||||
width: Long,
|
||||
|
||||
@@ -47,7 +47,7 @@ private open class RemoteImagesPigeonCodec : StandardMessageCodec() {
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface RemoteImageApi {
|
||||
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, preferEncoded: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
|
||||
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>?>) -> Unit)
|
||||
fun cancelRequest(requestId: Long)
|
||||
fun clearCache(callback: (Result<Long>) -> Unit)
|
||||
|
||||
@@ -68,8 +68,7 @@ interface RemoteImageApi {
|
||||
val urlArg = args[0] as String
|
||||
val headersArg = args[1] as Map<String, String>
|
||||
val requestIdArg = args[2] as Long
|
||||
val preferEncodedArg = args[3] as Boolean
|
||||
api.requestImage(urlArg, headersArg, requestIdArg, preferEncodedArg) { result: Result<Map<String, Long>?> ->
|
||||
api.requestImage(urlArg, headersArg, requestIdArg) { result: Result<Map<String, Long>?> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(RemoteImagesPigeonUtils.wrapError(error))
|
||||
|
||||
@@ -51,7 +51,6 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||
url: String,
|
||||
headers: Map<String, String>,
|
||||
requestId: Long,
|
||||
@Suppress("UNUSED_PARAMETER") preferEncoded: Boolean, // always returns encoded; setting has no effect on Android
|
||||
callback: (Result<Map<String, Long>?>) -> Unit
|
||||
) {
|
||||
val signal = CancellationSignal()
|
||||
|
||||
@@ -78,21 +78,6 @@ class FlutterError (
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
|
||||
enum class PlatformAssetPlaybackStyle(val raw: Int) {
|
||||
UNKNOWN(0),
|
||||
IMAGE(1),
|
||||
VIDEO(2),
|
||||
IMAGE_ANIMATED(3),
|
||||
LIVE_PHOTO(4),
|
||||
VIDEO_LOOPING(5);
|
||||
|
||||
companion object {
|
||||
fun ofRaw(raw: Int): PlatformAssetPlaybackStyle? {
|
||||
return values().firstOrNull { it.raw == raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class PlatformAsset (
|
||||
val id: String,
|
||||
@@ -107,8 +92,7 @@ data class PlatformAsset (
|
||||
val isFavorite: Boolean,
|
||||
val adjustmentTime: Long? = null,
|
||||
val latitude: Double? = null,
|
||||
val longitude: Double? = null,
|
||||
val playbackStyle: PlatformAssetPlaybackStyle
|
||||
val longitude: Double? = null
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
@@ -126,8 +110,7 @@ data class PlatformAsset (
|
||||
val adjustmentTime = pigeonVar_list[10] as Long?
|
||||
val latitude = pigeonVar_list[11] as Double?
|
||||
val longitude = pigeonVar_list[12] as Double?
|
||||
val playbackStyle = pigeonVar_list[13] as PlatformAssetPlaybackStyle
|
||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle)
|
||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
@@ -145,7 +128,6 @@ data class PlatformAsset (
|
||||
adjustmentTime,
|
||||
latitude,
|
||||
longitude,
|
||||
playbackStyle,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -308,31 +290,26 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
129.toByte() -> {
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
PlatformAssetPlaybackStyle.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
130.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
PlatformAsset.fromList(it)
|
||||
}
|
||||
}
|
||||
131.toByte() -> {
|
||||
130.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
PlatformAlbum.fromList(it)
|
||||
}
|
||||
}
|
||||
132.toByte() -> {
|
||||
131.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
SyncDelta.fromList(it)
|
||||
}
|
||||
}
|
||||
133.toByte() -> {
|
||||
132.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
HashResult.fromList(it)
|
||||
}
|
||||
}
|
||||
134.toByte() -> {
|
||||
133.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
CloudIdResult.fromList(it)
|
||||
}
|
||||
@@ -342,28 +319,24 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
when (value) {
|
||||
is PlatformAssetPlaybackStyle -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.raw)
|
||||
}
|
||||
is PlatformAsset -> {
|
||||
stream.write(130)
|
||||
stream.write(129)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is PlatformAlbum -> {
|
||||
stream.write(131)
|
||||
stream.write(130)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is SyncDelta -> {
|
||||
stream.write(132)
|
||||
stream.write(131)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is HashResult -> {
|
||||
stream.write(133)
|
||||
stream.write(132)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is CloudIdResult -> {
|
||||
stream.write(134)
|
||||
stream.write(133)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
|
||||
@@ -4,17 +4,11 @@ import android.annotation.SuppressLint
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.core.database.getStringOrNull
|
||||
import app.alextran.immich.core.ImmichPlugin
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.ImageHeaderParser
|
||||
import com.bumptech.glide.load.ImageHeaderParserUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -34,8 +28,6 @@ sealed class AssetResult {
|
||||
data class InvalidAsset(val assetId: String) : AssetResult()
|
||||
}
|
||||
|
||||
private const val TAG = "NativeSyncApiImplBase"
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
private val ctx: Context = context.applicationContext
|
||||
@@ -47,13 +39,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
|
||||
private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED"
|
||||
|
||||
// MediaStore.Files.FileColumns.SPECIAL_FORMAT — S Extensions 21+
|
||||
// https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT
|
||||
private const val SPECIAL_FORMAT_COLUMN = "_special_format"
|
||||
private const val SPECIAL_FORMAT_GIF = 1
|
||||
private const val SPECIAL_FORMAT_MOTION_PHOTO = 2
|
||||
private const val SPECIAL_FORMAT_ANIMATED_WEBP = 3
|
||||
|
||||
const val MEDIA_SELECTION =
|
||||
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
|
||||
val MEDIA_SELECTION_ARGS = arrayOf(
|
||||
@@ -75,15 +60,9 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
add(MediaStore.MediaColumns.DURATION)
|
||||
add(MediaStore.MediaColumns.ORIENTATION)
|
||||
// IS_FAVORITE is only available on Android 11 and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
add(MediaStore.MediaColumns.IS_FAVORITE)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
add(SPECIAL_FORMAT_COLUMN)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Fallback: read XMP from MediaStore to detect Motion Photos
|
||||
add(MediaStore.MediaColumns.XMP)
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
||||
@@ -130,12 +109,9 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
val orientationColumn =
|
||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
||||
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
|
||||
val specialFormatColumn = c.getColumnIndex(SPECIAL_FORMAT_COLUMN)
|
||||
val xmpColumn = c.getColumnIndex(MediaStore.MediaColumns.XMP)
|
||||
|
||||
while (c.moveToNext()) {
|
||||
val numericId = c.getLong(idColumn)
|
||||
val id = numericId.toString()
|
||||
val id = c.getLong(idColumn).toString()
|
||||
val name = c.getStringOrNull(nameColumn)
|
||||
val bucketId = c.getStringOrNull(bucketIdColumn)
|
||||
val path = c.getStringOrNull(dataColumn)
|
||||
@@ -149,11 +125,10 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
continue
|
||||
}
|
||||
|
||||
val rawMediaType = c.getInt(mediaTypeColumn)
|
||||
val assetType: Long = when (rawMediaType) {
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1L
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2L
|
||||
else -> 0L
|
||||
val mediaType = when (c.getInt(mediaTypeColumn)) {
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2
|
||||
else -> 0
|
||||
}
|
||||
// Date taken is milliseconds since epoch, Date added is seconds since epoch
|
||||
val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
|
||||
@@ -163,19 +138,15 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
val width = c.getInt(widthColumn).toLong()
|
||||
val height = c.getInt(heightColumn).toLong()
|
||||
// Duration is milliseconds
|
||||
val duration = if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0L
|
||||
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
|
||||
else c.getLong(durationColumn) / 1000
|
||||
val orientation = c.getInt(orientationColumn)
|
||||
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
||||
|
||||
val playbackStyle = detectPlaybackStyle(
|
||||
numericId, rawMediaType, specialFormatColumn, xmpColumn, c
|
||||
)
|
||||
|
||||
val asset = PlatformAsset(
|
||||
id,
|
||||
name,
|
||||
assetType,
|
||||
mediaType.toLong(),
|
||||
createdAt,
|
||||
modifiedAt,
|
||||
width,
|
||||
@@ -183,7 +154,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
duration,
|
||||
orientation.toLong(),
|
||||
isFavorite,
|
||||
playbackStyle = playbackStyle,
|
||||
)
|
||||
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||
}
|
||||
@@ -191,81 +161,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the playback style for an asset using _special_format (API 33+)
|
||||
* or XMP / MIME / RIFF header fallbacks (pre-33).
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
private fun detectPlaybackStyle(
|
||||
assetId: Long,
|
||||
rawMediaType: Int,
|
||||
specialFormatColumn: Int,
|
||||
xmpColumn: Int,
|
||||
cursor: Cursor
|
||||
): PlatformAssetPlaybackStyle {
|
||||
// video currently has no special formats, so we can short circuit and avoid unnecessary work
|
||||
if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) {
|
||||
return PlatformAssetPlaybackStyle.VIDEO
|
||||
}
|
||||
|
||||
// API 33+: use _special_format from cursor
|
||||
if (specialFormatColumn != -1) {
|
||||
val specialFormat = cursor.getInt(specialFormatColumn)
|
||||
return when {
|
||||
specialFormat == SPECIAL_FORMAT_MOTION_PHOTO -> PlatformAssetPlaybackStyle.LIVE_PHOTO
|
||||
specialFormat == SPECIAL_FORMAT_GIF || specialFormat == SPECIAL_FORMAT_ANIMATED_WEBP -> PlatformAssetPlaybackStyle.IMAGE_ANIMATED
|
||||
rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> PlatformAssetPlaybackStyle.IMAGE
|
||||
else -> PlatformAssetPlaybackStyle.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
if (rawMediaType != MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) {
|
||||
return PlatformAssetPlaybackStyle.UNKNOWN
|
||||
}
|
||||
|
||||
// Pre-API 33 fallback
|
||||
val uri = ContentUris.withAppendedId(
|
||||
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
|
||||
assetId
|
||||
)
|
||||
|
||||
// Read XMP from cursor (API 30+) or ExifInterface stream (pre-30)
|
||||
val xmp: String? = if (xmpColumn != -1) {
|
||||
cursor.getBlob(xmpColumn)?.toString(Charsets.UTF_8)
|
||||
} else {
|
||||
try {
|
||||
ctx.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
ExifInterface(stream).getAttribute(ExifInterface.TAG_XMP)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to read XMP for asset $assetId", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (xmp != null && "Camera:MotionPhoto" in xmp) {
|
||||
return PlatformAssetPlaybackStyle.LIVE_PHOTO
|
||||
}
|
||||
|
||||
try {
|
||||
ctx.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
val glide = Glide.get(ctx)
|
||||
val type = ImageHeaderParserUtils.getType(
|
||||
glide.registry.imageHeaderParsers,
|
||||
stream,
|
||||
glide.arrayPool
|
||||
)
|
||||
if (type == ImageHeaderParser.ImageType.GIF || type == ImageHeaderParser.ImageType.ANIMATED_WEBP) {
|
||||
return PlatformAssetPlaybackStyle.IMAGE_ANIMATED
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to parse image header for asset $assetId", e)
|
||||
}
|
||||
|
||||
return PlatformAssetPlaybackStyle.IMAGE
|
||||
}
|
||||
|
||||
fun getAlbums(): List<PlatformAlbum> {
|
||||
val albums = mutableListOf<PlatformAlbum>()
|
||||
val albumsCount = mutableMapOf<String, Int>()
|
||||
|
||||
2
mobile/drift_schemas/main/drift_schema_v20.json
generated
2
mobile/drift_schemas/main/drift_schema_v20.json
generated
File diff suppressed because one or more lines are too long
@@ -70,7 +70,7 @@ class LocalImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol LocalImageApi {
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
||||
func cancelRequest(requestId: Int64) throws
|
||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
||||
}
|
||||
@@ -90,8 +90,7 @@ class LocalImageApiSetup {
|
||||
let widthArg = args[2] as! Int64
|
||||
let heightArg = args[3] as! Int64
|
||||
let isVideoArg = args[4] as! Bool
|
||||
let preferEncodedArg = args[5] as! Bool
|
||||
api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg, preferEncoded: preferEncodedArg) { result in
|
||||
api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
|
||||
@@ -7,7 +7,7 @@ class LocalImageRequest {
|
||||
weak var workItem: DispatchWorkItem?
|
||||
var isCancelled = false
|
||||
let callback: (Result<[String: Int64]?, any Error>) -> Void
|
||||
|
||||
|
||||
init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||
self.callback = callback
|
||||
}
|
||||
@@ -30,11 +30,11 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
requestOptions.version = .current
|
||||
return requestOptions
|
||||
}()
|
||||
|
||||
|
||||
private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated)
|
||||
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
|
||||
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
|
||||
|
||||
|
||||
private static var rgbaFormat = vImage_CGImageFormat(
|
||||
bitsPerComponent: 8,
|
||||
bitsPerPixel: 32,
|
||||
@@ -48,12 +48,12 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
assetCache.countLimit = 10000
|
||||
return assetCache
|
||||
}()
|
||||
|
||||
|
||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
|
||||
ImageProcessing.queue.async {
|
||||
guard let data = Data(base64Encoded: thumbhash)
|
||||
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
|
||||
|
||||
|
||||
let (width, height, pointer) = thumbHashToRGBA(hash: data)
|
||||
completion(.success([
|
||||
"pointer": Int64(Int(bitPattern: pointer.baseAddress)),
|
||||
@@ -63,77 +63,34 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||
let request = LocalImageRequest(callback: completion)
|
||||
let item = DispatchWorkItem {
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
ImageProcessing.semaphore.wait()
|
||||
defer {
|
||||
ImageProcessing.semaphore.signal()
|
||||
}
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
guard let asset = Self.requestAsset(assetId: assetId)
|
||||
else {
|
||||
Self.remove(requestId: requestId)
|
||||
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
if preferEncoded {
|
||||
let dataOptions = PHImageRequestOptions()
|
||||
dataOptions.isNetworkAccessAllowed = true
|
||||
dataOptions.isSynchronous = true
|
||||
dataOptions.version = .current
|
||||
|
||||
var imageData: Data?
|
||||
Self.imageManager.requestImageDataAndOrientation(
|
||||
for: asset,
|
||||
options: dataOptions,
|
||||
resultHandler: { (data, _, _, _) in
|
||||
imageData = data
|
||||
}
|
||||
)
|
||||
|
||||
if request.isCancelled {
|
||||
Self.remove(requestId: requestId)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
guard let data = imageData else {
|
||||
Self.remove(requestId: requestId)
|
||||
return completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil)))
|
||||
}
|
||||
|
||||
let length = data.count
|
||||
let pointer = malloc(length)!
|
||||
data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)
|
||||
|
||||
if request.isCancelled {
|
||||
free(pointer)
|
||||
Self.remove(requestId: requestId)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
request.callback(.success([
|
||||
"pointer": Int64(Int(bitPattern: pointer)),
|
||||
"length": Int64(length),
|
||||
]))
|
||||
Self.remove(requestId: requestId)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var image: UIImage?
|
||||
Self.imageManager.requestImage(
|
||||
for: asset,
|
||||
@@ -144,29 +101,29 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
image = _image
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
guard let image = image,
|
||||
let cgImage = image.cgImage else {
|
||||
Self.remove(requestId: requestId)
|
||||
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
|
||||
}
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let buffer = try vImage_Buffer(cgImage: cgImage, format: Self.rgbaFormat)
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
buffer.free()
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
request.callback(.success([
|
||||
"pointer": Int64(Int(bitPattern: buffer.data)),
|
||||
"width": Int64(buffer.width),
|
||||
@@ -179,24 +136,24 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
request.workItem = item
|
||||
Self.add(requestId: requestId, request: request)
|
||||
ImageProcessing.queue.async(execute: item)
|
||||
}
|
||||
|
||||
|
||||
func cancelRequest(requestId: Int64) {
|
||||
Self.cancel(requestId: requestId)
|
||||
}
|
||||
|
||||
|
||||
private static func add(requestId: Int64, request: LocalImageRequest) -> Void {
|
||||
requestQueue.sync { requests[requestId] = request }
|
||||
}
|
||||
|
||||
|
||||
private static func remove(requestId: Int64) -> Void {
|
||||
requestQueue.sync { requests[requestId] = nil }
|
||||
}
|
||||
|
||||
|
||||
private static func cancel(requestId: Int64) -> Void {
|
||||
requestQueue.async {
|
||||
guard let request = requests.removeValue(forKey: requestId) else { return }
|
||||
@@ -207,12 +164,12 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static func requestAsset(assetId: String) -> PHAsset? {
|
||||
var asset: PHAsset?
|
||||
assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) }
|
||||
if asset != nil { return asset }
|
||||
|
||||
|
||||
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
|
||||
else { return nil }
|
||||
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
||||
|
||||
@@ -70,7 +70,7 @@ class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol RemoteImageApi {
|
||||
func requestImage(url: String, headers: [String: String], requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
||||
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
||||
func cancelRequest(requestId: Int64) throws
|
||||
func clearCache(completion: @escaping (Result<Int64, Error>) -> Void)
|
||||
}
|
||||
@@ -88,8 +88,7 @@ class RemoteImageApiSetup {
|
||||
let urlArg = args[0] as! String
|
||||
let headersArg = args[1] as! [String: String]
|
||||
let requestIdArg = args[2] as! Int64
|
||||
let preferEncodedArg = args[3] as! Bool
|
||||
api.requestImage(url: urlArg, headers: headersArg, requestId: requestIdArg, preferEncoded: preferEncodedArg) { result in
|
||||
api.requestImage(url: urlArg, headers: headersArg, requestId: requestIdArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
|
||||
@@ -8,7 +8,7 @@ class RemoteImageRequest {
|
||||
let id: Int64
|
||||
var isCancelled = false
|
||||
let completion: (Result<[String: Int64]?, any Error>) -> Void
|
||||
|
||||
|
||||
init(id: Int64, task: URLSessionDataTask, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||
self.id = id
|
||||
self.task = task
|
||||
@@ -32,93 +32,75 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true
|
||||
] as CFDictionary
|
||||
|
||||
func requestImage(url: String, headers: [String : String], requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
|
||||
|
||||
func requestImage(url: String, headers: [String : String], requestId: Int64, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
|
||||
var urlRequest = URLRequest(url: URL(string: url)!)
|
||||
urlRequest.cachePolicy = .returnCacheDataElseLoad
|
||||
for (key, value) in headers {
|
||||
urlRequest.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
|
||||
let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in
|
||||
Self.handleCompletion(requestId: requestId, encoded: preferEncoded, data: data, response: response, error: error)
|
||||
Self.handleCompletion(requestId: requestId, data: data, response: response, error: error)
|
||||
}
|
||||
|
||||
|
||||
let request = RemoteImageRequest(id: requestId, task: task, completion: completion)
|
||||
|
||||
|
||||
os_unfair_lock_lock(&Self.lock)
|
||||
Self.requests[requestId] = request
|
||||
os_unfair_lock_unlock(&Self.lock)
|
||||
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) {
|
||||
|
||||
private static func handleCompletion(requestId: Int64, data: Data?, response: URLResponse?, error: Error?) {
|
||||
os_unfair_lock_lock(&Self.lock)
|
||||
guard let request = requests[requestId] else {
|
||||
return os_unfair_lock_unlock(&Self.lock)
|
||||
}
|
||||
requests[requestId] = nil
|
||||
os_unfair_lock_unlock(&Self.lock)
|
||||
|
||||
|
||||
if let error = error {
|
||||
if request.isCancelled || (error as NSError).code == NSURLErrorCancelled {
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
return request.completion(.failure(error))
|
||||
}
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
guard let data = data else {
|
||||
return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil)))
|
||||
}
|
||||
|
||||
|
||||
ImageProcessing.queue.async {
|
||||
ImageProcessing.semaphore.wait()
|
||||
defer { ImageProcessing.semaphore.signal() }
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
// Return raw encoded bytes when requested (for animated images)
|
||||
if encoded {
|
||||
let length = data.count
|
||||
let pointer = malloc(length)!
|
||||
data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)
|
||||
|
||||
if request.isCancelled {
|
||||
free(pointer)
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
return request.completion(
|
||||
.success([
|
||||
"pointer": Int64(Int(bitPattern: pointer)),
|
||||
"length": Int64(length),
|
||||
]))
|
||||
}
|
||||
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil),
|
||||
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else {
|
||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil)))
|
||||
}
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let buffer = try vImage_Buffer(cgImage: cgImage, format: rgbaFormat)
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
buffer.free()
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
request.completion(
|
||||
.success([
|
||||
"pointer": Int64(Int(bitPattern: buffer.data)),
|
||||
@@ -131,17 +113,17 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func cancelRequest(requestId: Int64) {
|
||||
os_unfair_lock_lock(&Self.lock)
|
||||
let request = Self.requests[requestId]
|
||||
os_unfair_lock_unlock(&Self.lock)
|
||||
|
||||
|
||||
guard let request = request else { return }
|
||||
request.isCancelled = true
|
||||
request.task?.cancel()
|
||||
}
|
||||
|
||||
|
||||
func clearCache(completion: @escaping (Result<Int64, any Error>) -> Void) {
|
||||
Task {
|
||||
let cache = URLSessionManager.shared.session.configuration.urlCache!
|
||||
|
||||
@@ -128,15 +128,6 @@ func deepHashMessages(value: Any?, hasher: inout Hasher) {
|
||||
|
||||
|
||||
|
||||
enum PlatformAssetPlaybackStyle: Int {
|
||||
case unknown = 0
|
||||
case image = 1
|
||||
case video = 2
|
||||
case imageAnimated = 3
|
||||
case livePhoto = 4
|
||||
case videoLooping = 5
|
||||
}
|
||||
|
||||
/// Generated class from Pigeon that represents data sent in messages.
|
||||
struct PlatformAsset: Hashable {
|
||||
var id: String
|
||||
@@ -152,7 +143,6 @@ struct PlatformAsset: Hashable {
|
||||
var adjustmentTime: Int64? = nil
|
||||
var latitude: Double? = nil
|
||||
var longitude: Double? = nil
|
||||
var playbackStyle: PlatformAssetPlaybackStyle
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
@@ -170,7 +160,6 @@ struct PlatformAsset: Hashable {
|
||||
let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10])
|
||||
let latitude: Double? = nilOrValue(pigeonVar_list[11])
|
||||
let longitude: Double? = nilOrValue(pigeonVar_list[12])
|
||||
let playbackStyle = pigeonVar_list[13] as! PlatformAssetPlaybackStyle
|
||||
|
||||
return PlatformAsset(
|
||||
id: id,
|
||||
@@ -185,8 +174,7 @@ struct PlatformAsset: Hashable {
|
||||
isFavorite: isFavorite,
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
playbackStyle: playbackStyle
|
||||
longitude: longitude
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
@@ -204,7 +192,6 @@ struct PlatformAsset: Hashable {
|
||||
adjustmentTime,
|
||||
latitude,
|
||||
longitude,
|
||||
playbackStyle,
|
||||
]
|
||||
}
|
||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||
@@ -362,20 +349,14 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
|
||||
override func readValue(ofType type: UInt8) -> Any? {
|
||||
switch type {
|
||||
case 129:
|
||||
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
|
||||
if let enumResultAsInt = enumResultAsInt {
|
||||
return PlatformAssetPlaybackStyle(rawValue: enumResultAsInt)
|
||||
}
|
||||
return nil
|
||||
case 130:
|
||||
return PlatformAsset.fromList(self.readValue() as! [Any?])
|
||||
case 131:
|
||||
case 130:
|
||||
return PlatformAlbum.fromList(self.readValue() as! [Any?])
|
||||
case 132:
|
||||
case 131:
|
||||
return SyncDelta.fromList(self.readValue() as! [Any?])
|
||||
case 133:
|
||||
case 132:
|
||||
return HashResult.fromList(self.readValue() as! [Any?])
|
||||
case 134:
|
||||
case 133:
|
||||
return CloudIdResult.fromList(self.readValue() as! [Any?])
|
||||
default:
|
||||
return super.readValue(ofType: type)
|
||||
@@ -385,23 +366,20 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
|
||||
|
||||
private class MessagesPigeonCodecWriter: FlutterStandardWriter {
|
||||
override func writeValue(_ value: Any) {
|
||||
if let value = value as? PlatformAssetPlaybackStyle {
|
||||
if let value = value as? PlatformAsset {
|
||||
super.writeByte(129)
|
||||
super.writeValue(value.rawValue)
|
||||
} else if let value = value as? PlatformAsset {
|
||||
super.writeByte(130)
|
||||
super.writeValue(value.toList())
|
||||
} else if let value = value as? PlatformAlbum {
|
||||
super.writeByte(131)
|
||||
super.writeByte(130)
|
||||
super.writeValue(value.toList())
|
||||
} else if let value = value as? SyncDelta {
|
||||
super.writeByte(132)
|
||||
super.writeByte(131)
|
||||
super.writeValue(value.toList())
|
||||
} else if let value = value as? HashResult {
|
||||
super.writeByte(133)
|
||||
super.writeByte(132)
|
||||
super.writeValue(value.toList())
|
||||
} else if let value = value as? CloudIdResult {
|
||||
super.writeByte(134)
|
||||
super.writeByte(133)
|
||||
super.writeValue(value.toList())
|
||||
} else {
|
||||
super.writeValue(value)
|
||||
|
||||
@@ -173,8 +173,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
type: 0,
|
||||
durationInSeconds: 0,
|
||||
orientation: 0,
|
||||
isFavorite: false,
|
||||
playbackStyle: .unknown
|
||||
isFavorite: false
|
||||
)
|
||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||
continue
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import Photos
|
||||
|
||||
extension PHAsset {
|
||||
var platformPlaybackStyle: PlatformAssetPlaybackStyle {
|
||||
switch playbackStyle {
|
||||
case .image: return .image
|
||||
case .imageAnimated: return .imageAnimated
|
||||
case .livePhoto: return .livePhoto
|
||||
case .video: return .video
|
||||
case .videoLooping: return .videoLooping
|
||||
@unknown default: return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
func toPlatformAsset() -> PlatformAsset {
|
||||
return PlatformAsset(
|
||||
id: localIdentifier,
|
||||
@@ -26,8 +15,7 @@ extension PHAsset {
|
||||
isFavorite: isFavorite,
|
||||
adjustmentTime: adjustmentTimestamp,
|
||||
latitude: location?.coordinate.latitude,
|
||||
longitude: location?.coordinate.longitude,
|
||||
playbackStyle: platformPlaybackStyle
|
||||
longitude: location?.coordinate.longitude
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,7 +26,7 @@ extension PHAsset {
|
||||
var filename: String? {
|
||||
return value(forKey: "filename") as? String
|
||||
}
|
||||
|
||||
|
||||
var adjustmentTimestamp: Int64? {
|
||||
if let date = value(forKey: "adjustmentTimestamp") as? Date {
|
||||
return Int64(date.timeIntervalSince1970)
|
||||
|
||||
@@ -18,5 +18,3 @@ enum ActionSource { timeline, viewer }
|
||||
enum CleanupStep { selectDate, scan, delete }
|
||||
|
||||
enum AssetKeepType { none, photosOnly, videosOnly }
|
||||
|
||||
enum AssetDateAggregation { start, end }
|
||||
|
||||
@@ -1,52 +1,276 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const List<ColorFilter> filters = [
|
||||
class EditFilter {
|
||||
final String name;
|
||||
final double rrBias;
|
||||
final double rgBias;
|
||||
final double rbBias;
|
||||
final double grBias;
|
||||
final double ggBias;
|
||||
final double gbBias;
|
||||
final double brBias;
|
||||
final double bgBias;
|
||||
final double bbBias;
|
||||
final double rOffset;
|
||||
final double gOffset;
|
||||
final double bOffset;
|
||||
|
||||
const EditFilter({
|
||||
required this.name,
|
||||
required this.rrBias,
|
||||
required this.rgBias,
|
||||
required this.rbBias,
|
||||
required this.grBias,
|
||||
required this.ggBias,
|
||||
required this.gbBias,
|
||||
required this.brBias,
|
||||
required this.bgBias,
|
||||
required this.bbBias,
|
||||
required this.rOffset,
|
||||
required this.gOffset,
|
||||
required this.bOffset,
|
||||
});
|
||||
|
||||
bool get isIdentity =>
|
||||
rrBias == 1 &&
|
||||
rgBias == 0 &&
|
||||
rbBias == 0 &&
|
||||
grBias == 0 &&
|
||||
ggBias == 1 &&
|
||||
gbBias == 0 &&
|
||||
brBias == 0 &&
|
||||
bgBias == 0 &&
|
||||
bbBias == 1 &&
|
||||
rOffset == 0 &&
|
||||
gOffset == 0 &&
|
||||
bOffset == 0;
|
||||
|
||||
factory EditFilter.fromMatrix(List<double> matrix, String name) {
|
||||
if (matrix.length != 20) {
|
||||
throw ArgumentError('Color filter matrix must have 20 elements');
|
||||
}
|
||||
|
||||
return EditFilter(
|
||||
name: name,
|
||||
rrBias: matrix[0],
|
||||
rgBias: matrix[1],
|
||||
rbBias: matrix[2],
|
||||
grBias: matrix[5],
|
||||
ggBias: matrix[6],
|
||||
gbBias: matrix[7],
|
||||
brBias: matrix[10],
|
||||
bgBias: matrix[11],
|
||||
bbBias: matrix[12],
|
||||
rOffset: matrix[4],
|
||||
gOffset: matrix[9],
|
||||
bOffset: matrix[14],
|
||||
);
|
||||
}
|
||||
|
||||
factory EditFilter.fromDtoParams(Map<String, dynamic> params, String name) {
|
||||
return EditFilter(
|
||||
name: name,
|
||||
rrBias: (params['rrBias'] as num).toDouble(),
|
||||
rgBias: (params['rgBias'] as num).toDouble(),
|
||||
rbBias: (params['rbBias'] as num).toDouble(),
|
||||
grBias: (params['grBias'] as num).toDouble(),
|
||||
ggBias: (params['ggBias'] as num).toDouble(),
|
||||
gbBias: (params['gbBias'] as num).toDouble(),
|
||||
brBias: (params['brBias'] as num).toDouble(),
|
||||
bgBias: (params['bgBias'] as num).toDouble(),
|
||||
bbBias: (params['bbBias'] as num).toDouble(),
|
||||
rOffset: (params['rOffset'] as num).toDouble(),
|
||||
gOffset: (params['gOffset'] as num).toDouble(),
|
||||
bOffset: (params['bOffset'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
ColorFilter get colorFilter {
|
||||
final colorMatrix = <double>[
|
||||
rrBias,
|
||||
rgBias,
|
||||
rbBias,
|
||||
0,
|
||||
rOffset,
|
||||
grBias,
|
||||
ggBias,
|
||||
gbBias,
|
||||
0,
|
||||
gOffset,
|
||||
brBias,
|
||||
bgBias,
|
||||
bbBias,
|
||||
0,
|
||||
bOffset,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
];
|
||||
|
||||
return ColorFilter.matrix(colorMatrix);
|
||||
}
|
||||
|
||||
Map<String, dynamic> get dtoParameters {
|
||||
return {
|
||||
"rrBias": rrBias,
|
||||
"rgBias": rgBias,
|
||||
"rbBias": rbBias,
|
||||
"grBias": grBias,
|
||||
"ggBias": ggBias,
|
||||
"gbBias": gbBias,
|
||||
"brBias": brBias,
|
||||
"bgBias": bgBias,
|
||||
"bbBias": bbBias,
|
||||
"rOffset": rOffset,
|
||||
"gOffset": gOffset,
|
||||
"bOffset": bOffset,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! EditFilter) return false;
|
||||
|
||||
return rrBias == other.rrBias &&
|
||||
rgBias == other.rgBias &&
|
||||
rbBias == other.rbBias &&
|
||||
grBias == other.grBias &&
|
||||
ggBias == other.ggBias &&
|
||||
gbBias == other.gbBias &&
|
||||
brBias == other.brBias &&
|
||||
bgBias == other.bgBias &&
|
||||
bbBias == other.bbBias &&
|
||||
rOffset == other.rOffset &&
|
||||
gOffset == other.gOffset &&
|
||||
bOffset == other.bOffset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
name.hashCode ^
|
||||
rrBias.hashCode ^
|
||||
rgBias.hashCode ^
|
||||
rbBias.hashCode ^
|
||||
grBias.hashCode ^
|
||||
ggBias.hashCode ^
|
||||
gbBias.hashCode ^
|
||||
brBias.hashCode ^
|
||||
bgBias.hashCode ^
|
||||
bbBias.hashCode ^
|
||||
rOffset.hashCode ^
|
||||
gOffset.hashCode ^
|
||||
bOffset.hashCode;
|
||||
}
|
||||
|
||||
final List<EditFilter> filters = [
|
||||
//Original
|
||||
ColorFilter.matrix([1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0], "Original"),
|
||||
//Vintage
|
||||
ColorFilter.matrix([0.8, 0.1, 0.1, 0, 20, 0.1, 0.8, 0.1, 0, 20, 0.1, 0.1, 0.8, 0, 20, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([0.8, 0.1, 0.1, 0, 20, 0.1, 0.8, 0.1, 0, 20, 0.1, 0.1, 0.8, 0, 20, 0, 0, 0, 1, 0], "Vintage"),
|
||||
//Mood
|
||||
ColorFilter.matrix([1.2, 0.1, 0.1, 0, 10, 0.1, 1, 0.1, 0, 10, 0.1, 0.1, 1, 0, 10, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.2, 0.1, 0.1, 0, 10, 0.1, 1, 0.1, 0, 10, 0.1, 0.1, 1, 0, 10, 0, 0, 0, 1, 0], "Mood"),
|
||||
//Crisp
|
||||
ColorFilter.matrix([1.2, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.2, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1, 0], "Crisp"),
|
||||
//Cool
|
||||
ColorFilter.matrix([0.9, 0, 0.2, 0, 0, 0, 1, 0.1, 0, 0, 0.1, 0, 1.2, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([0.9, 0, 0.2, 0, 0, 0, 1, 0.1, 0, 0, 0.1, 0, 1.2, 0, 0, 0, 0, 0, 1, 0], "Cool"),
|
||||
//Blush
|
||||
ColorFilter.matrix([1.1, 0.1, 0.1, 0, 10, 0.1, 1, 0.1, 0, 10, 0.1, 0.1, 1, 0, 5, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.1, 0.1, 0.1, 0, 10, 0.1, 1, 0.1, 0, 10, 0.1, 0.1, 1, 0, 5, 0, 0, 0, 1, 0], "Blush"),
|
||||
//Sunkissed
|
||||
ColorFilter.matrix([1.3, 0, 0.1, 0, 15, 0, 1.1, 0.1, 0, 10, 0, 0, 0.9, 0, 5, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.3, 0, 0.1, 0, 15, 0, 1.1, 0.1, 0, 10, 0, 0, 0.9, 0, 5, 0, 0, 0, 1, 0], "Sunkissed"),
|
||||
//Fresh
|
||||
ColorFilter.matrix([1.2, 0, 0, 0, 20, 0, 1.2, 0, 0, 20, 0, 0, 1.1, 0, 20, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.2, 0, 0, 0, 20, 0, 1.2, 0, 0, 20, 0, 0, 1.1, 0, 20, 0, 0, 0, 1, 0], "Fresh"),
|
||||
//Classic
|
||||
ColorFilter.matrix([1.1, 0, -0.1, 0, 10, -0.1, 1.1, 0.1, 0, 5, 0, -0.1, 1.1, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.1, 0, -0.1, 0, 10, -0.1, 1.1, 0.1, 0, 5, 0, -0.1, 1.1, 0, 0, 0, 0, 0, 1, 0], "Classic"),
|
||||
//Lomo-ish
|
||||
ColorFilter.matrix([1.5, 0, 0.1, 0, 0, 0, 1.45, 0, 0, 0, 0.1, 0, 1.3, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.5, 0, 0.1, 0, 0, 0, 1.45, 0, 0, 0, 0.1, 0, 1.3, 0, 0, 0, 0, 0, 1, 0], "Lomo-ish"),
|
||||
//Nashville
|
||||
ColorFilter.matrix([1.2, 0.15, -0.15, 0, 15, 0.1, 1.1, 0.1, 0, 10, -0.05, 0.2, 1.25, 0, 5, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([
|
||||
1.2,
|
||||
0.15,
|
||||
-0.15,
|
||||
0,
|
||||
15,
|
||||
0.1,
|
||||
1.1,
|
||||
0.1,
|
||||
0,
|
||||
10,
|
||||
-0.05,
|
||||
0.2,
|
||||
1.25,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
], "Nashville"),
|
||||
//Valencia
|
||||
ColorFilter.matrix([1.15, 0.1, 0.1, 0, 20, 0.1, 1.1, 0, 0, 10, 0.1, 0.1, 1.2, 0, 5, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.15, 0.1, 0.1, 0, 20, 0.1, 1.1, 0, 0, 10, 0.1, 0.1, 1.2, 0, 5, 0, 0, 0, 1, 0], "Valencia"),
|
||||
//Clarendon
|
||||
ColorFilter.matrix([1.2, 0, 0, 0, 10, 0, 1.25, 0, 0, 10, 0, 0, 1.3, 0, 10, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.2, 0, 0, 0, 10, 0, 1.25, 0, 0, 10, 0, 0, 1.3, 0, 10, 0, 0, 0, 1, 0], "Clarendon"),
|
||||
//Moon
|
||||
ColorFilter.matrix([0.33, 0.33, 0.33, 0, 0, 0.33, 0.33, 0.33, 0, 0, 0.33, 0.33, 0.33, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([
|
||||
0.33,
|
||||
0.33,
|
||||
0.33,
|
||||
0,
|
||||
0,
|
||||
0.33,
|
||||
0.33,
|
||||
0.33,
|
||||
0,
|
||||
0,
|
||||
0.33,
|
||||
0.33,
|
||||
0.33,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
], "Moon"),
|
||||
//Willow
|
||||
ColorFilter.matrix([0.5, 0.5, 0.5, 0, 20, 0.5, 0.5, 0.5, 0, 20, 0.5, 0.5, 0.5, 0, 20, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([0.5, 0.5, 0.5, 0, 20, 0.5, 0.5, 0.5, 0, 20, 0.5, 0.5, 0.5, 0, 20, 0, 0, 0, 1, 0], "Willow"),
|
||||
//Kodak
|
||||
ColorFilter.matrix([1.3, 0.1, -0.1, 0, 10, 0, 1.25, 0.1, 0, 10, 0, -0.1, 1.1, 0, 5, 0, 0, 0, 1, 0]),
|
||||
//Frost
|
||||
ColorFilter.matrix([0.8, 0.2, 0.1, 0, 0, 0.2, 1.1, 0.1, 0, 0, 0.1, 0.1, 1.2, 0, 10, 0, 0, 0, 1, 0]),
|
||||
//Night Vision
|
||||
ColorFilter.matrix([0.1, 0.95, 0.2, 0, 0, 0.1, 1.5, 0.1, 0, 0, 0.2, 0.7, 0, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.3, 0.1, -0.1, 0, 10, 0, 1.25, 0.1, 0, 10, 0, -0.1, 1.1, 0, 5, 0, 0, 0, 1, 0], "Kodak"),
|
||||
//Sunset
|
||||
ColorFilter.matrix([1.5, 0.2, 0, 0, 0, 0.1, 0.9, 0.1, 0, 0, -0.1, -0.2, 1.3, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.5, 0.2, 0, 0, 0, 0.1, 0.9, 0.1, 0, 0, -0.1, -0.2, 1.3, 0, 0, 0, 0, 0, 1, 0], "Sunset"),
|
||||
//Noir
|
||||
ColorFilter.matrix([1.3, -0.3, 0.1, 0, 0, -0.1, 1.2, -0.1, 0, 0, 0.1, -0.2, 1.3, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.3, -0.3, 0.1, 0, 0, -0.1, 1.2, -0.1, 0, 0, 0.1, -0.2, 1.3, 0, 0, 0, 0, 0, 1, 0], "Noir"),
|
||||
//Dreamy
|
||||
ColorFilter.matrix([1.1, 0.1, 0.1, 0, 0, 0.1, 1.1, 0.1, 0, 0, 0.1, 0.1, 1.1, 0, 15, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.1, 0.1, 0.1, 0, 0, 0.1, 1.1, 0.1, 0, 0, 0.1, 0.1, 1.1, 0, 15, 0, 0, 0, 1, 0], "Dreamy"),
|
||||
//Sepia
|
||||
ColorFilter.matrix([0.393, 0.769, 0.189, 0, 0, 0.349, 0.686, 0.168, 0, 0, 0.272, 0.534, 0.131, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([
|
||||
0.393,
|
||||
0.769,
|
||||
0.189,
|
||||
0,
|
||||
0,
|
||||
0.349,
|
||||
0.686,
|
||||
0.168,
|
||||
0,
|
||||
0,
|
||||
0.272,
|
||||
0.534,
|
||||
0.131,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
], "Sepia"),
|
||||
//Radium
|
||||
ColorFilter.matrix([
|
||||
EditFilter.fromMatrix([
|
||||
1.438,
|
||||
-0.062,
|
||||
-0.062,
|
||||
@@ -67,9 +291,9 @@ const List<ColorFilter> filters = [
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]),
|
||||
], "Radium"),
|
||||
//Aqua
|
||||
ColorFilter.matrix([
|
||||
EditFilter.fromMatrix([
|
||||
0.2126,
|
||||
0.7152,
|
||||
0.0722,
|
||||
@@ -90,59 +314,23 @@ const List<ColorFilter> filters = [
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]),
|
||||
], "Aqua"),
|
||||
//Purple Haze
|
||||
ColorFilter.matrix([1.3, 0, 1.2, 0, 0, 0, 1.1, 0, 0, 0, 0.2, 0, 1.3, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.3, 0, 1.2, 0, 0, 0, 1.1, 0, 0, 0, 0.2, 0, 1.3, 0, 0, 0, 0, 0, 1, 0], "Purple Haze"),
|
||||
//Lemonade
|
||||
ColorFilter.matrix([1.2, 0.1, 0, 0, 0, 0, 1.1, 0.2, 0, 0, 0.1, 0, 0.7, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.2, 0.1, 0, 0, 0, 0, 1.1, 0.2, 0, 0, 0.1, 0, 0.7, 0, 0, 0, 0, 0, 1, 0], "Lemonade"),
|
||||
//Caramel
|
||||
ColorFilter.matrix([1.6, 0.2, 0, 0, 0, 0.1, 1.3, 0.1, 0, 0, 0, 0.1, 0.9, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.6, 0.2, 0, 0, 0, 0.1, 1.3, 0.1, 0, 0, 0, 0.1, 0.9, 0, 0, 0, 0, 0, 1, 0], "Caramel"),
|
||||
//Peachy
|
||||
ColorFilter.matrix([1.3, 0.5, 0, 0, 0, 0.2, 1.1, 0.3, 0, 0, 0.1, 0.1, 1.2, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.3, 0.5, 0, 0, 0, 0.2, 1.1, 0.3, 0, 0, 0.1, 0.1, 1.2, 0, 0, 0, 0, 0, 1, 0], "Peachy"),
|
||||
//Neon
|
||||
ColorFilter.matrix([1, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 0], "Neon"),
|
||||
//Cold Morning
|
||||
ColorFilter.matrix([0.9, 0.1, 0.2, 0, 0, 0, 1, 0.1, 0, 0, 0.1, 0, 1.2, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([0.9, 0.1, 0.2, 0, 0, 0, 1, 0.1, 0, 0, 0.1, 0, 1.2, 0, 0, 0, 0, 0, 1, 0], "Cold Morning"),
|
||||
//Lush
|
||||
ColorFilter.matrix([0.9, 0.2, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([0.9, 0.2, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 0, 1, 0], "Lush"),
|
||||
//Urban Neon
|
||||
ColorFilter.matrix([1.1, 0, 0.3, 0, 0, 0, 0.9, 0.3, 0, 0, 0.3, 0.1, 1.2, 0, 0, 0, 0, 0, 1, 0]),
|
||||
EditFilter.fromMatrix([1.1, 0, 0.3, 0, 0, 0, 0.9, 0.3, 0, 0, 0.3, 0.1, 1.2, 0, 0, 0, 0, 0, 1, 0], "Urban Neon"),
|
||||
//Monochrome
|
||||
ColorFilter.matrix([0.6, 0.2, 0.2, 0, 0, 0.2, 0.6, 0.2, 0, 0, 0.2, 0.2, 0.7, 0, 0, 0, 0, 0, 1, 0]),
|
||||
];
|
||||
|
||||
const List<String> filterNames = [
|
||||
'Original',
|
||||
'Vintage',
|
||||
'Mood',
|
||||
'Crisp',
|
||||
'Cool',
|
||||
'Blush',
|
||||
'Sunkissed',
|
||||
'Fresh',
|
||||
'Classic',
|
||||
'Lomo-ish',
|
||||
'Nashville',
|
||||
'Valencia',
|
||||
'Clarendon',
|
||||
'Moon',
|
||||
'Willow',
|
||||
'Kodak',
|
||||
'Frost',
|
||||
'Night Vision',
|
||||
'Sunset',
|
||||
'Noir',
|
||||
'Dreamy',
|
||||
'Sepia',
|
||||
'Radium',
|
||||
'Aqua',
|
||||
'Purple Haze',
|
||||
'Lemonade',
|
||||
'Caramel',
|
||||
'Peachy',
|
||||
'Neon',
|
||||
'Cold Morning',
|
||||
'Lush',
|
||||
'Urban Neon',
|
||||
'Monochrome',
|
||||
EditFilter.fromMatrix([0.6, 0.2, 0.2, 0, 0, 0.2, 0.6, 0.2, 0, 0, 0.2, 0.2, 0.7, 0, 0, 0, 0, 0, 1, 0], "Monochrome"),
|
||||
];
|
||||
|
||||
@@ -56,6 +56,8 @@ sealed class BaseAsset {
|
||||
bool get isLocalOnly => storage == AssetState.local;
|
||||
bool get isRemoteOnly => storage == AssetState.remote;
|
||||
|
||||
bool get isEditable => isImage && !isMotionPhoto && this is RemoteAsset;
|
||||
|
||||
// Overridden in subclasses
|
||||
AssetState get storage;
|
||||
String? get localId;
|
||||
|
||||
22
mobile/lib/domain/models/asset_edit.model.dart
Normal file
22
mobile/lib/domain/models/asset_edit.model.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import "package:openapi/api.dart" as api show AssetEditAction;
|
||||
|
||||
enum AssetEditAction { rotate, crop, mirror, filter, other }
|
||||
|
||||
extension AssetEditActionExtension on AssetEditAction {
|
||||
api.AssetEditAction? toDto() {
|
||||
return switch (this) {
|
||||
AssetEditAction.rotate => api.AssetEditAction.rotate,
|
||||
AssetEditAction.crop => api.AssetEditAction.crop,
|
||||
AssetEditAction.mirror => api.AssetEditAction.mirror,
|
||||
AssetEditAction.filter => api.AssetEditAction.filter,
|
||||
AssetEditAction.other => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class AssetEdit {
|
||||
final AssetEditAction action;
|
||||
final Map<String, dynamic> parameters;
|
||||
|
||||
const AssetEdit({required this.action, required this.parameters});
|
||||
}
|
||||
@@ -7,6 +7,8 @@ class ExifInfo {
|
||||
final String? timeZone;
|
||||
final DateTime? dateTimeOriginal;
|
||||
final int? rating;
|
||||
final int? width;
|
||||
final int? height;
|
||||
|
||||
// GPS
|
||||
final double? latitude;
|
||||
@@ -48,6 +50,8 @@ class ExifInfo {
|
||||
this.timeZone,
|
||||
this.dateTimeOriginal,
|
||||
this.rating,
|
||||
this.width,
|
||||
this.height,
|
||||
this.isFlipped = false,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
@@ -74,6 +78,8 @@ class ExifInfo {
|
||||
other.timeZone == timeZone &&
|
||||
other.dateTimeOriginal == dateTimeOriginal &&
|
||||
other.rating == rating &&
|
||||
other.width == width &&
|
||||
other.height == height &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude &&
|
||||
other.city == city &&
|
||||
@@ -98,6 +104,8 @@ class ExifInfo {
|
||||
timeZone.hashCode ^
|
||||
dateTimeOriginal.hashCode ^
|
||||
rating.hashCode ^
|
||||
width.hashCode ^
|
||||
height.hashCode ^
|
||||
latitude.hashCode ^
|
||||
longitude.hashCode ^
|
||||
city.hashCode ^
|
||||
@@ -123,6 +131,8 @@ isFlipped: $isFlipped,
|
||||
timeZone: ${timeZone ?? 'NA'},
|
||||
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
|
||||
rating: ${rating ?? 'NA'},
|
||||
width: ${width ?? 'NA'},
|
||||
height: ${height ?? 'NA'},
|
||||
latitude: ${latitude ?? 'NA'},
|
||||
longitude: ${longitude ?? 'NA'},
|
||||
city: ${city ?? 'NA'},
|
||||
@@ -146,6 +156,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
|
||||
String? timeZone,
|
||||
DateTime? dateTimeOriginal,
|
||||
int? rating,
|
||||
int? width,
|
||||
int? height,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? city,
|
||||
@@ -168,6 +180,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
|
||||
timeZone: timeZone ?? this.timeZone,
|
||||
dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
|
||||
rating: rating ?? this.rating,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
isFlipped: isFlipped ?? this.isFlipped,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
|
||||
@@ -73,9 +73,6 @@ enum StoreKey<T> {
|
||||
autoPlayVideo<bool>._(139),
|
||||
albumGridView<bool>._(140),
|
||||
|
||||
// Image viewer navigation settings
|
||||
tapToNavigate<bool>._(141),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000),
|
||||
betaPromptShown<bool>._(1001),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
@@ -116,4 +117,12 @@ class AssetService {
|
||||
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
|
||||
return _localAssetRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
|
||||
}
|
||||
|
||||
Future<List<AssetEdit>> getAssetEdits(String assetId) {
|
||||
return _remoteAssetRepository.getAssetEdits(assetId);
|
||||
}
|
||||
|
||||
Future<void> editAsset(String assetId, List<AssetEdit> edits) {
|
||||
return _remoteAssetRepository.editAsset(assetId, edits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ class RemoteAlbumService {
|
||||
AlbumSortMode.title => albums.sortedBy((album) => album.name),
|
||||
AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
||||
AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
||||
AlbumSortMode.mostRecent => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.end),
|
||||
AlbumSortMode.mostOldest => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.start),
|
||||
AlbumSortMode.mostRecent => await _sortByNewestAsset(albums),
|
||||
AlbumSortMode.mostOldest => await _sortByOldestAsset(albums),
|
||||
};
|
||||
final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder;
|
||||
|
||||
@@ -172,25 +172,46 @@ class RemoteAlbumService {
|
||||
return _repository.getAlbumsContainingAsset(assetId);
|
||||
}
|
||||
|
||||
Future<List<RemoteAlbum>> _sortByAssetDate(
|
||||
List<RemoteAlbum> albums, {
|
||||
required AssetDateAggregation aggregation,
|
||||
}) async {
|
||||
if (albums.isEmpty) return [];
|
||||
|
||||
final albumIds = albums.map((e) => e.id).toList();
|
||||
final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation);
|
||||
|
||||
final albumMap = Map<String, RemoteAlbum>.fromEntries(albums.map((a) => MapEntry(a.id, a)));
|
||||
|
||||
final sortedAlbums = sortedIds.map((id) => albumMap[id]).whereType<RemoteAlbum>().toList();
|
||||
|
||||
if (sortedAlbums.length < albums.length) {
|
||||
final returnedIdSet = sortedIds.toSet();
|
||||
final emptyAlbums = albums.where((a) => !returnedIdSet.contains(a.id));
|
||||
sortedAlbums.addAll(emptyAlbums);
|
||||
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
|
||||
// map album IDs to their newest asset dates
|
||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {};
|
||||
for (final album in albums) {
|
||||
assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(album.id);
|
||||
}
|
||||
|
||||
return sortedAlbums;
|
||||
// await all database queries
|
||||
final entries = await Future.wait(
|
||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
||||
);
|
||||
final assetTimestamps = Map.fromEntries(entries);
|
||||
|
||||
final sorted = albums.sorted((a, b) {
|
||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
return aDate.compareTo(bDate);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
Future<List<RemoteAlbum>> _sortByOldestAsset(List<RemoteAlbum> albums) async {
|
||||
// map album IDs to their oldest asset dates
|
||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {
|
||||
for (final album in albums) album.id: _repository.getOldestAssetTimestamp(album.id),
|
||||
};
|
||||
|
||||
// await all database queries
|
||||
final entries = await Future.wait(
|
||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
||||
);
|
||||
final assetTimestamps = Map.fromEntries(entries);
|
||||
|
||||
final sorted = albums.sorted((a, b) {
|
||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
return aDate.compareTo(bDate);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +68,12 @@ class SyncStreamService {
|
||||
return false;
|
||||
}
|
||||
|
||||
final serverSemVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
|
||||
final semVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
|
||||
|
||||
final value = Store.get(StoreKey.syncMigrationStatus, "[]");
|
||||
final migrations = (jsonDecode(value) as List).cast<String>();
|
||||
int previousLength = migrations.length;
|
||||
await _runPreSyncTasks(migrations, serverSemVer);
|
||||
await _runPreSyncTasks(migrations, semVer);
|
||||
|
||||
if (migrations.length != previousLength) {
|
||||
_logger.info("Updated pre-sync migration status: $migrations");
|
||||
@@ -82,14 +82,10 @@ class SyncStreamService {
|
||||
|
||||
// Start the sync stream and handle events
|
||||
bool shouldReset = false;
|
||||
await _syncApiRepository.streamChanges(
|
||||
_handleEvents,
|
||||
serverVersion: serverSemVer,
|
||||
onReset: () => shouldReset = true,
|
||||
);
|
||||
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
|
||||
if (shouldReset) {
|
||||
_logger.info("Resetting sync state as requested by server");
|
||||
await _syncApiRepository.streamChanges(_handleEvents, serverVersion: serverSemVer);
|
||||
await _syncApiRepository.streamChanges(_handleEvents);
|
||||
}
|
||||
|
||||
previousLength = migrations.length;
|
||||
@@ -205,6 +201,10 @@ class SyncStreamService {
|
||||
return _syncStreamRepository.deleteAssetsV1(data.cast());
|
||||
case SyncEntityType.assetExifV1:
|
||||
return _syncStreamRepository.updateAssetsExifV1(data.cast());
|
||||
case SyncEntityType.assetEditV1:
|
||||
return _syncStreamRepository.updateAssetEditsV1(data.cast());
|
||||
case SyncEntityType.assetEditDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetEditsV1(data.cast());
|
||||
case SyncEntityType.assetMetadataV1:
|
||||
return _syncStreamRepository.updateAssetsMetadataV1(data.cast());
|
||||
case SyncEntityType.assetMetadataDeleteV1:
|
||||
@@ -286,8 +286,6 @@ class SyncStreamService {
|
||||
return _syncStreamRepository.deletePeopleV1(data.cast());
|
||||
case SyncEntityType.assetFaceV1:
|
||||
return _syncStreamRepository.updateAssetFacesV1(data.cast());
|
||||
case SyncEntityType.assetFaceV2:
|
||||
return _syncStreamRepository.updateAssetFacesV2(data.cast());
|
||||
case SyncEntityType.assetFaceDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetFacesV1(data.cast());
|
||||
default:
|
||||
@@ -342,6 +340,7 @@ class SyncStreamService {
|
||||
_logger.info('Processing batch of ${batchData.length} AssetEditReadyV1 events');
|
||||
|
||||
final List<SyncAssetV1> assets = [];
|
||||
final List<SyncAssetEditV1> assetEdits = [];
|
||||
|
||||
try {
|
||||
for (final data in batchData) {
|
||||
@@ -351,6 +350,7 @@ class SyncStreamService {
|
||||
|
||||
final payload = data;
|
||||
final assetData = payload['asset'];
|
||||
final editData = payload['edit'];
|
||||
|
||||
if (assetData == null) {
|
||||
continue;
|
||||
@@ -360,11 +360,28 @@ class SyncStreamService {
|
||||
|
||||
if (asset != null) {
|
||||
assets.add(asset);
|
||||
|
||||
// Edits are only send on v2.6.0+
|
||||
if (editData != null) {
|
||||
final edits = (editData as List<dynamic>)
|
||||
.map((e) => SyncAssetEditV1.fromJson(e))
|
||||
.whereType<SyncAssetEditV1>()
|
||||
.toList();
|
||||
|
||||
assetEdits.addAll(edits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (assets.isNotEmpty) {
|
||||
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-edit');
|
||||
|
||||
// edits that are sent replace previous edits, so we delete existing ones first
|
||||
await _syncStreamRepository.deleteAssetEditsV1(
|
||||
assets.map((asset) => SyncAssetEditDeleteV1(assetId: asset.id)).toList(),
|
||||
debugLabel: 'websocket-edit',
|
||||
);
|
||||
await _syncStreamRepository.updateAssetEditsV1(assetEdits, debugLabel: 'websocket-edit');
|
||||
_logger.info('Successfully processed ${assets.length} edited assets');
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
|
||||
33
mobile/lib/infrastructure/entities/asset_edit.entity.dart
Normal file
33
mobile/lib/infrastructure/entities/asset_edit.entity.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)')
|
||||
class AssetEditEntity extends Table with DriftDefaultsMixin {
|
||||
const AssetEditEntity();
|
||||
|
||||
TextColumn get id => text()();
|
||||
|
||||
TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get action => intEnum<AssetEditAction>()();
|
||||
|
||||
BlobColumn get parameters => blob().map(editParameterConverter)();
|
||||
|
||||
IntColumn get sequence => integer()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
final JsonTypeConverter2<Map<String, Object?>, Uint8List, Object?> editParameterConverter = TypeConverter.jsonb(
|
||||
fromJson: (json) => json as Map<String, Object?>,
|
||||
);
|
||||
|
||||
extension AssetEditEntityDataDomainEx on AssetEditEntityData {
|
||||
AssetEdit toDto() {
|
||||
return AssetEdit(action: action, parameters: parameters);
|
||||
}
|
||||
}
|
||||
752
mobile/lib/infrastructure/entities/asset_edit.entity.drift.dart
generated
Normal file
752
mobile/lib/infrastructure/entities/asset_edit.entity.drift.dart
generated
Normal file
@@ -0,0 +1,752 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart' as i2;
|
||||
import 'dart:typed_data' as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:drift/internal/modular.dart' as i6;
|
||||
|
||||
typedef $$AssetEditEntityTableCreateCompanionBuilder =
|
||||
i1.AssetEditEntityCompanion Function({
|
||||
required String id,
|
||||
required String assetId,
|
||||
required i2.AssetEditAction action,
|
||||
required Map<String, Object?> parameters,
|
||||
required int sequence,
|
||||
});
|
||||
typedef $$AssetEditEntityTableUpdateCompanionBuilder =
|
||||
i1.AssetEditEntityCompanion Function({
|
||||
i0.Value<String> id,
|
||||
i0.Value<String> assetId,
|
||||
i0.Value<i2.AssetEditAction> action,
|
||||
i0.Value<Map<String, Object?>> parameters,
|
||||
i0.Value<int> sequence,
|
||||
});
|
||||
|
||||
final class $$AssetEditEntityTableReferences
|
||||
extends
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AssetEditEntityTable,
|
||||
i1.AssetEditEntityData
|
||||
> {
|
||||
$$AssetEditEntityTableReferences(
|
||||
super.$_db,
|
||||
super.$_table,
|
||||
super.$_typedResult,
|
||||
);
|
||||
|
||||
static i5.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||
.createAlias(
|
||||
i0.$_aliasNameGenerator(
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$AssetEditEntityTable>('asset_edit_entity')
|
||||
.assetId,
|
||||
i6.ReadDatabaseContainer(
|
||||
db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
||||
),
|
||||
);
|
||||
|
||||
i5.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
||||
final $_column = $_itemColumn<String>('asset_id')!;
|
||||
|
||||
final manager = i5
|
||||
.$$RemoteAssetEntityTableTableManager(
|
||||
$_db,
|
||||
i6.ReadDatabaseContainer(
|
||||
$_db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
)
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return i0.ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class $$AssetEditEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AssetEditEntityTable> {
|
||||
$$AssetEditEntityTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnFilters<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnWithTypeConverterFilters<i2.AssetEditAction, i2.AssetEditAction, int>
|
||||
get action => $composableBuilder(
|
||||
column: $table.action,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnWithTypeConverterFilters<
|
||||
Map<String, Object?>,
|
||||
Map<String, Object>,
|
||||
i3.Uint8List
|
||||
>
|
||||
get parameters => $composableBuilder(
|
||||
column: $table.parameters,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<int> get sequence => $composableBuilder(
|
||||
column: $table.sequence,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i5.$$RemoteAssetEntityTableFilterComposer get assetId {
|
||||
final i5.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$RemoteAssetEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$AssetEditEntityTableOrderingComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AssetEditEntityTable> {
|
||||
$$AssetEditEntityTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnOrderings<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<int> get action => $composableBuilder(
|
||||
column: $table.action,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<i3.Uint8List> get parameters => $composableBuilder(
|
||||
column: $table.parameters,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<int> get sequence => $composableBuilder(
|
||||
column: $table.sequence,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i5.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
||||
final i5.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$RemoteAssetEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$AssetEditEntityTableAnnotationComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AssetEditEntityTable> {
|
||||
$$AssetEditEntityTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.GeneratedColumn<String> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumnWithTypeConverter<i2.AssetEditAction, int> get action =>
|
||||
$composableBuilder(column: $table.action, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List>
|
||||
get parameters => $composableBuilder(
|
||||
column: $table.parameters,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumn<int> get sequence =>
|
||||
$composableBuilder(column: $table.sequence, builder: (column) => column);
|
||||
|
||||
i5.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
||||
final i5.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$RemoteAssetEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$AssetEditEntityTableTableManager
|
||||
extends
|
||||
i0.RootTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AssetEditEntityTable,
|
||||
i1.AssetEditEntityData,
|
||||
i1.$$AssetEditEntityTableFilterComposer,
|
||||
i1.$$AssetEditEntityTableOrderingComposer,
|
||||
i1.$$AssetEditEntityTableAnnotationComposer,
|
||||
$$AssetEditEntityTableCreateCompanionBuilder,
|
||||
$$AssetEditEntityTableUpdateCompanionBuilder,
|
||||
(i1.AssetEditEntityData, i1.$$AssetEditEntityTableReferences),
|
||||
i1.AssetEditEntityData,
|
||||
i0.PrefetchHooks Function({bool assetId})
|
||||
> {
|
||||
$$AssetEditEntityTableTableManager(
|
||||
i0.GeneratedDatabase db,
|
||||
i1.$AssetEditEntityTable table,
|
||||
) : super(
|
||||
i0.TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
i1.$$AssetEditEntityTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
i1.$$AssetEditEntityTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () => i1
|
||||
.$$AssetEditEntityTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
i0.Value<String> id = const i0.Value.absent(),
|
||||
i0.Value<String> assetId = const i0.Value.absent(),
|
||||
i0.Value<i2.AssetEditAction> action = const i0.Value.absent(),
|
||||
i0.Value<Map<String, Object?>> parameters =
|
||||
const i0.Value.absent(),
|
||||
i0.Value<int> sequence = const i0.Value.absent(),
|
||||
}) => i1.AssetEditEntityCompanion(
|
||||
id: id,
|
||||
assetId: assetId,
|
||||
action: action,
|
||||
parameters: parameters,
|
||||
sequence: sequence,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String id,
|
||||
required String assetId,
|
||||
required i2.AssetEditAction action,
|
||||
required Map<String, Object?> parameters,
|
||||
required int sequence,
|
||||
}) => i1.AssetEditEntityCompanion.insert(
|
||||
id: id,
|
||||
assetId: assetId,
|
||||
action: action,
|
||||
parameters: parameters,
|
||||
sequence: sequence,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map(
|
||||
(e) => (
|
||||
e.readTable(table),
|
||||
i1.$$AssetEditEntityTableReferences(db, table, e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({assetId = false}) {
|
||||
return i0.PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
addJoins:
|
||||
<
|
||||
T extends i0.TableManagerState<
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic
|
||||
>
|
||||
>(state) {
|
||||
if (assetId) {
|
||||
state =
|
||||
state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.assetId,
|
||||
referencedTable: i1
|
||||
.$$AssetEditEntityTableReferences
|
||||
._assetIdTable(db),
|
||||
referencedColumn: i1
|
||||
.$$AssetEditEntityTableReferences
|
||||
._assetIdTable(db)
|
||||
.id,
|
||||
)
|
||||
as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [];
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$AssetEditEntityTableProcessedTableManager =
|
||||
i0.ProcessedTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AssetEditEntityTable,
|
||||
i1.AssetEditEntityData,
|
||||
i1.$$AssetEditEntityTableFilterComposer,
|
||||
i1.$$AssetEditEntityTableOrderingComposer,
|
||||
i1.$$AssetEditEntityTableAnnotationComposer,
|
||||
$$AssetEditEntityTableCreateCompanionBuilder,
|
||||
$$AssetEditEntityTableUpdateCompanionBuilder,
|
||||
(i1.AssetEditEntityData, i1.$$AssetEditEntityTableReferences),
|
||||
i1.AssetEditEntityData,
|
||||
i0.PrefetchHooks Function({bool assetId})
|
||||
>;
|
||||
i0.Index get idxAssetEditAssetId => i0.Index(
|
||||
'idx_asset_edit_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
||||
);
|
||||
|
||||
class $AssetEditEntityTable extends i4.AssetEditEntity
|
||||
with i0.TableInfo<$AssetEditEntityTable, i1.AssetEditEntityData> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$AssetEditEntityTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _assetIdMeta = const i0.VerificationMeta(
|
||||
'assetId',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
|
||||
'asset_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE',
|
||||
),
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumnWithTypeConverter<i2.AssetEditAction, int>
|
||||
action =
|
||||
i0.GeneratedColumn<int>(
|
||||
'action',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
).withConverter<i2.AssetEditAction>(
|
||||
i1.$AssetEditEntityTable.$converteraction,
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumnWithTypeConverter<
|
||||
Map<String, Object?>,
|
||||
i3.Uint8List
|
||||
>
|
||||
parameters =
|
||||
i0.GeneratedColumn<i3.Uint8List>(
|
||||
'parameters',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.blob,
|
||||
requiredDuringInsert: true,
|
||||
).withConverter<Map<String, Object?>>(
|
||||
i1.$AssetEditEntityTable.$converterparameters,
|
||||
);
|
||||
static const i0.VerificationMeta _sequenceMeta = const i0.VerificationMeta(
|
||||
'sequence',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> sequence = i0.GeneratedColumn<int>(
|
||||
'sequence',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
id,
|
||||
assetId,
|
||||
action,
|
||||
parameters,
|
||||
sequence,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'asset_edit_entity';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(
|
||||
i0.Insertable<i1.AssetEditEntityData> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
if (data.containsKey('asset_id')) {
|
||||
context.handle(
|
||||
_assetIdMeta,
|
||||
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_assetIdMeta);
|
||||
}
|
||||
if (data.containsKey('sequence')) {
|
||||
context.handle(
|
||||
_sequenceMeta,
|
||||
sequence.isAcceptableOrUnknown(data['sequence']!, _sequenceMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_sequenceMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
i1.AssetEditEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.AssetEditEntityData(
|
||||
id: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}id'],
|
||||
)!,
|
||||
assetId: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}asset_id'],
|
||||
)!,
|
||||
action: i1.$AssetEditEntityTable.$converteraction.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}action'],
|
||||
)!,
|
||||
),
|
||||
parameters: i1.$AssetEditEntityTable.$converterparameters.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.blob,
|
||||
data['${effectivePrefix}parameters'],
|
||||
)!,
|
||||
),
|
||||
sequence: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}sequence'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$AssetEditEntityTable createAlias(String alias) {
|
||||
return $AssetEditEntityTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static i0.JsonTypeConverter2<i2.AssetEditAction, int, int> $converteraction =
|
||||
const i0.EnumIndexConverter<i2.AssetEditAction>(
|
||||
i2.AssetEditAction.values,
|
||||
);
|
||||
static i0.JsonTypeConverter2<Map<String, Object?>, i3.Uint8List, Object?>
|
||||
$converterparameters = i4.editParameterConverter;
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
}
|
||||
|
||||
class AssetEditEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.AssetEditEntityData> {
|
||||
final String id;
|
||||
final String assetId;
|
||||
final i2.AssetEditAction action;
|
||||
final Map<String, Object?> parameters;
|
||||
final int sequence;
|
||||
const AssetEditEntityData({
|
||||
required this.id,
|
||||
required this.assetId,
|
||||
required this.action,
|
||||
required this.parameters,
|
||||
required this.sequence,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<String>(id);
|
||||
map['asset_id'] = i0.Variable<String>(assetId);
|
||||
{
|
||||
map['action'] = i0.Variable<int>(
|
||||
i1.$AssetEditEntityTable.$converteraction.toSql(action),
|
||||
);
|
||||
}
|
||||
{
|
||||
map['parameters'] = i0.Variable<i3.Uint8List>(
|
||||
i1.$AssetEditEntityTable.$converterparameters.toSql(parameters),
|
||||
);
|
||||
}
|
||||
map['sequence'] = i0.Variable<int>(sequence);
|
||||
return map;
|
||||
}
|
||||
|
||||
factory AssetEditEntityData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
i0.ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return AssetEditEntityData(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
assetId: serializer.fromJson<String>(json['assetId']),
|
||||
action: i1.$AssetEditEntityTable.$converteraction.fromJson(
|
||||
serializer.fromJson<int>(json['action']),
|
||||
),
|
||||
parameters: i1.$AssetEditEntityTable.$converterparameters.fromJson(
|
||||
serializer.fromJson<Object?>(json['parameters']),
|
||||
),
|
||||
sequence: serializer.fromJson<int>(json['sequence']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'assetId': serializer.toJson<String>(assetId),
|
||||
'action': serializer.toJson<int>(
|
||||
i1.$AssetEditEntityTable.$converteraction.toJson(action),
|
||||
),
|
||||
'parameters': serializer.toJson<Object?>(
|
||||
i1.$AssetEditEntityTable.$converterparameters.toJson(parameters),
|
||||
),
|
||||
'sequence': serializer.toJson<int>(sequence),
|
||||
};
|
||||
}
|
||||
|
||||
i1.AssetEditEntityData copyWith({
|
||||
String? id,
|
||||
String? assetId,
|
||||
i2.AssetEditAction? action,
|
||||
Map<String, Object?>? parameters,
|
||||
int? sequence,
|
||||
}) => i1.AssetEditEntityData(
|
||||
id: id ?? this.id,
|
||||
assetId: assetId ?? this.assetId,
|
||||
action: action ?? this.action,
|
||||
parameters: parameters ?? this.parameters,
|
||||
sequence: sequence ?? this.sequence,
|
||||
);
|
||||
AssetEditEntityData copyWithCompanion(i1.AssetEditEntityCompanion data) {
|
||||
return AssetEditEntityData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
assetId: data.assetId.present ? data.assetId.value : this.assetId,
|
||||
action: data.action.present ? data.action.value : this.action,
|
||||
parameters: data.parameters.present
|
||||
? data.parameters.value
|
||||
: this.parameters,
|
||||
sequence: data.sequence.present ? data.sequence.value : this.sequence,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('AssetEditEntityData(')
|
||||
..write('id: $id, ')
|
||||
..write('assetId: $assetId, ')
|
||||
..write('action: $action, ')
|
||||
..write('parameters: $parameters, ')
|
||||
..write('sequence: $sequence')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, assetId, action, parameters, sequence);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.AssetEditEntityData &&
|
||||
other.id == this.id &&
|
||||
other.assetId == this.assetId &&
|
||||
other.action == this.action &&
|
||||
other.parameters == this.parameters &&
|
||||
other.sequence == this.sequence);
|
||||
}
|
||||
|
||||
class AssetEditEntityCompanion
|
||||
extends i0.UpdateCompanion<i1.AssetEditEntityData> {
|
||||
final i0.Value<String> id;
|
||||
final i0.Value<String> assetId;
|
||||
final i0.Value<i2.AssetEditAction> action;
|
||||
final i0.Value<Map<String, Object?>> parameters;
|
||||
final i0.Value<int> sequence;
|
||||
const AssetEditEntityCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.assetId = const i0.Value.absent(),
|
||||
this.action = const i0.Value.absent(),
|
||||
this.parameters = const i0.Value.absent(),
|
||||
this.sequence = const i0.Value.absent(),
|
||||
});
|
||||
AssetEditEntityCompanion.insert({
|
||||
required String id,
|
||||
required String assetId,
|
||||
required i2.AssetEditAction action,
|
||||
required Map<String, Object?> parameters,
|
||||
required int sequence,
|
||||
}) : id = i0.Value(id),
|
||||
assetId = i0.Value(assetId),
|
||||
action = i0.Value(action),
|
||||
parameters = i0.Value(parameters),
|
||||
sequence = i0.Value(sequence);
|
||||
static i0.Insertable<i1.AssetEditEntityData> custom({
|
||||
i0.Expression<String>? id,
|
||||
i0.Expression<String>? assetId,
|
||||
i0.Expression<int>? action,
|
||||
i0.Expression<i3.Uint8List>? parameters,
|
||||
i0.Expression<int>? sequence,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (assetId != null) 'asset_id': assetId,
|
||||
if (action != null) 'action': action,
|
||||
if (parameters != null) 'parameters': parameters,
|
||||
if (sequence != null) 'sequence': sequence,
|
||||
});
|
||||
}
|
||||
|
||||
i1.AssetEditEntityCompanion copyWith({
|
||||
i0.Value<String>? id,
|
||||
i0.Value<String>? assetId,
|
||||
i0.Value<i2.AssetEditAction>? action,
|
||||
i0.Value<Map<String, Object?>>? parameters,
|
||||
i0.Value<int>? sequence,
|
||||
}) {
|
||||
return i1.AssetEditEntityCompanion(
|
||||
id: id ?? this.id,
|
||||
assetId: assetId ?? this.assetId,
|
||||
action: action ?? this.action,
|
||||
parameters: parameters ?? this.parameters,
|
||||
sequence: sequence ?? this.sequence,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<String>(id.value);
|
||||
}
|
||||
if (assetId.present) {
|
||||
map['asset_id'] = i0.Variable<String>(assetId.value);
|
||||
}
|
||||
if (action.present) {
|
||||
map['action'] = i0.Variable<int>(
|
||||
i1.$AssetEditEntityTable.$converteraction.toSql(action.value),
|
||||
);
|
||||
}
|
||||
if (parameters.present) {
|
||||
map['parameters'] = i0.Variable<i3.Uint8List>(
|
||||
i1.$AssetEditEntityTable.$converterparameters.toSql(parameters.value),
|
||||
);
|
||||
}
|
||||
if (sequence.present) {
|
||||
map['sequence'] = i0.Variable<int>(sequence.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('AssetEditEntityCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('assetId: $assetId, ')
|
||||
..write('action: $action, ')
|
||||
..write('parameters: $parameters, ')
|
||||
..write('sequence: $sequence')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,6 @@ class AssetFaceEntity extends Table with DriftDefaultsMixin {
|
||||
|
||||
TextColumn get sourceType => text()();
|
||||
|
||||
BoolColumn get isVisible => boolean().withDefault(const Constant(true))();
|
||||
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.da
|
||||
as i1;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'
|
||||
as i2;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:drift/internal/modular.dart' as i5;
|
||||
as i3;
|
||||
import 'package:drift/internal/modular.dart' as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||
as i6;
|
||||
as i5;
|
||||
|
||||
typedef $$AssetFaceEntityTableCreateCompanionBuilder =
|
||||
i1.AssetFaceEntityCompanion Function({
|
||||
@@ -24,8 +23,6 @@ typedef $$AssetFaceEntityTableCreateCompanionBuilder =
|
||||
required int boundingBoxX2,
|
||||
required int boundingBoxY2,
|
||||
required String sourceType,
|
||||
i0.Value<bool> isVisible,
|
||||
i0.Value<DateTime?> deletedAt,
|
||||
});
|
||||
typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
|
||||
i1.AssetFaceEntityCompanion Function({
|
||||
@@ -39,8 +36,6 @@ typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
|
||||
i0.Value<int> boundingBoxX2,
|
||||
i0.Value<int> boundingBoxY2,
|
||||
i0.Value<String> sourceType,
|
||||
i0.Value<bool> isVisible,
|
||||
i0.Value<DateTime?> deletedAt,
|
||||
});
|
||||
|
||||
final class $$AssetFaceEntityTableReferences
|
||||
@@ -56,29 +51,29 @@ final class $$AssetFaceEntityTableReferences
|
||||
super.$_typedResult,
|
||||
);
|
||||
|
||||
static i4.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
||||
i5.ReadDatabaseContainer(db)
|
||||
.resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||
static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
||||
i4.ReadDatabaseContainer(db)
|
||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||
.createAlias(
|
||||
i0.$_aliasNameGenerator(
|
||||
i5.ReadDatabaseContainer(db)
|
||||
i4.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
||||
.assetId,
|
||||
i5.ReadDatabaseContainer(
|
||||
i4.ReadDatabaseContainer(
|
||||
db,
|
||||
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
||||
),
|
||||
);
|
||||
|
||||
i4.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
||||
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
||||
final $_column = $_itemColumn<String>('asset_id')!;
|
||||
|
||||
final manager = i4
|
||||
final manager = i3
|
||||
.$$RemoteAssetEntityTableTableManager(
|
||||
$_db,
|
||||
i5.ReadDatabaseContainer(
|
||||
i4.ReadDatabaseContainer(
|
||||
$_db,
|
||||
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
)
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
||||
@@ -88,29 +83,29 @@ final class $$AssetFaceEntityTableReferences
|
||||
);
|
||||
}
|
||||
|
||||
static i6.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) =>
|
||||
i5.ReadDatabaseContainer(db)
|
||||
.resultSet<i6.$PersonEntityTable>('person_entity')
|
||||
static i5.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) =>
|
||||
i4.ReadDatabaseContainer(db)
|
||||
.resultSet<i5.$PersonEntityTable>('person_entity')
|
||||
.createAlias(
|
||||
i0.$_aliasNameGenerator(
|
||||
i5.ReadDatabaseContainer(db)
|
||||
i4.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
||||
.personId,
|
||||
i5.ReadDatabaseContainer(
|
||||
i4.ReadDatabaseContainer(
|
||||
db,
|
||||
).resultSet<i6.$PersonEntityTable>('person_entity').id,
|
||||
).resultSet<i5.$PersonEntityTable>('person_entity').id,
|
||||
),
|
||||
);
|
||||
|
||||
i6.$$PersonEntityTableProcessedTableManager? get personId {
|
||||
i5.$$PersonEntityTableProcessedTableManager? get personId {
|
||||
final $_column = $_itemColumn<String>('person_id');
|
||||
if ($_column == null) return null;
|
||||
final manager = i6
|
||||
final manager = i5
|
||||
.$$PersonEntityTableTableManager(
|
||||
$_db,
|
||||
i5.ReadDatabaseContainer(
|
||||
i4.ReadDatabaseContainer(
|
||||
$_db,
|
||||
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
||||
)
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_personIdTable($_db));
|
||||
@@ -170,34 +165,24 @@ class $$AssetFaceEntityTableFilterComposer
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<bool> get isVisible => $composableBuilder(
|
||||
column: $table.isVisible,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder(
|
||||
column: $table.deletedAt,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i4.$$RemoteAssetEntityTableFilterComposer get assetId {
|
||||
final i4.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||
i3.$$RemoteAssetEntityTableFilterComposer get assetId {
|
||||
final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i5.ReadDatabaseContainer(
|
||||
referencedTable: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i4.$$RemoteAssetEntityTableFilterComposer(
|
||||
}) => i3.$$RemoteAssetEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer(
|
||||
$table: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
@@ -207,24 +192,24 @@ class $$AssetFaceEntityTableFilterComposer
|
||||
return composer;
|
||||
}
|
||||
|
||||
i6.$$PersonEntityTableFilterComposer get personId {
|
||||
final i6.$$PersonEntityTableFilterComposer composer = $composerBuilder(
|
||||
i5.$$PersonEntityTableFilterComposer get personId {
|
||||
final i5.$$PersonEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.personId,
|
||||
referencedTable: i5.ReadDatabaseContainer(
|
||||
referencedTable: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i6.$$PersonEntityTableFilterComposer(
|
||||
}) => i5.$$PersonEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer(
|
||||
$table: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
@@ -284,35 +269,25 @@ class $$AssetFaceEntityTableOrderingComposer
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<bool> get isVisible => $composableBuilder(
|
||||
column: $table.isVisible,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<DateTime> get deletedAt => $composableBuilder(
|
||||
column: $table.deletedAt,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i4.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
||||
final i4.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||
i3.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
||||
final i3.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i5.ReadDatabaseContainer(
|
||||
referencedTable: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i4.$$RemoteAssetEntityTableOrderingComposer(
|
||||
}) => i3.$$RemoteAssetEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer(
|
||||
$table: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
@@ -322,24 +297,24 @@ class $$AssetFaceEntityTableOrderingComposer
|
||||
return composer;
|
||||
}
|
||||
|
||||
i6.$$PersonEntityTableOrderingComposer get personId {
|
||||
final i6.$$PersonEntityTableOrderingComposer composer = $composerBuilder(
|
||||
i5.$$PersonEntityTableOrderingComposer get personId {
|
||||
final i5.$$PersonEntityTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.personId,
|
||||
referencedTable: i5.ReadDatabaseContainer(
|
||||
referencedTable: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i6.$$PersonEntityTableOrderingComposer(
|
||||
}) => i5.$$PersonEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer(
|
||||
$table: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
@@ -397,31 +372,25 @@ class $$AssetFaceEntityTableAnnotationComposer
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumn<bool> get isVisible =>
|
||||
$composableBuilder(column: $table.isVisible, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get deletedAt =>
|
||||
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
|
||||
|
||||
i4.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
||||
final i4.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||
i3.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
||||
final i3.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i5.ReadDatabaseContainer(
|
||||
referencedTable: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i4.$$RemoteAssetEntityTableAnnotationComposer(
|
||||
}) => i3.$$RemoteAssetEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer(
|
||||
$table: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
@@ -431,24 +400,24 @@ class $$AssetFaceEntityTableAnnotationComposer
|
||||
return composer;
|
||||
}
|
||||
|
||||
i6.$$PersonEntityTableAnnotationComposer get personId {
|
||||
final i6.$$PersonEntityTableAnnotationComposer composer = $composerBuilder(
|
||||
i5.$$PersonEntityTableAnnotationComposer get personId {
|
||||
final i5.$$PersonEntityTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.personId,
|
||||
referencedTable: i5.ReadDatabaseContainer(
|
||||
referencedTable: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i6.$$PersonEntityTableAnnotationComposer(
|
||||
}) => i5.$$PersonEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer(
|
||||
$table: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
@@ -499,8 +468,6 @@ class $$AssetFaceEntityTableTableManager
|
||||
i0.Value<int> boundingBoxX2 = const i0.Value.absent(),
|
||||
i0.Value<int> boundingBoxY2 = const i0.Value.absent(),
|
||||
i0.Value<String> sourceType = const i0.Value.absent(),
|
||||
i0.Value<bool> isVisible = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||
}) => i1.AssetFaceEntityCompanion(
|
||||
id: id,
|
||||
assetId: assetId,
|
||||
@@ -512,8 +479,6 @@ class $$AssetFaceEntityTableTableManager
|
||||
boundingBoxX2: boundingBoxX2,
|
||||
boundingBoxY2: boundingBoxY2,
|
||||
sourceType: sourceType,
|
||||
isVisible: isVisible,
|
||||
deletedAt: deletedAt,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
@@ -527,8 +492,6 @@ class $$AssetFaceEntityTableTableManager
|
||||
required int boundingBoxX2,
|
||||
required int boundingBoxY2,
|
||||
required String sourceType,
|
||||
i0.Value<bool> isVisible = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||
}) => i1.AssetFaceEntityCompanion.insert(
|
||||
id: id,
|
||||
assetId: assetId,
|
||||
@@ -540,8 +503,6 @@ class $$AssetFaceEntityTableTableManager
|
||||
boundingBoxX2: boundingBoxX2,
|
||||
boundingBoxY2: boundingBoxY2,
|
||||
sourceType: sourceType,
|
||||
isVisible: isVisible,
|
||||
deletedAt: deletedAt,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map(
|
||||
@@ -748,33 +709,6 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _isVisibleMeta = const i0.VerificationMeta(
|
||||
'isVisible',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<bool> isVisible = i0.GeneratedColumn<bool>(
|
||||
'is_visible',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_visible" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const i3.Constant(true),
|
||||
);
|
||||
static const i0.VerificationMeta _deletedAtMeta = const i0.VerificationMeta(
|
||||
'deletedAt',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<DateTime> deletedAt =
|
||||
i0.GeneratedColumn<DateTime>(
|
||||
'deleted_at',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
id,
|
||||
@@ -787,8 +721,6 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
||||
boundingBoxX2,
|
||||
boundingBoxY2,
|
||||
sourceType,
|
||||
isVisible,
|
||||
deletedAt,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@@ -892,18 +824,6 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
||||
} else if (isInserting) {
|
||||
context.missing(_sourceTypeMeta);
|
||||
}
|
||||
if (data.containsKey('is_visible')) {
|
||||
context.handle(
|
||||
_isVisibleMeta,
|
||||
isVisible.isAcceptableOrUnknown(data['is_visible']!, _isVisibleMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('deleted_at')) {
|
||||
context.handle(
|
||||
_deletedAtMeta,
|
||||
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -953,14 +873,6 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}source_type'],
|
||||
)!,
|
||||
isVisible: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.bool,
|
||||
data['${effectivePrefix}is_visible'],
|
||||
)!,
|
||||
deletedAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}deleted_at'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -987,8 +899,6 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
final int boundingBoxX2;
|
||||
final int boundingBoxY2;
|
||||
final String sourceType;
|
||||
final bool isVisible;
|
||||
final DateTime? deletedAt;
|
||||
const AssetFaceEntityData({
|
||||
required this.id,
|
||||
required this.assetId,
|
||||
@@ -1000,8 +910,6 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
required this.boundingBoxX2,
|
||||
required this.boundingBoxY2,
|
||||
required this.sourceType,
|
||||
required this.isVisible,
|
||||
this.deletedAt,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
@@ -1018,10 +926,6 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
map['bounding_box_x2'] = i0.Variable<int>(boundingBoxX2);
|
||||
map['bounding_box_y2'] = i0.Variable<int>(boundingBoxY2);
|
||||
map['source_type'] = i0.Variable<String>(sourceType);
|
||||
map['is_visible'] = i0.Variable<bool>(isVisible);
|
||||
if (!nullToAbsent || deletedAt != null) {
|
||||
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1041,8 +945,6 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
boundingBoxX2: serializer.fromJson<int>(json['boundingBoxX2']),
|
||||
boundingBoxY2: serializer.fromJson<int>(json['boundingBoxY2']),
|
||||
sourceType: serializer.fromJson<String>(json['sourceType']),
|
||||
isVisible: serializer.fromJson<bool>(json['isVisible']),
|
||||
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -1059,8 +961,6 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
'boundingBoxX2': serializer.toJson<int>(boundingBoxX2),
|
||||
'boundingBoxY2': serializer.toJson<int>(boundingBoxY2),
|
||||
'sourceType': serializer.toJson<String>(sourceType),
|
||||
'isVisible': serializer.toJson<bool>(isVisible),
|
||||
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1075,8 +975,6 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
int? boundingBoxX2,
|
||||
int? boundingBoxY2,
|
||||
String? sourceType,
|
||||
bool? isVisible,
|
||||
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||
}) => i1.AssetFaceEntityData(
|
||||
id: id ?? this.id,
|
||||
assetId: assetId ?? this.assetId,
|
||||
@@ -1088,8 +986,6 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
||||
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
||||
sourceType: sourceType ?? this.sourceType,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
|
||||
);
|
||||
AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) {
|
||||
return AssetFaceEntityData(
|
||||
@@ -1117,8 +1013,6 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
sourceType: data.sourceType.present
|
||||
? data.sourceType.value
|
||||
: this.sourceType,
|
||||
isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible,
|
||||
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1134,9 +1028,7 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
..write('boundingBoxY1: $boundingBoxY1, ')
|
||||
..write('boundingBoxX2: $boundingBoxX2, ')
|
||||
..write('boundingBoxY2: $boundingBoxY2, ')
|
||||
..write('sourceType: $sourceType, ')
|
||||
..write('isVisible: $isVisible, ')
|
||||
..write('deletedAt: $deletedAt')
|
||||
..write('sourceType: $sourceType')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -1153,8 +1045,6 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
boundingBoxX2,
|
||||
boundingBoxY2,
|
||||
sourceType,
|
||||
isVisible,
|
||||
deletedAt,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -1169,9 +1059,7 @@ class AssetFaceEntityData extends i0.DataClass
|
||||
other.boundingBoxY1 == this.boundingBoxY1 &&
|
||||
other.boundingBoxX2 == this.boundingBoxX2 &&
|
||||
other.boundingBoxY2 == this.boundingBoxY2 &&
|
||||
other.sourceType == this.sourceType &&
|
||||
other.isVisible == this.isVisible &&
|
||||
other.deletedAt == this.deletedAt);
|
||||
other.sourceType == this.sourceType);
|
||||
}
|
||||
|
||||
class AssetFaceEntityCompanion
|
||||
@@ -1186,8 +1074,6 @@ class AssetFaceEntityCompanion
|
||||
final i0.Value<int> boundingBoxX2;
|
||||
final i0.Value<int> boundingBoxY2;
|
||||
final i0.Value<String> sourceType;
|
||||
final i0.Value<bool> isVisible;
|
||||
final i0.Value<DateTime?> deletedAt;
|
||||
const AssetFaceEntityCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.assetId = const i0.Value.absent(),
|
||||
@@ -1199,8 +1085,6 @@ class AssetFaceEntityCompanion
|
||||
this.boundingBoxX2 = const i0.Value.absent(),
|
||||
this.boundingBoxY2 = const i0.Value.absent(),
|
||||
this.sourceType = const i0.Value.absent(),
|
||||
this.isVisible = const i0.Value.absent(),
|
||||
this.deletedAt = const i0.Value.absent(),
|
||||
});
|
||||
AssetFaceEntityCompanion.insert({
|
||||
required String id,
|
||||
@@ -1213,8 +1097,6 @@ class AssetFaceEntityCompanion
|
||||
required int boundingBoxX2,
|
||||
required int boundingBoxY2,
|
||||
required String sourceType,
|
||||
this.isVisible = const i0.Value.absent(),
|
||||
this.deletedAt = const i0.Value.absent(),
|
||||
}) : id = i0.Value(id),
|
||||
assetId = i0.Value(assetId),
|
||||
imageWidth = i0.Value(imageWidth),
|
||||
@@ -1235,8 +1117,6 @@ class AssetFaceEntityCompanion
|
||||
i0.Expression<int>? boundingBoxX2,
|
||||
i0.Expression<int>? boundingBoxY2,
|
||||
i0.Expression<String>? sourceType,
|
||||
i0.Expression<bool>? isVisible,
|
||||
i0.Expression<DateTime>? deletedAt,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
@@ -1249,8 +1129,6 @@ class AssetFaceEntityCompanion
|
||||
if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2,
|
||||
if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2,
|
||||
if (sourceType != null) 'source_type': sourceType,
|
||||
if (isVisible != null) 'is_visible': isVisible,
|
||||
if (deletedAt != null) 'deleted_at': deletedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1265,8 +1143,6 @@ class AssetFaceEntityCompanion
|
||||
i0.Value<int>? boundingBoxX2,
|
||||
i0.Value<int>? boundingBoxY2,
|
||||
i0.Value<String>? sourceType,
|
||||
i0.Value<bool>? isVisible,
|
||||
i0.Value<DateTime?>? deletedAt,
|
||||
}) {
|
||||
return i1.AssetFaceEntityCompanion(
|
||||
id: id ?? this.id,
|
||||
@@ -1279,8 +1155,6 @@ class AssetFaceEntityCompanion
|
||||
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
||||
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
||||
sourceType: sourceType ?? this.sourceType,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1317,12 +1191,6 @@ class AssetFaceEntityCompanion
|
||||
if (sourceType.present) {
|
||||
map['source_type'] = i0.Variable<String>(sourceType.value);
|
||||
}
|
||||
if (isVisible.present) {
|
||||
map['is_visible'] = i0.Variable<bool>(isVisible.value);
|
||||
}
|
||||
if (deletedAt.present) {
|
||||
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1338,9 +1206,7 @@ class AssetFaceEntityCompanion
|
||||
..write('boundingBoxY1: $boundingBoxY1, ')
|
||||
..write('boundingBoxX2: $boundingBoxX2, ')
|
||||
..write('boundingBoxY2: $boundingBoxY2, ')
|
||||
..write('sourceType: $sourceType, ')
|
||||
..write('isVisible: $isVisible, ')
|
||||
..write('deletedAt: $deletedAt')
|
||||
..write('sourceType: $sourceType')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -152,6 +152,8 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData {
|
||||
fileSize: fileSize,
|
||||
dateTimeOriginal: dateTimeOriginal,
|
||||
rating: rating,
|
||||
width: width,
|
||||
height: height,
|
||||
timeZone: timeZone,
|
||||
make: make,
|
||||
model: model,
|
||||
|
||||
@@ -24,8 +24,6 @@ abstract class ImageRequest {
|
||||
|
||||
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0});
|
||||
|
||||
Future<ui.Codec?> loadCodec();
|
||||
|
||||
void cancel() {
|
||||
if (_isCancelled) {
|
||||
return;
|
||||
@@ -36,7 +34,7 @@ abstract class ImageRequest {
|
||||
|
||||
void _onCancelled();
|
||||
|
||||
Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromEncodedPlatformImage(int address, int length) async {
|
||||
Future<ui.FrameInfo?> _fromEncodedPlatformImage(int address, int length) async {
|
||||
final pointer = Pointer<Uint8>.fromAddress(address);
|
||||
if (_isCancelled) {
|
||||
malloc.free(pointer);
|
||||
@@ -69,20 +67,6 @@ abstract class ImageRequest {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (codec, descriptor);
|
||||
}
|
||||
|
||||
Future<ui.FrameInfo?> _fromEncodedPlatformImage(int address, int length) async {
|
||||
final result = await _codecFromEncodedPlatformImage(address, length);
|
||||
if (result == null) return null;
|
||||
|
||||
final (codec, descriptor) = result;
|
||||
if (_isCancelled) {
|
||||
descriptor.dispose();
|
||||
codec.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
final frame = await codec.getNextFrame();
|
||||
descriptor.dispose();
|
||||
codec.dispose();
|
||||
|
||||
@@ -22,7 +22,6 @@ class LocalImageRequest extends ImageRequest {
|
||||
width: width,
|
||||
height: height,
|
||||
isVideo: assetType == AssetType.video,
|
||||
preferEncoded: false,
|
||||
);
|
||||
if (info == null) {
|
||||
return null;
|
||||
@@ -32,26 +31,6 @@ class LocalImageRequest extends ImageRequest {
|
||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ui.Codec?> loadCodec() async {
|
||||
if (_isCancelled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final info = await localImageApi.requestImage(
|
||||
localId,
|
||||
requestId: requestId,
|
||||
width: width,
|
||||
height: height,
|
||||
isVideo: assetType == AssetType.video,
|
||||
preferEncoded: true,
|
||||
);
|
||||
if (info == null) return null;
|
||||
|
||||
final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null);
|
||||
return codec;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> _onCancelled() {
|
||||
return localImageApi.cancelRequest(requestId);
|
||||
|
||||
@@ -12,8 +12,7 @@ class RemoteImageRequest extends ImageRequest {
|
||||
return null;
|
||||
}
|
||||
|
||||
final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId, preferEncoded: false);
|
||||
// Android always returns encoded data, so we need to check for both shapes of the response.
|
||||
final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId);
|
||||
final frame = switch (info) {
|
||||
{'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length),
|
||||
{'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} =>
|
||||
@@ -23,19 +22,6 @@ class RemoteImageRequest extends ImageRequest {
|
||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ui.Codec?> loadCodec() async {
|
||||
if (_isCancelled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId, preferEncoded: true);
|
||||
if (info == null) return null;
|
||||
|
||||
final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null);
|
||||
return codec;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> _onCancelled() {
|
||||
return remoteImageApi.cancelRequest(requestId);
|
||||
|
||||
@@ -16,9 +16,6 @@ class ThumbhashImageRequest extends ImageRequest {
|
||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ui.Codec?> loadCodec() => throw UnsupportedError('Thumbhash does not support codec loading');
|
||||
|
||||
@override
|
||||
void _onCancelled() {}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
@@ -66,6 +67,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||
AssetFaceEntity,
|
||||
StoreEntity,
|
||||
TrashedLocalAssetEntity,
|
||||
AssetEditEntity,
|
||||
],
|
||||
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
||||
)
|
||||
@@ -227,8 +229,8 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
await m.createIndex(v19.idxStackPrimaryAssetId);
|
||||
},
|
||||
from19To20: (m, v20) async {
|
||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
|
||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
|
||||
await m.createTable(v20.assetEditEntity);
|
||||
await m.createIndex(v20.idxAssetEditAssetId);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -41,9 +41,11 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
||||
as i19;
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
||||
as i20;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
|
||||
as i21;
|
||||
import 'package:drift/internal/modular.dart' as i22;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
as i22;
|
||||
import 'package:drift/internal/modular.dart' as i23;
|
||||
|
||||
abstract class $Drift extends i0.GeneratedDatabase {
|
||||
$Drift(i0.QueryExecutor e) : super(e);
|
||||
@@ -85,9 +87,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
|
||||
late final i20.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i20
|
||||
.$TrashedLocalAssetEntityTable(this);
|
||||
i21.MergedAssetDrift get mergedAssetDrift => i22.ReadDatabaseContainer(
|
||||
late final i21.$AssetEditEntityTable assetEditEntity = i21
|
||||
.$AssetEditEntityTable(this);
|
||||
i22.MergedAssetDrift get mergedAssetDrift => i23.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i21.MergedAssetDrift>(i21.MergedAssetDrift.new);
|
||||
).accessor<i22.MergedAssetDrift>(i22.MergedAssetDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@@ -125,6 +129,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
i10.idxPartnerSharedWithId,
|
||||
i11.idxLatLng,
|
||||
i12.idxRemoteAlbumAssetAlbumAsset,
|
||||
@@ -134,6 +139,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
i18.idxAssetFaceAssetId,
|
||||
i20.idxTrashedLocalAssetChecksum,
|
||||
i20.idxTrashedLocalAssetAlbum,
|
||||
i21.idxAssetEditAssetId,
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules
|
||||
@@ -325,6 +331,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
),
|
||||
result: [i0.TableUpdate('asset_face_entity', kind: i0.UpdateKind.update)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'remote_asset_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('asset_edit_entity', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
]);
|
||||
@override
|
||||
i0.DriftDatabaseOptions get options =>
|
||||
@@ -384,4 +397,6 @@ class $DriftManager {
|
||||
_db,
|
||||
_db.trashedLocalAssetEntity,
|
||||
);
|
||||
i21.$$AssetEditEntityTableTableManager get assetEditEntity =>
|
||||
i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity);
|
||||
}
|
||||
|
||||
@@ -8396,6 +8396,7 @@ final class Schema20 extends i0.VersionedSchema {
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
idxPartnerSharedWithId,
|
||||
idxLatLng,
|
||||
idxRemoteAlbumAssetAlbumAsset,
|
||||
@@ -8405,6 +8406,7 @@ final class Schema20 extends i0.VersionedSchema {
|
||||
idxAssetFaceAssetId,
|
||||
idxTrashedLocalAssetChecksum,
|
||||
idxTrashedLocalAssetAlbum,
|
||||
idxAssetEditAssetId,
|
||||
];
|
||||
late final Shape20 userEntity = Shape20(
|
||||
source: i0.VersionedTable(
|
||||
@@ -8767,7 +8769,7 @@ final class Schema20 extends i0.VersionedSchema {
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape29 assetFaceEntity = Shape29(
|
||||
late final Shape15 assetFaceEntity = Shape15(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
@@ -8784,8 +8786,6 @@ final class Schema20 extends i0.VersionedSchema {
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_83,
|
||||
_column_102,
|
||||
_column_18,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
@@ -8827,6 +8827,17 @@ final class Schema20 extends i0.VersionedSchema {
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape29 assetEditEntity = Shape29(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_edit_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_0, _column_36, _column_102, _column_103, _column_104],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||
'idx_partner_shared_with_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||
@@ -8863,6 +8874,10 @@ final class Schema20 extends i0.VersionedSchema {
|
||||
'idx_trashed_local_asset_album',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||
);
|
||||
final i1.Index idxAssetEditAssetId = i1.Index(
|
||||
'idx_asset_edit_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
||||
);
|
||||
}
|
||||
|
||||
class Shape29 extends i0.VersionedTable {
|
||||
@@ -8871,38 +8886,34 @@ class Shape29 extends i0.VersionedTable {
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get assetId =>
|
||||
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get personId =>
|
||||
columnsByName['person_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get imageWidth =>
|
||||
columnsByName['image_width']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get imageHeight =>
|
||||
columnsByName['image_height']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get boundingBoxX1 =>
|
||||
columnsByName['bounding_box_x1']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get boundingBoxY1 =>
|
||||
columnsByName['bounding_box_y1']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get boundingBoxX2 =>
|
||||
columnsByName['bounding_box_x2']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get boundingBoxY2 =>
|
||||
columnsByName['bounding_box_y2']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get sourceType =>
|
||||
columnsByName['source_type']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get isVisible =>
|
||||
columnsByName['is_visible']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<int> get action =>
|
||||
columnsByName['action']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<i2.Uint8List> get parameters =>
|
||||
columnsByName['parameters']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||
i1.GeneratedColumn<int> get sequence =>
|
||||
columnsByName['sequence']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<bool> _column_102(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>(
|
||||
'is_visible',
|
||||
i1.GeneratedColumn<int> _column_102(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'action',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.bool,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_visible" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const CustomExpression('1'),
|
||||
type: i1.DriftSqlType.int,
|
||||
);
|
||||
i1.GeneratedColumn<i2.Uint8List> _column_103(String aliasedName) =>
|
||||
i1.GeneratedColumn<i2.Uint8List>(
|
||||
'parameters',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.blob,
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_104(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'sequence',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
|
||||
@@ -184,8 +184,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
if (keepFavorites) {
|
||||
whereClause =
|
||||
whereClause & _db.localAssetEntity.isFavorite.equals(false) & _db.remoteAssetEntity.isFavorite.equals(false);
|
||||
whereClause = whereClause & _db.localAssetEntity.isFavorite.equals(false);
|
||||
}
|
||||
|
||||
query.where(whereClause);
|
||||
|
||||
@@ -16,15 +16,9 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
Future<List<DriftPerson>> getAssetPeople(String assetId) async {
|
||||
final query =
|
||||
_db.select(_db.assetFaceEntity).join([
|
||||
innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)),
|
||||
])..where(
|
||||
_db.assetFaceEntity.assetId.equals(assetId) &
|
||||
_db.assetFaceEntity.isVisible.equals(true) &
|
||||
_db.assetFaceEntity.deletedAt.isNull() &
|
||||
_db.personEntity.isHidden.equals(false),
|
||||
);
|
||||
final query = _db.select(_db.assetFaceEntity).join([
|
||||
innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)),
|
||||
])..where(_db.assetFaceEntity.assetId.equals(assetId) & _db.personEntity.isHidden.equals(false));
|
||||
|
||||
return query.map((row) {
|
||||
final person = row.readTable(_db.personEntity);
|
||||
@@ -45,9 +39,7 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
|
||||
..where(
|
||||
people.isHidden.equals(false) &
|
||||
assets.deletedAt.isNull() &
|
||||
assets.visibility.equalsValue(AssetVisibility.timeline) &
|
||||
faces.isVisible.equals(true) &
|
||||
faces.deletedAt.isNull(),
|
||||
assets.visibility.equalsValue(AssetVisibility.timeline),
|
||||
)
|
||||
..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not())
|
||||
..orderBy([
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
@@ -323,32 +321,26 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
}).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<String>> getSortedAlbumIds(List<String> albumIds, {required AssetDateAggregation aggregation}) async {
|
||||
if (albumIds.isEmpty) return [];
|
||||
Future<DateTime?> getNewestAssetTimestamp(String albumId) {
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||
..addColumns([_db.remoteAssetEntity.localDateTime.max()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
]);
|
||||
|
||||
final jsonIds = jsonEncode(albumIds);
|
||||
final sqlAgg = aggregation == AssetDateAggregation.start ? 'MIN' : 'MAX';
|
||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull();
|
||||
}
|
||||
|
||||
final rows = await _db
|
||||
.customSelect(
|
||||
'''
|
||||
SELECT
|
||||
raae.album_id,
|
||||
$sqlAgg(rae.local_date_time) AS asset_date
|
||||
FROM json_each(?) ids
|
||||
INNER JOIN remote_album_asset_entity raae
|
||||
ON raae.album_id = ids.value
|
||||
INNER JOIN remote_asset_entity rae
|
||||
ON rae.id = raae.asset_id
|
||||
GROUP BY raae.album_id
|
||||
ORDER BY asset_date ASC
|
||||
''',
|
||||
variables: [Variable<String>(jsonIds)],
|
||||
readsFrom: {_db.remoteAlbumAssetEntity, _db.remoteAssetEntity},
|
||||
)
|
||||
.get();
|
||||
Future<DateTime?> getOldestAssetTimestamp(String albumId) {
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||
..addColumns([_db.remoteAssetEntity.localDateTime.min()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
]);
|
||||
|
||||
return rows.map((row) => row.read<String>('album_id')).toList();
|
||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<int> getCount() {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/domain/models/stack.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
@@ -9,6 +12,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
@@ -264,4 +268,35 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
Future<int> getCount() {
|
||||
return _db.managers.remoteAssetEntity.count();
|
||||
}
|
||||
|
||||
Future<List<AssetEdit>> getAssetEdits(String assetId) async {
|
||||
final query = _db.assetEditEntity.select()
|
||||
..where((row) => row.assetId.equals(assetId))
|
||||
..orderBy([(row) => OrderingTerm.asc(row.sequence)]);
|
||||
|
||||
return query.map((row) => row.toDto()).get();
|
||||
}
|
||||
|
||||
Future<void> editAsset(String assetId, List<AssetEdit> edits) async {
|
||||
await _db.transaction(() async {
|
||||
await _db.batch((batch) async {
|
||||
// delete existing edits
|
||||
batch.deleteWhere(_db.assetEditEntity, (row) => row.assetId.equals(assetId));
|
||||
|
||||
// insert new edits
|
||||
for (var i = 0; i < edits.length; i++) {
|
||||
final edit = edits[i];
|
||||
final companion = AssetEditEntityCompanion(
|
||||
id: Value(const Uuid().v4()),
|
||||
assetId: Value(assetId),
|
||||
action: Value(edit.action),
|
||||
parameters: Value(edit.parameters),
|
||||
sequence: Value(i),
|
||||
);
|
||||
|
||||
batch.insert(_db.assetEditEntity, companion);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
@@ -26,7 +25,6 @@ class SyncApiRepository {
|
||||
|
||||
Future<void> streamChanges(
|
||||
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
|
||||
required SemVer serverVersion,
|
||||
Function()? onReset,
|
||||
int batchSize = kSyncEventBatchSize,
|
||||
http.Client? httpClient,
|
||||
@@ -51,6 +49,7 @@ class SyncApiRepository {
|
||||
SyncRequestType.usersV1,
|
||||
SyncRequestType.assetsV1,
|
||||
SyncRequestType.assetExifsV1,
|
||||
SyncRequestType.assetEditsV1,
|
||||
SyncRequestType.assetMetadataV1,
|
||||
SyncRequestType.partnersV1,
|
||||
SyncRequestType.partnerAssetsV1,
|
||||
@@ -66,8 +65,7 @@ class SyncApiRepository {
|
||||
SyncRequestType.partnerStacksV1,
|
||||
SyncRequestType.userMetadataV1,
|
||||
SyncRequestType.peopleV1,
|
||||
if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1,
|
||||
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2,
|
||||
SyncRequestType.assetFacesV1,
|
||||
],
|
||||
reset: shouldReset,
|
||||
).toJson(),
|
||||
@@ -156,6 +154,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.assetV1: SyncAssetV1.fromJson,
|
||||
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
|
||||
SyncEntityType.assetEditV1: SyncAssetEditV1.fromJson,
|
||||
SyncEntityType.assetEditDeleteV1: SyncAssetEditDeleteV1.fromJson,
|
||||
SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson,
|
||||
SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson,
|
||||
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
|
||||
@@ -193,7 +193,6 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.personV1: SyncPersonV1.fromJson,
|
||||
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
|
||||
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
|
||||
SyncEntityType.assetFaceV2: SyncAssetFaceV2.fromJson,
|
||||
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
|
||||
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
|
||||
};
|
||||
|
||||
@@ -5,9 +5,11 @@ import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
@@ -26,8 +28,8 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey;
|
||||
import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey;
|
||||
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction;
|
||||
import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction;
|
||||
|
||||
class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
final Logger _logger = Logger('DriftSyncStreamRepository');
|
||||
@@ -58,6 +60,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
await _db.userEntity.deleteAll();
|
||||
await _db.userMetadataEntity.deleteAll();
|
||||
await _db.remoteAssetCloudIdEntity.deleteAll();
|
||||
await _db.assetEditEntity.deleteAll();
|
||||
});
|
||||
await _db.customStatement('PRAGMA foreign_keys = ON');
|
||||
});
|
||||
@@ -278,6 +281,40 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAssetEditsV1(Iterable<SyncAssetEditV1> data, {String debugLabel = 'user'}) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final edit in data) {
|
||||
final companion = AssetEditEntityCompanion(
|
||||
id: Value(edit.id),
|
||||
assetId: Value(edit.assetId),
|
||||
action: Value(edit.action.toAssetEditAction()),
|
||||
parameters: Value(edit.parameters as Map<String, Object?>),
|
||||
sequence: Value(edit.sequence),
|
||||
);
|
||||
|
||||
batch.insert(_db.assetEditEntity, companion, onConflict: DoUpdate((_) => companion));
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateAssetEditsV1 - $debugLabel', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAssetEditsV1(Iterable<SyncAssetEditDeleteV1> data, {String debugLabel = 'user'}) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final edit in data) {
|
||||
batch.deleteWhere(_db.assetEditEntity, (row) => row.assetId.equals(edit.assetId));
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteAssetEditsV1 - $debugLabel', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAssetsMetadataV1(Iterable<SyncAssetMetadataDeleteV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
@@ -652,37 +689,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAssetFacesV2(Iterable<SyncAssetFaceV2> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final assetFace in data) {
|
||||
final companion = AssetFaceEntityCompanion(
|
||||
assetId: Value(assetFace.assetId),
|
||||
personId: Value(assetFace.personId),
|
||||
imageWidth: Value(assetFace.imageWidth),
|
||||
imageHeight: Value(assetFace.imageHeight),
|
||||
boundingBoxX1: Value(assetFace.boundingBoxX1),
|
||||
boundingBoxY1: Value(assetFace.boundingBoxY1),
|
||||
boundingBoxX2: Value(assetFace.boundingBoxX2),
|
||||
boundingBoxY2: Value(assetFace.boundingBoxY2),
|
||||
sourceType: Value(assetFace.sourceType),
|
||||
deletedAt: Value(assetFace.deletedAt),
|
||||
isVisible: Value(assetFace.isVisible),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.assetFaceEntity,
|
||||
companion.copyWith(id: Value(assetFace.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateAssetFacesV2', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAssetFacesV1(Iterable<SyncAssetFaceDeleteV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
@@ -798,3 +804,13 @@ extension on String {
|
||||
extension on UserAvatarColor {
|
||||
AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == value);
|
||||
}
|
||||
|
||||
extension on api.AssetEditAction {
|
||||
AssetEditAction toAssetEditAction() => switch (this) {
|
||||
api.AssetEditAction.crop => AssetEditAction.crop,
|
||||
api.AssetEditAction.rotate => AssetEditAction.rotate,
|
||||
api.AssetEditAction.mirror => AssetEditAction.mirror,
|
||||
api.AssetEditAction.filter => AssetEditAction.filter,
|
||||
_ => AssetEditAction.other,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -323,7 +323,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive),
|
||||
groupBy: groupBy,
|
||||
origin: TimelineOrigin.archive,
|
||||
joinLocal: true,
|
||||
);
|
||||
|
||||
TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
||||
@@ -422,9 +421,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||
_db.assetFaceEntity.personId.equals(personId) &
|
||||
_db.assetFaceEntity.isVisible.equals(true) &
|
||||
_db.assetFaceEntity.deletedAt.isNull(),
|
||||
_db.assetFaceEntity.personId.equals(personId),
|
||||
);
|
||||
|
||||
return query.map((row) {
|
||||
@@ -449,9 +446,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||
_db.assetFaceEntity.personId.equals(personId) &
|
||||
_db.assetFaceEntity.isVisible.equals(true) &
|
||||
_db.assetFaceEntity.deletedAt.isNull(),
|
||||
_db.assetFaceEntity.personId.equals(personId),
|
||||
)
|
||||
..groupBy([dateExp])
|
||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||
@@ -481,9 +476,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||
_db.assetFaceEntity.personId.equals(personId) &
|
||||
_db.assetFaceEntity.isVisible.equals(true) &
|
||||
_db.assetFaceEntity.deletedAt.isNull(),
|
||||
_db.assetFaceEntity.personId.equals(personId),
|
||||
)
|
||||
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
||||
..limit(count, offset: offset);
|
||||
|
||||
@@ -20,7 +20,6 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||
@@ -50,34 +49,30 @@ import 'package:logging/logging.dart';
|
||||
import 'package:timezone/data/latest.dart';
|
||||
|
||||
void main() async {
|
||||
try {
|
||||
ImmichWidgetsBinding();
|
||||
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
|
||||
await EasyLocalization.ensureInitialized();
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await initApp();
|
||||
// Warm-up isolate pool for worker manager
|
||||
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
||||
await migrateDatabaseIfNeeded(isar, drift);
|
||||
HttpSSLOptions.apply();
|
||||
ImmichWidgetsBinding();
|
||||
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await initApp();
|
||||
// Warm-up isolate pool for worker manager
|
||||
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
||||
await migrateDatabaseIfNeeded(isar, drift);
|
||||
HttpSSLOptions.apply();
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
child: const MainWidget(),
|
||||
),
|
||||
);
|
||||
} catch (error, stack) {
|
||||
runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString()));
|
||||
}
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
child: const MainWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> initApp() async {
|
||||
await EasyLocalization.ensureInitialized();
|
||||
await initializeDateFormatting();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
|
||||
@@ -14,7 +14,6 @@ class SharedLink {
|
||||
final String key;
|
||||
final bool showMetadata;
|
||||
final SharedLinkSource type;
|
||||
final String? slug;
|
||||
|
||||
const SharedLink({
|
||||
required this.id,
|
||||
@@ -28,7 +27,6 @@ class SharedLink {
|
||||
required this.key,
|
||||
required this.showMetadata,
|
||||
required this.type,
|
||||
required this.slug,
|
||||
});
|
||||
|
||||
SharedLink copyWith({
|
||||
@@ -43,7 +41,6 @@ class SharedLink {
|
||||
String? key,
|
||||
bool? showMetadata,
|
||||
SharedLinkSource? type,
|
||||
String? slug,
|
||||
}) {
|
||||
return SharedLink(
|
||||
id: id ?? this.id,
|
||||
@@ -57,7 +54,6 @@ class SharedLink {
|
||||
key: key ?? this.key,
|
||||
showMetadata: showMetadata ?? this.showMetadata,
|
||||
type: type ?? this.type,
|
||||
slug: slug ?? this.slug,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,7 +66,6 @@ class SharedLink {
|
||||
expiresAt = dto.expiresAt,
|
||||
key = dto.key,
|
||||
showMetadata = dto.showMetadata,
|
||||
slug = dto.slug,
|
||||
type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual,
|
||||
title = dto.type == SharedLinkType.ALBUM
|
||||
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
|
||||
@@ -83,7 +78,7 @@ class SharedLink {
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type, slug=$slug)';
|
||||
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -99,8 +94,7 @@ class SharedLink {
|
||||
other.expiresAt == expiresAt &&
|
||||
other.key == key &&
|
||||
other.showMetadata == showMetadata &&
|
||||
other.type == type &&
|
||||
other.slug == slug;
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -114,6 +108,5 @@ class SharedLink {
|
||||
expiresAt.hashCode ^
|
||||
key.hashCode ^
|
||||
showMetadata.hashCode ^
|
||||
type.hashCode ^
|
||||
slug.hashCode;
|
||||
type.hashCode;
|
||||
}
|
||||
|
||||
@@ -221,37 +221,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
onDragUpdate: (_, details, __) {
|
||||
handleSwipeUpDown(details);
|
||||
},
|
||||
onTapDown: (ctx, tapDownDetails, _) {
|
||||
final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.tapToNavigate);
|
||||
if (!tapToNavigate) {
|
||||
ref.read(showControlsProvider.notifier).toggle();
|
||||
return;
|
||||
}
|
||||
|
||||
double tapX = tapDownDetails.globalPosition.dx;
|
||||
double screenWidth = ctx.width;
|
||||
|
||||
// We want to change images if the user taps in the leftmost or
|
||||
// rightmost quarter of the screen
|
||||
bool tappedLeftSide = tapX < screenWidth / 4;
|
||||
bool tappedRightSide = tapX > screenWidth * (3 / 4);
|
||||
|
||||
int? currentPage = controller.page?.toInt();
|
||||
int maxPage = renderList.totalAssets - 1;
|
||||
|
||||
if (tappedLeftSide && currentPage != null) {
|
||||
// Nested if because we don't want to fallback to show/hide controls
|
||||
if (currentPage != 0) {
|
||||
controller.jumpToPage(currentPage - 1);
|
||||
}
|
||||
} else if (tappedRightSide && currentPage != null) {
|
||||
// Nested if because we don't want to fallback to show/hide controls
|
||||
if (currentPage != maxPage) {
|
||||
controller.jumpToPage(currentPage + 1);
|
||||
}
|
||||
} else {
|
||||
ref.read(showControlsProvider.notifier).toggle();
|
||||
}
|
||||
onTapDown: (_, __, ___) {
|
||||
ref.read(showControlsProvider.notifier).toggle();
|
||||
},
|
||||
onLongPressStart: asset.isMotionPhoto
|
||||
? (_, __, ___) {
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
@@ -20,254 +13,7 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/theme/color_scheme.dart';
|
||||
import 'package:immich_mobile/theme/theme_data.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_logo.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode;
|
||||
|
||||
class BootstrapErrorWidget extends StatelessWidget {
|
||||
final String error;
|
||||
final String stack;
|
||||
|
||||
const BootstrapErrorWidget({super.key, required this.error, required this.stack});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext _) {
|
||||
final immichTheme = defaultColorPreset.themeOfPreset;
|
||||
|
||||
return EasyLocalization(
|
||||
supportedLocales: locales.values.toList(),
|
||||
path: translationsPath,
|
||||
useFallbackTranslations: true,
|
||||
fallbackLocale: locales.values.first,
|
||||
assetLoader: const CodegenLoader(),
|
||||
child: Builder(
|
||||
builder: (lCtx) => MaterialApp(
|
||||
title: 'Immich',
|
||||
debugShowCheckedModeBanner: true,
|
||||
localizationsDelegates: lCtx.localizationDelegates,
|
||||
supportedLocales: lCtx.supportedLocales,
|
||||
locale: lCtx.locale,
|
||||
themeMode: ThemeMode.system,
|
||||
darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: lCtx.locale),
|
||||
theme: getThemeData(colorScheme: immichTheme.light, locale: lCtx.locale),
|
||||
home: Builder(
|
||||
builder: (ctx) => Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
const SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [ImmichLogo(size: 48), SizedBox(width: 12), ImmichTitleText(fontSize: 24)],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: _ErrorCard(error: error, stack: stack),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
const SafeArea(
|
||||
top: false,
|
||||
child: Padding(padding: EdgeInsets.fromLTRB(24, 16, 24, 16), child: _BottomPanel()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomPanel extends StatefulWidget {
|
||||
const _BottomPanel();
|
||||
|
||||
@override
|
||||
State<_BottomPanel> createState() => _BottomPanelState();
|
||||
}
|
||||
|
||||
class _BottomPanelState extends State<_BottomPanel> {
|
||||
bool _cleared = false;
|
||||
|
||||
Future<void> _clearDatabase() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: Text(context.t.reset_sqlite_clear_app_data),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.t.reset_sqlite_confirmation),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
context.t.reset_sqlite_confirmation_note,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(dialogCtx).pop(false), child: Text(context.t.cancel)),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogCtx).pop(true),
|
||||
child: Text(context.t.confirm, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final db = Drift();
|
||||
try {
|
||||
await db.reset();
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _cleared = true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
_cleared ? context.t.reset_sqlite_done : context.t.scaffold_body_error_unrecoverable,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_ActionLink(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
label: context.t.discord,
|
||||
onTap: () => launchUrl(Uri.parse('https://discord.immich.app/'), mode: LaunchMode.externalApplication),
|
||||
),
|
||||
_ActionLink(
|
||||
icon: Icons.bug_report_outlined,
|
||||
label: context.t.profile_drawer_github,
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse('https://github.com/immich-app/immich/issues'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
if (!_cleared)
|
||||
_ActionLink(
|
||||
icon: Icons.delete_outline,
|
||||
label: context.t.reset_sqlite_clear_app_data,
|
||||
onTap: _clearDatabase,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionLink extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionLink({required this.icon, required this.label, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 24),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorCard extends StatelessWidget {
|
||||
final String error;
|
||||
final String stack;
|
||||
|
||||
const _ErrorCard({required this.error, required this.stack});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ColoredBox(
|
||||
color: scheme.error,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.t.scaffold_body_error_occurred,
|
||||
style: textTheme.titleSmall?.copyWith(color: scheme.onError),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.t.copy_error,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
icon: Icon(Icons.copy_outlined, size: 16, color: scheme.onError),
|
||||
onPressed: () => Clipboard.setData(ClipboardData(text: '$error\n\n$stack')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(error, style: textTheme.bodyMedium),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.t.stacktrace, style: textTheme.labelMedium),
|
||||
const SizedBox(height: 4),
|
||||
SelectableText(stack, style: textTheme.bodySmall?.copyWith(fontFamily: 'GoogleSansCode')),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class SplashScreenPage extends StatefulHookConsumerWidget {
|
||||
@@ -363,43 +109,9 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
if (context.router.current.name == SplashScreenRoute.name) {
|
||||
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
|
||||
if (needBetaMigration) {
|
||||
bool migrate =
|
||||
(await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text("New Timeline Experience"),
|
||||
content: const Text(
|
||||
"The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?",
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")),
|
||||
ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")),
|
||||
],
|
||||
),
|
||||
)) ??
|
||||
false;
|
||||
if (migrate != true) {
|
||||
migrate =
|
||||
(await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text("Are you sure?"),
|
||||
content: const Text(
|
||||
"If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?",
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")),
|
||||
ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")),
|
||||
],
|
||||
),
|
||||
)) ??
|
||||
false;
|
||||
}
|
||||
await Store.put(StoreKey.needBetaMigration, false);
|
||||
if (migrate) {
|
||||
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]));
|
||||
return;
|
||||
}
|
||||
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]));
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()));
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -10,7 +12,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/image_converter.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
@@ -29,10 +30,27 @@ class EditImagePage extends ConsumerWidget {
|
||||
final bool isEdited;
|
||||
|
||||
const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited});
|
||||
Future<Uint8List> _imageToUint8List(Image image) async {
|
||||
final Completer<Uint8List> completer = Completer();
|
||||
image.image
|
||||
.resolve(const ImageConfiguration())
|
||||
.addListener(
|
||||
ImageStreamListener((ImageInfo info, bool _) {
|
||||
info.image.toByteData(format: ImageByteFormat.png).then((byteData) {
|
||||
if (byteData != null) {
|
||||
completer.complete(byteData.buffer.asUint8List());
|
||||
} else {
|
||||
completer.completeError('Failed to convert image to bytes');
|
||||
}
|
||||
});
|
||||
}, onError: (exception, stackTrace) => completer.completeError(exception)),
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async {
|
||||
try {
|
||||
final Uint8List imageData = await imageToUint8List(image);
|
||||
final Uint8List imageData = await _imageToUint8List(image);
|
||||
await ref
|
||||
.read(fileMediaRepositoryProvider)
|
||||
.saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg");
|
||||
|
||||
@@ -23,7 +23,7 @@ class FilterImagePage extends HookWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorFilter = useState<ColorFilter>(filters[0]);
|
||||
final colorFilter = useState<EditFilter>(filters[0]);
|
||||
final selectedFilterIndex = useState<int>(0);
|
||||
|
||||
Future<ui.Image> createFilteredImage(ui.Image inputImage, ColorFilter filter) {
|
||||
@@ -42,12 +42,12 @@ class FilterImagePage extends HookWidget {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void applyFilter(ColorFilter filter, int index) {
|
||||
void applyFilter(EditFilter filter, int index) {
|
||||
colorFilter.value = filter;
|
||||
selectedFilterIndex.value = index;
|
||||
}
|
||||
|
||||
Future<Image> applyFilterAndConvert(ColorFilter filter) async {
|
||||
Future<Image> applyFilterAndConvert(EditFilter filter) async {
|
||||
final completer = Completer<ui.Image>();
|
||||
image.image
|
||||
.resolve(ImageConfiguration.empty)
|
||||
@@ -58,7 +58,7 @@ class FilterImagePage extends HookWidget {
|
||||
);
|
||||
final uiImage = await completer.future;
|
||||
|
||||
final filteredUiImage = await createFilteredImage(uiImage, filter);
|
||||
final filteredUiImage = await createFilteredImage(uiImage, filter.colorFilter);
|
||||
final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png);
|
||||
final pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
@@ -86,7 +86,7 @@ class FilterImagePage extends HookWidget {
|
||||
SizedBox(
|
||||
height: context.height * 0.7,
|
||||
child: Center(
|
||||
child: ColorFiltered(colorFilter: colorFilter.value, child: image),
|
||||
child: ColorFiltered(colorFilter: colorFilter.value.colorFilter, child: image),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
@@ -99,7 +99,7 @@ class FilterImagePage extends HookWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _FilterButton(
|
||||
image: image,
|
||||
label: filterNames[index],
|
||||
label: filters[index].name,
|
||||
filter: filters[index],
|
||||
isSelected: selectedFilterIndex.value == index,
|
||||
onTap: () => applyFilter(filters[index], index),
|
||||
@@ -117,7 +117,7 @@ class FilterImagePage extends HookWidget {
|
||||
class _FilterButton extends StatelessWidget {
|
||||
final Image image;
|
||||
final String label;
|
||||
final ColorFilter filter;
|
||||
final EditFilter filter;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@@ -145,7 +145,7 @@ class _FilterButton extends StatelessWidget {
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
child: ColorFiltered(
|
||||
colorFilter: filter,
|
||||
colorFilter: filter.colorFilter,
|
||||
child: FittedBox(fit: BoxFit.cover, child: image),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -29,8 +29,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
|
||||
final descriptionFocusNode = useFocusNode();
|
||||
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
|
||||
final slugController = useTextEditingController(text: existingLink?.slug ?? "");
|
||||
final slugFocusNode = useFocusNode();
|
||||
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
||||
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
||||
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
||||
@@ -110,26 +108,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSlugField() {
|
||||
return TextField(
|
||||
controller: slugController,
|
||||
enabled: newShareLink.value.isEmpty,
|
||||
focusNode: slugFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
autofocus: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'custom_url'.tr(),
|
||||
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'custom_url'.tr(),
|
||||
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
||||
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
|
||||
),
|
||||
onTapOutside: (_) => slugFocusNode.unfocus(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildShowMetaButton() {
|
||||
return SwitchListTile.adaptive(
|
||||
value: showMetadata.value,
|
||||
@@ -283,7 +261,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
allowUpload: allowUpload.value,
|
||||
description: descriptionController.text.isEmpty ? null : descriptionController.text,
|
||||
password: passwordController.text.isEmpty ? null : passwordController.text,
|
||||
slug: slugController.text.isEmpty ? null : slugController.text,
|
||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||
);
|
||||
ref.invalidate(sharedLinksStateProvider);
|
||||
@@ -297,10 +274,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
if (newLink != null && serverUrl != null) {
|
||||
final hasSlug = newLink.slug?.isNotEmpty == true;
|
||||
final urlPath = hasSlug ? newLink.slug : newLink.key;
|
||||
final basePath = hasSlug ? 's' : 'share';
|
||||
newShareLink.value = "$serverUrl$basePath/$urlPath";
|
||||
newShareLink.value = "${serverUrl}share/${newLink.key}";
|
||||
copyLinkToClipboard();
|
||||
} else if (newLink == null) {
|
||||
ImmichToast.show(
|
||||
@@ -318,7 +292,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
bool? meta;
|
||||
String? desc;
|
||||
String? password;
|
||||
String? slug;
|
||||
DateTime? expiry;
|
||||
bool? changeExpiry;
|
||||
|
||||
@@ -342,12 +315,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
password = passwordController.text;
|
||||
}
|
||||
|
||||
if (slugController.text != (existingLink!.slug ?? "")) {
|
||||
slug = slugController.text.isEmpty ? null : slugController.text;
|
||||
} else {
|
||||
slug = existingLink!.slug;
|
||||
}
|
||||
|
||||
if (editExpiry.value) {
|
||||
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
||||
changeExpiry = true;
|
||||
@@ -362,7 +329,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
allowUpload: upload,
|
||||
description: desc,
|
||||
password: password,
|
||||
slug: slug,
|
||||
expiresAt: expiry,
|
||||
changeExpiry: changeExpiry,
|
||||
);
|
||||
@@ -383,7 +349,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()),
|
||||
Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()),
|
||||
Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()),
|
||||
Padding(padding: const EdgeInsets.all(padding), child: buildSlugField()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
||||
child: buildShowMetaButton(),
|
||||
|
||||
2
mobile/lib/platform/local_image_api.g.dart
generated
2
mobile/lib/platform/local_image_api.g.dart
generated
@@ -55,7 +55,6 @@ class LocalImageApi {
|
||||
required int width,
|
||||
required int height,
|
||||
required bool isVideo,
|
||||
required bool preferEncoded,
|
||||
}) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$pigeonVar_messageChannelSuffix';
|
||||
@@ -70,7 +69,6 @@ class LocalImageApi {
|
||||
width,
|
||||
height,
|
||||
isVideo,
|
||||
preferEncoded,
|
||||
]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
|
||||
31
mobile/lib/platform/native_sync_api.g.dart
generated
31
mobile/lib/platform/native_sync_api.g.dart
generated
@@ -29,8 +29,6 @@ bool _deepEquals(Object? a, Object? b) {
|
||||
return a == b;
|
||||
}
|
||||
|
||||
enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping }
|
||||
|
||||
class PlatformAsset {
|
||||
PlatformAsset({
|
||||
required this.id,
|
||||
@@ -46,7 +44,6 @@ class PlatformAsset {
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.playbackStyle,
|
||||
});
|
||||
|
||||
String id;
|
||||
@@ -75,8 +72,6 @@ class PlatformAsset {
|
||||
|
||||
double? longitude;
|
||||
|
||||
PlatformAssetPlaybackStyle playbackStyle;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
id,
|
||||
@@ -92,7 +87,6 @@ class PlatformAsset {
|
||||
adjustmentTime,
|
||||
latitude,
|
||||
longitude,
|
||||
playbackStyle,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -116,7 +110,6 @@ class PlatformAsset {
|
||||
adjustmentTime: result[10] as int?,
|
||||
latitude: result[11] as double?,
|
||||
longitude: result[12] as double?,
|
||||
playbackStyle: result[13]! as PlatformAssetPlaybackStyle,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -323,23 +316,20 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is PlatformAssetPlaybackStyle) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is PlatformAsset) {
|
||||
buffer.putUint8(130);
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is PlatformAlbum) {
|
||||
buffer.putUint8(131);
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SyncDelta) {
|
||||
buffer.putUint8(132);
|
||||
buffer.putUint8(131);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is HashResult) {
|
||||
buffer.putUint8(133);
|
||||
buffer.putUint8(132);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is CloudIdResult) {
|
||||
buffer.putUint8(134);
|
||||
buffer.putUint8(133);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
@@ -350,17 +340,14 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
return value == null ? null : PlatformAssetPlaybackStyle.values[value];
|
||||
case 130:
|
||||
return PlatformAsset.decode(readValue(buffer)!);
|
||||
case 131:
|
||||
case 130:
|
||||
return PlatformAlbum.decode(readValue(buffer)!);
|
||||
case 132:
|
||||
case 131:
|
||||
return SyncDelta.decode(readValue(buffer)!);
|
||||
case 133:
|
||||
case 132:
|
||||
return HashResult.decode(readValue(buffer)!);
|
||||
case 134:
|
||||
case 133:
|
||||
return CloudIdResult.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
|
||||
8
mobile/lib/platform/remote_image_api.g.dart
generated
8
mobile/lib/platform/remote_image_api.g.dart
generated
@@ -53,7 +53,6 @@ class RemoteImageApi {
|
||||
String url, {
|
||||
required Map<String, String> headers,
|
||||
required int requestId,
|
||||
required bool preferEncoded,
|
||||
}) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$pigeonVar_messageChannelSuffix';
|
||||
@@ -62,12 +61,7 @@ class RemoteImageApi {
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[
|
||||
url,
|
||||
headers,
|
||||
requestId,
|
||||
preferEncoded,
|
||||
]);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[url, headers, requestId]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
|
||||
100
mobile/lib/presentation/pages/dev/ui_showcase.page.dart
Normal file
100
mobile/lib/presentation/pages/dev/ui_showcase.page.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
List<Widget> _showcaseBuilder(Function(ImmichVariant variant, ImmichColor color) builder) {
|
||||
final children = <Widget>[];
|
||||
|
||||
final items = [
|
||||
(variant: ImmichVariant.filled, title: "Filled Variant"),
|
||||
(variant: ImmichVariant.ghost, title: "Ghost Variant"),
|
||||
];
|
||||
|
||||
for (final (:variant, :title) in items) {
|
||||
children.add(Text(title));
|
||||
children.add(Row(spacing: 10, children: [for (var color in ImmichColor.values) builder(variant, color)]));
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
class _ComponentTitle extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const _ComponentTitle(this.title);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(title, style: context.textTheme.titleLarge);
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class ImmichUIShowcasePage extends StatelessWidget {
|
||||
const ImmichUIShowcasePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Immich UI Showcase')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 10,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const _ComponentTitle("IconButton"),
|
||||
..._showcaseBuilder(
|
||||
(variant, color) =>
|
||||
ImmichIconButton(icon: Icons.favorite, color: color, variant: variant, onPressed: () {}),
|
||||
),
|
||||
const _ComponentTitle("CloseButton"),
|
||||
..._showcaseBuilder(
|
||||
(variant, color) => ImmichCloseButton(color: color, variant: variant, onPressed: () {}),
|
||||
),
|
||||
const _ComponentTitle("TextButton"),
|
||||
|
||||
ImmichTextButton(
|
||||
labelText: "Text Button",
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.filled,
|
||||
color: ImmichColor.primary,
|
||||
),
|
||||
ImmichTextButton(
|
||||
labelText: "Text Button",
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.filled,
|
||||
color: ImmichColor.primary,
|
||||
loading: true,
|
||||
),
|
||||
ImmichTextButton(
|
||||
labelText: "Text Button",
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.primary,
|
||||
),
|
||||
ImmichTextButton(
|
||||
labelText: "Text Button",
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.primary,
|
||||
loading: true,
|
||||
),
|
||||
const _ComponentTitle("Form"),
|
||||
ImmichForm(
|
||||
onSubmit: () {},
|
||||
child: const Column(
|
||||
spacing: 10,
|
||||
children: [ImmichTextInput(label: "Title", hintText: "Enter a title")],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
618
mobile/lib/presentation/pages/drift_edit.page.dart
Normal file
618
mobile/lib/presentation/pages/drift_edit.page.dart
Normal file
@@ -0,0 +1,618 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crop_image/crop_image.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/filters.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||
import 'package:immich_mobile/theme/theme_data.dart';
|
||||
import 'package:immich_mobile/utils/editor.utils.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:openapi/api.dart' show CropParameters, RotateParameters, MirrorParameters, MirrorAxis;
|
||||
|
||||
@RoutePage()
|
||||
class DriftEditImagePage extends ConsumerStatefulWidget {
|
||||
final Image image;
|
||||
final BaseAsset asset;
|
||||
final List<AssetEdit> edits;
|
||||
final ExifInfo exifInfo;
|
||||
final Future<void> Function(List<AssetEdit> edits) applyEdits;
|
||||
|
||||
const DriftEditImagePage({
|
||||
super.key,
|
||||
required this.image,
|
||||
required this.asset,
|
||||
required this.edits,
|
||||
required this.exifInfo,
|
||||
required this.applyEdits,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<DriftEditImagePage> createState() => _DriftEditImagePageState();
|
||||
}
|
||||
|
||||
typedef AspectRatio = ({double? ratio, String label});
|
||||
|
||||
class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with TickerProviderStateMixin {
|
||||
late final CropController cropController;
|
||||
|
||||
Duration _rotationAnimationDuration = const Duration(milliseconds: 250);
|
||||
|
||||
int _rotationAngle = 0;
|
||||
bool _flipHorizontal = false;
|
||||
bool _flipVertical = false;
|
||||
EditFilter? _filter;
|
||||
double? _aspectRatio;
|
||||
|
||||
late final originalWidth = widget.exifInfo.isFlipped ? widget.exifInfo.height : widget.exifInfo.width;
|
||||
late final originalHeight = widget.exifInfo.isFlipped ? widget.exifInfo.width : widget.exifInfo.height;
|
||||
|
||||
bool isEditing = false;
|
||||
String selectedSegment = 'transform';
|
||||
|
||||
List<AspectRatio> aspectRatios = const [
|
||||
(ratio: null, label: 'Free'),
|
||||
(ratio: 1.0, label: '1:1'),
|
||||
(ratio: 16.0 / 9.0, label: '16:9'),
|
||||
(ratio: 3.0 / 2.0, label: '3:2'),
|
||||
(ratio: 7.0 / 5.0, label: '7:5'),
|
||||
(ratio: 9.0 / 16.0, label: '9:16'),
|
||||
(ratio: 2.0 / 3.0, label: '2:3'),
|
||||
(ratio: 5.0 / 7.0, label: '5:7'),
|
||||
];
|
||||
|
||||
void initEditor() {
|
||||
final existingCrop = widget.edits.firstWhereOrNull((edit) => edit.action == AssetEditAction.crop);
|
||||
|
||||
Rect crop = existingCrop != null && originalWidth != null && originalHeight != null
|
||||
? convertCropParametersToRect(
|
||||
CropParameters.fromJson(existingCrop.parameters)!,
|
||||
originalWidth!,
|
||||
originalHeight!,
|
||||
)
|
||||
: const Rect.fromLTRB(0, 0, 1, 1);
|
||||
|
||||
cropController = CropController(defaultCrop: crop);
|
||||
|
||||
final transform = normalizeTransformEdits(widget.edits);
|
||||
|
||||
final existingFilter = widget.edits.firstWhereOrNull((edit) => edit.action == AssetEditAction.filter);
|
||||
if (existingFilter != null) {
|
||||
final parsedFilter = EditFilter.fromDtoParams(existingFilter.parameters, 'Custom');
|
||||
_filter = filters.firstWhereOrNull((filter) => filter == parsedFilter);
|
||||
}
|
||||
|
||||
// dont animate to initial rotation
|
||||
_rotationAnimationDuration = const Duration(milliseconds: 0);
|
||||
_rotationAngle = transform.rotation.toInt();
|
||||
|
||||
_flipHorizontal = transform.mirrorHorizontal;
|
||||
_flipVertical = transform.mirrorVertical;
|
||||
}
|
||||
|
||||
Future<void> _saveEditedImage() async {
|
||||
setState(() {
|
||||
isEditing = true;
|
||||
});
|
||||
|
||||
final cropParameters = convertRectToCropParameters(cropController.crop, originalWidth ?? 0, originalHeight ?? 0);
|
||||
final normalizedRotation = (_rotationAngle % 360 + 360) % 360;
|
||||
final edits = <AssetEdit>[];
|
||||
|
||||
if (cropParameters.width != originalWidth || cropParameters.height != originalHeight) {
|
||||
edits.add(AssetEdit(action: AssetEditAction.crop, parameters: cropParameters.toJson()));
|
||||
}
|
||||
|
||||
if (_flipHorizontal) {
|
||||
edits.add(
|
||||
AssetEdit(
|
||||
action: AssetEditAction.mirror,
|
||||
parameters: MirrorParameters(axis: MirrorAxis.horizontal).toJson(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_flipVertical) {
|
||||
edits.add(
|
||||
AssetEdit(
|
||||
action: AssetEditAction.mirror,
|
||||
parameters: MirrorParameters(axis: MirrorAxis.vertical).toJson(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (normalizedRotation != 0) {
|
||||
edits.add(
|
||||
AssetEdit(
|
||||
action: AssetEditAction.rotate,
|
||||
parameters: RotateParameters(angle: normalizedRotation).toJson(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_filter != null && !_filter!.isIdentity) {
|
||||
edits.add(AssetEdit(action: AssetEditAction.filter, parameters: _filter!.dtoParameters));
|
||||
}
|
||||
|
||||
await widget.applyEdits(edits);
|
||||
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initEditor();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cropController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _rotateLeft() {
|
||||
setState(() {
|
||||
_rotationAnimationDuration = const Duration(milliseconds: 150);
|
||||
_rotationAngle -= 90;
|
||||
});
|
||||
}
|
||||
|
||||
void _rotateRight() {
|
||||
setState(() {
|
||||
_rotationAnimationDuration = const Duration(milliseconds: 150);
|
||||
_rotationAngle += 90;
|
||||
});
|
||||
}
|
||||
|
||||
void _flipHorizontally() {
|
||||
setState(() {
|
||||
if (_rotationAngle % 180 != 0) {
|
||||
// When rotated 90 or 270 degrees, flipping horizontally is equivalent to flipping vertically
|
||||
_flipVertical = !_flipVertical;
|
||||
} else {
|
||||
_flipHorizontal = !_flipHorizontal;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _flipVertically() {
|
||||
setState(() {
|
||||
if (_rotationAngle % 180 != 0) {
|
||||
// When rotated 90 or 270 degrees, flipping vertically is equivalent to flipping horizontally
|
||||
_flipHorizontal = !_flipHorizontal;
|
||||
} else {
|
||||
_flipVertical = !_flipVertical;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _applyAspectRatio(double? ratio) {
|
||||
setState(() {
|
||||
if (ratio != null && _rotationAngle % 180 != 0) {
|
||||
// When rotated 90 or 270 degrees, swap width and height for aspect ratio calculations
|
||||
ratio = 1 / ratio!;
|
||||
}
|
||||
|
||||
cropController.aspectRatio = ratio;
|
||||
_aspectRatio = ratio;
|
||||
});
|
||||
}
|
||||
|
||||
void _applyFilter(EditFilter? filter) {
|
||||
setState(() {
|
||||
_filter = filter;
|
||||
});
|
||||
}
|
||||
|
||||
void _resetEdits() {
|
||||
setState(() {
|
||||
cropController.aspectRatio = null;
|
||||
cropController.crop = const Rect.fromLTRB(0, 0, 1, 1);
|
||||
_rotationAnimationDuration = const Duration(milliseconds: 250);
|
||||
_rotationAngle = 0;
|
||||
_flipHorizontal = false;
|
||||
_flipVertical = false;
|
||||
_filter = null;
|
||||
_aspectRatio = null;
|
||||
});
|
||||
}
|
||||
|
||||
bool get hasEdits {
|
||||
final isCropped = cropController.crop != const Rect.fromLTRB(0, 0, 1, 1);
|
||||
final isRotated = (_rotationAngle % 360 + 360) % 360 != 0;
|
||||
final isFlipped = _flipHorizontal || _flipVertical;
|
||||
final isFiltered = _filter != null && !_filter!.isIdentity;
|
||||
|
||||
return isCropped || isRotated || isFlipped || isFiltered;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: getThemeData(colorScheme: ref.watch(immichThemeProvider).dark, locale: context.locale),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
title: Text("edit".tr()),
|
||||
leading: const ImmichCloseButton(),
|
||||
actions: [
|
||||
isEditing
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2.5)),
|
||||
)
|
||||
: ImmichIconButton(
|
||||
icon: Icons.done_rounded,
|
||||
color: ImmichColor.primary,
|
||||
variant: ImmichVariant.ghost,
|
||||
onPressed: _saveEditedImage,
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
// Calculate the bounding box size needed for the rotated container
|
||||
final baseWidth = constraints.maxWidth * 0.9;
|
||||
final baseHeight = constraints.maxHeight * 0.95;
|
||||
|
||||
return Center(
|
||||
child: AnimatedRotation(
|
||||
turns: _rotationAngle / 360,
|
||||
duration: _rotationAnimationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.identity()
|
||||
..scaleByDouble(_flipHorizontal ? -1.0 : 1.0, _flipVertical ? -1.0 : 1.0, 1.0, 1.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
width: (_rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
||||
height: (_rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
||||
child: FutureBuilder(
|
||||
future: resolveImage(widget.image.image),
|
||||
builder: (context, data) {
|
||||
if (!data.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return CropImage(
|
||||
controller: cropController,
|
||||
image: widget.image,
|
||||
gridColor: Colors.white,
|
||||
overlayPainter: MatrixAdjustmentPainter(
|
||||
image: data.data!,
|
||||
filter: _filter?.colorFilter,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: Alignment.bottomCenter,
|
||||
clipBehavior: Clip.none,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: ref.watch(immichThemeProvider).dark.surface,
|
||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
firstCurve: Curves.easeInOut,
|
||||
secondCurve: Curves.easeInOut,
|
||||
sizeCurve: Curves.easeInOut,
|
||||
crossFadeState: selectedSegment == 'transform'
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: _TransformControls(
|
||||
onRotateLeft: _rotateLeft,
|
||||
onRotateRight: _rotateRight,
|
||||
onFlipHorizontal: _flipHorizontally,
|
||||
onFlipVertical: _flipVertically,
|
||||
onAspectRatioSelected: _applyAspectRatio,
|
||||
aspectRatio: _aspectRatio,
|
||||
),
|
||||
secondChild: _FilterControls(
|
||||
currentFilter: _filter,
|
||||
previewImage: widget.image,
|
||||
onApplyFilter: _applyFilter,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 36, left: 24, right: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
SegmentedButton(
|
||||
segments: [
|
||||
const ButtonSegment<String>(
|
||||
value: 'transform',
|
||||
label: Text('Transform'),
|
||||
icon: Icon(Icons.transform),
|
||||
),
|
||||
const ButtonSegment<String>(
|
||||
value: 'filters',
|
||||
label: Text('Filters'),
|
||||
icon: Icon(Icons.color_lens),
|
||||
),
|
||||
],
|
||||
selected: {selectedSegment},
|
||||
onSelectionChanged: (value) => setState(() {
|
||||
selectedSegment = value.first;
|
||||
}),
|
||||
showSelectedIcon: false,
|
||||
),
|
||||
const Spacer(),
|
||||
ImmichTextButton(
|
||||
labelText: "Reset",
|
||||
onPressed: _resetEdits,
|
||||
variant: ImmichVariant.filled,
|
||||
expanded: false,
|
||||
disabled: !hasEdits,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AspectRatioButton extends StatelessWidget {
|
||||
final double? currentAspectRatio;
|
||||
final double? ratio;
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _AspectRatioButton({
|
||||
required this.currentAspectRatio,
|
||||
required this.ratio,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
IconButton(
|
||||
iconSize: 36,
|
||||
icon: Transform.rotate(
|
||||
angle: (ratio ?? 1.0) < 1.0 ? pi / 2 : 0,
|
||||
child: Icon(switch (label) {
|
||||
'Free' => Icons.crop_free_rounded,
|
||||
'1:1' => Icons.crop_square_rounded,
|
||||
'16:9' => Icons.crop_16_9_rounded,
|
||||
'3:2' => Icons.crop_3_2_rounded,
|
||||
'7:5' => Icons.crop_7_5_rounded,
|
||||
'9:16' => Icons.crop_16_9_rounded,
|
||||
'2:3' => Icons.crop_3_2_rounded,
|
||||
'5:7' => Icons.crop_7_5_rounded,
|
||||
_ => Icons.crop_free_rounded,
|
||||
}, color: currentAspectRatio == ratio ? context.primaryColor : context.themeData.iconTheme.color),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
Text(label, style: context.textTheme.displayMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AspectRatioSelector extends StatelessWidget {
|
||||
final double? currentAspectRatio;
|
||||
final void Function(double?) onAspectRatioSelected;
|
||||
|
||||
const _AspectRatioSelector({required this.currentAspectRatio, required this.onAspectRatioSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final aspectRatios = <String, double?>{
|
||||
'Free': null,
|
||||
'1:1': 1.0,
|
||||
'16:9': 16 / 9,
|
||||
'3:2': 3 / 2,
|
||||
'7:5': 7 / 5,
|
||||
'9:16': 9 / 16,
|
||||
'2:3': 2 / 3,
|
||||
'5:7': 5 / 7,
|
||||
};
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: aspectRatios.entries.map((entry) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _AspectRatioButton(
|
||||
currentAspectRatio: currentAspectRatio,
|
||||
ratio: entry.value,
|
||||
label: entry.key,
|
||||
onPressed: () => onAspectRatioSelected(entry.value),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TransformControls extends StatelessWidget {
|
||||
final VoidCallback onRotateLeft;
|
||||
final VoidCallback onRotateRight;
|
||||
final VoidCallback onFlipHorizontal;
|
||||
final VoidCallback onFlipVertical;
|
||||
final void Function(double?) onAspectRatioSelected;
|
||||
final double? aspectRatio;
|
||||
|
||||
const _TransformControls({
|
||||
required this.onRotateLeft,
|
||||
required this.onRotateRight,
|
||||
required this.onFlipHorizontal,
|
||||
required this.onFlipVertical,
|
||||
required this.onAspectRatioSelected,
|
||||
required this.aspectRatio,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, top: 20, bottom: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ImmichIconButton(
|
||||
icon: Icons.rotate_left,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onPressed: onRotateLeft,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ImmichIconButton(
|
||||
icon: Icons.rotate_right,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onPressed: onRotateRight,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ImmichIconButton(
|
||||
icon: Icons.flip,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onPressed: onFlipHorizontal,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Transform.rotate(
|
||||
angle: pi / 2,
|
||||
child: ImmichIconButton(
|
||||
icon: Icons.flip,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onPressed: onFlipVertical,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_AspectRatioSelector(currentAspectRatio: aspectRatio, onAspectRatioSelected: onAspectRatioSelected),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterControls extends StatelessWidget {
|
||||
final EditFilter? currentFilter;
|
||||
final Image previewImage;
|
||||
final void Function(EditFilter?) onApplyFilter;
|
||||
|
||||
const _FilterControls({required this.currentFilter, required this.previewImage, required this.onApplyFilter});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 24),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: filters.map((filter) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _FilterButton(
|
||||
image: previewImage,
|
||||
filter: filter,
|
||||
isSelected: currentFilter == filter,
|
||||
onTap: () => onApplyFilter(filter),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterButton extends StatelessWidget {
|
||||
final Image image;
|
||||
final EditFilter filter;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _FilterButton({required this.image, required this.filter, required this.isSelected, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(isSelected ? const Radius.circular(9) : const Radius.circular(12)),
|
||||
child: ColorFiltered(
|
||||
colorFilter: filter.colorFilter,
|
||||
child: Image(image: image.image, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(filter.name, style: context.themeData.textTheme.bodyMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:crop_image/crop_image.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
/// A widget for cropping an image.
|
||||
/// This widget uses [HookWidget] to manage its lifecycle and state. It allows
|
||||
/// users to crop an image and then navigate to the [EditImagePage] with the
|
||||
/// cropped image.
|
||||
|
||||
@RoutePage()
|
||||
class DriftCropImagePage extends HookWidget {
|
||||
final Image image;
|
||||
final BaseAsset asset;
|
||||
const DriftCropImagePage({super.key, required this.image, required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cropController = useCropController();
|
||||
final aspectRatio = useState<double?>(null);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
title: Text("crop".tr()),
|
||||
leading: const ImmichCloseButton(),
|
||||
actions: [
|
||||
ImmichIconButton(
|
||||
icon: Icons.done_rounded,
|
||||
color: ImmichColor.primary,
|
||||
variant: ImmichVariant.ghost,
|
||||
onPressed: () async {
|
||||
final croppedImage = await cropController.croppedImage();
|
||||
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true)));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
width: constraints.maxWidth * 0.9,
|
||||
height: constraints.maxHeight * 0.6,
|
||||
child: CropImage(controller: cropController, image: image, gridColor: Colors.white),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: context.scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ImmichIconButton(
|
||||
icon: Icons.rotate_left,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onPressed: () => cropController.rotateLeft(),
|
||||
),
|
||||
ImmichIconButton(
|
||||
icon: Icons.rotate_right,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onPressed: () => cropController.rotateRight(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
_AspectRatioButton(
|
||||
cropController: cropController,
|
||||
aspectRatio: aspectRatio,
|
||||
ratio: null,
|
||||
label: 'Free',
|
||||
),
|
||||
_AspectRatioButton(
|
||||
cropController: cropController,
|
||||
aspectRatio: aspectRatio,
|
||||
ratio: 1.0,
|
||||
label: '1:1',
|
||||
),
|
||||
_AspectRatioButton(
|
||||
cropController: cropController,
|
||||
aspectRatio: aspectRatio,
|
||||
ratio: 16.0 / 9.0,
|
||||
label: '16:9',
|
||||
),
|
||||
_AspectRatioButton(
|
||||
cropController: cropController,
|
||||
aspectRatio: aspectRatio,
|
||||
ratio: 3.0 / 2.0,
|
||||
label: '3:2',
|
||||
),
|
||||
_AspectRatioButton(
|
||||
cropController: cropController,
|
||||
aspectRatio: aspectRatio,
|
||||
ratio: 7.0 / 5.0,
|
||||
label: '7:5',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AspectRatioButton extends StatelessWidget {
|
||||
final CropController cropController;
|
||||
final ValueNotifier<double?> aspectRatio;
|
||||
final double? ratio;
|
||||
final String label;
|
||||
|
||||
const _AspectRatioButton({
|
||||
required this.cropController,
|
||||
required this.aspectRatio,
|
||||
required this.ratio,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(switch (label) {
|
||||
'Free' => Icons.crop_free_rounded,
|
||||
'1:1' => Icons.crop_square_rounded,
|
||||
'16:9' => Icons.crop_16_9_rounded,
|
||||
'3:2' => Icons.crop_3_2_rounded,
|
||||
'7:5' => Icons.crop_7_5_rounded,
|
||||
_ => Icons.crop_free_rounded,
|
||||
}, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color),
|
||||
onPressed: () {
|
||||
cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9);
|
||||
aspectRatio.value = ratio;
|
||||
cropController.aspectRatio = ratio;
|
||||
},
|
||||
),
|
||||
Text(label, style: context.textTheme.displayMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:immich_mobile/utils/image_converter.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// A stateless widget that provides functionality for editing an image.
|
||||
///
|
||||
/// This widget allows users to edit an image provided either as an [Asset] or
|
||||
/// directly as an [Image]. It ensures that exactly one of these is provided.
|
||||
///
|
||||
/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone
|
||||
/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server.
|
||||
@immutable
|
||||
@RoutePage()
|
||||
class DriftEditImagePage extends ConsumerWidget {
|
||||
final BaseAsset asset;
|
||||
final Image image;
|
||||
final bool isEdited;
|
||||
|
||||
const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited});
|
||||
|
||||
void _exitEditing(BuildContext context) {
|
||||
// this assumes that the only way to get to this page is from the AssetViewerRoute
|
||||
context.navigator.popUntil((route) => route.data?.name == AssetViewerRoute.name);
|
||||
}
|
||||
|
||||
Future<void> _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async {
|
||||
try {
|
||||
final Uint8List imageData = await imageToUint8List(image);
|
||||
LocalAsset? localAsset;
|
||||
|
||||
try {
|
||||
localAsset = await ref
|
||||
.read(fileMediaRepositoryProvider)
|
||||
.saveLocalAsset(imageData, title: "${p.withoutExtension(asset.name)}_edited.jpg");
|
||||
} on PlatformException catch (e) {
|
||||
// OS might not return the saved image back, so we handle that gracefully
|
||||
// This can happen if app does not have full library access
|
||||
Logger("SaveEditedImage").warning("Failed to retrieve the saved image back from OS", e);
|
||||
}
|
||||
|
||||
unawaited(ref.read(backgroundSyncProvider).syncLocal(full: true));
|
||||
_exitEditing(context);
|
||||
ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!');
|
||||
|
||||
if (localAsset == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(foregroundUploadServiceProvider).uploadManual([localAsset], CancellationToken());
|
||||
} catch (e) {
|
||||
ImmichToast.show(
|
||||
durationInSecond: 6,
|
||||
context: context,
|
||||
msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("edit".tr()),
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24),
|
||||
onPressed: () => _exitEditing(context),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null,
|
||||
child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(7)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(7)),
|
||||
child: Image(image: image.image, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
height: 70,
|
||||
margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: context.scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(30)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25),
|
||||
onPressed: () {
|
||||
context.pushRoute(DriftCropImageRoute(asset: asset, image: image));
|
||||
},
|
||||
),
|
||||
Text("crop".tr(), style: context.textTheme.displayMedium),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25),
|
||||
onPressed: () {
|
||||
context.pushRoute(DriftFilterImageRoute(asset: asset, image: image));
|
||||
},
|
||||
),
|
||||
Text("filter".tr(), style: context.textTheme.displayMedium),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/constants/filters.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
/// A widget for filtering an image.
|
||||
/// This widget uses [HookWidget] to manage its lifecycle and state. It allows
|
||||
/// users to add filters to an image and then navigate to the [EditImagePage] with the
|
||||
/// final composition.'
|
||||
@RoutePage()
|
||||
class DriftFilterImagePage extends HookWidget {
|
||||
final Image image;
|
||||
final BaseAsset asset;
|
||||
|
||||
const DriftFilterImagePage({super.key, required this.image, required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorFilter = useState<ColorFilter>(filters[0]);
|
||||
final selectedFilterIndex = useState<int>(0);
|
||||
|
||||
Future<ui.Image> createFilteredImage(ui.Image inputImage, ColorFilter filter) {
|
||||
final completer = Completer<ui.Image>();
|
||||
final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble());
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
|
||||
final paint = Paint()..colorFilter = filter;
|
||||
canvas.drawImage(inputImage, Offset.zero, paint);
|
||||
|
||||
recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) {
|
||||
completer.complete(image);
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void applyFilter(ColorFilter filter, int index) {
|
||||
colorFilter.value = filter;
|
||||
selectedFilterIndex.value = index;
|
||||
}
|
||||
|
||||
Future<Image> applyFilterAndConvert(ColorFilter filter) async {
|
||||
final completer = Completer<ui.Image>();
|
||||
image.image
|
||||
.resolve(ImageConfiguration.empty)
|
||||
.addListener(
|
||||
ImageStreamListener((ImageInfo info, bool _) {
|
||||
completer.complete(info.image);
|
||||
}),
|
||||
);
|
||||
final uiImage = await completer.future;
|
||||
|
||||
final filteredUiImage = await createFilteredImage(uiImage, filter);
|
||||
final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png);
|
||||
final pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
return Image.memory(pngBytes, fit: BoxFit.contain);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
title: Text("filter".tr()),
|
||||
leading: CloseButton(color: context.primaryColor),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24),
|
||||
onPressed: () async {
|
||||
final filteredImage = await applyFilterAndConvert(colorFilter.value);
|
||||
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: filteredImage, isEdited: true)));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
body: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: context.height * 0.7,
|
||||
child: Center(
|
||||
child: ColorFiltered(colorFilter: colorFilter.value, child: image),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: filters.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _FilterButton(
|
||||
image: image,
|
||||
label: filterNames[index],
|
||||
filter: filters[index],
|
||||
isSelected: selectedFilterIndex.value == index,
|
||||
onTap: () => applyFilter(filters[index], index),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterButton extends StatelessWidget {
|
||||
final Image image;
|
||||
final String label;
|
||||
final ColorFilter filter;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _FilterButton({
|
||||
required this.image,
|
||||
required this.label,
|
||||
required this.filter,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
child: ColorFiltered(
|
||||
colorFilter: filter,
|
||||
child: FittedBox(fit: BoxFit.cover, child: image),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(label, style: context.themeData.textTheme.bodyMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:crop_image/crop_image.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/utils/image_converter.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ProfilePictureCropPage extends ConsumerStatefulWidget {
|
||||
final BaseAsset asset;
|
||||
|
||||
const ProfilePictureCropPage({super.key, required this.asset});
|
||||
|
||||
@override
|
||||
ConsumerState<ProfilePictureCropPage> createState() => _ProfilePictureCropPageState();
|
||||
}
|
||||
|
||||
class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage> {
|
||||
late final CropController _cropController;
|
||||
bool _isLoading = false;
|
||||
bool _didInitCropController = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cropController = CropController(defaultCrop: const Rect.fromLTRB(0, 0, 1, 1));
|
||||
|
||||
// Lock aspect ratio to 1:1 for circular/square crop
|
||||
// CropController depends on CropImage initializing its bitmap size.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _didInitCropController) {
|
||||
return;
|
||||
}
|
||||
_didInitCropController = true;
|
||||
|
||||
_cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9);
|
||||
_cropController.aspectRatio = 1.0;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cropController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleDone() async {
|
||||
if (_isLoading) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final croppedImage = await _cropController.croppedImage();
|
||||
final pngBytes = await imageToUint8List(croppedImage);
|
||||
final xFile = XFile.fromData(pngBytes, mimeType: 'image/png');
|
||||
final success = await ref
|
||||
.read(uploadProfileImageProvider.notifier)
|
||||
.upload(xFile, fileName: 'profile-picture.png');
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (success) {
|
||||
final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath;
|
||||
ref.read(authProvider.notifier).updateUserProfileImagePath(profileImagePath);
|
||||
final user = ref.read(currentUserProvider);
|
||||
if (user != null) {
|
||||
unawaited(ref.read(currentUserProvider.notifier).refresh());
|
||||
}
|
||||
unawaited(ref.read(backupProvider.notifier).updateDiskInfo());
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'profile_picture_set'.tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: ToastType.success,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
unawaited(context.maybePop());
|
||||
}
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'errors.unable_to_set_profile_picture'.tr(),
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'errors.unable_to_set_profile_picture'.tr(),
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Create Image widget from asset
|
||||
final image = Image(image: getFullImageProvider(widget.asset));
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
title: Text("set_profile_picture".tr()),
|
||||
leading: _isLoading ? null : const ImmichCloseButton(),
|
||||
actions: [
|
||||
if (_isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
)
|
||||
else
|
||||
ImmichIconButton(
|
||||
icon: Icons.done_rounded,
|
||||
color: ImmichColor.primary,
|
||||
variant: ImmichVariant.ghost,
|
||||
onPressed: _handleDone,
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(7)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
child: CropImage(controller: _cropController, image: image, gridColor: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -150,12 +150,10 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
handleOnSelect(Set<PersonDto> value) {
|
||||
filter.value = filter.value.copyWith(people: value);
|
||||
|
||||
final label = value.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', ');
|
||||
if (label.isNotEmpty) {
|
||||
peopleCurrentFilterWidget.value = Text(label, style: context.textTheme.labelLarge);
|
||||
} else {
|
||||
peopleCurrentFilterWidget.value = null;
|
||||
}
|
||||
peopleCurrentFilterWidget.value = Text(
|
||||
value.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '),
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
@@ -698,7 +696,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
label: 'search_filter_location'.t(context: context),
|
||||
currentFilter: locationCurrentFilterWidget.value,
|
||||
),
|
||||
if (userPreferences.valueOrNull?.tagsEnabled ?? false)
|
||||
if (userPreferences.value?.tagsEnabled ?? false)
|
||||
SearchFilterChip(
|
||||
icon: Icons.sell_outlined,
|
||||
onTap: showTagPicker,
|
||||
@@ -724,13 +722,14 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
label: 'search_filter_media_type'.t(context: context),
|
||||
currentFilter: mediaTypeCurrentFilterWidget.value,
|
||||
),
|
||||
if (userPreferences.valueOrNull?.ratingsEnabled ?? false)
|
||||
if (userPreferences.value?.ratingsEnabled ?? false) ...[
|
||||
SearchFilterChip(
|
||||
icon: Icons.star_outline_rounded,
|
||||
onTap: showStarRatingPicker,
|
||||
label: 'search_filter_star_rating'.t(context: context),
|
||||
currentFilter: ratingCurrentFilterWidget.value,
|
||||
),
|
||||
],
|
||||
SearchFilterChip(
|
||||
icon: Icons.display_settings_outlined,
|
||||
onTap: showDisplayOptionPicker,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user