mirror of
https://github.com/immich-app/immich.git
synced 2026-05-21 23:26:31 -04:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cbdac440fd | |||
| 02d356f5dd | |||
| e963eedd26 | |||
| 3da4acfe67 | |||
| e06cedb626 | |||
| ac5ef6a56d | |||
| d6c724b13b | |||
| aa87d1b9a3 | |||
| dc4da4b3d6 | |||
| 7dbd08a747 | |||
| 1d89190f96 | |||
| c2d8400899 | |||
| a100a4025e | |||
| 334fc250d3 | |||
| 28ca5f59fe | |||
| 789d82632a | |||
| 9f9569c152 | |||
| fae05270a3 | |||
| 771816f601 | |||
| e25ec4ec17 | |||
| dd9046508d | |||
| 177d1c9a30 | |||
| ded8d4e2b4 | |||
| e454c3566b | |||
| 4c79c3c902 | |||
| 3bed1b6131 | |||
| 3c9fb651d0 | |||
| 55e625a2ac | |||
| ca6c486a80 | |||
| d94d9600a7 | |||
| 11e5c42bc9 | |||
| 33c6cf8325 | |||
| dd97395f3a | |||
| 7ae268e287 | |||
| f07e2b58f0 | |||
| 4b8f90aa55 | |||
| 55ee9f76da | |||
| 30f6d4439e | |||
| f62d98a0d1 | |||
| db3d580761 | |||
| 0bc38fefe6 | |||
| acc4219849 | |||
| 5234e21241 | |||
| 17b327bfcd | |||
| d14d0a9b9b | |||
| bf47147fbb | |||
| 9ea0a69a72 | |||
| 00f43ffc25 | |||
| 96dc4a77a0 | |||
| db7158b967 | |||
| e5722c525b | |||
| f616de5af8 | |||
| 4f39663d27 | |||
| 367025a3a8 | |||
| 60dafecdc9 | |||
| 16c1c3c780 | |||
| e633bc3f24 | |||
| a07d7b0c82 | |||
| a469d350be | |||
| ccab4c88bb | |||
| 430638e129 | |||
| caebe5166a | |||
| 1bd28c3e78 | |||
| 31a55aaa73 | |||
| 8b2e1509ff | |||
| d0cb97f994 | |||
| f0cf3311d5 | |||
| 3ce0654cab | |||
| f0e2fced57 | |||
| 8ba20cbd44 | |||
| 1d25267f22 | |||
| a4d95b7aba | |||
| 25d0bdc9f5 | |||
| 905b9bd560 | |||
| 672743f543 | |||
| 27c45b5ddb | |||
| 82c6302549 | |||
| aae64b5e2f | |||
| 18bf96b4b2 | |||
| 84f2956941 | |||
| 6044b41648 | |||
| b4e16efdf4 | |||
| 19da655390 |
@@ -2,6 +2,7 @@
|
|||||||
"name": "Immich - Backend, Frontend and ML",
|
"name": "Immich - Backend, Frontend and ML",
|
||||||
"service": "immich-server",
|
"service": "immich-server",
|
||||||
"runServices": [
|
"runServices": [
|
||||||
|
"immich-init",
|
||||||
"immich-server",
|
"immich-server",
|
||||||
"redis",
|
"redis",
|
||||||
"database",
|
"database",
|
||||||
@@ -31,29 +32,8 @@
|
|||||||
"tasks": {
|
"tasks": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"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)",
|
"label": "Immich API Server (Nest)",
|
||||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
@@ -74,7 +54,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Immich Web Server (Vite)",
|
"label": "Immich Web Server (Vite)",
|
||||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
@@ -130,8 +109,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overrideCommand": true,
|
"overrideCommand": true,
|
||||||
"workspaceFolder": "/workspaces/immich",
|
"workspaceFolder": "/usr/src/app",
|
||||||
"remoteUser": "node",
|
"remoteUser": "root",
|
||||||
"userEnvProbe": "loginInteractiveShell",
|
"userEnvProbe": "loginInteractiveShell",
|
||||||
"remoteEnv": {
|
"remoteEnv": {
|
||||||
// The location where your uploaded files are stored
|
// The location where your uploaded files are stored
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
|
immich-app-base:
|
||||||
|
image: busybox
|
||||||
immich-server:
|
immich-server:
|
||||||
|
extends:
|
||||||
|
service: immich-app-base
|
||||||
|
profiles: !reset []
|
||||||
|
image: immich-server-dev:latest
|
||||||
build:
|
build:
|
||||||
target: dev-container-mobile
|
target: dev-container-mobile
|
||||||
environment:
|
environment:
|
||||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||||
volumes: !override # bind mount host to /workspaces/immich
|
volumes:
|
||||||
- ..:/workspaces/immich
|
|
||||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
- ${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
|
- /etc/localtime:/etc/localtime:ro
|
||||||
immich-web:
|
immich-web:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "Immich - Mobile",
|
"name": "Immich - Mobile",
|
||||||
"service": "immich-server",
|
"service": "immich-server",
|
||||||
"runServices": [
|
"runServices": [
|
||||||
|
"immich-init",
|
||||||
"immich-server",
|
"immich-server",
|
||||||
"redis",
|
"redis",
|
||||||
"database",
|
"database",
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"forwardPorts": [],
|
"forwardPorts": [],
|
||||||
"overrideCommand": true,
|
"overrideCommand": true,
|
||||||
"workspaceFolder": "/workspaces/immich",
|
"workspaceFolder": "/usr/src/app",
|
||||||
"remoteUser": "node",
|
"remoteUser": "node",
|
||||||
"userEnvProbe": "loginInteractiveShell",
|
"userEnvProbe": "loginInteractiveShell",
|
||||||
"remoteEnv": {
|
"remoteEnv": {
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
|
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
|
||||||
export DEV_PORT="${DEV_PORT:-3000}"
|
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"
|
IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log"
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
@@ -30,52 +25,8 @@ run_cmd() {
|
|||||||
return "${PIPESTATUS[0]}"
|
return "${PIPESTATUS[0]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find directories excluding /workspaces/immich
|
export IMMICH_WORKSPACE="/usr/src/app"
|
||||||
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 "Found immich workspace in $IMMICH_WORKSPACE"
|
||||||
log ""
|
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,26 +1,21 @@
|
|||||||
services:
|
services:
|
||||||
|
immich-app-base:
|
||||||
|
image: busybox
|
||||||
immich-server:
|
immich-server:
|
||||||
|
extends:
|
||||||
|
service: immich-app-base
|
||||||
|
profiles: !reset []
|
||||||
|
image: immich-server-dev:latest
|
||||||
build:
|
build:
|
||||||
target: dev-container-server
|
target: dev-container-server
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
hostname: immich-dev
|
hostname: immich-dev
|
||||||
environment:
|
environment:
|
||||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||||
volumes: !override
|
volumes:
|
||||||
- ..:/workspaces/immich
|
|
||||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- pnpm-store:/usr/src/app/.pnpm-store
|
- pnpm_store_server:/buildcache/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
|
- ../plugins:/build/corePlugin
|
||||||
immich-web:
|
immich-web:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install chromium --only-shell
|
run: pnpm exec playwright install chromium --only-shell
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
|||||||
+11
-5
@@ -4,12 +4,18 @@ module.exports = {
|
|||||||
if (!pkg.name) {
|
if (!pkg.name) {
|
||||||
return pkg;
|
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") {
|
if (pkg.name === "exiftool-vendored") {
|
||||||
if (pkg.optionalDependencies["exiftool-vendored.pl"]) {
|
const binaryPackage =
|
||||||
// make exiftool-vendored.pl a regular dependency
|
process.platform === "win32"
|
||||||
pkg.dependencies["exiftool-vendored.pl"] =
|
? "exiftool-vendored.exe"
|
||||||
pkg.optionalDependencies["exiftool-vendored.pl"];
|
: "exiftool-vendored.pl";
|
||||||
delete pkg.optionalDependencies["exiftool-vendored.pl"];
|
|
||||||
|
if (pkg.optionalDependencies[binaryPackage]) {
|
||||||
|
pkg.dependencies[binaryPackage] =
|
||||||
|
pkg.optionalDependencies[binaryPackage];
|
||||||
|
delete pkg.optionalDependencies[binaryPackage];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pkg;
|
return pkg;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ attach-server:
|
|||||||
docker exec -it docker_immich-server_1 sh
|
docker exec -it docker_immich-server_1 sh
|
||||||
|
|
||||||
renovate:
|
renovate:
|
||||||
LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
|
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset
|
||||||
|
|
||||||
# Directories that need to be created for volumes or build output
|
# Directories that need to be created for volumes or build output
|
||||||
VOLUME_DIRS = \
|
VOLUME_DIRS = \
|
||||||
|
|||||||
+6
-6
@@ -13,7 +13,7 @@
|
|||||||
"cli"
|
"cli"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^10.0.0",
|
||||||
"@immich/sdk": "workspace:*",
|
"@immich/sdk": "workspace:*",
|
||||||
"@types/byte-size": "^8.1.0",
|
"@types/byte-size": "^8.1.0",
|
||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.11.0",
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^10.0.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^62.0.0",
|
"eslint-plugin-unicorn": "^63.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^17.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
@@ -45,8 +45,8 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:dev": "vite build --sourcemap true",
|
"build:dev": "vite build --sourcemap true",
|
||||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "pnpm run lint --fix",
|
||||||
"prepack": "npm run build",
|
"prepack": "pnpm run build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:cov": "vitest --coverage",
|
"test:cov": "vitest --coverage",
|
||||||
"format": "prettier --check .",
|
"format": "prettier --check .",
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ import { describe, expect, it, MockedFunction, vi } from 'vitest';
|
|||||||
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
|
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
|
||||||
import createFetchMock from 'vitest-fetch-mock';
|
import createFetchMock from 'vitest-fetch-mock';
|
||||||
|
|
||||||
import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset';
|
import {
|
||||||
|
checkForDuplicates,
|
||||||
|
deleteFiles,
|
||||||
|
findSidecar,
|
||||||
|
getAlbumName,
|
||||||
|
startWatch,
|
||||||
|
uploadFiles,
|
||||||
|
UploadOptionsDto,
|
||||||
|
} from 'src/commands/asset';
|
||||||
|
|
||||||
vi.mock('@immich/sdk');
|
vi.mock('@immich/sdk');
|
||||||
|
|
||||||
@@ -309,3 +317,85 @@ describe('startWatch', () => {
|
|||||||
await fs.promises.rm(testFolder, { recursive: true, force: true });
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+32
-22
@@ -17,7 +17,7 @@ import { Matcher, watch as watchFs } from 'chokidar';
|
|||||||
import { MultiBar, Presets, SingleBar } from 'cli-progress';
|
import { MultiBar, Presets, SingleBar } from 'cli-progress';
|
||||||
import { chunk } from 'lodash-es';
|
import { chunk } from 'lodash-es';
|
||||||
import micromatch from 'micromatch';
|
import micromatch from 'micromatch';
|
||||||
import { Stats, createReadStream } from 'node:fs';
|
import { Stats, createReadStream, existsSync } from 'node:fs';
|
||||||
import { stat, unlink } from 'node:fs/promises';
|
import { stat, unlink } from 'node:fs/promises';
|
||||||
import path, { basename } from 'node:path';
|
import path, { basename } from 'node:path';
|
||||||
import { Queue } from 'src/queue';
|
import { Queue } from 'src/queue';
|
||||||
@@ -403,23 +403,6 @@ export const uploadFiles = async (
|
|||||||
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
|
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
|
||||||
const { baseUrl, headers } = defaults;
|
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();
|
const formData = new FormData();
|
||||||
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
|
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
|
||||||
formData.append('deviceId', 'CLI');
|
formData.append('deviceId', 'CLI');
|
||||||
@@ -429,8 +412,15 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
|
|||||||
formData.append('isFavorite', 'false');
|
formData.append('isFavorite', 'false');
|
||||||
formData.append('assetData', new UploadFile(input, stats.size));
|
formData.append('assetData', new UploadFile(input, stats.size));
|
||||||
|
|
||||||
if (sidecarData) {
|
const sidecarPath = findSidecar(input);
|
||||||
formData.append('sidecarData', sidecarData);
|
if (sidecarPath) {
|
||||||
|
try {
|
||||||
|
const stats = await stat(sidecarPath);
|
||||||
|
const sidecarData = new UploadFile(sidecarPath, stats.size);
|
||||||
|
formData.append('sidecarData', sidecarData);
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/assets`, {
|
const response = await fetch(`${baseUrl}/assets`, {
|
||||||
@@ -446,7 +436,19 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
|
|||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: UploadOptionsDto): Promise<void> => {
|
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> => {
|
||||||
let fileCount = 0;
|
let fileCount = 0;
|
||||||
if (options.delete) {
|
if (options.delete) {
|
||||||
fileCount += uploaded.length;
|
fileCount += uploaded.length;
|
||||||
@@ -474,7 +476,15 @@ const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: Uplo
|
|||||||
|
|
||||||
const chunkDelete = async (files: Asset[]) => {
|
const chunkDelete = async (files: Asset[]) => {
|
||||||
for (const assetBatch of chunk(files, options.concurrency)) {
|
for (const assetBatch of chunk(files, options.concurrency)) {
|
||||||
await Promise.all(assetBatch.map((input: Asset) => unlink(input.filepath)));
|
await Promise.all(
|
||||||
|
assetBatch.map(async (input: Asset) => {
|
||||||
|
await unlink(input.filepath);
|
||||||
|
const sidecarPath = findSidecar(input.filepath);
|
||||||
|
if (sidecarPath) {
|
||||||
|
await unlink(sidecarPath);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
deletionProgress.update(assetBatch.length);
|
deletionProgress.update(assetBatch.length);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# OpenAPI
|
# API
|
||||||
|
|
||||||
Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generate API documentation. To view the published docs see [here](https://api.immich.app/).
|
Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generate API documentation. To view the published docs see [here](https://api.immich.app/).
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ Immich has three main clients:
|
|||||||
3. CLI - Command-line utility for bulk upload
|
3. CLI - Command-line utility for bulk upload
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
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).
|
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).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Mobile App
|
### Mobile App
|
||||||
@@ -71,7 +71,7 @@ An incoming HTTP request is mapped to a controller (`src/controllers`). Controll
|
|||||||
|
|
||||||
### Domain Transfer Objects (DTOs)
|
### 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](./open-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](/api.md) schemas and control the generated code used by each client.
|
||||||
|
|
||||||
### Background Jobs
|
### Background Jobs
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ You can use `dart fix --apply` and `dcm fix lib` to potentially correct some iss
|
|||||||
|
|
||||||
## OpenAPI
|
## 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](/developer/open-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](/api.md) for more details.
|
||||||
|
|
||||||
## Database Migrations
|
## Database Migrations
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ 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.
|
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
|
## 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:
|
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,6 +50,7 @@ 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 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.
|
- 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).
|
- 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
|
#### OpenVINO
|
||||||
|
|
||||||
|
|||||||
+70
-48
@@ -6,7 +6,7 @@ const prism = require('prism-react-renderer');
|
|||||||
/** @type {import('@docusaurus/types').Config} */
|
/** @type {import('@docusaurus/types').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
title: 'Immich',
|
title: 'Immich',
|
||||||
tagline: 'High performance self-hosted photo and video backup solution directly from your mobile phone',
|
tagline: 'Self-hosted photo and video management solution',
|
||||||
url: 'https://docs.immich.app',
|
url: 'https://docs.immich.app',
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
@@ -93,35 +93,15 @@ const config = {
|
|||||||
position: 'right',
|
position: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/overview/quick-start',
|
href: 'https://immich.app/',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
label: 'Docs',
|
label: 'Home',
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
href: 'https://github.com/immich-app/immich',
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
href: 'https://discord.immich.app',
|
|
||||||
label: 'Discord',
|
|
||||||
position: 'right',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'html',
|
type: 'html',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
@@ -134,19 +114,78 @@ const config = {
|
|||||||
style: 'light',
|
style: 'light',
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
title: 'Overview',
|
title: 'Download',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'Quick start',
|
label: 'Android',
|
||||||
to: '/overview/quick-start',
|
href: 'https://get.immich.app/android',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Installation',
|
label: 'iOS',
|
||||||
to: '/install/requirements',
|
href: 'https://get.immich.app/ios',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Contributing',
|
label: 'Server',
|
||||||
to: '/overview/support-the-project',
|
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: 'Privacy Policy',
|
label: 'Privacy Policy',
|
||||||
@@ -155,24 +194,7 @@ const config = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Documentation',
|
title: 'Social',
|
||||||
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: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
"format:fix": "prettier --write .",
|
"format:fix": "prettier --write .",
|
||||||
"start": "docusaurus start --port 3005",
|
"start": "docusaurus start --port 3005",
|
||||||
"copy:openapi": "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
|
"copy:openapi": "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
|
||||||
"build": "npm run copy:openapi && docusaurus build",
|
"build": "pnpm run copy:openapi && docusaurus build",
|
||||||
"swizzle": "docusaurus swizzle",
|
"swizzle": "docusaurus swizzle",
|
||||||
"deploy": "docusaurus deploy",
|
"deploy": "docusaurus deploy",
|
||||||
"clear": "docusaurus clear",
|
"clear": "docusaurus clear",
|
||||||
|
|||||||
Vendored
+1
@@ -23,6 +23,7 @@
|
|||||||
/features/storage-template /administration/storage-template 307
|
/features/storage-template /administration/storage-template 307
|
||||||
/features/user-management /administration/user-management 307
|
/features/user-management /administration/user-management 307
|
||||||
/developer/contributing /developer/pr-checklist 307
|
/developer/contributing /developer/pr-checklist 307
|
||||||
|
/developer/open-api /api 307
|
||||||
/guides/machine-learning /guides/remote-machine-learning 307
|
/guides/machine-learning /guides/remote-machine-learning 307
|
||||||
/administration/password-login /administration/system-settings 307
|
/administration/password-login /administration/system-settings 307
|
||||||
/features/search /features/searching 307
|
/features/search /features/searching 307
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ services:
|
|||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich-e2e-server
|
container_name: immich-e2e-server
|
||||||
image: immich-server:latest
|
image: immich-server:latest
|
||||||
|
shm_size: 128mb
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
|
|||||||
+12
-12
@@ -8,23 +8,23 @@
|
|||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:maintenance": "vitest --run --config vitest.maintenance.config.ts",
|
"test:maintenance": "vitest --run --config vitest.maintenance.config.ts",
|
||||||
"test:web": "npx playwright test --project=web",
|
"test:web": "pnpm exec playwright test --project=web",
|
||||||
"test:web:maintenance": "npx playwright test --project=maintenance",
|
"test:web:maintenance": "pnpm exec playwright test --project=maintenance",
|
||||||
"test:web:ui": "npx playwright test --project=ui",
|
"test:web:ui": "pnpm exec playwright test --project=ui",
|
||||||
"start:web": "npx playwright test --ui --project=web",
|
"start:web": "pnpm exec playwright test --ui --project=web",
|
||||||
"start:web:maintenance": "npx playwright test --ui --project=maintenance",
|
"start:web:maintenance": "pnpm exec playwright test --ui --project=maintenance",
|
||||||
"start:web:ui": "npx playwright test --ui --project=ui",
|
"start:web:ui": "pnpm exec playwright test --ui --project=ui",
|
||||||
"format": "prettier --check .",
|
"format": "prettier --check .",
|
||||||
"format:fix": "prettier --write .",
|
"format:fix": "prettier --write .",
|
||||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "pnpm run lint --fix",
|
||||||
"check": "tsc --noEmit"
|
"check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^10.0.0",
|
||||||
"@faker-js/faker": "^10.1.0",
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@immich/cli": "workspace:*",
|
"@immich/cli": "workspace:*",
|
||||||
"@immich/e2e-auth-server": "workspace:*",
|
"@immich/e2e-auth-server": "workspace:*",
|
||||||
@@ -37,12 +37,12 @@
|
|||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^10.0.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^62.0.0",
|
"eslint-plugin-unicorn": "^63.0.0",
|
||||||
"exiftool-vendored": "^34.3.0",
|
"exiftool-vendored": "^35.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^17.0.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body.id).toEqual(facesAsset.id);
|
expect(body.id).toEqual(facesAsset.id);
|
||||||
expect(body.people).toMatchObject(expectedFaces);
|
const sortedPeople = body.people.toSorted((a: any, b: any) => a.name.localeCompare(b.name));
|
||||||
|
expect(sortedPeople).toMatchObject(expectedFaces);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ test.describe('Shared Links', () => {
|
|||||||
await page.goto(`/share/${sharedLink.key}`);
|
await page.goto(`/share/${sharedLink.key}`);
|
||||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||||
await page.locator(`[data-asset-id="${asset.id}"]`).hover();
|
await page.locator(`[data-asset-id="${asset.id}"]`).hover();
|
||||||
await page.waitForSelector('[data-group] svg');
|
await page.waitForSelector(`[data-asset-id="${asset.id}"] [role="checkbox"]`);
|
||||||
await page.getByRole('checkbox').click();
|
|
||||||
await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).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])!;
|
const asset = getAsset(timelineRestData, album.assetIds[0])!;
|
||||||
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
||||||
await thumbnailUtils.expectInViewport(page, asset.id);
|
await thumbnailUtils.expectInViewport(page, asset.id);
|
||||||
await thumbnailUtils.expectSelectedReadonly(page, asset.id);
|
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
|
||||||
});
|
});
|
||||||
test('Add photos to album', async ({ page }) => {
|
test('Add photos to album', async ({ page }) => {
|
||||||
const album = timelineRestData.album;
|
const album = timelineRestData.album;
|
||||||
@@ -447,7 +447,7 @@ test.describe('Timeline', () => {
|
|||||||
const asset = getAsset(timelineRestData, album.assetIds[0])!;
|
const asset = getAsset(timelineRestData, album.assetIds[0])!;
|
||||||
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
||||||
await thumbnailUtils.expectInViewport(page, asset.id);
|
await thumbnailUtils.expectInViewport(page, asset.id);
|
||||||
await thumbnailUtils.expectSelectedReadonly(page, asset.id);
|
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
|
||||||
await pageUtils.selectDay(page, 'Tue, Feb 27, 2024');
|
await pageUtils.selectDay(page, 'Tue, Feb 27, 2024');
|
||||||
const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => {
|
const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => {
|
||||||
const requestJson = request.postDataJSON();
|
const requestJson = request.postDataJSON();
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const thumbnailUtils = {
|
|||||||
return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`);
|
return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`);
|
||||||
},
|
},
|
||||||
selectedAsset(page: Page) {
|
selectedAsset(page: Page) {
|
||||||
return page.locator('[data-thumbnail-focus-container]:has(button[aria-checked])');
|
return page.locator('[data-thumbnail-focus-container][data-selected]');
|
||||||
},
|
},
|
||||||
async clickAssetId(page: Page, assetId: string) {
|
async clickAssetId(page: Page, assetId: string) {
|
||||||
await thumbnailUtils.withAssetId(page, assetId).click();
|
await thumbnailUtils.withAssetId(page, assetId).click();
|
||||||
@@ -102,12 +102,9 @@ export const thumbnailUtils = {
|
|||||||
async expectThumbnailIsNotArchive(page: Page, assetId: string) {
|
async expectThumbnailIsNotArchive(page: Page, assetId: string) {
|
||||||
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
|
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
|
||||||
},
|
},
|
||||||
async expectSelectedReadonly(page: Page, assetId: string) {
|
async expectSelectedDisabled(page: Page, assetId: string) {
|
||||||
// todo - need a data attribute for selected
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(
|
page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected][data-disabled]`),
|
||||||
`[data-thumbnail-focus-container][data-asset="${assetId}"] > .group.cursor-not-allowed > .rounded-xl`,
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
},
|
},
|
||||||
async expectTimelineHasOnScreenAssets(page: Page) {
|
async expectTimelineHasOnScreenAssets(page: Page) {
|
||||||
|
|||||||
+11
-3
@@ -1074,6 +1074,7 @@
|
|||||||
"failed_to_update_notification_status": "Failed to update notification status",
|
"failed_to_update_notification_status": "Failed to update notification status",
|
||||||
"incorrect_email_or_password": "Incorrect email or password",
|
"incorrect_email_or_password": "Incorrect email or password",
|
||||||
"library_folder_already_exists": "This import path already exists.",
|
"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",
|
"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.",
|
"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",
|
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
|
||||||
@@ -1809,9 +1810,8 @@
|
|||||||
"rate_asset": "Rate Asset",
|
"rate_asset": "Rate Asset",
|
||||||
"rating": "Star rating",
|
"rating": "Star rating",
|
||||||
"rating_clear": "Clear rating",
|
"rating_clear": "Clear rating",
|
||||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
"rating_count": "{count, plural, =0 {Unrated} one {# star} other {# stars}}",
|
||||||
"rating_description": "Display the EXIF rating in the info panel",
|
"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",
|
"reaction_options": "Reaction options",
|
||||||
"read_changelog": "Read Changelog",
|
"read_changelog": "Read Changelog",
|
||||||
"readonly_mode_disabled": "Read-only mode disabled",
|
"readonly_mode_disabled": "Read-only mode disabled",
|
||||||
@@ -1883,7 +1883,10 @@
|
|||||||
"reset_pin_code_success": "Successfully reset PIN code",
|
"reset_pin_code_success": "Successfully reset PIN code",
|
||||||
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
|
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
|
||||||
"reset_sqlite": "Reset SQLite Database",
|
"reset_sqlite": "Reset SQLite Database",
|
||||||
"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_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_success": "Successfully reset the SQLite database",
|
"reset_sqlite_success": "Successfully reset the SQLite database",
|
||||||
"reset_to_default": "Reset to default",
|
"reset_to_default": "Reset to default",
|
||||||
"resolution": "Resolution",
|
"resolution": "Resolution",
|
||||||
@@ -1911,6 +1914,7 @@
|
|||||||
"saved_settings": "Saved settings",
|
"saved_settings": "Saved settings",
|
||||||
"say_something": "Say something",
|
"say_something": "Say something",
|
||||||
"scaffold_body_error_occurred": "Error occurred",
|
"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": "Scan",
|
||||||
"scan_all_libraries": "Scan All Libraries",
|
"scan_all_libraries": "Scan All Libraries",
|
||||||
"scan_library": "Scan",
|
"scan_library": "Scan",
|
||||||
@@ -2026,6 +2030,9 @@
|
|||||||
"set_profile_picture": "Set profile picture",
|
"set_profile_picture": "Set profile picture",
|
||||||
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
|
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
|
||||||
"set_stack_primary_asset": "Set as primary asset",
|
"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_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_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",
|
"setting_image_viewer_original_title": "Load original image",
|
||||||
@@ -2304,6 +2311,7 @@
|
|||||||
"unstack_action_prompt": "{count} unstacked",
|
"unstack_action_prompt": "{count} unstacked",
|
||||||
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
||||||
"unsupported_field_type": "Unsupported field type",
|
"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",
|
"untagged": "Untagged",
|
||||||
"untitled_workflow": "Untitled workflow",
|
"untitled_workflow": "Untitled workflow",
|
||||||
"up_next": "Up next",
|
"up_next": "Up next",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
+10
-50
@@ -22,48 +22,7 @@ FROM builder-cpu AS builder-rknn
|
|||||||
|
|
||||||
# Warning: 25GiB+ disk space required to pull this image
|
# Warning: 25GiB+ disk space required to pull this image
|
||||||
# TODO: find a way to reduce the image size
|
# TODO: find a way to reduce the image size
|
||||||
FROM rocm/dev-ubuntu-24.04:6.4.4-complete@sha256:31418ac10a3769a71eaef330c07280d1d999d7074621339b8f93c484c35f6078 AS builder-rocm
|
FROM rocm/dev-ubuntu-24.04:7.2-complete@sha256:86e11093b4a7ec2a79b1b6701d10e840a6994f21c7e05929b51eb9be361c683a 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
|
FROM builder-${DEVICE} AS builder
|
||||||
|
|
||||||
@@ -79,9 +38,6 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|||||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
--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
|
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
|
FROM python:3.11-slim-bookworm@sha256:04cd27899595a99dfe77709d96f08876bf2ee99139ee2f0fe9ac948005034e5b AS prod-cpu
|
||||||
|
|
||||||
@@ -92,14 +48,14 @@ FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c3
|
|||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||||
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.28.4/intel-igc-core-2_2.28.4+20760_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/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.01.36711.4/intel-opencl-icd_26.01.36711.4-0_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/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-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/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 && \
|
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
|
# 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.01.36711.4/libigdgmm12_22.9.0_amd64.deb && \
|
wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/libigdgmm12_22.9.0_amd64.deb && \
|
||||||
dpkg -i *.deb && \
|
dpkg -i *.deb && \
|
||||||
rm *.deb && \
|
rm *.deb && \
|
||||||
apt-get remove wget -yqq && \
|
apt-get remove wget -yqq && \
|
||||||
@@ -120,7 +76,11 @@ 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/python3.11 /usr/local/lib/python3.11
|
||||||
COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so
|
COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so
|
||||||
|
|
||||||
FROM rocm/dev-ubuntu-24.04:6.4.4-complete@sha256:31418ac10a3769a71eaef330c07280d1d999d7074621339b8f93c484c35f6078 AS prod-rocm
|
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 prod-cpu AS prod-armnn
|
FROM prod-cpu AS prod-armnn
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class Settings(BaseSettings):
|
|||||||
preload: PreloadModelData | None = None
|
preload: PreloadModelData | None = None
|
||||||
max_batch_size: MaxBatchSize | None = None
|
max_batch_size: MaxBatchSize | None = None
|
||||||
openvino_precision: ModelPrecision = ModelPrecision.FP32
|
openvino_precision: ModelPrecision = ModelPrecision.FP32
|
||||||
|
rocm_precision: ModelPrecision = ModelPrecision.FP32
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_id(self) -> str:
|
def device_id(self) -> str:
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ _PADDLE_MODELS = {
|
|||||||
|
|
||||||
SUPPORTED_PROVIDERS = [
|
SUPPORTED_PROVIDERS = [
|
||||||
"CUDAExecutionProvider",
|
"CUDAExecutionProvider",
|
||||||
"ROCMExecutionProvider",
|
"MIGraphXExecutionProvider",
|
||||||
"OpenVINOExecutionProvider",
|
"OpenVINOExecutionProvider",
|
||||||
"CoreMLExecutionProvider",
|
"CoreMLExecutionProvider",
|
||||||
"CPUExecutionProvider",
|
"CPUExecutionProvider",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import onnxruntime as ort
|
|||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from immich_ml.models.constants import SUPPORTED_PROVIDERS
|
from immich_ml.models.constants import SUPPORTED_PROVIDERS
|
||||||
from immich_ml.schemas import SessionNode
|
from immich_ml.schemas import ModelPrecision, SessionNode
|
||||||
|
|
||||||
from ..config import log, settings
|
from ..config import log, settings
|
||||||
|
|
||||||
@@ -90,8 +90,17 @@ class OrtSession:
|
|||||||
match provider:
|
match provider:
|
||||||
case "CPUExecutionProvider":
|
case "CPUExecutionProvider":
|
||||||
options = {"arena_extend_strategy": "kSameAsRequested"}
|
options = {"arena_extend_strategy": "kSameAsRequested"}
|
||||||
case "CUDAExecutionProvider" | "ROCMExecutionProvider":
|
case "CUDAExecutionProvider":
|
||||||
options = {"arena_extend_strategy": "kSameAsRequested", "device_id": settings.device_id}
|
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":
|
case "OpenVINOExecutionProvider":
|
||||||
openvino_dir = self.model_path.parent / "openvino"
|
openvino_dir = self.model_path.parent / "openvino"
|
||||||
device = f"GPU.{settings.device_id}"
|
device = f"GPU.{settings.device_id}"
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
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) {
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -49,10 +49,10 @@ dev = ["locust>=2.15.1", { include-group = "test" }, { include-group = "lint" }]
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
cpu = ["onnxruntime>=1.23.2,<2"]
|
cpu = ["onnxruntime>=1.23.2,<2"]
|
||||||
cuda = ["onnxruntime-gpu>=1.23.2,<2"]
|
cuda = ["onnxruntime-gpu>=1.23.2,<2"]
|
||||||
openvino = ["onnxruntime-openvino>=1.23.0,<2"]
|
openvino = ["onnxruntime-openvino>=1.24.1,<2"]
|
||||||
armnn = ["onnxruntime>=1.23.2,<2"]
|
armnn = ["onnxruntime>=1.23.2,<2"]
|
||||||
rknn = ["onnxruntime>=1.23.2,<2", "rknn-toolkit-lite2>=2.3.0,<3"]
|
rknn = ["onnxruntime>=1.23.2,<2", "rknn-toolkit-lite2>=2.3.0,<3"]
|
||||||
rocm = []
|
rocm = ["onnxruntime-migraphx>=1.23.2,<2"]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
compile-bytecode = true
|
compile-bytecode = true
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ class TestOrtSession:
|
|||||||
OV_EP = ["OpenVINOExecutionProvider", "CPUExecutionProvider"]
|
OV_EP = ["OpenVINOExecutionProvider", "CPUExecutionProvider"]
|
||||||
CUDA_EP_OUT_OF_ORDER = ["CPUExecutionProvider", "CUDAExecutionProvider"]
|
CUDA_EP_OUT_OF_ORDER = ["CPUExecutionProvider", "CUDAExecutionProvider"]
|
||||||
TRT_EP = ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"]
|
TRT_EP = ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"]
|
||||||
ROCM_EP = ["ROCMExecutionProvider", "CPUExecutionProvider"]
|
ROCM_EP = ["MIGraphXExecutionProvider", "CPUExecutionProvider"]
|
||||||
COREML_EP = ["CoreMLExecutionProvider", "CPUExecutionProvider"]
|
COREML_EP = ["CoreMLExecutionProvider", "CPUExecutionProvider"]
|
||||||
|
|
||||||
@pytest.mark.providers(CPU_EP)
|
@pytest.mark.providers(CPU_EP)
|
||||||
@@ -289,12 +289,38 @@ class TestOrtSession:
|
|||||||
|
|
||||||
assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}]
|
assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}]
|
||||||
|
|
||||||
def test_sets_provider_options_for_rocm(self) -> None:
|
def test_sets_provider_options_for_rocm(self, mocker: MockerFixture) -> None:
|
||||||
|
model_path = "/cache/ViT-B-32__openai/textual/model.onnx"
|
||||||
os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
|
os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
|
||||||
|
mkdir = mocker.patch("immich_ml.sessions.ort.Path.mkdir")
|
||||||
|
|
||||||
session = OrtSession("ViT-B-32__openai", providers=["ROCMExecutionProvider"])
|
session = OrtSession(model_path, providers=["MIGraphXExecutionProvider"])
|
||||||
|
|
||||||
assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}]
|
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)
|
||||||
|
|
||||||
def test_sets_provider_options_kwarg(self) -> None:
|
def test_sets_provider_options_kwarg(self) -> None:
|
||||||
session = OrtSession(
|
session = OrtSession(
|
||||||
|
|||||||
Generated
+81
-84
@@ -262,18 +262,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "colorlog"
|
name = "colorlog"
|
||||||
version = "6.9.0"
|
version = "6.9.0"
|
||||||
@@ -654,18 +642,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "gevent"
|
name = "gevent"
|
||||||
version = "24.10.3"
|
version = "24.10.3"
|
||||||
@@ -788,14 +764,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gunicorn"
|
name = "gunicorn"
|
||||||
version = "23.0.0"
|
version = "25.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "packaging" },
|
{ name = "packaging" },
|
||||||
]
|
]
|
||||||
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" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -898,18 +874,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.11"
|
version = "3.11"
|
||||||
@@ -939,7 +903,6 @@ source = { editable = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiocache" },
|
{ name = "aiocache" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "ftfy" },
|
|
||||||
{ name = "gunicorn" },
|
{ name = "gunicorn" },
|
||||||
{ name = "huggingface-hub" },
|
{ name = "huggingface-hub" },
|
||||||
{ name = "insightface" },
|
{ name = "insightface" },
|
||||||
@@ -973,6 +936,9 @@ rknn = [
|
|||||||
{ name = "onnxruntime" },
|
{ name = "onnxruntime" },
|
||||||
{ name = "rknn-toolkit-lite2" },
|
{ name = "rknn-toolkit-lite2" },
|
||||||
]
|
]
|
||||||
|
rocm = [
|
||||||
|
{ name = "onnxruntime-migraphx" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -1018,7 +984,6 @@ types = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiocache", specifier = ">=0.12.1,<1.0" },
|
{ name = "aiocache", specifier = ">=0.12.1,<1.0" },
|
||||||
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
|
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
|
||||||
{ name = "ftfy", specifier = ">=6.1.1" },
|
|
||||||
{ name = "gunicorn", specifier = ">=21.1.0" },
|
{ name = "gunicorn", specifier = ">=21.1.0" },
|
||||||
{ name = "huggingface-hub", specifier = ">=0.20.1,<1.0" },
|
{ name = "huggingface-hub", specifier = ">=0.20.1,<1.0" },
|
||||||
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
|
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
|
||||||
@@ -1027,7 +992,8 @@ requires-dist = [
|
|||||||
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
|
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
|
||||||
{ name = "onnxruntime", marker = "extra == 'rknn'", 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-gpu", marker = "extra == 'cuda'", specifier = ">=1.23.2,<2" },
|
||||||
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.23.0,<2" },
|
{ name = "onnxruntime-migraphx", marker = "extra == 'rocm'", specifier = ">=1.23.2,<2" },
|
||||||
|
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" },
|
||||||
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
||||||
{ name = "orjson", specifier = ">=3.9.5" },
|
{ name = "orjson", specifier = ">=3.9.5" },
|
||||||
{ name = "pillow", specifier = ">=12.1.1,<12.2" },
|
{ name = "pillow", specifier = ">=12.1.1,<12.2" },
|
||||||
@@ -1443,32 +1409,55 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "msgpack"
|
name = "msgpack"
|
||||||
version = "1.0.7"
|
version = "1.1.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
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" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1716,11 +1705,10 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime-openvino"
|
name = "onnxruntime-migraphx"
|
||||||
version = "1.23.0"
|
version = "1.24.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "coloredlogs" },
|
|
||||||
{ name = "flatbuffers" },
|
{ name = "flatbuffers" },
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
{ name = "packaging" },
|
{ name = "packaging" },
|
||||||
@@ -1728,12 +1716,30 @@ dependencies = [
|
|||||||
{ name = "sympy" },
|
{ name = "sympy" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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/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/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/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/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/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/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/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" },
|
||||||
{ 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]]
|
||||||
|
name = "onnxruntime-openvino"
|
||||||
|
version = "1.24.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "flatbuffers" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "protobuf" },
|
||||||
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2173,15 +2179,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.0.2"
|
version = "9.0.2"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ config_roots = [
|
|||||||
[tools]
|
[tools]
|
||||||
node = "24.13.1"
|
node = "24.13.1"
|
||||||
flutter = "3.35.7"
|
flutter = "3.35.7"
|
||||||
pnpm = "10.29.3"
|
pnpm = "10.30.0"
|
||||||
terragrunt = "0.98.0"
|
terragrunt = "0.98.0"
|
||||||
opentofu = "1.11.4"
|
opentofu = "1.11.4"
|
||||||
java = "21.0.2"
|
java = "21.0.2"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ private open class LocalImagesPigeonCodec : StandardMessageCodec() {
|
|||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface LocalImageApi {
|
interface LocalImageApi {
|
||||||
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
|
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, preferEncoded: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
|
||||||
fun cancelRequest(requestId: Long)
|
fun cancelRequest(requestId: Long)
|
||||||
fun getThumbhash(thumbhash: String, callback: (Result<Map<String, Long>>) -> Unit)
|
fun getThumbhash(thumbhash: String, callback: (Result<Map<String, Long>>) -> Unit)
|
||||||
|
|
||||||
@@ -82,7 +82,8 @@ interface LocalImageApi {
|
|||||||
val widthArg = args[2] as Long
|
val widthArg = args[2] as Long
|
||||||
val heightArg = args[3] as Long
|
val heightArg = args[3] as Long
|
||||||
val isVideoArg = args[4] as Boolean
|
val isVideoArg = args[4] as Boolean
|
||||||
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg) { result: Result<Map<String, Long>?> ->
|
val preferEncodedArg = args[5] as Boolean
|
||||||
|
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg, preferEncodedArg) { result: Result<Map<String, Long>?> ->
|
||||||
val error = result.exceptionOrNull()
|
val error = result.exceptionOrNull()
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
reply.reply(LocalImagesPigeonUtils.wrapError(error))
|
reply.reply(LocalImagesPigeonUtils.wrapError(error))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import android.util.Size
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import app.alextran.immich.NativeBuffer
|
import app.alextran.immich.NativeBuffer
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.Priority
|
import com.bumptech.glide.Priority
|
||||||
@@ -48,7 +49,6 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
|||||||
try {
|
try {
|
||||||
val buffer = NativeBuffer.wrap(pointer, size)
|
val buffer = NativeBuffer.wrap(pointer, size)
|
||||||
copyPixelsToBuffer(buffer)
|
copyPixelsToBuffer(buffer)
|
||||||
recycle()
|
|
||||||
return mapOf(
|
return mapOf(
|
||||||
"pointer" to pointer,
|
"pointer" to pointer,
|
||||||
"width" to width.toLong(),
|
"width" to width.toLong(),
|
||||||
@@ -57,8 +57,9 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
|||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
NativeBuffer.free(pointer)
|
NativeBuffer.free(pointer)
|
||||||
recycle()
|
|
||||||
throw e
|
throw e
|
||||||
|
} finally {
|
||||||
|
recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,12 +100,17 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
|||||||
width: Long,
|
width: Long,
|
||||||
height: Long,
|
height: Long,
|
||||||
isVideo: Boolean,
|
isVideo: Boolean,
|
||||||
|
preferEncoded: Boolean,
|
||||||
callback: (Result<Map<String, Long>?>) -> Unit
|
callback: (Result<Map<String, Long>?>) -> Unit
|
||||||
) {
|
) {
|
||||||
val signal = CancellationSignal()
|
val signal = CancellationSignal()
|
||||||
val task = threadPool.submit {
|
val task = threadPool.submit {
|
||||||
try {
|
try {
|
||||||
getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal)
|
if (preferEncoded) {
|
||||||
|
getEncodedImageInternal(assetId, callback, signal)
|
||||||
|
} else {
|
||||||
|
getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is OperationCanceledException -> callback(CANCELLED)
|
is OperationCanceledException -> callback(CANCELLED)
|
||||||
@@ -133,6 +139,35 @@ 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(
|
private fun getThumbnailBufferInternal(
|
||||||
assetId: String,
|
assetId: String,
|
||||||
width: Long,
|
width: Long,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ private open class RemoteImagesPigeonCodec : StandardMessageCodec() {
|
|||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface RemoteImageApi {
|
interface RemoteImageApi {
|
||||||
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>?>) -> Unit)
|
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, preferEncoded: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
|
||||||
fun cancelRequest(requestId: Long)
|
fun cancelRequest(requestId: Long)
|
||||||
fun clearCache(callback: (Result<Long>) -> Unit)
|
fun clearCache(callback: (Result<Long>) -> Unit)
|
||||||
|
|
||||||
@@ -68,7 +68,8 @@ interface RemoteImageApi {
|
|||||||
val urlArg = args[0] as String
|
val urlArg = args[0] as String
|
||||||
val headersArg = args[1] as Map<String, String>
|
val headersArg = args[1] as Map<String, String>
|
||||||
val requestIdArg = args[2] as Long
|
val requestIdArg = args[2] as Long
|
||||||
api.requestImage(urlArg, headersArg, requestIdArg) { result: Result<Map<String, Long>?> ->
|
val preferEncodedArg = args[3] as Boolean
|
||||||
|
api.requestImage(urlArg, headersArg, requestIdArg, preferEncodedArg) { result: Result<Map<String, Long>?> ->
|
||||||
val error = result.exceptionOrNull()
|
val error = result.exceptionOrNull()
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
reply.reply(RemoteImagesPigeonUtils.wrapError(error))
|
reply.reply(RemoteImagesPigeonUtils.wrapError(error))
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
url: String,
|
url: String,
|
||||||
headers: Map<String, String>,
|
headers: Map<String, String>,
|
||||||
requestId: Long,
|
requestId: Long,
|
||||||
|
@Suppress("UNUSED_PARAMETER") preferEncoded: Boolean, // always returns encoded; setting has no effect on Android
|
||||||
callback: (Result<Map<String, Long>?>) -> Unit
|
callback: (Result<Map<String, Long>?>) -> Unit
|
||||||
) {
|
) {
|
||||||
val signal = CancellationSignal()
|
val signal = CancellationSignal()
|
||||||
|
|||||||
@@ -78,6 +78,21 @@ class FlutterError (
|
|||||||
val details: Any? = null
|
val details: Any? = null
|
||||||
) : Throwable()
|
) : 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. */
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
data class PlatformAsset (
|
data class PlatformAsset (
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -92,7 +107,8 @@ data class PlatformAsset (
|
|||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
val adjustmentTime: Long? = null,
|
val adjustmentTime: Long? = null,
|
||||||
val latitude: Double? = null,
|
val latitude: Double? = null,
|
||||||
val longitude: Double? = null
|
val longitude: Double? = null,
|
||||||
|
val playbackStyle: PlatformAssetPlaybackStyle
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
@@ -110,7 +126,8 @@ data class PlatformAsset (
|
|||||||
val adjustmentTime = pigeonVar_list[10] as Long?
|
val adjustmentTime = pigeonVar_list[10] as Long?
|
||||||
val latitude = pigeonVar_list[11] as Double?
|
val latitude = pigeonVar_list[11] as Double?
|
||||||
val longitude = pigeonVar_list[12] as Double?
|
val longitude = pigeonVar_list[12] as Double?
|
||||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude)
|
val playbackStyle = pigeonVar_list[13] as PlatformAssetPlaybackStyle
|
||||||
|
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@@ -128,6 +145,7 @@ data class PlatformAsset (
|
|||||||
adjustmentTime,
|
adjustmentTime,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
|
playbackStyle,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -290,26 +308,31 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
|||||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
129.toByte() -> {
|
129.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as Long?)?.let {
|
||||||
PlatformAsset.fromList(it)
|
PlatformAssetPlaybackStyle.ofRaw(it.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
130.toByte() -> {
|
130.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
PlatformAlbum.fromList(it)
|
PlatformAsset.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
131.toByte() -> {
|
131.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
SyncDelta.fromList(it)
|
PlatformAlbum.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
132.toByte() -> {
|
132.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
HashResult.fromList(it)
|
SyncDelta.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
133.toByte() -> {
|
133.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
HashResult.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
134.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
CloudIdResult.fromList(it)
|
CloudIdResult.fromList(it)
|
||||||
}
|
}
|
||||||
@@ -319,26 +342,30 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
|||||||
}
|
}
|
||||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
when (value) {
|
when (value) {
|
||||||
is PlatformAsset -> {
|
is PlatformAssetPlaybackStyle -> {
|
||||||
stream.write(129)
|
stream.write(129)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.raw)
|
||||||
}
|
}
|
||||||
is PlatformAlbum -> {
|
is PlatformAsset -> {
|
||||||
stream.write(130)
|
stream.write(130)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is SyncDelta -> {
|
is PlatformAlbum -> {
|
||||||
stream.write(131)
|
stream.write(131)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is HashResult -> {
|
is SyncDelta -> {
|
||||||
stream.write(132)
|
stream.write(132)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is CloudIdResult -> {
|
is HashResult -> {
|
||||||
stream.write(133)
|
stream.write(133)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
|
is CloudIdResult -> {
|
||||||
|
stream.write(134)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
else -> super.writeValue(stream, value)
|
else -> super.writeValue(stream, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import app.alextran.immich.core.ImmichPlugin
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -28,6 +34,8 @@ sealed class AssetResult {
|
|||||||
data class InvalidAsset(val assetId: String) : AssetResult()
|
data class InvalidAsset(val assetId: String) : AssetResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val TAG = "NativeSyncApiImplBase"
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
@@ -39,6 +47,13 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
|||||||
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
|
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
|
||||||
private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED"
|
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 =
|
const val MEDIA_SELECTION =
|
||||||
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
|
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
|
||||||
val MEDIA_SELECTION_ARGS = arrayOf(
|
val MEDIA_SELECTION_ARGS = arrayOf(
|
||||||
@@ -60,9 +75,15 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
|||||||
add(MediaStore.MediaColumns.DURATION)
|
add(MediaStore.MediaColumns.DURATION)
|
||||||
add(MediaStore.MediaColumns.ORIENTATION)
|
add(MediaStore.MediaColumns.ORIENTATION)
|
||||||
// IS_FAVORITE is only available on Android 11 and above
|
// IS_FAVORITE is only available on Android 11 and above
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
add(MediaStore.MediaColumns.IS_FAVORITE)
|
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()
|
}.toTypedArray()
|
||||||
|
|
||||||
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
||||||
@@ -109,9 +130,12 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
|||||||
val orientationColumn =
|
val orientationColumn =
|
||||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
||||||
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
|
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()) {
|
while (c.moveToNext()) {
|
||||||
val id = c.getLong(idColumn).toString()
|
val numericId = c.getLong(idColumn)
|
||||||
|
val id = numericId.toString()
|
||||||
val name = c.getStringOrNull(nameColumn)
|
val name = c.getStringOrNull(nameColumn)
|
||||||
val bucketId = c.getStringOrNull(bucketIdColumn)
|
val bucketId = c.getStringOrNull(bucketIdColumn)
|
||||||
val path = c.getStringOrNull(dataColumn)
|
val path = c.getStringOrNull(dataColumn)
|
||||||
@@ -125,10 +149,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaType = when (c.getInt(mediaTypeColumn)) {
|
val rawMediaType = c.getInt(mediaTypeColumn)
|
||||||
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1
|
val assetType: Long = when (rawMediaType) {
|
||||||
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2
|
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1L
|
||||||
else -> 0
|
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2L
|
||||||
|
else -> 0L
|
||||||
}
|
}
|
||||||
// Date taken is milliseconds since epoch, Date added is seconds since epoch
|
// Date taken is milliseconds since epoch, Date added is seconds since epoch
|
||||||
val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
|
val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
|
||||||
@@ -138,15 +163,19 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
|||||||
val width = c.getInt(widthColumn).toLong()
|
val width = c.getInt(widthColumn).toLong()
|
||||||
val height = c.getInt(heightColumn).toLong()
|
val height = c.getInt(heightColumn).toLong()
|
||||||
// Duration is milliseconds
|
// Duration is milliseconds
|
||||||
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
|
val duration = if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0L
|
||||||
else c.getLong(durationColumn) / 1000
|
else c.getLong(durationColumn) / 1000
|
||||||
val orientation = c.getInt(orientationColumn)
|
val orientation = c.getInt(orientationColumn)
|
||||||
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
||||||
|
|
||||||
|
val playbackStyle = detectPlaybackStyle(
|
||||||
|
numericId, rawMediaType, specialFormatColumn, xmpColumn, c
|
||||||
|
)
|
||||||
|
|
||||||
val asset = PlatformAsset(
|
val asset = PlatformAsset(
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
mediaType.toLong(),
|
assetType,
|
||||||
createdAt,
|
createdAt,
|
||||||
modifiedAt,
|
modifiedAt,
|
||||||
width,
|
width,
|
||||||
@@ -154,6 +183,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
|||||||
duration,
|
duration,
|
||||||
orientation.toLong(),
|
orientation.toLong(),
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
playbackStyle = playbackStyle,
|
||||||
)
|
)
|
||||||
yield(AssetResult.ValidAsset(asset, bucketId))
|
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||||
}
|
}
|
||||||
@@ -161,6 +191,81 @@ 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> {
|
fun getAlbums(): List<PlatformAlbum> {
|
||||||
val albums = mutableListOf<PlatformAlbum>()
|
val albums = mutableListOf<PlatformAlbum>()
|
||||||
val albumsCount = mutableMapOf<String, Int>()
|
val albumsCount = mutableMapOf<String, Int>()
|
||||||
|
|||||||
+1
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.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol LocalImageApi {
|
protocol LocalImageApi {
|
||||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
||||||
func cancelRequest(requestId: Int64) throws
|
func cancelRequest(requestId: Int64) throws
|
||||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,8 @@ class LocalImageApiSetup {
|
|||||||
let widthArg = args[2] as! Int64
|
let widthArg = args[2] as! Int64
|
||||||
let heightArg = args[3] as! Int64
|
let heightArg = args[3] as! Int64
|
||||||
let isVideoArg = args[4] as! Bool
|
let isVideoArg = args[4] as! Bool
|
||||||
api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg) { result in
|
let preferEncodedArg = args[5] as! Bool
|
||||||
|
api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg, preferEncoded: preferEncodedArg) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let res):
|
case .success(let res):
|
||||||
reply(wrapResult(res))
|
reply(wrapResult(res))
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class LocalImageApiImpl: LocalImageApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||||
let request = LocalImageRequest(callback: completion)
|
let request = LocalImageRequest(callback: completion)
|
||||||
let item = DispatchWorkItem {
|
let item = DispatchWorkItem {
|
||||||
if request.isCancelled {
|
if request.isCancelled {
|
||||||
@@ -91,6 +91,49 @@ class LocalImageApiImpl: LocalImageApi {
|
|||||||
return completion(ImageProcessing.cancelledResult)
|
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?
|
var image: UIImage?
|
||||||
Self.imageManager.requestImage(
|
Self.imageManager.requestImage(
|
||||||
for: asset,
|
for: asset,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
|
|||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol RemoteImageApi {
|
protocol RemoteImageApi {
|
||||||
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
func requestImage(url: String, headers: [String: String], requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
||||||
func cancelRequest(requestId: Int64) throws
|
func cancelRequest(requestId: Int64) throws
|
||||||
func clearCache(completion: @escaping (Result<Int64, Error>) -> Void)
|
func clearCache(completion: @escaping (Result<Int64, Error>) -> Void)
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,8 @@ class RemoteImageApiSetup {
|
|||||||
let urlArg = args[0] as! String
|
let urlArg = args[0] as! String
|
||||||
let headersArg = args[1] as! [String: String]
|
let headersArg = args[1] as! [String: String]
|
||||||
let requestIdArg = args[2] as! Int64
|
let requestIdArg = args[2] as! Int64
|
||||||
api.requestImage(url: urlArg, headers: headersArg, requestId: requestIdArg) { result in
|
let preferEncodedArg = args[3] as! Bool
|
||||||
|
api.requestImage(url: urlArg, headers: headersArg, requestId: requestIdArg, preferEncoded: preferEncodedArg) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let res):
|
case .success(let res):
|
||||||
reply(wrapResult(res))
|
reply(wrapResult(res))
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||||||
kCGImageSourceCreateThumbnailFromImageAlways: true
|
kCGImageSourceCreateThumbnailFromImageAlways: true
|
||||||
] as CFDictionary
|
] as CFDictionary
|
||||||
|
|
||||||
func requestImage(url: String, headers: [String : String], requestId: Int64, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
|
func requestImage(url: String, headers: [String : String], requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
|
||||||
var urlRequest = URLRequest(url: URL(string: url)!)
|
var urlRequest = URLRequest(url: URL(string: url)!)
|
||||||
urlRequest.cachePolicy = .returnCacheDataElseLoad
|
urlRequest.cachePolicy = .returnCacheDataElseLoad
|
||||||
for (key, value) in headers {
|
for (key, value) in headers {
|
||||||
@@ -41,7 +41,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in
|
let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in
|
||||||
Self.handleCompletion(requestId: requestId, data: data, response: response, error: error)
|
Self.handleCompletion(requestId: requestId, encoded: preferEncoded, data: data, response: response, error: error)
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = RemoteImageRequest(id: requestId, task: task, completion: completion)
|
let request = RemoteImageRequest(id: requestId, task: task, completion: completion)
|
||||||
@@ -53,7 +53,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||||||
task.resume()
|
task.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleCompletion(requestId: Int64, data: Data?, response: URLResponse?, error: Error?) {
|
private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) {
|
||||||
os_unfair_lock_lock(&Self.lock)
|
os_unfair_lock_lock(&Self.lock)
|
||||||
guard let request = requests[requestId] else {
|
guard let request = requests[requestId] else {
|
||||||
return os_unfair_lock_unlock(&Self.lock)
|
return os_unfair_lock_unlock(&Self.lock)
|
||||||
@@ -84,6 +84,24 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||||||
return request.completion(ImageProcessing.cancelledResult)
|
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),
|
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil),
|
||||||
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else {
|
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else {
|
||||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil)))
|
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil)))
|
||||||
|
|||||||
@@ -128,6 +128,15 @@ 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.
|
/// Generated class from Pigeon that represents data sent in messages.
|
||||||
struct PlatformAsset: Hashable {
|
struct PlatformAsset: Hashable {
|
||||||
var id: String
|
var id: String
|
||||||
@@ -143,6 +152,7 @@ struct PlatformAsset: Hashable {
|
|||||||
var adjustmentTime: Int64? = nil
|
var adjustmentTime: Int64? = nil
|
||||||
var latitude: Double? = nil
|
var latitude: Double? = nil
|
||||||
var longitude: Double? = nil
|
var longitude: Double? = nil
|
||||||
|
var playbackStyle: PlatformAssetPlaybackStyle
|
||||||
|
|
||||||
|
|
||||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
@@ -160,6 +170,7 @@ struct PlatformAsset: Hashable {
|
|||||||
let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10])
|
let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10])
|
||||||
let latitude: Double? = nilOrValue(pigeonVar_list[11])
|
let latitude: Double? = nilOrValue(pigeonVar_list[11])
|
||||||
let longitude: Double? = nilOrValue(pigeonVar_list[12])
|
let longitude: Double? = nilOrValue(pigeonVar_list[12])
|
||||||
|
let playbackStyle = pigeonVar_list[13] as! PlatformAssetPlaybackStyle
|
||||||
|
|
||||||
return PlatformAsset(
|
return PlatformAsset(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -174,7 +185,8 @@ struct PlatformAsset: Hashable {
|
|||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
adjustmentTime: adjustmentTime,
|
adjustmentTime: adjustmentTime,
|
||||||
latitude: latitude,
|
latitude: latitude,
|
||||||
longitude: longitude
|
longitude: longitude,
|
||||||
|
playbackStyle: playbackStyle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func toList() -> [Any?] {
|
func toList() -> [Any?] {
|
||||||
@@ -192,6 +204,7 @@ struct PlatformAsset: Hashable {
|
|||||||
adjustmentTime,
|
adjustmentTime,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
|
playbackStyle,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||||
@@ -349,14 +362,20 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
|
|||||||
override func readValue(ofType type: UInt8) -> Any? {
|
override func readValue(ofType type: UInt8) -> Any? {
|
||||||
switch type {
|
switch type {
|
||||||
case 129:
|
case 129:
|
||||||
return PlatformAsset.fromList(self.readValue() as! [Any?])
|
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
|
||||||
|
if let enumResultAsInt = enumResultAsInt {
|
||||||
|
return PlatformAssetPlaybackStyle(rawValue: enumResultAsInt)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
case 130:
|
case 130:
|
||||||
return PlatformAlbum.fromList(self.readValue() as! [Any?])
|
return PlatformAsset.fromList(self.readValue() as! [Any?])
|
||||||
case 131:
|
case 131:
|
||||||
return SyncDelta.fromList(self.readValue() as! [Any?])
|
return PlatformAlbum.fromList(self.readValue() as! [Any?])
|
||||||
case 132:
|
case 132:
|
||||||
return HashResult.fromList(self.readValue() as! [Any?])
|
return SyncDelta.fromList(self.readValue() as! [Any?])
|
||||||
case 133:
|
case 133:
|
||||||
|
return HashResult.fromList(self.readValue() as! [Any?])
|
||||||
|
case 134:
|
||||||
return CloudIdResult.fromList(self.readValue() as! [Any?])
|
return CloudIdResult.fromList(self.readValue() as! [Any?])
|
||||||
default:
|
default:
|
||||||
return super.readValue(ofType: type)
|
return super.readValue(ofType: type)
|
||||||
@@ -366,21 +385,24 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
|
|||||||
|
|
||||||
private class MessagesPigeonCodecWriter: FlutterStandardWriter {
|
private class MessagesPigeonCodecWriter: FlutterStandardWriter {
|
||||||
override func writeValue(_ value: Any) {
|
override func writeValue(_ value: Any) {
|
||||||
if let value = value as? PlatformAsset {
|
if let value = value as? PlatformAssetPlaybackStyle {
|
||||||
super.writeByte(129)
|
super.writeByte(129)
|
||||||
super.writeValue(value.toList())
|
super.writeValue(value.rawValue)
|
||||||
} else if let value = value as? PlatformAlbum {
|
} else if let value = value as? PlatformAsset {
|
||||||
super.writeByte(130)
|
super.writeByte(130)
|
||||||
super.writeValue(value.toList())
|
super.writeValue(value.toList())
|
||||||
} else if let value = value as? SyncDelta {
|
} else if let value = value as? PlatformAlbum {
|
||||||
super.writeByte(131)
|
super.writeByte(131)
|
||||||
super.writeValue(value.toList())
|
super.writeValue(value.toList())
|
||||||
} else if let value = value as? HashResult {
|
} else if let value = value as? SyncDelta {
|
||||||
super.writeByte(132)
|
super.writeByte(132)
|
||||||
super.writeValue(value.toList())
|
super.writeValue(value.toList())
|
||||||
} else if let value = value as? CloudIdResult {
|
} else if let value = value as? HashResult {
|
||||||
super.writeByte(133)
|
super.writeByte(133)
|
||||||
super.writeValue(value.toList())
|
super.writeValue(value.toList())
|
||||||
|
} else if let value = value as? CloudIdResult {
|
||||||
|
super.writeByte(134)
|
||||||
|
super.writeValue(value.toList())
|
||||||
} else {
|
} else {
|
||||||
super.writeValue(value)
|
super.writeValue(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,8 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
type: 0,
|
type: 0,
|
||||||
durationInSeconds: 0,
|
durationInSeconds: 0,
|
||||||
orientation: 0,
|
orientation: 0,
|
||||||
isFavorite: false
|
isFavorite: false,
|
||||||
|
playbackStyle: .unknown
|
||||||
)
|
)
|
||||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import Photos
|
import Photos
|
||||||
|
|
||||||
extension PHAsset {
|
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 {
|
func toPlatformAsset() -> PlatformAsset {
|
||||||
return PlatformAsset(
|
return PlatformAsset(
|
||||||
id: localIdentifier,
|
id: localIdentifier,
|
||||||
@@ -15,7 +26,8 @@ extension PHAsset {
|
|||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
adjustmentTime: adjustmentTimestamp,
|
adjustmentTime: adjustmentTimestamp,
|
||||||
latitude: location?.coordinate.latitude,
|
latitude: location?.coordinate.latitude,
|
||||||
longitude: location?.coordinate.longitude
|
longitude: location?.coordinate.longitude,
|
||||||
|
playbackStyle: platformPlaybackStyle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,5 @@ enum ActionSource { timeline, viewer }
|
|||||||
enum CleanupStep { selectDate, scan, delete }
|
enum CleanupStep { selectDate, scan, delete }
|
||||||
|
|
||||||
enum AssetKeepType { none, photosOnly, videosOnly }
|
enum AssetKeepType { none, photosOnly, videosOnly }
|
||||||
|
|
||||||
|
enum AssetDateAggregation { start, end }
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ enum StoreKey<T> {
|
|||||||
autoPlayVideo<bool>._(139),
|
autoPlayVideo<bool>._(139),
|
||||||
albumGridView<bool>._(140),
|
albumGridView<bool>._(140),
|
||||||
|
|
||||||
|
// Image viewer navigation settings
|
||||||
|
tapToNavigate<bool>._(141),
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
photoManagerCustomFilter<bool>._(1000),
|
photoManagerCustomFilter<bool>._(1000),
|
||||||
betaPromptShown<bool>._(1001),
|
betaPromptShown<bool>._(1001),
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ class RemoteAlbumService {
|
|||||||
AlbumSortMode.title => albums.sortedBy((album) => album.name),
|
AlbumSortMode.title => albums.sortedBy((album) => album.name),
|
||||||
AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
||||||
AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
||||||
AlbumSortMode.mostRecent => await _sortByNewestAsset(albums),
|
AlbumSortMode.mostRecent => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.end),
|
||||||
AlbumSortMode.mostOldest => await _sortByOldestAsset(albums),
|
AlbumSortMode.mostOldest => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.start),
|
||||||
};
|
};
|
||||||
final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder;
|
final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder;
|
||||||
|
|
||||||
@@ -172,46 +172,25 @@ class RemoteAlbumService {
|
|||||||
return _repository.getAlbumsContainingAsset(assetId);
|
return _repository.getAlbumsContainingAsset(assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
|
Future<List<RemoteAlbum>> _sortByAssetDate(
|
||||||
// map album IDs to their newest asset dates
|
List<RemoteAlbum> albums, {
|
||||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {};
|
required AssetDateAggregation aggregation,
|
||||||
for (final album in albums) {
|
}) async {
|
||||||
assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(album.id);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// await all database queries
|
return sortedAlbums;
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final semVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
|
final serverSemVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
|
||||||
|
|
||||||
final value = Store.get(StoreKey.syncMigrationStatus, "[]");
|
final value = Store.get(StoreKey.syncMigrationStatus, "[]");
|
||||||
final migrations = (jsonDecode(value) as List).cast<String>();
|
final migrations = (jsonDecode(value) as List).cast<String>();
|
||||||
int previousLength = migrations.length;
|
int previousLength = migrations.length;
|
||||||
await _runPreSyncTasks(migrations, semVer);
|
await _runPreSyncTasks(migrations, serverSemVer);
|
||||||
|
|
||||||
if (migrations.length != previousLength) {
|
if (migrations.length != previousLength) {
|
||||||
_logger.info("Updated pre-sync migration status: $migrations");
|
_logger.info("Updated pre-sync migration status: $migrations");
|
||||||
@@ -82,10 +82,14 @@ class SyncStreamService {
|
|||||||
|
|
||||||
// Start the sync stream and handle events
|
// Start the sync stream and handle events
|
||||||
bool shouldReset = false;
|
bool shouldReset = false;
|
||||||
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
|
await _syncApiRepository.streamChanges(
|
||||||
|
_handleEvents,
|
||||||
|
serverVersion: serverSemVer,
|
||||||
|
onReset: () => shouldReset = true,
|
||||||
|
);
|
||||||
if (shouldReset) {
|
if (shouldReset) {
|
||||||
_logger.info("Resetting sync state as requested by server");
|
_logger.info("Resetting sync state as requested by server");
|
||||||
await _syncApiRepository.streamChanges(_handleEvents);
|
await _syncApiRepository.streamChanges(_handleEvents, serverVersion: serverSemVer);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousLength = migrations.length;
|
previousLength = migrations.length;
|
||||||
@@ -282,6 +286,8 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.deletePeopleV1(data.cast());
|
return _syncStreamRepository.deletePeopleV1(data.cast());
|
||||||
case SyncEntityType.assetFaceV1:
|
case SyncEntityType.assetFaceV1:
|
||||||
return _syncStreamRepository.updateAssetFacesV1(data.cast());
|
return _syncStreamRepository.updateAssetFacesV1(data.cast());
|
||||||
|
case SyncEntityType.assetFaceV2:
|
||||||
|
return _syncStreamRepository.updateAssetFacesV2(data.cast());
|
||||||
case SyncEntityType.assetFaceDeleteV1:
|
case SyncEntityType.assetFaceDeleteV1:
|
||||||
return _syncStreamRepository.deleteAssetFacesV1(data.cast());
|
return _syncStreamRepository.deleteAssetFacesV1(data.cast());
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class AssetFaceEntity extends Table with DriftDefaultsMixin {
|
|||||||
|
|
||||||
TextColumn get sourceType => text()();
|
TextColumn get sourceType => text()();
|
||||||
|
|
||||||
|
BoolColumn get isVisible => boolean().withDefault(const Constant(true))();
|
||||||
|
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|||||||
+202
-68
@@ -5,11 +5,12 @@ import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.da
|
|||||||
as i1;
|
as i1;
|
||||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'
|
||||||
as i2;
|
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'
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||||
as i3;
|
as i4;
|
||||||
import 'package:drift/internal/modular.dart' as i4;
|
import 'package:drift/internal/modular.dart' as i5;
|
||||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||||
as i5;
|
as i6;
|
||||||
|
|
||||||
typedef $$AssetFaceEntityTableCreateCompanionBuilder =
|
typedef $$AssetFaceEntityTableCreateCompanionBuilder =
|
||||||
i1.AssetFaceEntityCompanion Function({
|
i1.AssetFaceEntityCompanion Function({
|
||||||
@@ -23,6 +24,8 @@ typedef $$AssetFaceEntityTableCreateCompanionBuilder =
|
|||||||
required int boundingBoxX2,
|
required int boundingBoxX2,
|
||||||
required int boundingBoxY2,
|
required int boundingBoxY2,
|
||||||
required String sourceType,
|
required String sourceType,
|
||||||
|
i0.Value<bool> isVisible,
|
||||||
|
i0.Value<DateTime?> deletedAt,
|
||||||
});
|
});
|
||||||
typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
|
typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
|
||||||
i1.AssetFaceEntityCompanion Function({
|
i1.AssetFaceEntityCompanion Function({
|
||||||
@@ -36,6 +39,8 @@ typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
|
|||||||
i0.Value<int> boundingBoxX2,
|
i0.Value<int> boundingBoxX2,
|
||||||
i0.Value<int> boundingBoxY2,
|
i0.Value<int> boundingBoxY2,
|
||||||
i0.Value<String> sourceType,
|
i0.Value<String> sourceType,
|
||||||
|
i0.Value<bool> isVisible,
|
||||||
|
i0.Value<DateTime?> deletedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
final class $$AssetFaceEntityTableReferences
|
final class $$AssetFaceEntityTableReferences
|
||||||
@@ -51,29 +56,29 @@ final class $$AssetFaceEntityTableReferences
|
|||||||
super.$_typedResult,
|
super.$_typedResult,
|
||||||
);
|
);
|
||||||
|
|
||||||
static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
static i4.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
||||||
i4.ReadDatabaseContainer(db)
|
i5.ReadDatabaseContainer(db)
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
|
.resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||||
.createAlias(
|
.createAlias(
|
||||||
i0.$_aliasNameGenerator(
|
i0.$_aliasNameGenerator(
|
||||||
i4.ReadDatabaseContainer(db)
|
i5.ReadDatabaseContainer(db)
|
||||||
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
||||||
.assetId,
|
.assetId,
|
||||||
i4.ReadDatabaseContainer(
|
i5.ReadDatabaseContainer(
|
||||||
db,
|
db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
i4.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
||||||
final $_column = $_itemColumn<String>('asset_id')!;
|
final $_column = $_itemColumn<String>('asset_id')!;
|
||||||
|
|
||||||
final manager = i3
|
final manager = i4
|
||||||
.$$RemoteAssetEntityTableTableManager(
|
.$$RemoteAssetEntityTableTableManager(
|
||||||
$_db,
|
$_db,
|
||||||
i4.ReadDatabaseContainer(
|
i5.ReadDatabaseContainer(
|
||||||
$_db,
|
$_db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
)
|
)
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
||||||
@@ -83,29 +88,29 @@ final class $$AssetFaceEntityTableReferences
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static i5.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) =>
|
static i6.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) =>
|
||||||
i4.ReadDatabaseContainer(db)
|
i5.ReadDatabaseContainer(db)
|
||||||
.resultSet<i5.$PersonEntityTable>('person_entity')
|
.resultSet<i6.$PersonEntityTable>('person_entity')
|
||||||
.createAlias(
|
.createAlias(
|
||||||
i0.$_aliasNameGenerator(
|
i0.$_aliasNameGenerator(
|
||||||
i4.ReadDatabaseContainer(db)
|
i5.ReadDatabaseContainer(db)
|
||||||
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
||||||
.personId,
|
.personId,
|
||||||
i4.ReadDatabaseContainer(
|
i5.ReadDatabaseContainer(
|
||||||
db,
|
db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity').id,
|
).resultSet<i6.$PersonEntityTable>('person_entity').id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
i5.$$PersonEntityTableProcessedTableManager? get personId {
|
i6.$$PersonEntityTableProcessedTableManager? get personId {
|
||||||
final $_column = $_itemColumn<String>('person_id');
|
final $_column = $_itemColumn<String>('person_id');
|
||||||
if ($_column == null) return null;
|
if ($_column == null) return null;
|
||||||
final manager = i5
|
final manager = i6
|
||||||
.$$PersonEntityTableTableManager(
|
.$$PersonEntityTableTableManager(
|
||||||
$_db,
|
$_db,
|
||||||
i4.ReadDatabaseContainer(
|
i5.ReadDatabaseContainer(
|
||||||
$_db,
|
$_db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
)
|
)
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
final item = $_typedResult.readTableOrNull(_personIdTable($_db));
|
final item = $_typedResult.readTableOrNull(_personIdTable($_db));
|
||||||
@@ -165,24 +170,34 @@ class $$AssetFaceEntityTableFilterComposer
|
|||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
i3.$$RemoteAssetEntityTableFilterComposer get assetId {
|
i0.ColumnFilters<bool> get isVisible => $composableBuilder(
|
||||||
final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
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(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.assetId,
|
getCurrentColumn: (t) => t.assetId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$$RemoteAssetEntityTableFilterComposer(
|
}) => i4.$$RemoteAssetEntityTableFilterComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -192,24 +207,24 @@ class $$AssetFaceEntityTableFilterComposer
|
|||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
i5.$$PersonEntityTableFilterComposer get personId {
|
i6.$$PersonEntityTableFilterComposer get personId {
|
||||||
final i5.$$PersonEntityTableFilterComposer composer = $composerBuilder(
|
final i6.$$PersonEntityTableFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.personId,
|
getCurrentColumn: (t) => t.personId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i5.$$PersonEntityTableFilterComposer(
|
}) => i6.$$PersonEntityTableFilterComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -269,25 +284,35 @@ class $$AssetFaceEntityTableOrderingComposer
|
|||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
i3.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
i0.ColumnOrderings<bool> get isVisible => $composableBuilder(
|
||||||
final i3.$$RemoteAssetEntityTableOrderingComposer composer =
|
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 =
|
||||||
$composerBuilder(
|
$composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.assetId,
|
getCurrentColumn: (t) => t.assetId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$$RemoteAssetEntityTableOrderingComposer(
|
}) => i4.$$RemoteAssetEntityTableOrderingComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -297,24 +322,24 @@ class $$AssetFaceEntityTableOrderingComposer
|
|||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
i5.$$PersonEntityTableOrderingComposer get personId {
|
i6.$$PersonEntityTableOrderingComposer get personId {
|
||||||
final i5.$$PersonEntityTableOrderingComposer composer = $composerBuilder(
|
final i6.$$PersonEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.personId,
|
getCurrentColumn: (t) => t.personId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i5.$$PersonEntityTableOrderingComposer(
|
}) => i6.$$PersonEntityTableOrderingComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -372,25 +397,31 @@ class $$AssetFaceEntityTableAnnotationComposer
|
|||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
);
|
);
|
||||||
|
|
||||||
i3.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
i0.GeneratedColumn<bool> get isVisible =>
|
||||||
final i3.$$RemoteAssetEntityTableAnnotationComposer composer =
|
$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 =
|
||||||
$composerBuilder(
|
$composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.assetId,
|
getCurrentColumn: (t) => t.assetId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$$RemoteAssetEntityTableAnnotationComposer(
|
}) => i4.$$RemoteAssetEntityTableAnnotationComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -400,24 +431,24 @@ class $$AssetFaceEntityTableAnnotationComposer
|
|||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
i5.$$PersonEntityTableAnnotationComposer get personId {
|
i6.$$PersonEntityTableAnnotationComposer get personId {
|
||||||
final i5.$$PersonEntityTableAnnotationComposer composer = $composerBuilder(
|
final i6.$$PersonEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.personId,
|
getCurrentColumn: (t) => t.personId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i5.$$PersonEntityTableAnnotationComposer(
|
}) => i6.$$PersonEntityTableAnnotationComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -468,6 +499,8 @@ class $$AssetFaceEntityTableTableManager
|
|||||||
i0.Value<int> boundingBoxX2 = const i0.Value.absent(),
|
i0.Value<int> boundingBoxX2 = const i0.Value.absent(),
|
||||||
i0.Value<int> boundingBoxY2 = const i0.Value.absent(),
|
i0.Value<int> boundingBoxY2 = const i0.Value.absent(),
|
||||||
i0.Value<String> sourceType = 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(
|
}) => i1.AssetFaceEntityCompanion(
|
||||||
id: id,
|
id: id,
|
||||||
assetId: assetId,
|
assetId: assetId,
|
||||||
@@ -479,6 +512,8 @@ class $$AssetFaceEntityTableTableManager
|
|||||||
boundingBoxX2: boundingBoxX2,
|
boundingBoxX2: boundingBoxX2,
|
||||||
boundingBoxY2: boundingBoxY2,
|
boundingBoxY2: boundingBoxY2,
|
||||||
sourceType: sourceType,
|
sourceType: sourceType,
|
||||||
|
isVisible: isVisible,
|
||||||
|
deletedAt: deletedAt,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
({
|
({
|
||||||
@@ -492,6 +527,8 @@ class $$AssetFaceEntityTableTableManager
|
|||||||
required int boundingBoxX2,
|
required int boundingBoxX2,
|
||||||
required int boundingBoxY2,
|
required int boundingBoxY2,
|
||||||
required String sourceType,
|
required String sourceType,
|
||||||
|
i0.Value<bool> isVisible = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
}) => i1.AssetFaceEntityCompanion.insert(
|
}) => i1.AssetFaceEntityCompanion.insert(
|
||||||
id: id,
|
id: id,
|
||||||
assetId: assetId,
|
assetId: assetId,
|
||||||
@@ -503,6 +540,8 @@ class $$AssetFaceEntityTableTableManager
|
|||||||
boundingBoxX2: boundingBoxX2,
|
boundingBoxX2: boundingBoxX2,
|
||||||
boundingBoxY2: boundingBoxY2,
|
boundingBoxY2: boundingBoxY2,
|
||||||
sourceType: sourceType,
|
sourceType: sourceType,
|
||||||
|
isVisible: isVisible,
|
||||||
|
deletedAt: deletedAt,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map(
|
.map(
|
||||||
@@ -709,6 +748,33 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
|||||||
type: i0.DriftSqlType.string,
|
type: i0.DriftSqlType.string,
|
||||||
requiredDuringInsert: true,
|
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
|
@override
|
||||||
List<i0.GeneratedColumn> get $columns => [
|
List<i0.GeneratedColumn> get $columns => [
|
||||||
id,
|
id,
|
||||||
@@ -721,6 +787,8 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
|||||||
boundingBoxX2,
|
boundingBoxX2,
|
||||||
boundingBoxY2,
|
boundingBoxY2,
|
||||||
sourceType,
|
sourceType,
|
||||||
|
isVisible,
|
||||||
|
deletedAt,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@@ -824,6 +892,18 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
|||||||
} else if (isInserting) {
|
} else if (isInserting) {
|
||||||
context.missing(_sourceTypeMeta);
|
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;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,6 +953,14 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
|||||||
i0.DriftSqlType.string,
|
i0.DriftSqlType.string,
|
||||||
data['${effectivePrefix}source_type'],
|
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'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,6 +987,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
final int boundingBoxX2;
|
final int boundingBoxX2;
|
||||||
final int boundingBoxY2;
|
final int boundingBoxY2;
|
||||||
final String sourceType;
|
final String sourceType;
|
||||||
|
final bool isVisible;
|
||||||
|
final DateTime? deletedAt;
|
||||||
const AssetFaceEntityData({
|
const AssetFaceEntityData({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.assetId,
|
required this.assetId,
|
||||||
@@ -910,6 +1000,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
required this.boundingBoxX2,
|
required this.boundingBoxX2,
|
||||||
required this.boundingBoxY2,
|
required this.boundingBoxY2,
|
||||||
required this.sourceType,
|
required this.sourceType,
|
||||||
|
required this.isVisible,
|
||||||
|
this.deletedAt,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
@@ -926,6 +1018,10 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
map['bounding_box_x2'] = i0.Variable<int>(boundingBoxX2);
|
map['bounding_box_x2'] = i0.Variable<int>(boundingBoxX2);
|
||||||
map['bounding_box_y2'] = i0.Variable<int>(boundingBoxY2);
|
map['bounding_box_y2'] = i0.Variable<int>(boundingBoxY2);
|
||||||
map['source_type'] = i0.Variable<String>(sourceType);
|
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;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,6 +1041,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
boundingBoxX2: serializer.fromJson<int>(json['boundingBoxX2']),
|
boundingBoxX2: serializer.fromJson<int>(json['boundingBoxX2']),
|
||||||
boundingBoxY2: serializer.fromJson<int>(json['boundingBoxY2']),
|
boundingBoxY2: serializer.fromJson<int>(json['boundingBoxY2']),
|
||||||
sourceType: serializer.fromJson<String>(json['sourceType']),
|
sourceType: serializer.fromJson<String>(json['sourceType']),
|
||||||
|
isVisible: serializer.fromJson<bool>(json['isVisible']),
|
||||||
|
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
@@ -961,6 +1059,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
'boundingBoxX2': serializer.toJson<int>(boundingBoxX2),
|
'boundingBoxX2': serializer.toJson<int>(boundingBoxX2),
|
||||||
'boundingBoxY2': serializer.toJson<int>(boundingBoxY2),
|
'boundingBoxY2': serializer.toJson<int>(boundingBoxY2),
|
||||||
'sourceType': serializer.toJson<String>(sourceType),
|
'sourceType': serializer.toJson<String>(sourceType),
|
||||||
|
'isVisible': serializer.toJson<bool>(isVisible),
|
||||||
|
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,6 +1075,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
int? boundingBoxX2,
|
int? boundingBoxX2,
|
||||||
int? boundingBoxY2,
|
int? boundingBoxY2,
|
||||||
String? sourceType,
|
String? sourceType,
|
||||||
|
bool? isVisible,
|
||||||
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
}) => i1.AssetFaceEntityData(
|
}) => i1.AssetFaceEntityData(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
assetId: assetId ?? this.assetId,
|
assetId: assetId ?? this.assetId,
|
||||||
@@ -986,6 +1088,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
||||||
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
||||||
sourceType: sourceType ?? this.sourceType,
|
sourceType: sourceType ?? this.sourceType,
|
||||||
|
isVisible: isVisible ?? this.isVisible,
|
||||||
|
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
|
||||||
);
|
);
|
||||||
AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) {
|
AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) {
|
||||||
return AssetFaceEntityData(
|
return AssetFaceEntityData(
|
||||||
@@ -1013,6 +1117,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
sourceType: data.sourceType.present
|
sourceType: data.sourceType.present
|
||||||
? data.sourceType.value
|
? data.sourceType.value
|
||||||
: this.sourceType,
|
: this.sourceType,
|
||||||
|
isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible,
|
||||||
|
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1028,7 +1134,9 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
..write('boundingBoxY1: $boundingBoxY1, ')
|
..write('boundingBoxY1: $boundingBoxY1, ')
|
||||||
..write('boundingBoxX2: $boundingBoxX2, ')
|
..write('boundingBoxX2: $boundingBoxX2, ')
|
||||||
..write('boundingBoxY2: $boundingBoxY2, ')
|
..write('boundingBoxY2: $boundingBoxY2, ')
|
||||||
..write('sourceType: $sourceType')
|
..write('sourceType: $sourceType, ')
|
||||||
|
..write('isVisible: $isVisible, ')
|
||||||
|
..write('deletedAt: $deletedAt')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
@@ -1045,6 +1153,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
boundingBoxX2,
|
boundingBoxX2,
|
||||||
boundingBoxY2,
|
boundingBoxY2,
|
||||||
sourceType,
|
sourceType,
|
||||||
|
isVisible,
|
||||||
|
deletedAt,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -1059,7 +1169,9 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
other.boundingBoxY1 == this.boundingBoxY1 &&
|
other.boundingBoxY1 == this.boundingBoxY1 &&
|
||||||
other.boundingBoxX2 == this.boundingBoxX2 &&
|
other.boundingBoxX2 == this.boundingBoxX2 &&
|
||||||
other.boundingBoxY2 == this.boundingBoxY2 &&
|
other.boundingBoxY2 == this.boundingBoxY2 &&
|
||||||
other.sourceType == this.sourceType);
|
other.sourceType == this.sourceType &&
|
||||||
|
other.isVisible == this.isVisible &&
|
||||||
|
other.deletedAt == this.deletedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AssetFaceEntityCompanion
|
class AssetFaceEntityCompanion
|
||||||
@@ -1074,6 +1186,8 @@ class AssetFaceEntityCompanion
|
|||||||
final i0.Value<int> boundingBoxX2;
|
final i0.Value<int> boundingBoxX2;
|
||||||
final i0.Value<int> boundingBoxY2;
|
final i0.Value<int> boundingBoxY2;
|
||||||
final i0.Value<String> sourceType;
|
final i0.Value<String> sourceType;
|
||||||
|
final i0.Value<bool> isVisible;
|
||||||
|
final i0.Value<DateTime?> deletedAt;
|
||||||
const AssetFaceEntityCompanion({
|
const AssetFaceEntityCompanion({
|
||||||
this.id = const i0.Value.absent(),
|
this.id = const i0.Value.absent(),
|
||||||
this.assetId = const i0.Value.absent(),
|
this.assetId = const i0.Value.absent(),
|
||||||
@@ -1085,6 +1199,8 @@ class AssetFaceEntityCompanion
|
|||||||
this.boundingBoxX2 = const i0.Value.absent(),
|
this.boundingBoxX2 = const i0.Value.absent(),
|
||||||
this.boundingBoxY2 = const i0.Value.absent(),
|
this.boundingBoxY2 = const i0.Value.absent(),
|
||||||
this.sourceType = const i0.Value.absent(),
|
this.sourceType = const i0.Value.absent(),
|
||||||
|
this.isVisible = const i0.Value.absent(),
|
||||||
|
this.deletedAt = const i0.Value.absent(),
|
||||||
});
|
});
|
||||||
AssetFaceEntityCompanion.insert({
|
AssetFaceEntityCompanion.insert({
|
||||||
required String id,
|
required String id,
|
||||||
@@ -1097,6 +1213,8 @@ class AssetFaceEntityCompanion
|
|||||||
required int boundingBoxX2,
|
required int boundingBoxX2,
|
||||||
required int boundingBoxY2,
|
required int boundingBoxY2,
|
||||||
required String sourceType,
|
required String sourceType,
|
||||||
|
this.isVisible = const i0.Value.absent(),
|
||||||
|
this.deletedAt = const i0.Value.absent(),
|
||||||
}) : id = i0.Value(id),
|
}) : id = i0.Value(id),
|
||||||
assetId = i0.Value(assetId),
|
assetId = i0.Value(assetId),
|
||||||
imageWidth = i0.Value(imageWidth),
|
imageWidth = i0.Value(imageWidth),
|
||||||
@@ -1117,6 +1235,8 @@ class AssetFaceEntityCompanion
|
|||||||
i0.Expression<int>? boundingBoxX2,
|
i0.Expression<int>? boundingBoxX2,
|
||||||
i0.Expression<int>? boundingBoxY2,
|
i0.Expression<int>? boundingBoxY2,
|
||||||
i0.Expression<String>? sourceType,
|
i0.Expression<String>? sourceType,
|
||||||
|
i0.Expression<bool>? isVisible,
|
||||||
|
i0.Expression<DateTime>? deletedAt,
|
||||||
}) {
|
}) {
|
||||||
return i0.RawValuesInsertable({
|
return i0.RawValuesInsertable({
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
@@ -1129,6 +1249,8 @@ class AssetFaceEntityCompanion
|
|||||||
if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2,
|
if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2,
|
||||||
if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2,
|
if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2,
|
||||||
if (sourceType != null) 'source_type': sourceType,
|
if (sourceType != null) 'source_type': sourceType,
|
||||||
|
if (isVisible != null) 'is_visible': isVisible,
|
||||||
|
if (deletedAt != null) 'deleted_at': deletedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1143,6 +1265,8 @@ class AssetFaceEntityCompanion
|
|||||||
i0.Value<int>? boundingBoxX2,
|
i0.Value<int>? boundingBoxX2,
|
||||||
i0.Value<int>? boundingBoxY2,
|
i0.Value<int>? boundingBoxY2,
|
||||||
i0.Value<String>? sourceType,
|
i0.Value<String>? sourceType,
|
||||||
|
i0.Value<bool>? isVisible,
|
||||||
|
i0.Value<DateTime?>? deletedAt,
|
||||||
}) {
|
}) {
|
||||||
return i1.AssetFaceEntityCompanion(
|
return i1.AssetFaceEntityCompanion(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -1155,6 +1279,8 @@ class AssetFaceEntityCompanion
|
|||||||
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
||||||
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
||||||
sourceType: sourceType ?? this.sourceType,
|
sourceType: sourceType ?? this.sourceType,
|
||||||
|
isVisible: isVisible ?? this.isVisible,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,6 +1317,12 @@ class AssetFaceEntityCompanion
|
|||||||
if (sourceType.present) {
|
if (sourceType.present) {
|
||||||
map['source_type'] = i0.Variable<String>(sourceType.value);
|
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;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1206,7 +1338,9 @@ class AssetFaceEntityCompanion
|
|||||||
..write('boundingBoxY1: $boundingBoxY1, ')
|
..write('boundingBoxY1: $boundingBoxY1, ')
|
||||||
..write('boundingBoxX2: $boundingBoxX2, ')
|
..write('boundingBoxX2: $boundingBoxX2, ')
|
||||||
..write('boundingBoxY2: $boundingBoxY2, ')
|
..write('boundingBoxY2: $boundingBoxY2, ')
|
||||||
..write('sourceType: $sourceType')
|
..write('sourceType: $sourceType, ')
|
||||||
|
..write('isVisible: $isVisible, ')
|
||||||
|
..write('deletedAt: $deletedAt')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ abstract class ImageRequest {
|
|||||||
|
|
||||||
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0});
|
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0});
|
||||||
|
|
||||||
|
Future<ui.Codec?> loadCodec();
|
||||||
|
|
||||||
void cancel() {
|
void cancel() {
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
return;
|
return;
|
||||||
@@ -34,7 +36,7 @@ abstract class ImageRequest {
|
|||||||
|
|
||||||
void _onCancelled();
|
void _onCancelled();
|
||||||
|
|
||||||
Future<ui.FrameInfo?> _fromEncodedPlatformImage(int address, int length) async {
|
Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromEncodedPlatformImage(int address, int length) async {
|
||||||
final pointer = Pointer<Uint8>.fromAddress(address);
|
final pointer = Pointer<Uint8>.fromAddress(address);
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
malloc.free(pointer);
|
malloc.free(pointer);
|
||||||
@@ -67,6 +69,20 @@ abstract class ImageRequest {
|
|||||||
return null;
|
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();
|
final frame = await codec.getNextFrame();
|
||||||
descriptor.dispose();
|
descriptor.dispose();
|
||||||
codec.dispose();
|
codec.dispose();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class LocalImageRequest extends ImageRequest {
|
|||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
isVideo: assetType == AssetType.video,
|
isVideo: assetType == AssetType.video,
|
||||||
|
preferEncoded: false,
|
||||||
);
|
);
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -31,6 +32,26 @@ class LocalImageRequest extends ImageRequest {
|
|||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
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
|
@override
|
||||||
Future<void> _onCancelled() {
|
Future<void> _onCancelled() {
|
||||||
return localImageApi.cancelRequest(requestId);
|
return localImageApi.cancelRequest(requestId);
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId);
|
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 frame = switch (info) {
|
final frame = switch (info) {
|
||||||
{'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length),
|
{'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length),
|
||||||
{'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} =>
|
{'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} =>
|
||||||
@@ -22,6 +23,19 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
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
|
@override
|
||||||
Future<void> _onCancelled() {
|
Future<void> _onCancelled() {
|
||||||
return remoteImageApi.cancelRequest(requestId);
|
return remoteImageApi.cancelRequest(requestId);
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ class ThumbhashImageRequest extends ImageRequest {
|
|||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ui.Codec?> loadCodec() => throw UnsupportedError('Thumbhash does not support codec loading');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void _onCancelled() {}
|
void _onCancelled() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 19;
|
int get schemaVersion => 20;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -226,6 +226,10 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
|
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
|
||||||
await m.createIndex(v19.idxStackPrimaryAssetId);
|
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);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8360,6 +8360,550 @@ final class Schema19 extends i0.VersionedSchema {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class Schema20 extends i0.VersionedSchema {
|
||||||
|
Schema20({required super.database}) : super(version: 20);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
idxLocalAlbumAssetAlbumAsset,
|
||||||
|
idxRemoteAlbumOwnerId,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
idxLocalAssetCloudId,
|
||||||
|
idxStackPrimaryAssetId,
|
||||||
|
idxRemoteAssetOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
idxRemoteAssetStackId,
|
||||||
|
idxRemoteAssetLocalDateTimeDay,
|
||||||
|
idxRemoteAssetLocalDateTimeMonth,
|
||||||
|
authUserEntity,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
remoteAssetCloudIdEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
assetFaceEntity,
|
||||||
|
storeEntity,
|
||||||
|
trashedLocalAssetEntity,
|
||||||
|
idxPartnerSharedWithId,
|
||||||
|
idxLatLng,
|
||||||
|
idxRemoteAlbumAssetAlbumAsset,
|
||||||
|
idxRemoteAssetCloudId,
|
||||||
|
idxPersonOwnerId,
|
||||||
|
idxAssetFacePersonId,
|
||||||
|
idxAssetFaceAssetId,
|
||||||
|
idxTrashedLocalAssetChecksum,
|
||||||
|
idxTrashedLocalAssetAlbum,
|
||||||
|
];
|
||||||
|
late final Shape20 userEntity = Shape20(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_91,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape28 remoteAssetEntity = Shape28(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_86,
|
||||||
|
_column_101,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 stackEntity = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape26 localAssetEntity = Shape26(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
_column_98,
|
||||||
|
_column_96,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape9 remoteAlbumEntity = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_56,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_57,
|
||||||
|
_column_58,
|
||||||
|
_column_59,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape19 localAlbumEntity = Shape19(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_5,
|
||||||
|
_column_31,
|
||||||
|
_column_32,
|
||||||
|
_column_90,
|
||||||
|
_column_33,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape22 localAlbumAssetEntity = Shape22(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_34, _column_35, _column_33],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_local_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAlbumOwnerId = i1.Index(
|
||||||
|
'idx_remote_album_owner_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||||
|
'idx_local_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||||
|
'idx_stack_primary_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_owner_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_library_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||||
|
'idx_remote_asset_stack_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
|
||||||
|
'idx_remote_asset_local_date_time_day',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
|
||||||
|
'idx_remote_asset_local_date_time_month',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
|
||||||
|
);
|
||||||
|
late final Shape21 authUserEntity = Shape21(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'auth_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_2,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_92,
|
||||||
|
_column_93,
|
||||||
|
_column_7,
|
||||||
|
_column_94,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape4 userMetadataEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||||
|
columns: [_column_25, _column_26, _column_27],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape5 partnerEntity = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||||
|
columns: [_column_28, _column_29, _column_30],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape8 remoteExifEntity = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
_column_40,
|
||||||
|
_column_41,
|
||||||
|
_column_11,
|
||||||
|
_column_10,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
_column_52,
|
||||||
|
_column_53,
|
||||||
|
_column_54,
|
||||||
|
_column_55,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_36, _column_60],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||||
|
columns: [_column_60, _column_25, _column_61],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape27 remoteAssetCloudIdEntity = Shape27(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_cloud_id_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_99,
|
||||||
|
_column_100,
|
||||||
|
_column_96,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape11 memoryEntity = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_18,
|
||||||
|
_column_15,
|
||||||
|
_column_8,
|
||||||
|
_column_62,
|
||||||
|
_column_63,
|
||||||
|
_column_64,
|
||||||
|
_column_65,
|
||||||
|
_column_66,
|
||||||
|
_column_67,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape12 memoryAssetEntity = Shape12(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||||
|
columns: [_column_36, _column_68],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape14 personEntity = Shape14(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_1,
|
||||||
|
_column_69,
|
||||||
|
_column_71,
|
||||||
|
_column_72,
|
||||||
|
_column_73,
|
||||||
|
_column_74,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape29 assetFaceEntity = Shape29(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_face_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_36,
|
||||||
|
_column_76,
|
||||||
|
_column_77,
|
||||||
|
_column_78,
|
||||||
|
_column_79,
|
||||||
|
_column_80,
|
||||||
|
_column_81,
|
||||||
|
_column_82,
|
||||||
|
_column_83,
|
||||||
|
_column_102,
|
||||||
|
_column_18,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape18 storeEntity = Shape18(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'store_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_87, _column_88, _column_89],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape25 trashedLocalAssetEntity = Shape25(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'trashed_local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_95,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
_column_97,
|
||||||
|
],
|
||||||
|
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)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLatLng = i1.Index(
|
||||||
|
'idx_lat_lng',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_remote_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||||
|
'idx_remote_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxPersonOwnerId = i1.Index(
|
||||||
|
'idx_person_owner_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||||
|
'idx_asset_face_person_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||||
|
'idx_asset_face_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_album',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape29 extends i0.VersionedTable {
|
||||||
|
Shape29({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
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<bool> _column_102(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_visible',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_visible" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('1'),
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
@@ -8379,6 +8923,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||||
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -8472,6 +9017,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from18To19(migrator, schema);
|
await from18To19(migrator, schema);
|
||||||
return 19;
|
return 19;
|
||||||
|
case 19:
|
||||||
|
final schema = Schema20(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from19To20(migrator, schema);
|
||||||
|
return 20;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -8497,6 +9047,7 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||||
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -8517,5 +9068,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
from16To17: from16To17,
|
from16To17: from16To17,
|
||||||
from17To18: from17To18,
|
from17To18: from17To18,
|
||||||
from18To19: from18To19,
|
from18To19: from18To19,
|
||||||
|
from19To20: from19To20,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (keepFavorites) {
|
if (keepFavorites) {
|
||||||
whereClause = whereClause & _db.localAssetEntity.isFavorite.equals(false);
|
whereClause =
|
||||||
|
whereClause & _db.localAssetEntity.isFavorite.equals(false) & _db.remoteAssetEntity.isFavorite.equals(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
query.where(whereClause);
|
query.where(whereClause);
|
||||||
|
|||||||
@@ -16,9 +16,15 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DriftPerson>> getAssetPeople(String assetId) async {
|
Future<List<DriftPerson>> getAssetPeople(String assetId) async {
|
||||||
final query = _db.select(_db.assetFaceEntity).join([
|
final query =
|
||||||
innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)),
|
_db.select(_db.assetFaceEntity).join([
|
||||||
])..where(_db.assetFaceEntity.assetId.equals(assetId) & _db.personEntity.isHidden.equals(false));
|
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),
|
||||||
|
);
|
||||||
|
|
||||||
return query.map((row) {
|
return query.map((row) {
|
||||||
final person = row.readTable(_db.personEntity);
|
final person = row.readTable(_db.personEntity);
|
||||||
@@ -39,7 +45,9 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
|
|||||||
..where(
|
..where(
|
||||||
people.isHidden.equals(false) &
|
people.isHidden.equals(false) &
|
||||||
assets.deletedAt.isNull() &
|
assets.deletedAt.isNull() &
|
||||||
assets.visibility.equalsValue(AssetVisibility.timeline),
|
assets.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
|
faces.isVisible.equals(true) &
|
||||||
|
faces.deletedAt.isNull(),
|
||||||
)
|
)
|
||||||
..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not())
|
..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not())
|
||||||
..orderBy([
|
..orderBy([
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
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/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
@@ -321,26 +323,32 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
}).watchSingleOrNull();
|
}).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DateTime?> getNewestAssetTimestamp(String albumId) {
|
Future<List<String>> getSortedAlbumIds(List<String> albumIds, {required AssetDateAggregation aggregation}) async {
|
||||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
if (albumIds.isEmpty) return [];
|
||||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
|
||||||
..addColumns([_db.remoteAssetEntity.localDateTime.max()])
|
|
||||||
..join([
|
|
||||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull();
|
final jsonIds = jsonEncode(albumIds);
|
||||||
}
|
final sqlAgg = aggregation == AssetDateAggregation.start ? 'MIN' : 'MAX';
|
||||||
|
|
||||||
Future<DateTime?> getOldestAssetTimestamp(String albumId) {
|
final rows = await _db
|
||||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
.customSelect(
|
||||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
'''
|
||||||
..addColumns([_db.remoteAssetEntity.localDateTime.min()])
|
SELECT
|
||||||
..join([
|
raae.album_id,
|
||||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
$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();
|
||||||
|
|
||||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull();
|
return rows.map((row) => row.read<String>('album_id')).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ class SyncApiRepository {
|
|||||||
|
|
||||||
Future<void> streamChanges(
|
Future<void> streamChanges(
|
||||||
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
|
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
|
||||||
|
required SemVer serverVersion,
|
||||||
Function()? onReset,
|
Function()? onReset,
|
||||||
int batchSize = kSyncEventBatchSize,
|
int batchSize = kSyncEventBatchSize,
|
||||||
http.Client? httpClient,
|
http.Client? httpClient,
|
||||||
@@ -64,7 +66,8 @@ class SyncApiRepository {
|
|||||||
SyncRequestType.partnerStacksV1,
|
SyncRequestType.partnerStacksV1,
|
||||||
SyncRequestType.userMetadataV1,
|
SyncRequestType.userMetadataV1,
|
||||||
SyncRequestType.peopleV1,
|
SyncRequestType.peopleV1,
|
||||||
SyncRequestType.assetFacesV1,
|
if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1,
|
||||||
|
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2,
|
||||||
],
|
],
|
||||||
reset: shouldReset,
|
reset: shouldReset,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
@@ -190,6 +193,7 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.personV1: SyncPersonV1.fromJson,
|
SyncEntityType.personV1: SyncPersonV1.fromJson,
|
||||||
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
|
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
|
||||||
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
|
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
|
||||||
|
SyncEntityType.assetFaceV2: SyncAssetFaceV2.fromJson,
|
||||||
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
|
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
|
||||||
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
|
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -652,6 +652,37 @@ 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 {
|
Future<void> deleteAssetFacesV1(Iterable<SyncAssetFaceDeleteV1> data) async {
|
||||||
try {
|
try {
|
||||||
await _db.batch((batch) {
|
await _db.batch((batch) {
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive),
|
row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive),
|
||||||
groupBy: groupBy,
|
groupBy: groupBy,
|
||||||
origin: TimelineOrigin.archive,
|
origin: TimelineOrigin.archive,
|
||||||
|
joinLocal: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
||||||
@@ -421,7 +422,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
_db.assetFaceEntity.personId.equals(personId),
|
_db.assetFaceEntity.personId.equals(personId) &
|
||||||
|
_db.assetFaceEntity.isVisible.equals(true) &
|
||||||
|
_db.assetFaceEntity.deletedAt.isNull(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return query.map((row) {
|
return query.map((row) {
|
||||||
@@ -446,7 +449,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
_db.assetFaceEntity.personId.equals(personId),
|
_db.assetFaceEntity.personId.equals(personId) &
|
||||||
|
_db.assetFaceEntity.isVisible.equals(true) &
|
||||||
|
_db.assetFaceEntity.deletedAt.isNull(),
|
||||||
)
|
)
|
||||||
..groupBy([dateExp])
|
..groupBy([dateExp])
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
@@ -476,7 +481,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
_db.assetFaceEntity.personId.equals(personId),
|
_db.assetFaceEntity.personId.equals(personId) &
|
||||||
|
_db.assetFaceEntity.isVisible.equals(true) &
|
||||||
|
_db.assetFaceEntity.deletedAt.isNull(),
|
||||||
)
|
)
|
||||||
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|||||||
+25
-20
@@ -20,6 +20,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.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/platform/background_worker_lock_api.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
@@ -49,30 +50,34 @@ import 'package:logging/logging.dart';
|
|||||||
import 'package:timezone/data/latest.dart';
|
import 'package:timezone/data/latest.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ImmichWidgetsBinding();
|
try {
|
||||||
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
|
ImmichWidgetsBinding();
|
||||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
|
||||||
await Bootstrap.initDomain(isar, drift, logDb);
|
await EasyLocalization.ensureInitialized();
|
||||||
await initApp();
|
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||||
// Warm-up isolate pool for worker manager
|
await Bootstrap.initDomain(isar, drift, logDb);
|
||||||
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
await initApp();
|
||||||
await migrateDatabaseIfNeeded(isar, drift);
|
// Warm-up isolate pool for worker manager
|
||||||
HttpSSLOptions.apply();
|
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
||||||
|
await migrateDatabaseIfNeeded(isar, drift);
|
||||||
|
HttpSSLOptions.apply();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
dbProvider.overrideWithValue(isar),
|
dbProvider.overrideWithValue(isar),
|
||||||
isarProvider.overrideWithValue(isar),
|
isarProvider.overrideWithValue(isar),
|
||||||
driftProvider.overrideWith(driftOverride(drift)),
|
driftProvider.overrideWith(driftOverride(drift)),
|
||||||
],
|
],
|
||||||
child: const MainWidget(),
|
child: const MainWidget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} catch (error, stack) {
|
||||||
|
runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initApp() async {
|
Future<void> initApp() async {
|
||||||
await EasyLocalization.ensureInitialized();
|
|
||||||
await initializeDateFormatting();
|
await initializeDateFormatting();
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class SharedLink {
|
|||||||
final String key;
|
final String key;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
final SharedLinkSource type;
|
final SharedLinkSource type;
|
||||||
|
final String? slug;
|
||||||
|
|
||||||
const SharedLink({
|
const SharedLink({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -27,6 +28,7 @@ class SharedLink {
|
|||||||
required this.key,
|
required this.key,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
required this.slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
SharedLink copyWith({
|
SharedLink copyWith({
|
||||||
@@ -41,6 +43,7 @@ class SharedLink {
|
|||||||
String? key,
|
String? key,
|
||||||
bool? showMetadata,
|
bool? showMetadata,
|
||||||
SharedLinkSource? type,
|
SharedLinkSource? type,
|
||||||
|
String? slug,
|
||||||
}) {
|
}) {
|
||||||
return SharedLink(
|
return SharedLink(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -54,6 +57,7 @@ class SharedLink {
|
|||||||
key: key ?? this.key,
|
key: key ?? this.key,
|
||||||
showMetadata: showMetadata ?? this.showMetadata,
|
showMetadata: showMetadata ?? this.showMetadata,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
slug: slug ?? this.slug,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +70,7 @@ class SharedLink {
|
|||||||
expiresAt = dto.expiresAt,
|
expiresAt = dto.expiresAt,
|
||||||
key = dto.key,
|
key = dto.key,
|
||||||
showMetadata = dto.showMetadata,
|
showMetadata = dto.showMetadata,
|
||||||
|
slug = dto.slug,
|
||||||
type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual,
|
type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual,
|
||||||
title = dto.type == SharedLinkType.ALBUM
|
title = dto.type == SharedLinkType.ALBUM
|
||||||
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
|
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
|
||||||
@@ -78,7 +83,7 @@ class SharedLink {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
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)';
|
'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)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -94,7 +99,8 @@ class SharedLink {
|
|||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.key == key &&
|
other.key == key &&
|
||||||
other.showMetadata == showMetadata &&
|
other.showMetadata == showMetadata &&
|
||||||
other.type == type;
|
other.type == type &&
|
||||||
|
other.slug == slug;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@@ -108,5 +114,6 @@ class SharedLink {
|
|||||||
expiresAt.hashCode ^
|
expiresAt.hashCode ^
|
||||||
key.hashCode ^
|
key.hashCode ^
|
||||||
showMetadata.hashCode ^
|
showMetadata.hashCode ^
|
||||||
type.hashCode;
|
type.hashCode ^
|
||||||
|
slug.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,8 +221,37 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
onDragUpdate: (_, details, __) {
|
onDragUpdate: (_, details, __) {
|
||||||
handleSwipeUpDown(details);
|
handleSwipeUpDown(details);
|
||||||
},
|
},
|
||||||
onTapDown: (_, __, ___) {
|
onTapDown: (ctx, tapDownDetails, _) {
|
||||||
ref.read(showControlsProvider.notifier).toggle();
|
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();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongPressStart: asset.isMotionPhoto
|
onLongPressStart: asset.isMotionPhoto
|
||||||
? (_, __, ___) {
|
? (_, __, ___) {
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.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/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
@@ -13,7 +20,254 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.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: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()
|
@RoutePage()
|
||||||
class SplashScreenPage extends StatefulHookConsumerWidget {
|
class SplashScreenPage extends StatefulHookConsumerWidget {
|
||||||
@@ -109,9 +363,43 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
if (context.router.current.name == SplashScreenRoute.name) {
|
if (context.router.current.name == SplashScreenRoute.name) {
|
||||||
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
|
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
|
||||||
if (needBetaMigration) {
|
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);
|
await Store.put(StoreKey.needBetaMigration, false);
|
||||||
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]));
|
if (migrate) {
|
||||||
return;
|
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()));
|
unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()));
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -12,6 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.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:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
@@ -30,27 +29,10 @@ class EditImagePage extends ConsumerWidget {
|
|||||||
final bool isEdited;
|
final bool isEdited;
|
||||||
|
|
||||||
const EditImagePage({super.key, required this.asset, required this.image, required this.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 {
|
Future<void> _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async {
|
||||||
try {
|
try {
|
||||||
final Uint8List imageData = await _imageToUint8List(image);
|
final Uint8List imageData = await imageToUint8List(image);
|
||||||
await ref
|
await ref
|
||||||
.read(fileMediaRepositoryProvider)
|
.read(fileMediaRepositoryProvider)
|
||||||
.saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg");
|
.saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg");
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
|
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
|
||||||
final descriptionFocusNode = useFocusNode();
|
final descriptionFocusNode = useFocusNode();
|
||||||
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
|
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
|
||||||
|
final slugController = useTextEditingController(text: existingLink?.slug ?? "");
|
||||||
|
final slugFocusNode = useFocusNode();
|
||||||
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
||||||
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
||||||
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
||||||
@@ -108,6 +110,26 @@ 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() {
|
Widget buildShowMetaButton() {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
value: showMetadata.value,
|
value: showMetadata.value,
|
||||||
@@ -261,6 +283,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
allowUpload: allowUpload.value,
|
allowUpload: allowUpload.value,
|
||||||
description: descriptionController.text.isEmpty ? null : descriptionController.text,
|
description: descriptionController.text.isEmpty ? null : descriptionController.text,
|
||||||
password: passwordController.text.isEmpty ? null : passwordController.text,
|
password: passwordController.text.isEmpty ? null : passwordController.text,
|
||||||
|
slug: slugController.text.isEmpty ? null : slugController.text,
|
||||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||||
);
|
);
|
||||||
ref.invalidate(sharedLinksStateProvider);
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
@@ -274,7 +297,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newLink != null && serverUrl != null) {
|
if (newLink != null && serverUrl != null) {
|
||||||
newShareLink.value = "${serverUrl}share/${newLink.key}";
|
final hasSlug = newLink.slug?.isNotEmpty == true;
|
||||||
|
final urlPath = hasSlug ? newLink.slug : newLink.key;
|
||||||
|
final basePath = hasSlug ? 's' : 'share';
|
||||||
|
newShareLink.value = "$serverUrl$basePath/$urlPath";
|
||||||
copyLinkToClipboard();
|
copyLinkToClipboard();
|
||||||
} else if (newLink == null) {
|
} else if (newLink == null) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -292,6 +318,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
bool? meta;
|
bool? meta;
|
||||||
String? desc;
|
String? desc;
|
||||||
String? password;
|
String? password;
|
||||||
|
String? slug;
|
||||||
DateTime? expiry;
|
DateTime? expiry;
|
||||||
bool? changeExpiry;
|
bool? changeExpiry;
|
||||||
|
|
||||||
@@ -315,6 +342,12 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
password = passwordController.text;
|
password = passwordController.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (slugController.text != (existingLink!.slug ?? "")) {
|
||||||
|
slug = slugController.text.isEmpty ? null : slugController.text;
|
||||||
|
} else {
|
||||||
|
slug = existingLink!.slug;
|
||||||
|
}
|
||||||
|
|
||||||
if (editExpiry.value) {
|
if (editExpiry.value) {
|
||||||
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
||||||
changeExpiry = true;
|
changeExpiry = true;
|
||||||
@@ -329,6 +362,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
allowUpload: upload,
|
allowUpload: upload,
|
||||||
description: desc,
|
description: desc,
|
||||||
password: password,
|
password: password,
|
||||||
|
slug: slug,
|
||||||
expiresAt: expiry,
|
expiresAt: expiry,
|
||||||
changeExpiry: changeExpiry,
|
changeExpiry: changeExpiry,
|
||||||
);
|
);
|
||||||
@@ -349,6 +383,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()),
|
Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()),
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()),
|
Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()),
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()),
|
Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()),
|
||||||
|
Padding(padding: const EdgeInsets.all(padding), child: buildSlugField()),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
||||||
child: buildShowMetaButton(),
|
child: buildShowMetaButton(),
|
||||||
|
|||||||
+2
@@ -55,6 +55,7 @@ class LocalImageApi {
|
|||||||
required int width,
|
required int width,
|
||||||
required int height,
|
required int height,
|
||||||
required bool isVideo,
|
required bool isVideo,
|
||||||
|
required bool preferEncoded,
|
||||||
}) async {
|
}) async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$pigeonVar_messageChannelSuffix';
|
||||||
@@ -69,6 +70,7 @@ class LocalImageApi {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
isVideo,
|
isVideo,
|
||||||
|
preferEncoded,
|
||||||
]);
|
]);
|
||||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
if (pigeonVar_replyList == null) {
|
if (pigeonVar_replyList == null) {
|
||||||
|
|||||||
+23
-10
@@ -29,6 +29,8 @@ bool _deepEquals(Object? a, Object? b) {
|
|||||||
return a == b;
|
return a == b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping }
|
||||||
|
|
||||||
class PlatformAsset {
|
class PlatformAsset {
|
||||||
PlatformAsset({
|
PlatformAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -44,6 +46,7 @@ class PlatformAsset {
|
|||||||
this.adjustmentTime,
|
this.adjustmentTime,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
|
required this.playbackStyle,
|
||||||
});
|
});
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
@@ -72,6 +75,8 @@ class PlatformAsset {
|
|||||||
|
|
||||||
double? longitude;
|
double? longitude;
|
||||||
|
|
||||||
|
PlatformAssetPlaybackStyle playbackStyle;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[
|
return <Object?>[
|
||||||
id,
|
id,
|
||||||
@@ -87,6 +92,7 @@ class PlatformAsset {
|
|||||||
adjustmentTime,
|
adjustmentTime,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
|
playbackStyle,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +116,7 @@ class PlatformAsset {
|
|||||||
adjustmentTime: result[10] as int?,
|
adjustmentTime: result[10] as int?,
|
||||||
latitude: result[11] as double?,
|
latitude: result[11] as double?,
|
||||||
longitude: result[12] as double?,
|
longitude: result[12] as double?,
|
||||||
|
playbackStyle: result[13]! as PlatformAssetPlaybackStyle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,21 +323,24 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||||||
if (value is int) {
|
if (value is int) {
|
||||||
buffer.putUint8(4);
|
buffer.putUint8(4);
|
||||||
buffer.putInt64(value);
|
buffer.putInt64(value);
|
||||||
} else if (value is PlatformAsset) {
|
} else if (value is PlatformAssetPlaybackStyle) {
|
||||||
buffer.putUint8(129);
|
buffer.putUint8(129);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.index);
|
||||||
} else if (value is PlatformAlbum) {
|
} else if (value is PlatformAsset) {
|
||||||
buffer.putUint8(130);
|
buffer.putUint8(130);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is SyncDelta) {
|
} else if (value is PlatformAlbum) {
|
||||||
buffer.putUint8(131);
|
buffer.putUint8(131);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is HashResult) {
|
} else if (value is SyncDelta) {
|
||||||
buffer.putUint8(132);
|
buffer.putUint8(132);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is CloudIdResult) {
|
} else if (value is HashResult) {
|
||||||
buffer.putUint8(133);
|
buffer.putUint8(133);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
|
} else if (value is CloudIdResult) {
|
||||||
|
buffer.putUint8(134);
|
||||||
|
writeValue(buffer, value.encode());
|
||||||
} else {
|
} else {
|
||||||
super.writeValue(buffer, value);
|
super.writeValue(buffer, value);
|
||||||
}
|
}
|
||||||
@@ -340,14 +350,17 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 129:
|
case 129:
|
||||||
return PlatformAsset.decode(readValue(buffer)!);
|
final int? value = readValue(buffer) as int?;
|
||||||
|
return value == null ? null : PlatformAssetPlaybackStyle.values[value];
|
||||||
case 130:
|
case 130:
|
||||||
return PlatformAlbum.decode(readValue(buffer)!);
|
return PlatformAsset.decode(readValue(buffer)!);
|
||||||
case 131:
|
case 131:
|
||||||
return SyncDelta.decode(readValue(buffer)!);
|
return PlatformAlbum.decode(readValue(buffer)!);
|
||||||
case 132:
|
case 132:
|
||||||
return HashResult.decode(readValue(buffer)!);
|
return SyncDelta.decode(readValue(buffer)!);
|
||||||
case 133:
|
case 133:
|
||||||
|
return HashResult.decode(readValue(buffer)!);
|
||||||
|
case 134:
|
||||||
return CloudIdResult.decode(readValue(buffer)!);
|
return CloudIdResult.decode(readValue(buffer)!);
|
||||||
default:
|
default:
|
||||||
return super.readValueOfType(type, buffer);
|
return super.readValueOfType(type, buffer);
|
||||||
|
|||||||
+7
-1
@@ -53,6 +53,7 @@ class RemoteImageApi {
|
|||||||
String url, {
|
String url, {
|
||||||
required Map<String, String> headers,
|
required Map<String, String> headers,
|
||||||
required int requestId,
|
required int requestId,
|
||||||
|
required bool preferEncoded,
|
||||||
}) async {
|
}) async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$pigeonVar_messageChannelSuffix';
|
||||||
@@ -61,7 +62,12 @@ class RemoteImageApi {
|
|||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
);
|
);
|
||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[url, headers, requestId]);
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
requestId,
|
||||||
|
preferEncoded,
|
||||||
|
]);
|
||||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
if (pigeonVar_replyList == null) {
|
if (pigeonVar_replyList == null) {
|
||||||
throw _createConnectionError(pigeonVar_channelName);
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
@@ -14,6 +13,7 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
|
|||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/foreground_upload.service.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:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -33,23 +33,6 @@ class DriftEditImagePage extends ConsumerWidget {
|
|||||||
final bool isEdited;
|
final bool isEdited;
|
||||||
|
|
||||||
const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited});
|
const DriftEditImagePage({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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _exitEditing(BuildContext context) {
|
void _exitEditing(BuildContext context) {
|
||||||
// this assumes that the only way to get to this page is from the AssetViewerRoute
|
// this assumes that the only way to get to this page is from the AssetViewerRoute
|
||||||
@@ -58,7 +41,7 @@ class DriftEditImagePage extends ConsumerWidget {
|
|||||||
|
|
||||||
Future<void> _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async {
|
Future<void> _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async {
|
||||||
try {
|
try {
|
||||||
final Uint8List imageData = await _imageToUint8List(image);
|
final Uint8List imageData = await imageToUint8List(image);
|
||||||
LocalAsset? localAsset;
|
LocalAsset? localAsset;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -698,7 +698,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||||||
label: 'search_filter_location'.t(context: context),
|
label: 'search_filter_location'.t(context: context),
|
||||||
currentFilter: locationCurrentFilterWidget.value,
|
currentFilter: locationCurrentFilterWidget.value,
|
||||||
),
|
),
|
||||||
if (userPreferences.value?.tagsEnabled ?? false)
|
if (userPreferences.valueOrNull?.tagsEnabled ?? false)
|
||||||
SearchFilterChip(
|
SearchFilterChip(
|
||||||
icon: Icons.sell_outlined,
|
icon: Icons.sell_outlined,
|
||||||
onTap: showTagPicker,
|
onTap: showTagPicker,
|
||||||
@@ -724,14 +724,13 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||||||
label: 'search_filter_media_type'.t(context: context),
|
label: 'search_filter_media_type'.t(context: context),
|
||||||
currentFilter: mediaTypeCurrentFilterWidget.value,
|
currentFilter: mediaTypeCurrentFilterWidget.value,
|
||||||
),
|
),
|
||||||
if (userPreferences.value?.ratingsEnabled ?? false) ...[
|
if (userPreferences.valueOrNull?.ratingsEnabled ?? false)
|
||||||
SearchFilterChip(
|
SearchFilterChip(
|
||||||
icon: Icons.star_outline_rounded,
|
icon: Icons.star_outline_rounded,
|
||||||
onTap: showStarRatingPicker,
|
onTap: showStarRatingPicker,
|
||||||
label: 'search_filter_star_rating'.t(context: context),
|
label: 'search_filter_star_rating'.t(context: context),
|
||||||
currentFilter: ratingCurrentFilterWidget.value,
|
currentFilter: ratingCurrentFilterWidget.value,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
SearchFilterChip(
|
SearchFilterChip(
|
||||||
icon: Icons.display_settings_outlined,
|
icon: Icons.display_settings_outlined,
|
||||||
onTap: showDisplayOptionPicker,
|
onTap: showDisplayOptionPicker,
|
||||||
|
|||||||
+10
@@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
/// This delete action has the following behavior:
|
/// This delete action has the following behavior:
|
||||||
@@ -25,6 +26,15 @@ class DeletePermanentActionButton extends ConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final count = source == ActionSource.viewer ? 1 : ref.read(multiSelectProvider).selectedAssets.length;
|
||||||
|
final confirm =
|
||||||
|
await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PermanentDeleteDialog(count: count),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source);
|
final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source);
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
/// This delete action has the following behavior:
|
/// This delete action has the following behavior:
|
||||||
@@ -22,6 +23,18 @@ class DeleteTrashActionButton extends ConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
|
||||||
|
|
||||||
|
final confirmDelete =
|
||||||
|
await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PermanentDeleteDialog(count: selectCount),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
if (!confirmDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source);
|
final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source);
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
|
class SetAlbumCoverActionButton extends ConsumerWidget {
|
||||||
|
final String albumId;
|
||||||
|
final ActionSource source;
|
||||||
|
final bool iconOnly;
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
|
const SetAlbumCoverActionButton({
|
||||||
|
super.key,
|
||||||
|
required this.albumId,
|
||||||
|
required this.source,
|
||||||
|
this.iconOnly = false,
|
||||||
|
this.menuItem = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).setAlbumCover(source, albumId);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'album_cover_updated'.t(context: context);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
iconData: Icons.image_outlined,
|
||||||
|
label: 'set_as_album_cover'.t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
|
maxWidth: 100,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
class SetProfilePictureActionButton extends ConsumerWidget {
|
||||||
|
final BaseAsset asset;
|
||||||
|
final bool iconOnly;
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
|
const SetProfilePictureActionButton({super.key, required this.asset, this.iconOnly = false, this.menuItem = false});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context) {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.pushRoute(ProfilePictureCropRoute(asset: asset));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
iconData: Icons.account_circle_outlined,
|
||||||
|
label: "set_as_profile_picture".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
|
onPressed: () => _onTap(context),
|
||||||
|
maxWidth: 100,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,10 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.sta
|
|||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
@@ -29,8 +31,9 @@ enum _DragIntent { none, scroll, dismiss }
|
|||||||
class AssetPage extends ConsumerStatefulWidget {
|
class AssetPage extends ConsumerStatefulWidget {
|
||||||
final int index;
|
final int index;
|
||||||
final int heroOffset;
|
final int heroOffset;
|
||||||
|
final void Function(int direction)? onTapNavigate;
|
||||||
|
|
||||||
const AssetPage({super.key, required this.index, required this.heroOffset});
|
const AssetPage({super.key, required this.index, required this.heroOffset, this.onTapNavigate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState createState() => _AssetPageState();
|
ConsumerState createState() => _AssetPageState();
|
||||||
@@ -50,19 +53,17 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
|
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
late final _proxyScrollController = ProxyScrollController(scrollController: _scrollController);
|
late final _proxyScrollController = ProxyScrollController(scrollController: _scrollController);
|
||||||
|
final ValueNotifier<PhotoViewScaleState> _videoScaleStateNotifier = ValueNotifier(PhotoViewScaleState.initial);
|
||||||
|
|
||||||
double _snapOffset = 0.0;
|
double _snapOffset = 0.0;
|
||||||
double _lastScrollOffset = 0.0;
|
|
||||||
|
|
||||||
DragStartDetails? _dragStart;
|
DragStartDetails? _dragStart;
|
||||||
_DragIntent _dragIntent = _DragIntent.none;
|
_DragIntent _dragIntent = _DragIntent.none;
|
||||||
Drag? _drag;
|
Drag? _drag;
|
||||||
bool _shouldPopOnDrag = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_proxyScrollController.addListener(_onScroll);
|
|
||||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
_eventSubscription = EventStream.shared.listen(_onEvent);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted || !_proxyScrollController.hasClients) return;
|
if (!mounted || !_proxyScrollController.hasClients) return;
|
||||||
@@ -78,6 +79,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
_proxyScrollController.dispose();
|
_proxyScrollController.dispose();
|
||||||
_scaleBoundarySub?.cancel();
|
_scaleBoundarySub?.cancel();
|
||||||
_eventSubscription?.cancel();
|
_eventSubscription?.cancel();
|
||||||
|
_videoScaleStateNotifier.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
|
|
||||||
void _showDetails() {
|
void _showDetails() {
|
||||||
if (!_proxyScrollController.hasClients || _snapOffset <= 0) return;
|
if (!_proxyScrollController.hasClients || _snapOffset <= 0) return;
|
||||||
_lastScrollOffset = _proxyScrollController.offset;
|
_viewer.setShowingDetails(true);
|
||||||
_proxyScrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic);
|
_proxyScrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,20 +105,17 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
SnapScrollPhysics.target(position, scrollVelocity, _snapOffset) < SnapScrollPhysics.minSnapDistance;
|
SnapScrollPhysics.target(position, scrollVelocity, _snapOffset) < SnapScrollPhysics.minSnapDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScroll() {
|
void _syncShowingDetails() {
|
||||||
final offset = _proxyScrollController.offset;
|
final offset = _proxyScrollController.offset;
|
||||||
if (offset > SnapScrollPhysics.minSnapDistance && offset > _lastScrollOffset) {
|
if (offset > SnapScrollPhysics.minSnapDistance) {
|
||||||
_viewer.setShowingDetails(true);
|
_viewer.setShowingDetails(true);
|
||||||
} else if (offset < SnapScrollPhysics.minSnapDistance - kTouchSlop) {
|
} else if (offset < SnapScrollPhysics.minSnapDistance - kTouchSlop) {
|
||||||
_viewer.setShowingDetails(false);
|
_viewer.setShowingDetails(false);
|
||||||
}
|
}
|
||||||
_lastScrollOffset = offset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _beginDrag(DragStartDetails details) {
|
void _beginDrag(DragStartDetails details) {
|
||||||
_dragStart = details;
|
_dragStart = details;
|
||||||
_shouldPopOnDrag = false;
|
|
||||||
_lastScrollOffset = _proxyScrollController.hasClients ? _proxyScrollController.offset : 0.0;
|
|
||||||
|
|
||||||
if (_viewController != null) {
|
if (_viewController != null) {
|
||||||
_initialPhotoViewState = _viewController!.value;
|
_initialPhotoViewState = _viewController!.value;
|
||||||
@@ -150,6 +149,8 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
case _DragIntent.scroll:
|
case _DragIntent.scroll:
|
||||||
if (_drag == null) _startProxyDrag();
|
if (_drag == null) _startProxyDrag();
|
||||||
_drag?.update(details);
|
_drag?.update(details);
|
||||||
|
|
||||||
|
_syncShowingDetails();
|
||||||
case _DragIntent.dismiss:
|
case _DragIntent.dismiss:
|
||||||
_handleDragDown(context, details.localPosition - _dragStart!.localPosition);
|
_handleDragDown(context, details.localPosition - _dragStart!.localPosition);
|
||||||
}
|
}
|
||||||
@@ -158,6 +159,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
void _endDrag(DragEndDetails details) {
|
void _endDrag(DragEndDetails details) {
|
||||||
if (_dragStart == null) return;
|
if (_dragStart == null) return;
|
||||||
|
|
||||||
|
final start = _dragStart;
|
||||||
_dragStart = null;
|
_dragStart = null;
|
||||||
|
|
||||||
final intent = _dragIntent;
|
final intent = _dragIntent;
|
||||||
@@ -167,13 +169,13 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
case _DragIntent.none:
|
case _DragIntent.none:
|
||||||
case _DragIntent.scroll:
|
case _DragIntent.scroll:
|
||||||
final scrollVelocity = -(details.primaryVelocity ?? 0.0);
|
final scrollVelocity = -(details.primaryVelocity ?? 0.0);
|
||||||
if (_willClose(scrollVelocity)) {
|
_viewer.setShowingDetails(!_willClose(scrollVelocity));
|
||||||
_viewer.setShowingDetails(false);
|
|
||||||
}
|
|
||||||
_drag?.end(details);
|
_drag?.end(details);
|
||||||
_drag = null;
|
_drag = null;
|
||||||
case _DragIntent.dismiss:
|
case _DragIntent.dismiss:
|
||||||
if (_shouldPopOnDrag) {
|
const popThreshold = 75.0;
|
||||||
|
if (details.localPosition.dy - start!.localPosition.dy > popThreshold) {
|
||||||
context.maybePop();
|
context.maybePop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -192,7 +194,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
PhotoViewControllerBase controller,
|
PhotoViewControllerBase controller,
|
||||||
PhotoViewScaleStateController scaleStateController,
|
PhotoViewScaleStateController scaleStateController,
|
||||||
) {
|
) {
|
||||||
_viewController = controller;
|
|
||||||
if (!_showingDetails && _isZoomed) return;
|
if (!_showingDetails && _isZoomed) return;
|
||||||
_beginDrag(details);
|
_beginDrag(details);
|
||||||
}
|
}
|
||||||
@@ -206,12 +207,8 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
|
|
||||||
void _handleDragDown(BuildContext context, Offset delta) {
|
void _handleDragDown(BuildContext context, Offset delta) {
|
||||||
const dragRatio = 0.2;
|
const dragRatio = 0.2;
|
||||||
const popThreshold = 75.0;
|
|
||||||
|
|
||||||
_shouldPopOnDrag = delta.dy > popThreshold;
|
|
||||||
|
|
||||||
final distance = delta.dy.abs();
|
final distance = delta.dy.abs();
|
||||||
|
|
||||||
final maxScaleDistance = context.height * 0.5;
|
final maxScaleDistance = context.height * 0.5;
|
||||||
final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio);
|
final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio);
|
||||||
final initialScale = _viewController?.initialScale ?? _initialPhotoViewState.scale;
|
final initialScale = _viewController?.initialScale ?? _initialPhotoViewState.scale;
|
||||||
@@ -224,17 +221,39 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onTapUp(BuildContext context, TapUpDetails details, PhotoViewControllerValue controllerValue) {
|
void _onTapUp(BuildContext context, TapUpDetails details, PhotoViewControllerValue controllerValue) {
|
||||||
if (!_showingDetails && _dragStart == null) _viewer.toggleControls();
|
if (_showingDetails || _dragStart != null) return;
|
||||||
|
|
||||||
|
final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.tapToNavigate);
|
||||||
|
if (!tapToNavigate) {
|
||||||
|
_viewer.toggleControls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final tapX = details.globalPosition.dx;
|
||||||
|
final screenWidth = context.width;
|
||||||
|
|
||||||
|
// Navigate if the user taps in the leftmost or rightmost quarter of the screen
|
||||||
|
final tappedLeftSide = tapX < screenWidth / 4;
|
||||||
|
final tappedRightSide = tapX > screenWidth * (3 / 4);
|
||||||
|
|
||||||
|
if (tappedLeftSide) {
|
||||||
|
widget.onTapNavigate?.call(-1);
|
||||||
|
} else if (tappedRightSide) {
|
||||||
|
widget.onTapNavigate?.call(1);
|
||||||
|
} else {
|
||||||
|
_viewer.toggleControls();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLongPress(BuildContext context, LongPressStartDetails details, PhotoViewControllerValue controllerValue) =>
|
void _onLongPress(BuildContext context, LongPressStartDetails details, PhotoViewControllerValue controllerValue) =>
|
||||||
ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
|
ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
|
||||||
|
|
||||||
void _onScaleStateChanged(PhotoViewScaleState scaleState) {
|
void _onScaleStateChanged(PhotoViewScaleState scaleState) {
|
||||||
_isZoomed = switch (scaleState) {
|
_isZoomed =
|
||||||
PhotoViewScaleState.zoomedIn || PhotoViewScaleState.covering => true,
|
scaleState == PhotoViewScaleState.zoomedIn ||
|
||||||
_ => false,
|
scaleState == PhotoViewScaleState.covering ||
|
||||||
};
|
_videoScaleStateNotifier.value == PhotoViewScaleState.zoomedIn ||
|
||||||
|
_videoScaleStateNotifier.value == PhotoViewScaleState.covering;
|
||||||
_viewer.setZoomed(_isZoomed);
|
_viewer.setZoomed(_isZoomed);
|
||||||
|
|
||||||
if (scaleState != PhotoViewScaleState.initial) {
|
if (scaleState != PhotoViewScaleState.initial) {
|
||||||
@@ -288,7 +307,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
if (displayAsset.isImage && !isPlayingMotionVideo) {
|
if (displayAsset.isImage && !isPlayingMotionVideo) {
|
||||||
final size = context.sizeData;
|
final size = context.sizeData;
|
||||||
return PhotoView(
|
return PhotoView(
|
||||||
key: ValueKey(displayAsset.heroTag),
|
key: Key(displayAsset.heroTag),
|
||||||
index: widget.index,
|
index: widget.index,
|
||||||
imageProvider: getFullImageProvider(displayAsset, size: size),
|
imageProvider: getFullImageProvider(displayAsset, size: size),
|
||||||
heroAttributes: heroAttributes,
|
heroAttributes: heroAttributes,
|
||||||
@@ -316,34 +335,32 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return PhotoView.customChild(
|
return PhotoView.customChild(
|
||||||
|
key: Key(displayAsset.heroTag),
|
||||||
onDragStart: _onDragStart,
|
onDragStart: _onDragStart,
|
||||||
onDragUpdate: _onDragUpdate,
|
onDragUpdate: _onDragUpdate,
|
||||||
onDragEnd: _onDragEnd,
|
onDragEnd: _onDragEnd,
|
||||||
onDragCancel: _onDragCancel,
|
onDragCancel: _onDragCancel,
|
||||||
onTapUp: _onTapUp,
|
|
||||||
heroAttributes: heroAttributes,
|
heroAttributes: heroAttributes,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
maxScale: 1.0,
|
|
||||||
basePosition: Alignment.center,
|
basePosition: Alignment.center,
|
||||||
disableScaleGestures: true,
|
disableScaleGestures: true,
|
||||||
scaleStateChangedCallback: _onScaleStateChanged,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
|
tightMode: true,
|
||||||
onPageBuild: _onPageBuild,
|
onPageBuild: _onPageBuild,
|
||||||
enablePanAlways: true,
|
enablePanAlways: true,
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
child: SizedBox(
|
child: NativeVideoViewer(
|
||||||
width: context.width,
|
key: _NativeVideoViewerKey(displayAsset.heroTag),
|
||||||
height: context.height,
|
asset: displayAsset,
|
||||||
child: NativeVideoViewer(
|
scaleStateNotifier: _videoScaleStateNotifier,
|
||||||
key: ValueKey(displayAsset.heroTag),
|
disableScaleGestures: showingDetails,
|
||||||
asset: displayAsset,
|
image: Image(
|
||||||
image: Image(
|
image: getFullImageProvider(displayAsset, size: context.sizeData),
|
||||||
key: ValueKey(displayAsset),
|
height: context.height,
|
||||||
image: getFullImageProvider(displayAsset, size: context.sizeData),
|
width: context.width,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
height: context.height,
|
alignment: Alignment.center,
|
||||||
width: context.width,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -442,3 +459,25 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A global key is used for video viewers to prevent them from being
|
||||||
|
// unnecessarily recreated. They're quite expensive, and maintain internal
|
||||||
|
// state. This can cause videos to restart multiple times during normal usage,
|
||||||
|
// like a hero animation.
|
||||||
|
//
|
||||||
|
// A plain ValueKey is insufficient, as it does not allow widgets to reparent. A
|
||||||
|
// GlobalObjectKey is fragile, as it checks if the given objects are identical,
|
||||||
|
// rather than equal. Hero tags are created with string interpolation, which
|
||||||
|
// prevents Dart from interning them. As such, hero tags are not identical, even
|
||||||
|
// if they are equal.
|
||||||
|
class _NativeVideoViewerKey extends GlobalKey {
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
const _NativeVideoViewerKey(this.value) : super.constructor();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is _NativeVideoViewerKey && other.value == value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => value.hashCode;
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,6 +96,16 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
|
|
||||||
bool _assetReloadRequested = false;
|
bool _assetReloadRequested = false;
|
||||||
|
|
||||||
|
void _onTapNavigate(int direction) {
|
||||||
|
final page = _pageController.page?.toInt();
|
||||||
|
if (page == null) return;
|
||||||
|
final target = page + direction;
|
||||||
|
final maxPage = ref.read(timelineServiceProvider).totalAssets - 1;
|
||||||
|
if (target >= 0 && target <= maxPage) {
|
||||||
|
_pageController.jumpToPage(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -107,6 +117,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
_reloadSubscription = EventStream.shared.listen(_onEvent);
|
_reloadSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
|
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
|
||||||
|
|
||||||
|
final assetViewer = ref.read(assetViewerProvider);
|
||||||
|
_setSystemUIMode(assetViewer.showingControls, assetViewer.showingDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -216,6 +229,13 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
_onAssetChanged(index);
|
_onAssetChanged(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setSystemUIMode(bool controls, bool details) {
|
||||||
|
final mode = !controls || (CurrentPlatform.isIOS && details)
|
||||||
|
? SystemUiMode.immersiveSticky
|
||||||
|
: SystemUiMode.edgeToEdge;
|
||||||
|
unawaited(SystemChrome.setEnabledSystemUIMode(mode));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||||
@@ -235,10 +255,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
|
|
||||||
ref.listen(assetViewerProvider.select((value) => (value.showingControls, value.showingDetails)), (_, state) {
|
ref.listen(assetViewerProvider.select((value) => (value.showingControls, value.showingDetails)), (_, state) {
|
||||||
final (controls, details) = state;
|
final (controls, details) = state;
|
||||||
final mode = !controls || (CurrentPlatform.isIOS && details)
|
_setSystemUIMode(controls, details);
|
||||||
? SystemUiMode.immersiveSticky
|
|
||||||
: SystemUiMode.edgeToEdge;
|
|
||||||
unawaited(SystemChrome.setEnabledSystemUIMode(mode));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
@@ -270,7 +287,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
: const FastClampingScrollPhysics(),
|
: const FastClampingScrollPhysics(),
|
||||||
itemCount: ref.read(timelineServiceProvider).totalAssets,
|
itemCount: ref.read(timelineServiceProvider).totalAssets,
|
||||||
onPageChanged: (index) => _onAssetChanged(index),
|
onPageChanged: (index) => _onAssetChanged(index),
|
||||||
itemBuilder: (context, index) => AssetPage(index: index, heroOffset: _heroOffset),
|
itemBuilder: (context, index) =>
|
||||||
|
AssetPage(index: index, heroOffset: _heroOffset, onTapNavigate: _onTapNavigate),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!CurrentPlatform.isIOS)
|
if (!CurrentPlatform.isIOS)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
@@ -25,6 +26,7 @@ import 'package:immich_mobile/services/api.service.dart';
|
|||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/interval_hook.dart';
|
import 'package:immich_mobile/utils/hooks/interval_hook.dart';
|
||||||
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:native_video_player/native_video_player.dart';
|
import 'package:native_video_player/native_video_player.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
@@ -52,6 +54,8 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
final bool showControls;
|
final bool showControls;
|
||||||
final int playbackDelayFactor;
|
final int playbackDelayFactor;
|
||||||
final Widget image;
|
final Widget image;
|
||||||
|
final ValueNotifier<PhotoViewScaleState>? scaleStateNotifier;
|
||||||
|
final bool disableScaleGestures;
|
||||||
|
|
||||||
const NativeVideoViewer({
|
const NativeVideoViewer({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -59,6 +63,8 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
required this.image,
|
required this.image,
|
||||||
this.showControls = true,
|
this.showControls = true,
|
||||||
this.playbackDelayFactor = 1,
|
this.playbackDelayFactor = 1,
|
||||||
|
this.scaleStateNotifier,
|
||||||
|
this.disableScaleGestures = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -138,6 +144,7 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
|
|
||||||
final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
|
final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
|
||||||
final aspectRatio = useState<double?>(null);
|
final aspectRatio = useState<double?>(null);
|
||||||
|
|
||||||
useMemoized(() async {
|
useMemoized(() async {
|
||||||
if (!context.mounted || aspectRatio.value != null) {
|
if (!context.mounted || aspectRatio.value != null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -313,6 +320,20 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Size? videoContextSize(double? videoAspectRatio, BuildContext? context) {
|
||||||
|
Size? videoContextSize;
|
||||||
|
if (videoAspectRatio == null || context == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final contextAspectRatio = context.width / context.height;
|
||||||
|
if (videoAspectRatio > contextAspectRatio) {
|
||||||
|
videoContextSize = Size(context.width, context.width / aspectRatio.value!);
|
||||||
|
} else {
|
||||||
|
videoContextSize = Size(context.height * aspectRatio.value!, context.height);
|
||||||
|
}
|
||||||
|
return videoContextSize;
|
||||||
|
}
|
||||||
|
|
||||||
ref.listen(currentAssetNotifier, (_, value) {
|
ref.listen(currentAssetNotifier, (_, value) {
|
||||||
final playerController = controller.value;
|
final playerController = controller.value;
|
||||||
if (playerController != null && value != asset) {
|
if (playerController != null && value != asset) {
|
||||||
@@ -393,26 +414,29 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Stack(
|
return SizedBox(
|
||||||
children: [
|
width: context.width,
|
||||||
// This remains under the video to avoid flickering
|
height: context.height,
|
||||||
// For motion videos, this is the image portion of the asset
|
child: Stack(
|
||||||
Center(key: ValueKey(asset.heroTag), child: image),
|
children: [
|
||||||
if (aspectRatio.value != null && !isCasting)
|
// Hide thumbnail once video is visible to avoid it showing in background when zooming out on video.
|
||||||
Visibility.maintain(
|
if (!isVisible.value || controller.value == null) Center(child: image),
|
||||||
key: ValueKey(asset),
|
if (aspectRatio.value != null && !isCasting && isCurrent)
|
||||||
visible: isVisible.value,
|
Visibility.maintain(
|
||||||
child: Center(
|
visible: isVisible.value,
|
||||||
key: ValueKey(asset),
|
child: PhotoView.customChild(
|
||||||
child: AspectRatio(
|
enableRotation: false,
|
||||||
key: ValueKey(asset),
|
disableScaleGestures: disableScaleGestures,
|
||||||
aspectRatio: aspectRatio.value!,
|
// Transparent to avoid a black flash when viewer becomes visible but video isn't loaded yet.
|
||||||
child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null,
|
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
|
||||||
|
scaleStateChangedCallback: (state) => scaleStateNotifier?.value = state,
|
||||||
|
childSize: videoContextSize(aspectRatio.value, context),
|
||||||
|
child: NativeVideoPlayerView(onViewReady: initController),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (showControls) const Center(child: VideoViewerControls()),
|
||||||
if (showControls) const Center(child: VideoViewerControls()),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,27 +81,35 @@ class VideoViewerControls extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toggleControlsVisibility() {
|
||||||
|
if (showBuffering) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (showControls) {
|
||||||
|
ref.read(assetViewerProvider.notifier).setControls(false);
|
||||||
|
} else {
|
||||||
|
showControlsAndStartHideTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: showControlsAndStartHideTimer,
|
onTap: toggleControlsVisibility,
|
||||||
child: AbsorbPointer(
|
child: IgnorePointer(
|
||||||
absorbing: !showControls,
|
ignoring: !showControls,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (showBuffering)
|
if (showBuffering)
|
||||||
const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400)))
|
const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400)))
|
||||||
else
|
else
|
||||||
GestureDetector(
|
CenterPlayButton(
|
||||||
onTap: () => ref.read(assetViewerProvider.notifier).setControls(false),
|
backgroundColor: Colors.black54,
|
||||||
child: CenterPlayButton(
|
iconColor: Colors.white,
|
||||||
backgroundColor: Colors.black54,
|
isFinished: state == VideoPlaybackState.completed,
|
||||||
iconColor: Colors.white,
|
isPlaying:
|
||||||
isFinished: state == VideoPlaybackState.completed,
|
state == VideoPlaybackState.playing || (cast.isCasting && cast.castState == CastState.playing),
|
||||||
isPlaying:
|
show: assetIsVideo && showControls,
|
||||||
state == VideoPlaybackState.playing || (cast.isCasting && cast.castState == CastState.playing),
|
onPressed: togglePlay,
|
||||||
show: assetIsVideo && showControls,
|
|
||||||
onPressed: togglePlay,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
|||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const UnArchiveActionButton(source: ActionSource.timeline),
|
const UnArchiveActionButton(source: ActionSource.timeline),
|
||||||
const FavoriteActionButton(source: ActionSource.timeline),
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(source: ActionSource.timeline),
|
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const UnFavoriteActionButton(source: ActionSource.timeline),
|
const UnFavoriteActionButton(source: ActionSource.timeline),
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(source: ActionSource.timeline),
|
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
|||||||
const ShareActionButton(source: ActionSource.timeline),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(source: ActionSource.timeline),
|
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
@@ -119,10 +119,11 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
|||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline),
|
if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
if (multiselect.onlyLocal || multiselect.hasMerged)
|
||||||
if (multiselect.hasLocal) const UploadActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
|
if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
slivers: multiselect.hasRemote
|
slivers: multiselect.hasRemote
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
@@ -113,6 +114,8 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
|||||||
],
|
],
|
||||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||||
|
if (ownsAlbum && multiselect.selectedAssets.length == 1)
|
||||||
|
SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||||
],
|
],
|
||||||
slivers: ownsAlbum
|
slivers: ownsAlbum
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
@@ -48,7 +50,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode) async* {
|
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {bool evictOnError = true}) async* {
|
||||||
if (isCancelled) {
|
if (isCancelled) {
|
||||||
this.request = null;
|
this.request = null;
|
||||||
PaintingBinding.instance.imageCache.evict(this);
|
PaintingBinding.instance.imageCache.evict(this);
|
||||||
@@ -57,11 +59,39 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final image = await request.load(decode);
|
final image = await request.load(decode);
|
||||||
if (image == null || isCancelled) {
|
if ((image == null && evictOnError) || isCancelled) {
|
||||||
PaintingBinding.instance.imageCache.evict(this);
|
PaintingBinding.instance.imageCache.evict(this);
|
||||||
return;
|
return;
|
||||||
|
} else if (image == null) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
yield image;
|
yield image;
|
||||||
|
} catch (e, stack) {
|
||||||
|
if (evictOnError) {
|
||||||
|
PaintingBinding.instance.imageCache.evict(this);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
_log.warning('Non-fatal image load error', e, stack);
|
||||||
|
} finally {
|
||||||
|
this.request = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ui.Codec?> loadCodecRequest(ImageRequest request) async {
|
||||||
|
if (isCancelled) {
|
||||||
|
this.request = null;
|
||||||
|
PaintingBinding.instance.imageCache.evict(this);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final codec = await request.loadCodec();
|
||||||
|
if (codec == null || isCancelled) {
|
||||||
|
codec?.dispose();
|
||||||
|
PaintingBinding.instance.imageCache.evict(this);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return codec;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
PaintingBinding.instance.imageCache.evict(this);
|
PaintingBinding.instance.imageCache.evict(this);
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
|||||||
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
||||||
assetType: key.assetType,
|
assetType: key.assetType,
|
||||||
);
|
);
|
||||||
|
|
||||||
yield* loadRequest(request, decode);
|
yield* loadRequest(request, decode);
|
||||||
|
|
||||||
if (!Store.get(StoreKey.loadOriginal, false)) {
|
if (!Store.get(StoreKey.loadOriginal, false)) {
|
||||||
|
|||||||
@@ -93,9 +93,10 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
|||||||
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
|
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
);
|
);
|
||||||
yield* loadRequest(previewRequest, decode);
|
final loadOriginal = assetType == AssetType.image && AppSetting.get(Setting.loadOriginal);
|
||||||
|
yield* loadRequest(previewRequest, decode, evictOnError: !loadOriginal);
|
||||||
|
|
||||||
if (assetType != AssetType.image || !AppSetting.get(Setting.loadOriginal)) {
|
if (!loadOriginal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedDate = widget.person.birthDate ?? DateTime.now();
|
_selectedDate = widget.person.birthDate ?? DateTime(DateTime.now().year - 30, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveBirthday() async {
|
void saveBirthday() async {
|
||||||
@@ -90,6 +90,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
|
|||||||
selectedDate: _selectedDate,
|
selectedDate: _selectedDate,
|
||||||
locale: context.locale,
|
locale: context.locale,
|
||||||
minimumDate: DateTime(1800, 1, 1),
|
minimumDate: DateTime(1800, 1, 1),
|
||||||
|
maximumDate: DateTime.now(),
|
||||||
onDateTimeChanged: (DateTime value) {
|
onDateTimeChanged: (DateTime value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedDate = value;
|
_selectedDate = value;
|
||||||
|
|||||||
@@ -29,38 +29,7 @@ import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
|||||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
||||||
|
|
||||||
class _TimelineRestorationState extends ChangeNotifier {
|
class Timeline extends StatelessWidget {
|
||||||
int? _restoreAssetIndex;
|
|
||||||
bool _shouldRestoreAssetPosition = false;
|
|
||||||
|
|
||||||
int? get restoreAssetIndex => _restoreAssetIndex;
|
|
||||||
bool get shouldRestoreAssetPosition => _shouldRestoreAssetPosition;
|
|
||||||
|
|
||||||
void setRestoreAssetIndex(int? index) {
|
|
||||||
_restoreAssetIndex = index;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setShouldRestoreAssetPosition(bool should) {
|
|
||||||
_shouldRestoreAssetPosition = should;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearRestoreAssetIndex() {
|
|
||||||
_restoreAssetIndex = null;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelineRestorationProvider extends InheritedNotifier<_TimelineRestorationState> {
|
|
||||||
const _TimelineRestorationProvider({required super.notifier, required super.child});
|
|
||||||
|
|
||||||
static _TimelineRestorationState of(BuildContext context) {
|
|
||||||
return context.dependOnInheritedWidgetOfExactType<_TimelineRestorationProvider>()!.notifier!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Timeline extends StatefulWidget {
|
|
||||||
const Timeline({
|
const Timeline({
|
||||||
super.key,
|
super.key,
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
@@ -74,6 +43,7 @@ class Timeline extends StatefulWidget {
|
|||||||
this.snapToMonth = true,
|
this.snapToMonth = true,
|
||||||
this.initialScrollOffset,
|
this.initialScrollOffset,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
|
this.persistentBottomBar = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
@@ -87,26 +57,7 @@ class Timeline extends StatefulWidget {
|
|||||||
final bool snapToMonth;
|
final bool snapToMonth;
|
||||||
final double? initialScrollOffset;
|
final double? initialScrollOffset;
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
|
final bool persistentBottomBar;
|
||||||
@override
|
|
||||||
State<Timeline> createState() => _TimelineState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelineState extends State<Timeline> {
|
|
||||||
double? _lastWidth;
|
|
||||||
late final _TimelineRestorationState _restorationState;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_restorationState = _TimelineRestorationState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_restorationState.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -114,41 +65,32 @@ class _TimelineState extends State<Timeline> {
|
|||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
floatingActionButton: const DownloadStatusFloatingButton(),
|
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (_, constraints) {
|
builder: (_, constraints) => ProviderScope(
|
||||||
if (_lastWidth != null && _lastWidth != constraints.maxWidth) {
|
overrides: [
|
||||||
_restorationState.setShouldRestoreAssetPosition(true);
|
timelineArgsProvider.overrideWith(
|
||||||
}
|
(ref) => TimelineArgs(
|
||||||
_lastWidth = constraints.maxWidth;
|
maxWidth: constraints.maxWidth,
|
||||||
return _TimelineRestorationProvider(
|
maxHeight: constraints.maxHeight,
|
||||||
notifier: _restorationState,
|
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
|
||||||
child: ProviderScope(
|
showStorageIndicator: showStorageIndicator,
|
||||||
key: ValueKey(_lastWidth),
|
withStack: withStack,
|
||||||
overrides: [
|
groupBy: groupBy,
|
||||||
timelineArgsProvider.overrideWith(
|
|
||||||
(ref) => TimelineArgs(
|
|
||||||
maxWidth: constraints.maxWidth,
|
|
||||||
maxHeight: constraints.maxHeight,
|
|
||||||
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
|
|
||||||
showStorageIndicator: widget.showStorageIndicator,
|
|
||||||
withStack: widget.withStack,
|
|
||||||
groupBy: widget.groupBy,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
|
|
||||||
],
|
|
||||||
child: _SliverTimeline(
|
|
||||||
key: const ValueKey('_sliver_timeline'),
|
|
||||||
topSliverWidget: widget.topSliverWidget,
|
|
||||||
topSliverWidgetHeight: widget.topSliverWidgetHeight,
|
|
||||||
appBar: widget.appBar,
|
|
||||||
bottomSheet: widget.bottomSheet,
|
|
||||||
withScrubber: widget.withScrubber,
|
|
||||||
snapToMonth: widget.snapToMonth,
|
|
||||||
initialScrollOffset: widget.initialScrollOffset,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
|
||||||
},
|
],
|
||||||
|
child: _SliverTimeline(
|
||||||
|
topSliverWidget: topSliverWidget,
|
||||||
|
topSliverWidgetHeight: topSliverWidgetHeight,
|
||||||
|
appBar: appBar,
|
||||||
|
bottomSheet: bottomSheet,
|
||||||
|
withScrubber: withScrubber,
|
||||||
|
persistentBottomBar: persistentBottomBar,
|
||||||
|
snapToMonth: snapToMonth,
|
||||||
|
initialScrollOffset: initialScrollOffset,
|
||||||
|
maxWidth: constraints.maxWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -167,14 +109,15 @@ class _AlwaysReadOnlyNotifier extends ReadOnlyModeNotifier {
|
|||||||
|
|
||||||
class _SliverTimeline extends ConsumerStatefulWidget {
|
class _SliverTimeline extends ConsumerStatefulWidget {
|
||||||
const _SliverTimeline({
|
const _SliverTimeline({
|
||||||
super.key,
|
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
this.topSliverWidgetHeight,
|
this.topSliverWidgetHeight,
|
||||||
this.appBar,
|
this.appBar,
|
||||||
this.bottomSheet,
|
this.bottomSheet,
|
||||||
this.withScrubber = true,
|
this.withScrubber = true,
|
||||||
|
this.persistentBottomBar = false,
|
||||||
this.snapToMonth = true,
|
this.snapToMonth = true,
|
||||||
this.initialScrollOffset,
|
this.initialScrollOffset,
|
||||||
|
this.maxWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
@@ -182,8 +125,10 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
final Widget? appBar;
|
final Widget? appBar;
|
||||||
final Widget? bottomSheet;
|
final Widget? bottomSheet;
|
||||||
final bool withScrubber;
|
final bool withScrubber;
|
||||||
|
final bool persistentBottomBar;
|
||||||
final bool snapToMonth;
|
final bool snapToMonth;
|
||||||
final double? initialScrollOffset;
|
final double? initialScrollOffset;
|
||||||
|
final double? maxWidth;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
@@ -202,6 +147,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
int _perRow = 4;
|
int _perRow = 4;
|
||||||
double _scaleFactor = 3.0;
|
double _scaleFactor = 3.0;
|
||||||
double _baseScaleFactor = 3.0;
|
double _baseScaleFactor = 3.0;
|
||||||
|
int? _restoreAssetIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -220,6 +166,20 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
|
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _SliverTimeline oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.maxWidth != oldWidget.maxWidth) {
|
||||||
|
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||||
|
asyncSegments.whenData((segments) {
|
||||||
|
final index = _getCurrentAssetIndex(segments);
|
||||||
|
// Refresh to wait for new segments to be generated with the updated width before restoring the scroll position
|
||||||
|
final _ = ref.refresh(timelineArgsProvider);
|
||||||
|
_restoreAssetIndex = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onEvent(Event event) {
|
void _onEvent(Event event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ScrollToTopEvent():
|
case ScrollToTopEvent():
|
||||||
@@ -237,21 +197,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMultiSelectionToggled(_, bool isEnabled) {
|
|
||||||
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _restoreAssetPosition(_) {
|
void _restoreAssetPosition(_) {
|
||||||
final restorationState = _TimelineRestorationProvider.of(context);
|
if (_restoreAssetIndex == null) return;
|
||||||
if (!restorationState.shouldRestoreAssetPosition || restorationState.restoreAssetIndex == null) return;
|
|
||||||
|
|
||||||
final asyncSegments = ref.read(timelineSegmentProvider);
|
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||||
asyncSegments.whenData((segments) {
|
asyncSegments.whenData((segments) {
|
||||||
final targetSegment = segments.lastWhereOrNull(
|
final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!);
|
||||||
(segment) => segment.firstAssetIndex <= restorationState.restoreAssetIndex!,
|
|
||||||
);
|
|
||||||
if (targetSegment != null) {
|
if (targetSegment != null) {
|
||||||
final assetIndexInSegment = restorationState.restoreAssetIndex! - targetSegment.firstAssetIndex;
|
final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex;
|
||||||
final newColumnCount = ref.read(timelineArgsProvider).columnCount;
|
final newColumnCount = ref.read(timelineArgsProvider).columnCount;
|
||||||
final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor();
|
final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor();
|
||||||
final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment;
|
final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment;
|
||||||
@@ -263,7 +216,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
restorationState.clearRestoreAssetIndex();
|
_restoreAssetIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMultiSelectionToggled(_, bool isEnabled) {
|
||||||
|
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
int? _getCurrentAssetIndex(List<Segment> segments) {
|
int? _getCurrentAssetIndex(List<Segment> segments) {
|
||||||
@@ -404,6 +361,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable));
|
final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable));
|
||||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
final isMultiSelectStatusVisible = !isSelectionMode && isMultiSelectEnabled;
|
||||||
|
final isBottomWidgetVisible =
|
||||||
|
widget.bottomSheet != null && (isMultiSelectStatusVisible || widget.persistentBottomBar);
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !isMultiSelectEnabled,
|
canPop: !isMultiSelectEnabled,
|
||||||
@@ -470,68 +430,56 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
|
|
||||||
return PrimaryScrollController(
|
return PrimaryScrollController(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: NotificationListener<ScrollEndNotification>(
|
child: RawGestureDetector(
|
||||||
onNotification: (notification) {
|
gestures: {
|
||||||
final currentIndex = _getCurrentAssetIndex(segments);
|
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
||||||
if (currentIndex != null && mounted) {
|
() => CustomScaleGestureRecognizer(),
|
||||||
_TimelineRestorationProvider.of(context).setRestoreAssetIndex(currentIndex);
|
(CustomScaleGestureRecognizer scale) {
|
||||||
}
|
scale.onStart = (details) {
|
||||||
return false;
|
_baseScaleFactor = _scaleFactor;
|
||||||
},
|
};
|
||||||
child: RawGestureDetector(
|
|
||||||
gestures: {
|
|
||||||
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
|
||||||
() => CustomScaleGestureRecognizer(),
|
|
||||||
(CustomScaleGestureRecognizer scale) {
|
|
||||||
scale.onStart = (details) {
|
|
||||||
_baseScaleFactor = _scaleFactor;
|
|
||||||
};
|
|
||||||
|
|
||||||
scale.onUpdate = (details) {
|
scale.onUpdate = (details) {
|
||||||
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
||||||
final newPerRow = 7 - newScaleFactor.toInt();
|
final newPerRow = 7 - newScaleFactor.toInt();
|
||||||
|
|
||||||
|
if (newPerRow != _perRow) {
|
||||||
final targetAssetIndex = _getCurrentAssetIndex(segments);
|
final targetAssetIndex = _getCurrentAssetIndex(segments);
|
||||||
|
setState(() {
|
||||||
|
_scaleFactor = newScaleFactor;
|
||||||
|
_perRow = newPerRow;
|
||||||
|
_restoreAssetIndex = targetAssetIndex;
|
||||||
|
});
|
||||||
|
|
||||||
if (newPerRow != _perRow) {
|
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
||||||
final restorationState = _TimelineRestorationProvider.of(context);
|
}
|
||||||
setState(() {
|
};
|
||||||
_scaleFactor = newScaleFactor;
|
|
||||||
_perRow = newPerRow;
|
|
||||||
});
|
|
||||||
|
|
||||||
restorationState.setRestoreAssetIndex(targetAssetIndex);
|
|
||||||
restorationState.setShouldRestoreAssetPosition(true);
|
|
||||||
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
child: TimelineDragRegion(
|
|
||||||
onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null,
|
|
||||||
onAssetEnter: _handleDragAssetEnter,
|
|
||||||
onEnd: !isReadonlyModeEnabled ? _stopDrag : null,
|
|
||||||
onScroll: _dragScroll,
|
|
||||||
onScrollStart: () {
|
|
||||||
// Minimize the bottom sheet when drag selection starts
|
|
||||||
ref.read(timelineStateProvider.notifier).setScrolling(true);
|
|
||||||
},
|
},
|
||||||
child: Stack(
|
),
|
||||||
children: [
|
},
|
||||||
timeline,
|
child: TimelineDragRegion(
|
||||||
if (!isSelectionMode && isMultiSelectEnabled) ...[
|
onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null,
|
||||||
Positioned(
|
onAssetEnter: _handleDragAssetEnter,
|
||||||
top: MediaQuery.paddingOf(context).top,
|
onEnd: !isReadonlyModeEnabled ? _stopDrag : null,
|
||||||
left: 25,
|
onScroll: _dragScroll,
|
||||||
child: const SizedBox(
|
onScrollStart: () {
|
||||||
height: kToolbarHeight,
|
// Minimize the bottom sheet when drag selection starts
|
||||||
child: Center(child: _MultiSelectStatusButton()),
|
ref.read(timelineStateProvider.notifier).setScrolling(true);
|
||||||
),
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
timeline,
|
||||||
|
if (isBottomWidgetVisible)
|
||||||
|
Positioned(
|
||||||
|
top: MediaQuery.paddingOf(context).top,
|
||||||
|
left: 25,
|
||||||
|
child: const SizedBox(
|
||||||
|
height: kToolbarHeight,
|
||||||
|
child: Center(child: _MultiSelectStatusButton()),
|
||||||
),
|
),
|
||||||
if (widget.bottomSheet != null) widget.bottomSheet!,
|
),
|
||||||
],
|
if (isBottomWidgetVisible) widget.bottomSheet!,
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user