Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca6fe89388 | |||
| 3b0a803089 | |||
| bcda2c6e22 | |||
| 7347f64958 | |||
| 176d53c1b3 | |||
| 5fc448bc97 | |||
| 3d0c851636 | |||
| 16fcb657b7 | |||
| 32b57bcbfc | |||
| 7f56443b24 | |||
| 189442e9c4 | |||
| 523fe5bef7 | |||
| 77a362f0c0 | |||
| 5f5308631e | |||
| 004c2f2496 | |||
| e2dfbd66c3 | |||
| de756d9497 | |||
| 103b83d2d6 | |||
| f54cfa7a5a | |||
| ed5b260eeb | |||
| 8923d5b0a3 | |||
| 2f3d4e15d2 | |||
| c9bcae813b | |||
| bddb43e1d4 | |||
| 176656b5f4 |
@@ -1,2 +0,0 @@
|
|||||||
.env
|
|
||||||
library
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:7c2e711a4f7b02f32d2da16192d5e05aa7c95279be4ce889cff5df316f251c1d
|
|
||||||
FROM ${BASEIMAGE}
|
|
||||||
|
|
||||||
# Flutter SDK
|
|
||||||
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
|
||||||
ENV FLUTTER_CHANNEL="stable"
|
|
||||||
ENV FLUTTER_VERSION="3.29.3"
|
|
||||||
ENV FLUTTER_HOME=/flutter
|
|
||||||
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
|
|
||||||
|
|
||||||
# Flutter SDK
|
|
||||||
RUN mkdir -p ${FLUTTER_HOME} \
|
|
||||||
&& curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \
|
|
||||||
&& tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \
|
|
||||||
&& rm flutter.tar.xz \
|
|
||||||
&& chown -R 1000:1000 ${FLUTTER_HOME}
|
|
||||||
@@ -1,26 +1,67 @@
|
|||||||
{
|
{
|
||||||
"name": "Immich",
|
"name": "Immich - Backend, Frontend and ML",
|
||||||
"service": "immich-devcontainer",
|
"service": "immich-server",
|
||||||
|
"runServices": [
|
||||||
|
"immich-server",
|
||||||
|
"redis",
|
||||||
|
"database",
|
||||||
|
"immich-machine-learning"
|
||||||
|
],
|
||||||
"dockerComposeFile": [
|
"dockerComposeFile": [
|
||||||
"docker-compose.yml",
|
"../docker/docker-compose.dev.yml",
|
||||||
"../docker/docker-compose.dev.yml"
|
"./server/container-compose-overrides.yml"
|
||||||
],
|
],
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"Dart-Code.dart-code",
|
|
||||||
"Dart-Code.flutter",
|
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"dcmdev.dcm-vscode-extension",
|
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"svelte.svelte-vscode"
|
"svelte.svelte-vscode",
|
||||||
|
"ms-vscode-remote.remote-containers",
|
||||||
|
"foxundermoon.shell-format",
|
||||||
|
"timonwong.shellcheck",
|
||||||
|
"rvest.vs-code-prettier-eslint",
|
||||||
|
"bluebrown.yamlfmt",
|
||||||
|
"vkrishna04.cspell-sync",
|
||||||
|
"vitest.explorer",
|
||||||
|
"ms-playwright.playwright",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"forwardPorts": [],
|
"forwardPorts": [3000, 9231, 9230, 2283],
|
||||||
"initializeCommand": "bash .devcontainer/scripts/initializeCommand.sh",
|
"portsAttributes": {
|
||||||
"onCreateCommand": "bash .devcontainer/scripts/onCreateCommand.sh",
|
"3000": {
|
||||||
|
"label": "Immich - Frontend HTTP",
|
||||||
|
"description": "The frontend of the Immich project",
|
||||||
|
"onAutoForward": "openBrowserOnce"
|
||||||
|
},
|
||||||
|
"2283": {
|
||||||
|
"label": "Immich - API Server - HTTP",
|
||||||
|
"description": "The API server of the Immich project"
|
||||||
|
},
|
||||||
|
"9231": {
|
||||||
|
"label": "Immich - API Server - DEBUG",
|
||||||
|
"description": "The API server of the Immich project"
|
||||||
|
},
|
||||||
|
"9230": {
|
||||||
|
"label": "Immich - Workers - DEBUG",
|
||||||
|
"description": "The workers of the Immich project"
|
||||||
|
}
|
||||||
|
},
|
||||||
"overrideCommand": true,
|
"overrideCommand": true,
|
||||||
"workspaceFolder": "/immich",
|
"workspaceFolder": "/workspaces/immich",
|
||||||
"remoteUser": "node"
|
"remoteUser": "node",
|
||||||
|
"userEnvProbe": "loginInteractiveShell",
|
||||||
|
"remoteEnv": {
|
||||||
|
// The location where your uploaded files are stored
|
||||||
|
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./Library}",
|
||||||
|
// Connection secret for postgres. You should change it to a random password
|
||||||
|
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
|
||||||
|
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
|
||||||
|
// The database username
|
||||||
|
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
|
||||||
|
// The database name
|
||||||
|
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
services:
|
|
||||||
immich-devcontainer:
|
|
||||||
build:
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
extra_hosts:
|
|
||||||
- 'host.docker.internal:host-gateway'
|
|
||||||
volumes:
|
|
||||||
- ..:/immich:cached
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
build:
|
||||||
|
target: dev-container-mobile
|
||||||
|
environment:
|
||||||
|
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||||
|
volumes: !override # bind mount host to /workspaces/immich
|
||||||
|
- ..:/workspaces/immich
|
||||||
|
- cli_node_modules:/workspaces/immich/cli/node_modules
|
||||||
|
- e2e_node_modules:/workspaces/immich/e2e/node_modules
|
||||||
|
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||||
|
- server_node_modules:/workspaces/immich/server/node_modules
|
||||||
|
- web_node_modules:/workspaces/immich/web/node_modules
|
||||||
|
- ${UPLOAD_LOCATION}/photos:/workspaces/immich/server/upload
|
||||||
|
- ${UPLOAD_LOCATION}/photos/upload:/workspaces/immich/server/upload/upload
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
||||||
|
database:
|
||||||
|
volumes:
|
||||||
|
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Node modules for each service to avoid conflicts and ensure consistent dependencies
|
||||||
|
cli_node_modules:
|
||||||
|
e2e_node_modules:
|
||||||
|
open_api_node_modules:
|
||||||
|
server_node_modules:
|
||||||
|
web_node_modules:
|
||||||
|
|
||||||
|
# UPLOAD_LOCATION must be set to a absolute path or vol-upload
|
||||||
|
vol-upload:
|
||||||
|
|
||||||
|
# DB_DATA_LOCATION must be set to a absolute path or vol-database
|
||||||
|
vol-database:
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "Immich - Mobile",
|
||||||
|
"service": "immich-server",
|
||||||
|
"runServices": [
|
||||||
|
"immich-server",
|
||||||
|
"redis",
|
||||||
|
"database",
|
||||||
|
"immich-machine-learning"
|
||||||
|
],
|
||||||
|
"dockerComposeFile": [
|
||||||
|
"../../docker/docker-compose.dev.yml",
|
||||||
|
"./container-compose-overrides.yml"
|
||||||
|
],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"Dart-Code.dart-code",
|
||||||
|
"Dart-Code.flutter",
|
||||||
|
"dcmdev.dcm-vscode-extension",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"svelte.svelte-vscode",
|
||||||
|
"ms-vscode-remote.remote-containers",
|
||||||
|
"foxundermoon.shell-format",
|
||||||
|
"timonwong.shellcheck",
|
||||||
|
"rvest.vs-code-prettier-eslint",
|
||||||
|
"bluebrown.yamlfmt",
|
||||||
|
"vkrishna04.cspell-sync",
|
||||||
|
"vitest.explorer",
|
||||||
|
"ms-playwright.playwright",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [],
|
||||||
|
"overrideCommand": true,
|
||||||
|
"workspaceFolder": "/workspaces/immich",
|
||||||
|
"remoteUser": "node",
|
||||||
|
"userEnvProbe": "loginInteractiveShell",
|
||||||
|
"remoteEnv": {
|
||||||
|
// The location where your uploaded files are stored
|
||||||
|
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./Library}",
|
||||||
|
// Connection secret for postgres. You should change it to a random password
|
||||||
|
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
|
||||||
|
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
|
||||||
|
// The database username
|
||||||
|
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
|
||||||
|
// The database name
|
||||||
|
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# If .env file does not exist, create it by copying example.env from the docker folder
|
|
||||||
if [ ! -f ".devcontainer/.env" ]; then
|
|
||||||
cp docker/example.env .devcontainer/.env
|
|
||||||
fi
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Enable multiarch for arm64 if necessary
|
|
||||||
if [ "$(dpkg --print-architecture)" = "arm64" ]; then
|
|
||||||
sudo dpkg --add-architecture amd64 && \
|
|
||||||
sudo apt-get update && \
|
|
||||||
sudo apt-get install -y --no-install-recommends \
|
|
||||||
qemu-user-static \
|
|
||||||
libc6:amd64 \
|
|
||||||
libstdc++6:amd64 \
|
|
||||||
libgcc1:amd64
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install DCM
|
|
||||||
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
|
||||||
sudo echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
|
|
||||||
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install dcm
|
|
||||||
|
|
||||||
dart --disable-analytics
|
|
||||||
|
|
||||||
# Install immich
|
|
||||||
cd /immich || exit
|
|
||||||
make install-all
|
|
||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
|
||||||
|
export DEV_PORT="${DEV_PORT:-3000}"
|
||||||
|
|
||||||
|
# search for immich directory inside workspace.
|
||||||
|
# /workspaces/immich is the bind mount, but other directories can be mounted if runing
|
||||||
|
# Devcontainer: Clone [repository|pull request] in container volumne
|
||||||
|
WORKSPACES_DIR="/workspaces"
|
||||||
|
IMMICH_DIR="$WORKSPACES_DIR/immich"
|
||||||
|
|
||||||
|
# Find directories excluding /workspaces/immich
|
||||||
|
mapfile -t other_dirs < <(find "$WORKSPACES_DIR" -mindepth 1 -maxdepth 1 -type d ! -path "$IMMICH_DIR" ! -name ".*")
|
||||||
|
|
||||||
|
if [ ${#other_dirs[@]} -gt 1 ]; then
|
||||||
|
echo "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
|
||||||
|
|
||||||
|
echo "Found immich workspace in $IMMICH_WORKSPACE"
|
||||||
|
|
||||||
|
run_cmd() {
|
||||||
|
echo "$@"
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_permissions() {
|
||||||
|
|
||||||
|
echo "Fixing permissions for ${IMMICH_WORKSPACE}"
|
||||||
|
|
||||||
|
run_cmd sudo find "${IMMICH_WORKSPACE}/server/upload" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres/*" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres" -exec chown node {} +
|
||||||
|
|
||||||
|
run_cmd sudo chown node -R "${IMMICH_WORKSPACE}/.vscode" \
|
||||||
|
"${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"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dependencies() {
|
||||||
|
|
||||||
|
echo "Installing dependencies"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||||
|
run_cmd make install-server
|
||||||
|
run_cmd make install-open-api
|
||||||
|
run_cmd make build-open-api
|
||||||
|
run_cmd make install-web
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
build:
|
||||||
|
target: dev-container-server
|
||||||
|
env_file: !reset []
|
||||||
|
environment:
|
||||||
|
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||||
|
volumes: !override
|
||||||
|
- ..:/workspaces/immich
|
||||||
|
- cli_node_modules:/workspaces/immich/cli/node_modules
|
||||||
|
- e2e_node_modules:/workspaces/immich/e2e/node_modules
|
||||||
|
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||||
|
- server_node_modules:/workspaces/immich/server/node_modules
|
||||||
|
- web_node_modules:/workspaces/immich/web/node_modules
|
||||||
|
- ${UPLOAD_LOCATION-./Library}/photos:/workspaces/immich/server/upload
|
||||||
|
- ${UPLOAD_LOCATION-./Library}/photos/upload:/workspaces/immich/server/upload/upload
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
||||||
|
immich-web:
|
||||||
|
env_file: !reset []
|
||||||
|
|
||||||
|
immich-machine-learning:
|
||||||
|
env_file: !reset []
|
||||||
|
|
||||||
|
database:
|
||||||
|
env_file: !reset []
|
||||||
|
environment: !override
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD-postgres}
|
||||||
|
POSTGRES_USER: ${DB_USERNAME-postgres}
|
||||||
|
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
|
||||||
|
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||||
|
volumes:
|
||||||
|
- ${UPLOAD_LOCATION-./Library}/postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
redis:
|
||||||
|
env_file: !reset []
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Node modules for each service to avoid conflicts and ensure consistent dependencies
|
||||||
|
cli_node_modules:
|
||||||
|
e2e_node_modules:
|
||||||
|
open_api_node_modules:
|
||||||
|
server_node_modules:
|
||||||
|
web_node_modules:
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /immich-devcontainer/container-common.sh
|
||||||
|
|
||||||
|
echo "Starting Nest API Server"
|
||||||
|
|
||||||
|
cd "${IMMICH_WORKSPACE}/server" || (
|
||||||
|
echo workspace not found
|
||||||
|
exit 1
|
||||||
|
)
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
node ./node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch
|
||||||
|
echo " Nest API Server crashed with exit code $?. Respawning in 3s ..."
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /immich-devcontainer/container-common.sh
|
||||||
|
|
||||||
|
echo "Starting Immich Web Frontend"
|
||||||
|
|
||||||
|
cd "${IMMICH_WORKSPACE}/web" || (
|
||||||
|
echo Workspace not found
|
||||||
|
exit 1
|
||||||
|
)
|
||||||
|
|
||||||
|
until curl --output /dev/null --silent --head --fail "http://127.0.0.1:${IMMICH_PORT}/api/server/config"; do
|
||||||
|
echo 'waiting for api server...'
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
node ./node_modules/.bin/vite dev --host 0.0.0.0 --port "${DEV_PORT}"
|
||||||
|
echo "Web crashed with exit code $?. Respawning in 3s ..."
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /immich-devcontainer/container-common.sh
|
||||||
|
|
||||||
|
fix_permissions
|
||||||
|
install_dependencies
|
||||||
Vendored
+72
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Fix Permissions, Install Dependencies",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "dedicated",
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": false,
|
||||||
|
"group": "Devcontainer tasks",
|
||||||
|
"close": true
|
||||||
|
},
|
||||||
|
"runOptions": {
|
||||||
|
"runOn": "default"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Immich API Server (Nest)",
|
||||||
|
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||||
|
"type": "shell",
|
||||||
|
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "dedicated",
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": false,
|
||||||
|
"group": "Devcontainer tasks",
|
||||||
|
"close": true
|
||||||
|
},
|
||||||
|
"runOptions": {
|
||||||
|
"runOn": "default"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Immich Web Server (Vite)",
|
||||||
|
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||||
|
"type": "shell",
|
||||||
|
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "dedicated",
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": false,
|
||||||
|
"group": "Devcontainer tasks",
|
||||||
|
"close": true
|
||||||
|
},
|
||||||
|
"runOptions": {
|
||||||
|
"runOn": "default"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Immich Server and Web",
|
||||||
|
"dependsOn": ["Immich Web Server (Vite)", "Immich API Server (Nest)"],
|
||||||
|
"runOptions": {
|
||||||
|
"runOn": "folderOpen"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -70,7 +70,10 @@ const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
|
|||||||
console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4));
|
console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4));
|
||||||
}
|
}
|
||||||
await updateAlbums([...newAssets, ...duplicates], options);
|
await updateAlbums([...newAssets, ...duplicates], options);
|
||||||
await deleteFiles(newFiles, options);
|
await deleteFiles(
|
||||||
|
newAssets.map(({ filepath }) => filepath),
|
||||||
|
options,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startWatch = async (
|
export const startWatch = async (
|
||||||
|
|||||||
@@ -0,0 +1,481 @@
|
|||||||
|
---
|
||||||
|
title: Devcontainers
|
||||||
|
sidebar_position: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Development with Dev Containers
|
||||||
|
|
||||||
|
Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces.
|
||||||
|
|
||||||
|
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/)
|
||||||
|
|
||||||
|
[](https://codespaces.new/immich-app/immich/)
|
||||||
|
|
||||||
|
[Learn more about Dev Containers](https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before getting started, ensure you have:
|
||||||
|
|
||||||
|
- **Docker Desktop** (latest version)
|
||||||
|
- [Mac](https://docs.docker.com/desktop/install/mac-install/)
|
||||||
|
- [Windows](https://docs.docker.com/desktop/install/windows-install/) (with WSL2 backend recommended)
|
||||||
|
- [Linux](https://docs.docker.com/desktop/install/linux-install/)
|
||||||
|
- **Visual Studio Code** with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||||
|
- **Git** for cloning the repository
|
||||||
|
- At least **8GB of RAM** (16GB recommended)
|
||||||
|
- **20GB of free disk space**
|
||||||
|
|
||||||
|
:::tip Alternative Development Environments
|
||||||
|
While this guide focuses on VS Code, you have many options for Dev Container development:
|
||||||
|
|
||||||
|
**Local Editors:**
|
||||||
|
|
||||||
|
- [IntelliJ IDEA](https://www.jetbrains.com/help/idea/connect-to-devcontainer.html) - Full JetBrains IDE support
|
||||||
|
- [neovim](https://github.com/jamestthompson3/nvim-remote-containers) - Lightweight terminal-based editor
|
||||||
|
- [Emacs](https://github.com/emacs-lsp/lsp-docker) - Extensible text editor
|
||||||
|
- [DevContainer CLI](https://github.com/devcontainers/cli) - Command-line interface
|
||||||
|
|
||||||
|
**Cloud-Based Solutions:**
|
||||||
|
|
||||||
|
- [GitHub Codespaces](https://github.com/features/codespaces) - Fully integrated with GitHub, excellent devcontainer.json support
|
||||||
|
- [GitPod](https://www.gitpod.io) - SaaS platform with recent Dev Container support (historically used gitpod.yml)
|
||||||
|
|
||||||
|
**Self-Hostable Options:**
|
||||||
|
|
||||||
|
- [Coder](https://coder.com) - Enterprise-focused, requires Terraform knowledge, self-managed
|
||||||
|
- [DevPod](https://devpod.sh) - Client-only tool with excellent devcontainer.json support, works with any provider (local, cloud, or on-premise)
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Dev Container Services
|
||||||
|
|
||||||
|
The Dev Container environment consists of the following services:
|
||||||
|
|
||||||
|
| Service | Container Name | Description | Ports |
|
||||||
|
| ---------------- | ------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||||
|
| Server & Web | `immich-server` | Runs both API server and web frontend in development mode | 2283 (API)<br/>3000 (Web)<br/>9230 (Workers Debug)<br/>9231 (API Debug) |
|
||||||
|
| Database | `database` | PostgreSQL database | 5432 |
|
||||||
|
| Cache | `redis` | Valkey cache server | 6379 |
|
||||||
|
| Machine Learning | `immich-machine-learning` | Immich ML model inference server | 3003 |
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Step 1: Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/immich-app/immich.git
|
||||||
|
cd immich
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Configure Environment Variables
|
||||||
|
|
||||||
|
The immich dev containers read environment variables from your shell environment, not from `.env` files. This allows them to work in cloud environments without pre-configuration.
|
||||||
|
|
||||||
|
:::important Required Configuration
|
||||||
|
When running locally, and if you want to create (or use an existing) DB and/or photo storage folder, you must set the `UPLOAD_LOCATION` variable in your shell environment before launching the Dev Container. This determines where uploaded files are stored and also where the DB stores it data.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set temporarily for current session
|
||||||
|
export UPLOAD_LOCATION=/opt/dev_upload_folder
|
||||||
|
|
||||||
|
# Or add to your shell profile for persistence
|
||||||
|
# (~/.bashrc, ~/.zshrc, ~/.bash_profile, etc.)
|
||||||
|
echo 'export UPLOAD_LOCATION=/opt/dev_upload_folder' >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Step 3: Launch the Dev Container
|
||||||
|
|
||||||
|
#### Using VS Code UI:
|
||||||
|
|
||||||
|
1. Open the cloned repository in VS Code
|
||||||
|
2. Press `F1` or `Ctrl/Cmd+Shift+P` to open the command palette
|
||||||
|
3. Type and select "Dev Containers: Rebuild and Reopen in Container"
|
||||||
|
4. Select "Immich - Backend, Frontend and ML" from the list
|
||||||
|
5. Wait for the container to build and start (this may take several minutes on first run)
|
||||||
|
|
||||||
|
#### Using VS Code Quick Actions:
|
||||||
|
|
||||||
|
1. Open the repository in VS Code
|
||||||
|
2. You should see a popup asking if you want to reopen in a container
|
||||||
|
3. Click "Reopen in Container"
|
||||||
|
|
||||||
|
#### Using Command Line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using the DevContainer CLI
|
||||||
|
devcontainer up --workspace-folder .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variable Details
|
||||||
|
|
||||||
|
### How Dev Containers Handle Environment Variables
|
||||||
|
|
||||||
|
Unlike the Immich developer setup based on Docker Compose which uses `.env` files, Immich Dev Containers read environment variables from your shell environment. This is configured in `.devcontainer/devcontainer.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"remoteEnv": {
|
||||||
|
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./Library}",
|
||||||
|
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
|
||||||
|
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
|
||||||
|
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `${localEnv:VARIABLE:default}` syntax reads from your shell environment with optional defaults.
|
||||||
|
|
||||||
|
### Upload Location Path Resolution
|
||||||
|
|
||||||
|
The `UPLOAD_LOCATION` environment variable controls where files are stored:
|
||||||
|
|
||||||
|
**Default:** `./Library` (relative to the `docker` directory)
|
||||||
|
**Resolved to:** `<immich-root>/docker/Library`
|
||||||
|
|
||||||
|
**Bind Mounts Created:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# From .devcontainer/server/container-compose-overrides.yml
|
||||||
|
- ${UPLOAD_LOCATION-./Library}/photos:/workspaces/immich/server/upload
|
||||||
|
- ${UPLOAD_LOCATION-./Library}/postgres:/var/lib/postgresql/data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Configuration
|
||||||
|
|
||||||
|
These variables have sensible defaults (for development) but can be customized:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| ------------------ | ---------- | ------------------- |
|
||||||
|
| `DB_PASSWORD` | `postgres` | PostgreSQL password |
|
||||||
|
| `DB_USERNAME` | `postgres` | PostgreSQL username |
|
||||||
|
| `DB_DATABASE_NAME` | `immich` | Database name |
|
||||||
|
|
||||||
|
### Setting Environment Variables
|
||||||
|
|
||||||
|
Add these to your shell profile (`~/.bashrc`, `~/.zshrc`, `~/.bash_profile`, etc.):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
export UPLOAD_LOCATION=./Library # or absolute path
|
||||||
|
|
||||||
|
# Optional (only if using non-default values)
|
||||||
|
export DB_PASSWORD=your_password
|
||||||
|
export DB_USERNAME=your_username
|
||||||
|
export DB_DATABASE_NAME=your_database
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember to reload your shell configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/.bashrc # or ~/.zshrc, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git Configuration
|
||||||
|
|
||||||
|
### SSH Keys and Authentication
|
||||||
|
|
||||||
|
To use your SSH keys for GitHub access inside the Dev Container:
|
||||||
|
|
||||||
|
1. **Start SSH Agent** on your host machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
eval "$(ssh-agent -s)"
|
||||||
|
ssh-add ~/.ssh/id_rsa # or your key path
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **VS Code automatically forwards your SSH agent** to the container
|
||||||
|
|
||||||
|
For detailed instructions, see the [VS Code guide on sharing Git credentials](https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials).
|
||||||
|
|
||||||
|
### Commit Signing
|
||||||
|
|
||||||
|
To use your SSH key for commit signing, see the [GitHub guide on SSH commit signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-ssh-key).
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Automatic Setup
|
||||||
|
|
||||||
|
When the Dev Container starts, it automatically:
|
||||||
|
|
||||||
|
1. **Runs post-create script** (`container-server-post-create.sh`):
|
||||||
|
|
||||||
|
- Adjusts file permissions for the `node` user
|
||||||
|
- Installs dependencies: `npm install` in all packages
|
||||||
|
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
|
||||||
|
|
||||||
|
2. **Starts development servers** via VS Code tasks:
|
||||||
|
|
||||||
|
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
|
||||||
|
- `Immich Web Server (Vite)` - Web frontend with hot-reloading on port 3000
|
||||||
|
- Both servers watch for file changes and recompile automatically
|
||||||
|
|
||||||
|
3. **Configures port forwarding**:
|
||||||
|
- Web UI: http://localhost:3000 (opens automatically)
|
||||||
|
- API: http://localhost:2283
|
||||||
|
- Debug ports: 9230 (workers), 9231 (API)
|
||||||
|
|
||||||
|
:::info
|
||||||
|
The Dev Container setup replaces the `make dev` command from the traditional setup. All services start automatically when you open the container.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Accessing Services
|
||||||
|
|
||||||
|
Once running, you can access:
|
||||||
|
|
||||||
|
| Service | URL | Description |
|
||||||
|
| -------- | --------------------- | ---------------------------------------------------------------------------------------------- |
|
||||||
|
| Web UI | http://localhost:3000 | Main web interface |
|
||||||
|
| API | http://localhost:2283 | REST API endpoints (Not used directly, web UI will expose this over http://localhost:3000/api) |
|
||||||
|
| Database | localhost:5432 | PostgreSQL (username: `postgres`) (Not used directly) |
|
||||||
|
|
||||||
|
### Connecting Mobile Apps
|
||||||
|
|
||||||
|
To connect the mobile app to your Dev Container:
|
||||||
|
|
||||||
|
1. Find your machine's IP address
|
||||||
|
2. In the mobile app, use: `http://YOUR_IP:3000/api`
|
||||||
|
3. Ensure your firewall allows connections on port 2283
|
||||||
|
|
||||||
|
### Making Code Changes
|
||||||
|
|
||||||
|
- **Server code** (`/server`): Changes trigger automatic restart
|
||||||
|
- **Web code** (`/web`): Changes trigger hot module replacement
|
||||||
|
- **Database migrations**: Run `npm run sync:sql` in the server directory
|
||||||
|
- **API changes**: Regenerate TypeScript SDK with `make open-api`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
The Dev Container supports multiple ways to run tests:
|
||||||
|
|
||||||
|
#### Using Make Commands (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests for specific components
|
||||||
|
make test-server # Server unit tests
|
||||||
|
make test-web # Web unit tests
|
||||||
|
make test-e2e # End-to-end tests
|
||||||
|
make test-cli # CLI tests
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
make test-all # Runs tests for all components
|
||||||
|
|
||||||
|
# Medium tests (integration tests)
|
||||||
|
make test-medium-dev # End-to-end tests
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using NPM Directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server tests
|
||||||
|
cd /workspaces/immich/server
|
||||||
|
npm test # Run all tests
|
||||||
|
npm run test:watch # Watch mode
|
||||||
|
npm run test:cov # Coverage report
|
||||||
|
|
||||||
|
# Web tests
|
||||||
|
cd /workspaces/immich/web
|
||||||
|
npm test # Run all tests
|
||||||
|
npm run test:watch # Watch mode
|
||||||
|
|
||||||
|
# E2E tests
|
||||||
|
cd /workspaces/immich/e2e
|
||||||
|
npm run test # Run API tests
|
||||||
|
npm run test:web # Run web UI tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linting
|
||||||
|
make lint-server # Lint server code
|
||||||
|
make lint-web # Lint web code
|
||||||
|
make lint-all # Lint all components
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
make format-server # Format server code
|
||||||
|
make format-web # Format web code
|
||||||
|
make format-all # Format all code
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
make check-server # Type check server
|
||||||
|
make check-web # Type check web
|
||||||
|
make check-all # Check all components
|
||||||
|
|
||||||
|
# Complete hygiene check
|
||||||
|
make hygiene-all # Runs lint, format, check, SQL sync, and audit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Make Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build commands
|
||||||
|
make build-server # Build server
|
||||||
|
make build-web # Build web app
|
||||||
|
make build-all # Build everything
|
||||||
|
|
||||||
|
# API generation
|
||||||
|
make open-api # Generate OpenAPI specs
|
||||||
|
make open-api-typescript # Generate TypeScript SDK
|
||||||
|
make open-api-dart # Generate Dart SDK
|
||||||
|
|
||||||
|
# Database
|
||||||
|
make sql # Sync database schema
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
make install-server # Install server dependencies
|
||||||
|
make install-web # Install web dependencies
|
||||||
|
make install-all # Install all dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
The Dev Container is pre-configured for debugging:
|
||||||
|
|
||||||
|
1. **API Server Debugging**:
|
||||||
|
|
||||||
|
- Set breakpoints in VS Code
|
||||||
|
- Press `F5` or use "Run and Debug" panel
|
||||||
|
- Select "Attach to Server" configuration
|
||||||
|
- Debug port: 9231
|
||||||
|
|
||||||
|
2. **Worker Debugging**:
|
||||||
|
|
||||||
|
- Use "Attach to Workers" configuration
|
||||||
|
- Debug port: 9230
|
||||||
|
|
||||||
|
3. **Web Debugging**:
|
||||||
|
- Use browser DevTools
|
||||||
|
- VS Code debugger for Chrome/Edge extensions supported
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Permission Errors
|
||||||
|
|
||||||
|
**Problem**: `EACCES` or permission denied errors
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
- The Dev Container runs as the `node` user (UID 1000)
|
||||||
|
- If your host UID differs, you may see permission issues
|
||||||
|
- Try rebuilding the container: "Dev Containers: Rebuild Container"
|
||||||
|
|
||||||
|
#### Container Won't Start
|
||||||
|
|
||||||
|
**Problem**: Dev Container fails to start or build
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
1. Check Docker is running: `docker ps`
|
||||||
|
2. Clean Docker resources: `docker system prune -a`
|
||||||
|
3. Check available disk space
|
||||||
|
4. Review Docker Desktop resource limits
|
||||||
|
|
||||||
|
#### Port Already in Use
|
||||||
|
|
||||||
|
**Problem**: "Port 3000/2283 is already in use"
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
1. Check for conflicting services: `lsof -i :3000` (macOS/Linux)
|
||||||
|
2. Stop conflicting services or change port mappings
|
||||||
|
3. Restart Docker Desktop
|
||||||
|
|
||||||
|
#### Upload Location Not Set
|
||||||
|
|
||||||
|
**Problem**: Errors about missing UPLOAD_LOCATION
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
1. Set the environment variable: `export UPLOAD_LOCATION=./Library`
|
||||||
|
2. Add to your shell profile for persistence
|
||||||
|
3. Restart your terminal and VS Code
|
||||||
|
|
||||||
|
#### Database Connection Failed
|
||||||
|
|
||||||
|
**Problem**: Cannot connect to PostgreSQL
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
1. Ensure all containers are running: `docker ps`
|
||||||
|
2. Check logs: "Dev Containers: Show Container Log"
|
||||||
|
3. Verify database credentials match environment variables
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
If you encounter issues:
|
||||||
|
|
||||||
|
1. Check container logs: View → Output → Select "Dev Containers"
|
||||||
|
2. Rebuild without cache: "Dev Containers: Rebuild Container Without Cache"
|
||||||
|
3. Review [common Docker issues](https://docs.docker.com/desktop/troubleshoot/)
|
||||||
|
4. Ask in [Discord](https://discord.immich.app) `#help-desk-support` channel
|
||||||
|
|
||||||
|
## Mobile Development
|
||||||
|
|
||||||
|
While the Dev Container focuses on server and web development, you can connect mobile apps for testing:
|
||||||
|
|
||||||
|
### Connecting iOS/Android Apps
|
||||||
|
|
||||||
|
1. **Ensure API is accessible**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find your machine's IP
|
||||||
|
# macOS
|
||||||
|
ipconfig getifaddr en0
|
||||||
|
# Linux
|
||||||
|
hostname -I
|
||||||
|
# Windows (in WSL2)
|
||||||
|
ip addr show eth0
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure mobile app**:
|
||||||
|
|
||||||
|
- Server URL: `http://YOUR_IP:2283/api`
|
||||||
|
- Ensure firewall allows port 2283
|
||||||
|
|
||||||
|
3. **For full mobile development**, see the [mobile development guide](/docs/developer/setup) which covers:
|
||||||
|
- Flutter setup
|
||||||
|
- Running on simulators/devices
|
||||||
|
- Mobile-specific debugging
|
||||||
|
|
||||||
|
## Advanced Configuration
|
||||||
|
|
||||||
|
### Custom VS Code Extensions
|
||||||
|
|
||||||
|
Add extensions to `.devcontainer/devcontainer.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"your.extension-id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Services
|
||||||
|
|
||||||
|
To add services (e.g., Redis Commander), modify:
|
||||||
|
|
||||||
|
1. `/docker/docker-compose.dev.yml` - Add service definition
|
||||||
|
2. `/.devcontainer/server/container-compose-overrides.yml` - Add overrides if needed
|
||||||
|
|
||||||
|
### Resource Limits
|
||||||
|
|
||||||
|
Adjust Docker Desktop resources:
|
||||||
|
|
||||||
|
- **macOS/Windows**: Docker Desktop → Settings → Resources
|
||||||
|
- **Linux**: Modify Docker daemon configuration
|
||||||
|
|
||||||
|
Recommended minimums:
|
||||||
|
|
||||||
|
- CPU: 4 cores
|
||||||
|
- Memory: 8GB
|
||||||
|
- Disk: 20GB
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Read the [architecture overview](/docs/developer/architecture)
|
||||||
|
- Learn about [database migrations](/docs/developer/database-migrations)
|
||||||
|
- Explore [API documentation](/docs/api)
|
||||||
|
- Join `#immich` on [Discord](https://discord.immich.app)
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 10 KiB |
@@ -123,7 +123,7 @@ The default configuration looks like this:
|
|||||||
"buttonText": "Login with OAuth",
|
"buttonText": "Login with OAuth",
|
||||||
"clientId": "",
|
"clientId": "",
|
||||||
"clientSecret": "",
|
"clientSecret": "",
|
||||||
"defaultStorageQuota": 0,
|
"defaultStorageQuota": null,
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"issuerUrl": "",
|
"issuerUrl": "",
|
||||||
"mobileOverrideEnabled": false,
|
"mobileOverrideEnabled": false,
|
||||||
|
|||||||
@@ -11,11 +11,24 @@ describe('/people', () => {
|
|||||||
let hiddenPerson: PersonResponseDto;
|
let hiddenPerson: PersonResponseDto;
|
||||||
let multipleAssetsPerson: PersonResponseDto;
|
let multipleAssetsPerson: PersonResponseDto;
|
||||||
|
|
||||||
|
let nameAlicePerson: PersonResponseDto;
|
||||||
|
let nameBobPerson: PersonResponseDto;
|
||||||
|
let nameCharliePerson: PersonResponseDto;
|
||||||
|
let nameNullPerson: PersonResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
|
|
||||||
[visiblePerson, hiddenPerson, multipleAssetsPerson] = await Promise.all([
|
[
|
||||||
|
visiblePerson,
|
||||||
|
hiddenPerson,
|
||||||
|
multipleAssetsPerson,
|
||||||
|
nameCharliePerson,
|
||||||
|
nameBobPerson,
|
||||||
|
nameAlicePerson,
|
||||||
|
nameNullPerson,
|
||||||
|
] = await Promise.all([
|
||||||
utils.createPerson(admin.accessToken, {
|
utils.createPerson(admin.accessToken, {
|
||||||
name: 'visible_person',
|
name: 'visible_person',
|
||||||
}),
|
}),
|
||||||
@@ -26,10 +39,24 @@ describe('/people', () => {
|
|||||||
utils.createPerson(admin.accessToken, {
|
utils.createPerson(admin.accessToken, {
|
||||||
name: 'multiple_assets_person',
|
name: 'multiple_assets_person',
|
||||||
}),
|
}),
|
||||||
|
// --- Setup for the specific sorting test ---
|
||||||
|
utils.createPerson(admin.accessToken, {
|
||||||
|
name: 'Charlie',
|
||||||
|
}),
|
||||||
|
utils.createPerson(admin.accessToken, {
|
||||||
|
name: 'Bob',
|
||||||
|
}),
|
||||||
|
utils.createPerson(admin.accessToken, {
|
||||||
|
name: 'Alice',
|
||||||
|
}),
|
||||||
|
utils.createPerson(admin.accessToken, {
|
||||||
|
name: '',
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const asset1 = await utils.createAsset(admin.accessToken);
|
const asset1 = await utils.createAsset(admin.accessToken);
|
||||||
const asset2 = await utils.createAsset(admin.accessToken);
|
const asset2 = await utils.createAsset(admin.accessToken);
|
||||||
|
const asset3 = await utils.createAsset(admin.accessToken);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
|
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
|
||||||
@@ -37,6 +64,15 @@ describe('/people', () => {
|
|||||||
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
|
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
|
||||||
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
|
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
|
||||||
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
|
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
|
||||||
|
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }),
|
||||||
|
// Named persons
|
||||||
|
utils.createFace({ assetId: asset1.id, personId: nameCharliePerson.id }), // 1 asset
|
||||||
|
utils.createFace({ assetId: asset1.id, personId: nameBobPerson.id }),
|
||||||
|
utils.createFace({ assetId: asset2.id, personId: nameBobPerson.id }), // 2 assets
|
||||||
|
utils.createFace({ assetId: asset1.id, personId: nameAlicePerson.id }), // 1 asset
|
||||||
|
// Null-named person
|
||||||
|
utils.createFace({ assetId: asset1.id, personId: nameNullPerson.id }),
|
||||||
|
utils.createFace({ assetId: asset2.id, personId: nameNullPerson.id }), // 2 assets
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,26 +87,53 @@ describe('/people', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
total: 3,
|
total: 7,
|
||||||
hidden: 1,
|
hidden: 1,
|
||||||
people: [
|
people: [
|
||||||
expect.objectContaining({ name: 'multiple_assets_person' }),
|
expect.objectContaining({ name: 'multiple_assets_person' }),
|
||||||
|
expect.objectContaining({ name: 'Bob' }),
|
||||||
|
expect.objectContaining({ name: 'Alice' }),
|
||||||
|
expect.objectContaining({ name: 'Charlie' }),
|
||||||
expect.objectContaining({ name: 'visible_person' }),
|
expect.objectContaining({ name: 'visible_person' }),
|
||||||
expect.objectContaining({ name: 'hidden_person' }),
|
expect.objectContaining({ name: 'hidden_person' }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should sort visible people by asset count (desc), then by name (asc, nulls last)', async () => {
|
||||||
|
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body.hasNextPage).toBe(false);
|
||||||
|
expect(body.total).toBe(7); // All persons
|
||||||
|
expect(body.hidden).toBe(1); // 'hidden_person'
|
||||||
|
|
||||||
|
const people = body.people as PersonResponseDto[];
|
||||||
|
|
||||||
|
expect(people.map((p) => p.id)).toEqual([
|
||||||
|
multipleAssetsPerson.id, // name: 'multiple_assets_person', count: 3
|
||||||
|
nameBobPerson.id, // name: 'Bob', count: 2
|
||||||
|
nameAlicePerson.id, // name: 'Alice', count: 1
|
||||||
|
nameCharliePerson.id, // name: 'Charlie', count: 1
|
||||||
|
visiblePerson.id, // name: 'visible_person', count: 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(people.some((p) => p.id === hiddenPerson.id)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return only visible people', async () => {
|
it('should return only visible people', async () => {
|
||||||
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
total: 3,
|
total: 7,
|
||||||
hidden: 1,
|
hidden: 1,
|
||||||
people: [
|
people: [
|
||||||
expect.objectContaining({ name: 'multiple_assets_person' }),
|
expect.objectContaining({ name: 'multiple_assets_person' }),
|
||||||
|
expect.objectContaining({ name: 'Bob' }),
|
||||||
|
expect.objectContaining({ name: 'Alice' }),
|
||||||
|
expect.objectContaining({ name: 'Charlie' }),
|
||||||
expect.objectContaining({ name: 'visible_person' }),
|
expect.objectContaining({ name: 'visible_person' }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -80,12 +143,12 @@ describe('/people', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/people')
|
.get('/people')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.query({ withHidden: true, page: 2, size: 1 });
|
.query({ withHidden: true, page: 5, size: 1 });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
hasNextPage: true,
|
hasNextPage: true,
|
||||||
total: 3,
|
total: 7,
|
||||||
hidden: 1,
|
hidden: 1,
|
||||||
people: [expect.objectContaining({ name: 'visible_person' })],
|
people: [expect.objectContaining({ name: 'visible_person' })],
|
||||||
});
|
});
|
||||||
@@ -128,7 +191,7 @@ describe('/people', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(expect.objectContaining({ assets: 2 }));
|
expect(body).toEqual(expect.objectContaining({ assets: 3 }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -204,7 +204,7 @@
|
|||||||
"oauth_storage_quota_claim": "Storage quota claim",
|
"oauth_storage_quota_claim": "Storage quota claim",
|
||||||
"oauth_storage_quota_claim_description": "Automatically set the user's storage quota to the value of this claim.",
|
"oauth_storage_quota_claim_description": "Automatically set the user's storage quota to the value of this claim.",
|
||||||
"oauth_storage_quota_default": "Default storage quota (GiB)",
|
"oauth_storage_quota_default": "Default storage quota (GiB)",
|
||||||
"oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).",
|
"oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided.",
|
||||||
"oauth_timeout": "Request Timeout",
|
"oauth_timeout": "Request Timeout",
|
||||||
"oauth_timeout_description": "Timeout for requests in milliseconds",
|
"oauth_timeout_description": "Timeout for requests in milliseconds",
|
||||||
"password_enable_description": "Login with email and password",
|
"password_enable_description": "Login with email and password",
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
"storage_template_migration_info": "The storage template will convert all extensions to lowercase. Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the <link>{job}</link>.",
|
"storage_template_migration_info": "The storage template will convert all extensions to lowercase. Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the <link>{job}</link>.",
|
||||||
"storage_template_migration_job": "Storage Template Migration Job",
|
"storage_template_migration_job": "Storage Template Migration Job",
|
||||||
"storage_template_more_details": "For more details about this feature, refer to the <template-link>Storage Template</template-link> and its <implications-link>implications</implications-link>",
|
"storage_template_more_details": "For more details about this feature, refer to the <template-link>Storage Template</template-link> and its <implications-link>implications</implications-link>",
|
||||||
"storage_template_onboarding_description": "When enabled, this feature will auto-organize files based on a user-defined template. Due to stability issues the feature has been turned off by default. For more information, please see the <link>documentation</link>.",
|
"storage_template_onboarding_description_v2": "When enabled, this feature will auto-organize files based on a user-defined template. For more information, please see the <link>documentation</link>.",
|
||||||
"storage_template_path_length": "Approximate path length limit: <b>{length, number}</b>/{limit, number}",
|
"storage_template_path_length": "Approximate path length limit: <b>{length, number}</b>/{limit, number}",
|
||||||
"storage_template_settings": "Storage Template",
|
"storage_template_settings": "Storage Template",
|
||||||
"storage_template_settings_description": "Manage the folder structure and file name of the upload asset",
|
"storage_template_settings_description": "Manage the folder structure and file name of the upload asset",
|
||||||
@@ -1150,6 +1150,7 @@
|
|||||||
"locked_folder": "Locked Folder",
|
"locked_folder": "Locked Folder",
|
||||||
"log_out": "Log out",
|
"log_out": "Log out",
|
||||||
"log_out_all_devices": "Log Out All Devices",
|
"log_out_all_devices": "Log Out All Devices",
|
||||||
|
"logged_in_as": "Logged in as {user}",
|
||||||
"logged_out_all_devices": "Logged out all devices",
|
"logged_out_all_devices": "Logged out all devices",
|
||||||
"logged_out_device": "Logged out device",
|
"logged_out_device": "Logged out device",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
@@ -1607,6 +1608,7 @@
|
|||||||
"select_album_cover": "Select album cover",
|
"select_album_cover": "Select album cover",
|
||||||
"select_all": "Select all",
|
"select_all": "Select all",
|
||||||
"select_all_duplicates": "Select all duplicates",
|
"select_all_duplicates": "Select all duplicates",
|
||||||
|
"select_all_in": "Select all in {group}",
|
||||||
"select_avatar_color": "Select avatar color",
|
"select_avatar_color": "Select avatar color",
|
||||||
"select_face": "Select face",
|
"select_face": "Select face",
|
||||||
"select_featured_photo": "Select featured photo",
|
"select_featured_photo": "Select featured photo",
|
||||||
@@ -1871,6 +1873,7 @@
|
|||||||
"unsaved_change": "Unsaved change",
|
"unsaved_change": "Unsaved change",
|
||||||
"unselect_all": "Unselect all",
|
"unselect_all": "Unselect all",
|
||||||
"unselect_all_duplicates": "Unselect all duplicates",
|
"unselect_all_duplicates": "Unselect all duplicates",
|
||||||
|
"unselect_all_in": "Unselect all in {group}",
|
||||||
"unstack": "Un-stack",
|
"unstack": "Un-stack",
|
||||||
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
||||||
"up_next": "Up next",
|
"up_next": "Up next",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ custom_lint:
|
|||||||
allowed:
|
allowed:
|
||||||
# required / wanted
|
# required / wanted
|
||||||
- 'lib/infrastructure/repositories/album_media.repository.dart'
|
- 'lib/infrastructure/repositories/album_media.repository.dart'
|
||||||
- 'lib/infrastructure/repositories/storage.repository.dart'
|
- 'lib/infrastructure/repositories/{storage,asset_media}.repository.dart'
|
||||||
- 'lib/repositories/{album,asset,file}_media.repository.dart'
|
- 'lib/repositories/{album,asset,file}_media.repository.dart'
|
||||||
# acceptable exceptions for the time being
|
# acceptable exceptions for the time being
|
||||||
- lib/entities/asset.entity.dart # to provide local AssetEntity for now
|
- lib/entities/asset.entity.dart # to provide local AssetEntity for now
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ data class PlatformAsset (
|
|||||||
val updatedAt: Long? = null,
|
val updatedAt: Long? = null,
|
||||||
val width: Long? = null,
|
val width: Long? = null,
|
||||||
val height: Long? = null,
|
val height: Long? = null,
|
||||||
val durationInSeconds: Long
|
val durationInSeconds: Long,
|
||||||
|
val isLivePhoto: Boolean
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
@@ -100,7 +101,8 @@ data class PlatformAsset (
|
|||||||
val width = pigeonVar_list[5] as Long?
|
val width = pigeonVar_list[5] as Long?
|
||||||
val height = pigeonVar_list[6] as Long?
|
val height = pigeonVar_list[6] as Long?
|
||||||
val durationInSeconds = pigeonVar_list[7] as Long
|
val durationInSeconds = pigeonVar_list[7] as Long
|
||||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds)
|
val isLivePhoto = pigeonVar_list[8] as Boolean
|
||||||
|
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, isLivePhoto)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@@ -113,6 +115,7 @@ data class PlatformAsset (
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
|
isLivePhoto,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -83,7 +83,11 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaType = c.getInt(mediaTypeColumn)
|
val mediaType = when (c.getInt(mediaTypeColumn)) {
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
val name = c.getString(nameColumn)
|
val name = c.getString(nameColumn)
|
||||||
// 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))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ targets:
|
|||||||
main: lib/infrastructure/repositories/db.repository.dart
|
main: lib/infrastructure/repositories/db.repository.dart
|
||||||
generate_for: &drift_generate_for
|
generate_for: &drift_generate_for
|
||||||
- lib/infrastructure/entities/*.dart
|
- lib/infrastructure/entities/*.dart
|
||||||
|
- lib/infrastructure/entities/*.drift
|
||||||
- lib/infrastructure/repositories/db.repository.dart
|
- lib/infrastructure/repositories/db.repository.dart
|
||||||
drift_dev:modular:
|
drift_dev:modular:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -138,6 +138,7 @@ struct PlatformAsset: Hashable {
|
|||||||
var width: Int64? = nil
|
var width: Int64? = nil
|
||||||
var height: Int64? = nil
|
var height: Int64? = nil
|
||||||
var durationInSeconds: Int64
|
var durationInSeconds: Int64
|
||||||
|
var isLivePhoto: Bool
|
||||||
|
|
||||||
|
|
||||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
@@ -150,6 +151,7 @@ struct PlatformAsset: Hashable {
|
|||||||
let width: Int64? = nilOrValue(pigeonVar_list[5])
|
let width: Int64? = nilOrValue(pigeonVar_list[5])
|
||||||
let height: Int64? = nilOrValue(pigeonVar_list[6])
|
let height: Int64? = nilOrValue(pigeonVar_list[6])
|
||||||
let durationInSeconds = pigeonVar_list[7] as! Int64
|
let durationInSeconds = pigeonVar_list[7] as! Int64
|
||||||
|
let isLivePhoto = pigeonVar_list[8] as! Bool
|
||||||
|
|
||||||
return PlatformAsset(
|
return PlatformAsset(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -159,7 +161,8 @@ struct PlatformAsset: Hashable {
|
|||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
durationInSeconds: durationInSeconds
|
durationInSeconds: durationInSeconds,
|
||||||
|
isLivePhoto: isLivePhoto
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func toList() -> [Any?] {
|
func toList() -> [Any?] {
|
||||||
@@ -172,6 +175,7 @@ struct PlatformAsset: Hashable {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
|
isLivePhoto,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ extension PHAsset {
|
|||||||
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
|
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
|
||||||
width: Int64(pixelWidth),
|
width: Int64(pixelWidth),
|
||||||
height: Int64(pixelHeight),
|
height: Int64(pixelHeight),
|
||||||
durationInSeconds: Int64(duration)
|
durationInSeconds: Int64(duration),
|
||||||
|
isLivePhoto: isLivePhoto()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +37,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
private let defaults: UserDefaults
|
private let defaults: UserDefaults
|
||||||
private let changeTokenKey = "immich:changeToken"
|
private let changeTokenKey = "immich:changeToken"
|
||||||
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
|
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
|
||||||
|
private let recoveredAlbumSubType = 1000000219
|
||||||
|
|
||||||
private let hashBufferSize = 2 * 1024 * 1024
|
private let hashBufferSize = 2 * 1024 * 1024
|
||||||
|
|
||||||
@@ -91,9 +93,17 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
|
|
||||||
albumTypes.forEach { type in
|
albumTypes.forEach { type in
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
||||||
collections.enumerateObjects { (album, _, _) in
|
for i in 0..<collections.count {
|
||||||
|
let album = collections.object(at: i)
|
||||||
|
|
||||||
|
// Ignore recovered album
|
||||||
|
if(album.assetCollectionSubtype.rawValue == self.recoveredAlbumSubType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
|
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
|
||||||
|
options.includeHiddenAssets = false
|
||||||
let assets = PHAsset.fetchAssets(in: album, options: options)
|
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||||
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
|
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
|
||||||
|
|
||||||
@@ -149,7 +159,9 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
|
|
||||||
if (updated.isEmpty) { continue }
|
if (updated.isEmpty) { continue }
|
||||||
|
|
||||||
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil)
|
let options = PHFetchOptions()
|
||||||
|
options.includeHiddenAssets = false
|
||||||
|
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
|
||||||
for i in 0..<result.count {
|
for i in 0..<result.count {
|
||||||
let asset = result.object(at: i)
|
let asset = result.object(at: i)
|
||||||
|
|
||||||
@@ -158,7 +170,8 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
id: asset.localIdentifier,
|
id: asset.localIdentifier,
|
||||||
name: "",
|
name: "",
|
||||||
type: 0,
|
type: 0,
|
||||||
durationInSeconds: 0
|
durationInSeconds: 0,
|
||||||
|
isLivePhoto: false
|
||||||
)
|
)
|
||||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||||
continue
|
continue
|
||||||
@@ -187,6 +200,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
collections.enumerateObjects { (album, _, _) in
|
collections.enumerateObjects { (album, _, _) in
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id))
|
options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id))
|
||||||
|
options.includeHiddenAssets = false
|
||||||
let result = PHAsset.fetchAssets(in: album, options: options)
|
let result = PHAsset.fetchAssets(in: album, options: options)
|
||||||
result.enumerateObjects { (asset, _, _) in
|
result.enumerateObjects { (asset, _, _) in
|
||||||
albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier)
|
albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier)
|
||||||
@@ -203,7 +217,9 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ids: [String] = []
|
var ids: [String] = []
|
||||||
let assets = PHAsset.fetchAssets(in: album, options: nil)
|
let options = PHFetchOptions()
|
||||||
|
options.includeHiddenAssets = false
|
||||||
|
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||||
assets.enumerateObjects { (asset, _, _) in
|
assets.enumerateObjects { (asset, _, _) in
|
||||||
ids.append(asset.localIdentifier)
|
ids.append(asset.localIdentifier)
|
||||||
}
|
}
|
||||||
@@ -219,6 +235,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
|
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||||
|
options.includeHiddenAssets = false
|
||||||
let assets = PHAsset.fetchAssets(in: album, options: options)
|
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||||
return Int64(assets.count)
|
return Int64(assets.count)
|
||||||
}
|
}
|
||||||
@@ -230,6 +247,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
|
options.includeHiddenAssets = false
|
||||||
if(updatedTimeCond != nil) {
|
if(updatedTimeCond != nil) {
|
||||||
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
|
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
|
||||||
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||||
|
|||||||
@@ -15,3 +15,8 @@ const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
|
|||||||
|
|
||||||
// Secure storage keys
|
// Secure storage keys
|
||||||
const String kSecuredPinCode = "secured_pin_code";
|
const String kSecuredPinCode = "secured_pin_code";
|
||||||
|
|
||||||
|
// Timeline constants
|
||||||
|
const int kTimelineNoneSegmentSize = 120;
|
||||||
|
const int kTimelineAssetLoadBatchSize = 256;
|
||||||
|
const int kTimelineAssetLoadOppositeSize = 64;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
abstract interface class IAssetMediaRepository {
|
||||||
|
Future<Uint8List?> getThumbnail(
|
||||||
|
String id, {
|
||||||
|
int quality = 80,
|
||||||
|
Size size = const Size.square(256),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,13 +15,4 @@ abstract interface class ISyncStreamRepository implements IDatabaseRepository {
|
|||||||
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data);
|
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data);
|
||||||
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data);
|
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data);
|
||||||
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data);
|
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data);
|
||||||
|
|
||||||
Future<void> updateAlbumsV1(Iterable<SyncAlbumV1> data);
|
|
||||||
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data);
|
|
||||||
|
|
||||||
// Future<void> updateAlbumAssetsV1(Iterable<SyncAlbumAssetV1> data);
|
|
||||||
// Future<void> deleteAlbumAssetsV1(Iterable<SyncAlbumAssetV1> data);
|
|
||||||
|
|
||||||
Future<void> updateAlbumUsersV1(Iterable<SyncAlbumUserV1> data);
|
|
||||||
Future<void> deleteAlbumUsersV1(Iterable<SyncAlbumUserDeleteV1> data);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
|
||||||
|
abstract interface class ITimelineRepository implements IDatabaseRepository {
|
||||||
|
Stream<List<Bucket>> watchMainBucket(
|
||||||
|
List<String> timelineUsers, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getMainBucketAssets(
|
||||||
|
List<String> timelineUsers, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
});
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchLocalBucket(
|
||||||
|
String albumId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getLocalBucketAssets(
|
||||||
|
String albumId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
enum AssetOrder {
|
|
||||||
// do not change this order!
|
|
||||||
asc,
|
|
||||||
desc,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model for an album stored in the server
|
|
||||||
class Album {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final DateTime createdAt;
|
|
||||||
final DateTime updatedAt;
|
|
||||||
final String? thumbnailAssetId;
|
|
||||||
final bool isActivityEnabled;
|
|
||||||
final AssetOrder order;
|
|
||||||
|
|
||||||
const Album({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.description,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.updatedAt,
|
|
||||||
this.thumbnailAssetId,
|
|
||||||
required this.isActivityEnabled,
|
|
||||||
required this.order,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return '''Album {
|
|
||||||
id: $id,
|
|
||||||
name: $name,
|
|
||||||
description: $description,
|
|
||||||
createdAt: $createdAt,
|
|
||||||
updatedAt: $updatedAt,
|
|
||||||
isActivityEnabled: $isActivityEnabled,
|
|
||||||
order: $order,
|
|
||||||
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
|
|
||||||
}''';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other is! Album) return false;
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
return id == other.id &&
|
|
||||||
name == other.name &&
|
|
||||||
description == other.description &&
|
|
||||||
createdAt == other.createdAt &&
|
|
||||||
updatedAt == other.updatedAt &&
|
|
||||||
thumbnailAssetId == other.thumbnailAssetId &&
|
|
||||||
isActivityEnabled == other.isActivityEnabled &&
|
|
||||||
order == other.order;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
name.hashCode ^
|
|
||||||
description.hashCode ^
|
|
||||||
createdAt.hashCode ^
|
|
||||||
updatedAt.hashCode ^
|
|
||||||
thumbnailAssetId.hashCode ^
|
|
||||||
isActivityEnabled.hashCode ^
|
|
||||||
order.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
enum AlbumUserRole {
|
|
||||||
// do not change this order!
|
|
||||||
editor,
|
|
||||||
viewer,
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@ enum AssetVisibility {
|
|||||||
class Asset extends BaseAsset {
|
class Asset extends BaseAsset {
|
||||||
final String id;
|
final String id;
|
||||||
final String? localId;
|
final String? localId;
|
||||||
|
final String? thumbHash;
|
||||||
final AssetVisibility visibility;
|
final AssetVisibility visibility;
|
||||||
|
|
||||||
const Asset({
|
const Asset({
|
||||||
@@ -25,9 +26,14 @@ class Asset extends BaseAsset {
|
|||||||
super.height,
|
super.height,
|
||||||
super.durationInSeconds,
|
super.durationInSeconds,
|
||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
|
this.thumbHash,
|
||||||
this.visibility = AssetVisibility.timeline,
|
this.visibility = AssetVisibility.timeline,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
AssetState get storage =>
|
||||||
|
localId == null ? AssetState.remote : AssetState.merged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Asset {
|
return '''Asset {
|
||||||
@@ -41,6 +47,7 @@ class Asset extends BaseAsset {
|
|||||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||||
localId: ${localId ?? "<NA>"},
|
localId: ${localId ?? "<NA>"},
|
||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
|
thumbHash: ${thumbHash ?? "<NA>"},
|
||||||
visibility: $visibility,
|
visibility: $visibility,
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
@@ -52,10 +59,15 @@ class Asset extends BaseAsset {
|
|||||||
return super == other &&
|
return super == other &&
|
||||||
id == other.id &&
|
id == other.id &&
|
||||||
localId == other.localId &&
|
localId == other.localId &&
|
||||||
|
thumbHash == other.thumbHash &&
|
||||||
visibility == other.visibility;
|
visibility == other.visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
super.hashCode ^ id.hashCode ^ localId.hashCode ^ visibility.hashCode;
|
super.hashCode ^
|
||||||
|
id.hashCode ^
|
||||||
|
localId.hashCode ^
|
||||||
|
thumbHash.hashCode ^
|
||||||
|
visibility.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ enum AssetType {
|
|||||||
audio,
|
audio,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AssetState {
|
||||||
|
local,
|
||||||
|
remote,
|
||||||
|
merged,
|
||||||
|
}
|
||||||
|
|
||||||
sealed class BaseAsset {
|
sealed class BaseAsset {
|
||||||
final String name;
|
final String name;
|
||||||
final String? checksum;
|
final String? checksum;
|
||||||
@@ -19,6 +25,9 @@ sealed class BaseAsset {
|
|||||||
final int? height;
|
final int? height;
|
||||||
final int? durationInSeconds;
|
final int? durationInSeconds;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
|
final bool isLivePhoto;
|
||||||
|
final bool livePhotoImageUploaded;
|
||||||
|
final bool livePhotoVideoUploaded;
|
||||||
|
|
||||||
const BaseAsset({
|
const BaseAsset({
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -30,8 +39,15 @@ sealed class BaseAsset {
|
|||||||
this.height,
|
this.height,
|
||||||
this.durationInSeconds,
|
this.durationInSeconds,
|
||||||
this.isFavorite = false,
|
this.isFavorite = false,
|
||||||
|
this.isLivePhoto = false,
|
||||||
|
this.livePhotoImageUploaded = false,
|
||||||
|
this.livePhotoVideoUploaded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool get isImage => type == AssetType.image;
|
||||||
|
bool get isVideo => type == AssetType.video;
|
||||||
|
AssetState get storage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''BaseAsset {
|
return '''BaseAsset {
|
||||||
@@ -43,6 +59,9 @@ sealed class BaseAsset {
|
|||||||
height: ${height ?? "<NA>"},
|
height: ${height ?? "<NA>"},
|
||||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
|
isLivePhoto: $isLivePhoto,
|
||||||
|
livePhotoImageUploaded: $livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded: $livePhotoVideoUploaded,
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +76,10 @@ sealed class BaseAsset {
|
|||||||
width == other.width &&
|
width == other.width &&
|
||||||
height == other.height &&
|
height == other.height &&
|
||||||
durationInSeconds == other.durationInSeconds &&
|
durationInSeconds == other.durationInSeconds &&
|
||||||
isFavorite == other.isFavorite;
|
isFavorite == other.isFavorite &&
|
||||||
|
isLivePhoto == other.isLivePhoto &&
|
||||||
|
livePhotoImageUploaded == other.livePhotoImageUploaded &&
|
||||||
|
livePhotoVideoUploaded == other.livePhotoVideoUploaded;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -71,6 +93,9 @@ sealed class BaseAsset {
|
|||||||
width.hashCode ^
|
width.hashCode ^
|
||||||
height.hashCode ^
|
height.hashCode ^
|
||||||
durationInSeconds.hashCode ^
|
durationInSeconds.hashCode ^
|
||||||
isFavorite.hashCode;
|
isFavorite.hashCode ^
|
||||||
|
isLivePhoto.hashCode ^
|
||||||
|
livePhotoImageUploaded.hashCode ^
|
||||||
|
livePhotoVideoUploaded.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,15 @@ class LocalAsset extends BaseAsset {
|
|||||||
super.height,
|
super.height,
|
||||||
super.durationInSeconds,
|
super.durationInSeconds,
|
||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
|
super.isLivePhoto = false,
|
||||||
|
super.livePhotoImageUploaded = false,
|
||||||
|
super.livePhotoVideoUploaded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
AssetState get storage =>
|
||||||
|
remoteId == null ? AssetState.local : AssetState.merged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''LocalAsset {
|
return '''LocalAsset {
|
||||||
@@ -31,6 +38,9 @@ class LocalAsset extends BaseAsset {
|
|||||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||||
remoteId: ${remoteId ?? "<NA>"}
|
remoteId: ${remoteId ?? "<NA>"}
|
||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
|
isLivePhoto: $isLivePhoto,
|
||||||
|
livePhotoImageUploaded: $livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded: $livePhotoVideoUploaded,
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +66,9 @@ class LocalAsset extends BaseAsset {
|
|||||||
int? height,
|
int? height,
|
||||||
int? durationInSeconds,
|
int? durationInSeconds,
|
||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
|
bool? isLivePhoto,
|
||||||
|
bool? livePhotoImageUploaded,
|
||||||
|
bool? livePhotoVideoUploaded,
|
||||||
}) {
|
}) {
|
||||||
return LocalAsset(
|
return LocalAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -69,6 +82,11 @@ class LocalAsset extends BaseAsset {
|
|||||||
height: height ?? this.height,
|
height: height ?? this.height,
|
||||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
isLivePhoto: isLivePhoto ?? this.isLivePhoto,
|
||||||
|
livePhotoImageUploaded:
|
||||||
|
livePhotoImageUploaded ?? this.livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded:
|
||||||
|
livePhotoVideoUploaded ?? this.livePhotoVideoUploaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
|
||||||
|
enum Setting<T> {
|
||||||
|
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
|
||||||
|
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
|
||||||
|
showStorageIndicator<bool>(StoreKey.storageIndicator, true);
|
||||||
|
|
||||||
|
const Setting(this.storeKey, this.defaultValue);
|
||||||
|
|
||||||
|
final StoreKey<T> storeKey;
|
||||||
|
final T defaultValue;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
enum GroupAssetsBy {
|
||||||
|
day,
|
||||||
|
month,
|
||||||
|
none;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HeaderType {
|
||||||
|
none,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
monthAndDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bucket {
|
||||||
|
final int assetCount;
|
||||||
|
|
||||||
|
const Bucket({required this.assetCount});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant Bucket other) {
|
||||||
|
return assetCount == other.assetCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => assetCount.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimeBucket extends Bucket {
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
const TimeBucket({required this.date, required super.assetCount});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant TimeBucket other) {
|
||||||
|
return super == other && date == other.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => super.hashCode ^ date.hashCode;
|
||||||
|
}
|
||||||
@@ -376,6 +376,9 @@ extension on Iterable<PlatformAsset> {
|
|||||||
width: e.width,
|
width: e.width,
|
||||||
height: e.height,
|
height: e.height,
|
||||||
durationInSeconds: e.durationInSeconds,
|
durationInSeconds: e.durationInSeconds,
|
||||||
|
isLivePhoto: e.isLivePhoto,
|
||||||
|
livePhotoImageUploaded: false,
|
||||||
|
livePhotoVideoUploaded: false,
|
||||||
),
|
),
|
||||||
).toList();
|
).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
|
|
||||||
|
class SettingsService {
|
||||||
|
final StoreService _storeService;
|
||||||
|
|
||||||
|
const SettingsService({required StoreService storeService})
|
||||||
|
: _storeService = storeService;
|
||||||
|
|
||||||
|
T get<T>(Setting<T> setting) =>
|
||||||
|
_storeService.get(setting.storeKey, setting.defaultValue);
|
||||||
|
|
||||||
|
Future<void> set<T>(Setting<T> setting, T value) =>
|
||||||
|
_storeService.put(setting.storeKey, value);
|
||||||
|
|
||||||
|
Stream<T> watch<T>(Setting<T> setting) => _storeService
|
||||||
|
.watch(setting.storeKey)
|
||||||
|
.map((v) => v ?? setting.defaultValue);
|
||||||
|
}
|
||||||
@@ -81,18 +81,6 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.deletePartnerAssetsV1(data.cast());
|
return _syncStreamRepository.deletePartnerAssetsV1(data.cast());
|
||||||
case SyncEntityType.partnerAssetExifV1:
|
case SyncEntityType.partnerAssetExifV1:
|
||||||
return _syncStreamRepository.updatePartnerAssetsExifV1(data.cast());
|
return _syncStreamRepository.updatePartnerAssetsExifV1(data.cast());
|
||||||
case SyncEntityType.albumV1:
|
|
||||||
return _syncStreamRepository.updateAlbumsV1(data.cast());
|
|
||||||
case SyncEntityType.albumDeleteV1:
|
|
||||||
return _syncStreamRepository.deleteAlbumsV1(data.cast());
|
|
||||||
// case SyncEntityType.albumAssetV1:
|
|
||||||
// return _syncStreamRepository.updateAlbumAssetsV1(data.cast());
|
|
||||||
// case SyncEntityType.albumAssetDeleteV1:
|
|
||||||
// return _syncStreamRepository.deleteAlbumAssetsV1(data.cast());
|
|
||||||
case SyncEntityType.albumUserV1:
|
|
||||||
return _syncStreamRepository.updateAlbumUsersV1(data.cast());
|
|
||||||
case SyncEntityType.albumUserDeleteV1:
|
|
||||||
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
|
|
||||||
default:
|
default:
|
||||||
_logger.warning("Unknown sync data type: $type");
|
_logger.warning("Unknown sync data type: $type");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/timeline.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
|
|
||||||
|
typedef TimelineAssetSource = Future<List<BaseAsset>> Function(
|
||||||
|
int index,
|
||||||
|
int count,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef TimelineBucketSource = Stream<List<Bucket>> Function();
|
||||||
|
|
||||||
|
class TimelineFactory {
|
||||||
|
final ITimelineRepository _timelineRepository;
|
||||||
|
final SettingsService _settingsService;
|
||||||
|
|
||||||
|
const TimelineFactory({
|
||||||
|
required ITimelineRepository timelineRepository,
|
||||||
|
required SettingsService settingsService,
|
||||||
|
}) : _timelineRepository = timelineRepository,
|
||||||
|
_settingsService = settingsService;
|
||||||
|
|
||||||
|
GroupAssetsBy get groupBy =>
|
||||||
|
GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)];
|
||||||
|
|
||||||
|
TimelineService main(List<String> timelineUsers) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getMainBucketAssets(timelineUsers, offset: offset, count: count),
|
||||||
|
bucketSource: () => _timelineRepository.watchMainBucket(
|
||||||
|
timelineUsers,
|
||||||
|
groupBy: groupBy,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineService localAlbum({required String albumId}) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getLocalBucketAssets(albumId, offset: offset, count: count),
|
||||||
|
bucketSource: () =>
|
||||||
|
_timelineRepository.watchLocalBucket(albumId, groupBy: groupBy),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineService {
|
||||||
|
final TimelineAssetSource _assetSource;
|
||||||
|
final TimelineBucketSource _bucketSource;
|
||||||
|
|
||||||
|
TimelineService({
|
||||||
|
required TimelineAssetSource assetSource,
|
||||||
|
required TimelineBucketSource bucketSource,
|
||||||
|
}) : _assetSource = assetSource,
|
||||||
|
_bucketSource = bucketSource {
|
||||||
|
_bucketSubscription =
|
||||||
|
_bucketSource().listen((_) => unawaited(_reloadBucket()));
|
||||||
|
}
|
||||||
|
|
||||||
|
final AsyncMutex _mutex = AsyncMutex();
|
||||||
|
int _bufferOffset = 0;
|
||||||
|
List<BaseAsset> _buffer = [];
|
||||||
|
StreamSubscription? _bucketSubscription;
|
||||||
|
|
||||||
|
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
||||||
|
|
||||||
|
Future<void> _reloadBucket() => _mutex.run(() async {
|
||||||
|
_buffer = await _assetSource(_bufferOffset, _buffer.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> loadAssets(int index, int count) =>
|
||||||
|
_mutex.run(() => _loadAssets(index, count));
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> _loadAssets(int index, int count) async {
|
||||||
|
if (hasRange(index, count)) {
|
||||||
|
return getAssets(index, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the requested offset is greater than the cached offset, the user scrolls forward "down"
|
||||||
|
final bool forward = _bufferOffset < index;
|
||||||
|
|
||||||
|
// make sure to load a meaningful amount of data (and not only the requested slice)
|
||||||
|
// otherwise, each call to [loadAssets] would result in DB call trashing performance
|
||||||
|
// fills small requests to [kTimelineAssetLoadBatchSize], adds some legroom into the opposite scroll direction for large requests
|
||||||
|
final len = math.max(
|
||||||
|
kTimelineAssetLoadBatchSize,
|
||||||
|
count + kTimelineAssetLoadOppositeSize,
|
||||||
|
);
|
||||||
|
// when scrolling forward, start shortly before the requested offset
|
||||||
|
// when scrolling backward, end shortly after the requested offset to guard against the user scrolling
|
||||||
|
// in the other direction a tiny bit resulting in another required load from the DB
|
||||||
|
final start = math.max(
|
||||||
|
0,
|
||||||
|
forward
|
||||||
|
? index - kTimelineAssetLoadOppositeSize
|
||||||
|
: (len > kTimelineAssetLoadBatchSize ? index : index + count - len),
|
||||||
|
);
|
||||||
|
|
||||||
|
final assets = await _assetSource(start, len);
|
||||||
|
_buffer = assets;
|
||||||
|
_bufferOffset = start;
|
||||||
|
|
||||||
|
return getAssets(index, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasRange(int index, int count) =>
|
||||||
|
index >= _bufferOffset && index + count <= _bufferOffset + _buffer.length;
|
||||||
|
|
||||||
|
List<BaseAsset> getAssets(int index, int count) {
|
||||||
|
if (!hasRange(index, count)) {
|
||||||
|
throw RangeError('TimelineService::getAssets Index out of range');
|
||||||
|
}
|
||||||
|
int start = index - _bufferOffset;
|
||||||
|
return _buffer.slice(start, start + count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await _bucketSubscription?.cancel();
|
||||||
|
_bucketSubscription = null;
|
||||||
|
_buffer.clear();
|
||||||
|
_bufferOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,9 +23,7 @@ extension LogOnError<T> on AsyncValue<T> {
|
|||||||
|
|
||||||
if (!skip) {
|
if (!skip) {
|
||||||
return onLoading?.call() ??
|
return onLoading?.call() ??
|
||||||
const Center(
|
const Center(child: ImmichLoadingIndicator());
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:intl/message_format.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
extension StringTranslateExtension on String {
|
||||||
|
String t({BuildContext? context, Map<String, Object>? args}) {
|
||||||
|
return _translateHelper(context, this, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextTranslateExtension on Text {
|
||||||
|
Text t({BuildContext? context, Map<String, Object>? args}) {
|
||||||
|
return Text(
|
||||||
|
_translateHelper(context, data ?? '', args),
|
||||||
|
key: key,
|
||||||
|
style: style,
|
||||||
|
strutStyle: strutStyle,
|
||||||
|
textAlign: textAlign,
|
||||||
|
textDirection: textDirection,
|
||||||
|
locale: locale,
|
||||||
|
softWrap: softWrap,
|
||||||
|
overflow: overflow,
|
||||||
|
textScaler: textScaler,
|
||||||
|
maxLines: maxLines,
|
||||||
|
semanticsLabel: semanticsLabel,
|
||||||
|
textWidthBasis: textWidthBasis,
|
||||||
|
textHeightBehavior: textHeightBehavior,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _translateHelper(
|
||||||
|
BuildContext? context,
|
||||||
|
String key, [
|
||||||
|
Map<String, Object>? args,
|
||||||
|
]) {
|
||||||
|
if (key.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final translatedMessage = key.tr(context: context);
|
||||||
|
return args != null
|
||||||
|
? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en')
|
||||||
|
.format(args)
|
||||||
|
: translatedMessage;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Translation failed for key "$key". Error: $e');
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/album_user.model.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
|
||||||
|
|
||||||
class AlbumUserEntity extends Table with DriftDefaultsMixin {
|
|
||||||
const AlbumUserEntity();
|
|
||||||
|
|
||||||
TextColumn get albumId =>
|
|
||||||
text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)();
|
|
||||||
|
|
||||||
TextColumn get userId =>
|
|
||||||
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
|
||||||
|
|
||||||
IntColumn get role => intEnum<AlbumUserRole>()();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Set<Column> get primaryKey => {albumId, userId};
|
|
||||||
}
|
|
||||||
@@ -1,602 +0,0 @@
|
|||||||
// dart format width=80
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
import 'package:drift/drift.dart' as i0;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/album_user.entity.drift.dart'
|
|
||||||
as i1;
|
|
||||||
import 'package:immich_mobile/domain/models/album_user.model.dart' as i2;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/album_user.entity.dart'
|
|
||||||
as i3;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
|
||||||
as i4;
|
|
||||||
import 'package:drift/internal/modular.dart' as i5;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
|
||||||
as i6;
|
|
||||||
|
|
||||||
typedef $$AlbumUserEntityTableCreateCompanionBuilder
|
|
||||||
= i1.AlbumUserEntityCompanion Function({
|
|
||||||
required String albumId,
|
|
||||||
required String userId,
|
|
||||||
required i2.AlbumUserRole role,
|
|
||||||
});
|
|
||||||
typedef $$AlbumUserEntityTableUpdateCompanionBuilder
|
|
||||||
= i1.AlbumUserEntityCompanion Function({
|
|
||||||
i0.Value<String> albumId,
|
|
||||||
i0.Value<String> userId,
|
|
||||||
i0.Value<i2.AlbumUserRole> role,
|
|
||||||
});
|
|
||||||
|
|
||||||
final class $$AlbumUserEntityTableReferences extends i0.BaseReferences<
|
|
||||||
i0.GeneratedDatabase, i1.$AlbumUserEntityTable, i1.AlbumUserEntityData> {
|
|
||||||
$$AlbumUserEntityTableReferences(
|
|
||||||
super.$_db, super.$_table, super.$_typedResult);
|
|
||||||
|
|
||||||
static i4.$RemoteAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) =>
|
|
||||||
i5.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i4.$RemoteAlbumEntityTable>('remote_album_entity')
|
|
||||||
.createAlias(i0.$_aliasNameGenerator(
|
|
||||||
i5.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i1.$AlbumUserEntityTable>('album_user_entity')
|
|
||||||
.albumId,
|
|
||||||
i5.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i4.$RemoteAlbumEntityTable>('remote_album_entity')
|
|
||||||
.id));
|
|
||||||
|
|
||||||
i4.$$RemoteAlbumEntityTableProcessedTableManager get albumId {
|
|
||||||
final $_column = $_itemColumn<String>('album_id')!;
|
|
||||||
|
|
||||||
final manager = i4
|
|
||||||
.$$RemoteAlbumEntityTableTableManager(
|
|
||||||
$_db,
|
|
||||||
i5.ReadDatabaseContainer($_db)
|
|
||||||
.resultSet<i4.$RemoteAlbumEntityTable>('remote_album_entity'))
|
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
|
||||||
final item = $_typedResult.readTableOrNull(_albumIdTable($_db));
|
|
||||||
if (item == null) return manager;
|
|
||||||
return i0.ProcessedTableManager(
|
|
||||||
manager.$state.copyWith(prefetchedData: [item]));
|
|
||||||
}
|
|
||||||
|
|
||||||
static i6.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
|
|
||||||
i5.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i6.$UserEntityTable>('user_entity')
|
|
||||||
.createAlias(i0.$_aliasNameGenerator(
|
|
||||||
i5.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i1.$AlbumUserEntityTable>('album_user_entity')
|
|
||||||
.userId,
|
|
||||||
i5.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i6.$UserEntityTable>('user_entity')
|
|
||||||
.id));
|
|
||||||
|
|
||||||
i6.$$UserEntityTableProcessedTableManager get userId {
|
|
||||||
final $_column = $_itemColumn<String>('user_id')!;
|
|
||||||
|
|
||||||
final manager = i6
|
|
||||||
.$$UserEntityTableTableManager(
|
|
||||||
$_db,
|
|
||||||
i5.ReadDatabaseContainer($_db)
|
|
||||||
.resultSet<i6.$UserEntityTable>('user_entity'))
|
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
|
||||||
final item = $_typedResult.readTableOrNull(_userIdTable($_db));
|
|
||||||
if (item == null) return manager;
|
|
||||||
return i0.ProcessedTableManager(
|
|
||||||
manager.$state.copyWith(prefetchedData: [item]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$AlbumUserEntityTableFilterComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AlbumUserEntityTable> {
|
|
||||||
$$AlbumUserEntityTableFilterComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i0.ColumnWithTypeConverterFilters<i2.AlbumUserRole, i2.AlbumUserRole, int>
|
|
||||||
get role => $composableBuilder(
|
|
||||||
column: $table.role,
|
|
||||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
|
|
||||||
|
|
||||||
i4.$$RemoteAlbumEntityTableFilterComposer get albumId {
|
|
||||||
final i4.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.albumId,
|
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i4.$RemoteAlbumEntityTable>('remote_album_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i4.$$RemoteAlbumEntityTableFilterComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i4.$RemoteAlbumEntityTable>('remote_album_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i6.$$UserEntityTableFilterComposer get userId {
|
|
||||||
final i6.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.userId,
|
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$UserEntityTable>('user_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i6.$$UserEntityTableFilterComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$UserEntityTable>('user_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$AlbumUserEntityTableOrderingComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AlbumUserEntityTable> {
|
|
||||||
$$AlbumUserEntityTableOrderingComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i0.ColumnOrderings<int> get role => $composableBuilder(
|
|
||||||
column: $table.role, builder: (column) => i0.ColumnOrderings(column));
|
|
||||||
|
|
||||||
i4.$$RemoteAlbumEntityTableOrderingComposer get albumId {
|
|
||||||
final i4.$$RemoteAlbumEntityTableOrderingComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.albumId,
|
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i4.$RemoteAlbumEntityTable>('remote_album_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i4.$$RemoteAlbumEntityTableOrderingComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i4.$RemoteAlbumEntityTable>(
|
|
||||||
'remote_album_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i6.$$UserEntityTableOrderingComposer get userId {
|
|
||||||
final i6.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.userId,
|
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$UserEntityTable>('user_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i6.$$UserEntityTableOrderingComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$UserEntityTable>('user_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$AlbumUserEntityTableAnnotationComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AlbumUserEntityTable> {
|
|
||||||
$$AlbumUserEntityTableAnnotationComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i0.GeneratedColumnWithTypeConverter<i2.AlbumUserRole, int> get role =>
|
|
||||||
$composableBuilder(column: $table.role, builder: (column) => column);
|
|
||||||
|
|
||||||
i4.$$RemoteAlbumEntityTableAnnotationComposer get albumId {
|
|
||||||
final i4.$$RemoteAlbumEntityTableAnnotationComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.albumId,
|
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i4.$RemoteAlbumEntityTable>('remote_album_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i4.$$RemoteAlbumEntityTableAnnotationComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i4.$RemoteAlbumEntityTable>(
|
|
||||||
'remote_album_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i6.$$UserEntityTableAnnotationComposer get userId {
|
|
||||||
final i6.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.userId,
|
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$UserEntityTable>('user_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i6.$$UserEntityTableAnnotationComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$UserEntityTable>('user_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$AlbumUserEntityTableTableManager extends i0.RootTableManager<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$AlbumUserEntityTable,
|
|
||||||
i1.AlbumUserEntityData,
|
|
||||||
i1.$$AlbumUserEntityTableFilterComposer,
|
|
||||||
i1.$$AlbumUserEntityTableOrderingComposer,
|
|
||||||
i1.$$AlbumUserEntityTableAnnotationComposer,
|
|
||||||
$$AlbumUserEntityTableCreateCompanionBuilder,
|
|
||||||
$$AlbumUserEntityTableUpdateCompanionBuilder,
|
|
||||||
(i1.AlbumUserEntityData, i1.$$AlbumUserEntityTableReferences),
|
|
||||||
i1.AlbumUserEntityData,
|
|
||||||
i0.PrefetchHooks Function({bool albumId, bool userId})> {
|
|
||||||
$$AlbumUserEntityTableTableManager(
|
|
||||||
i0.GeneratedDatabase db, i1.$AlbumUserEntityTable table)
|
|
||||||
: super(i0.TableManagerState(
|
|
||||||
db: db,
|
|
||||||
table: table,
|
|
||||||
createFilteringComposer: () =>
|
|
||||||
i1.$$AlbumUserEntityTableFilterComposer($db: db, $table: table),
|
|
||||||
createOrderingComposer: () =>
|
|
||||||
i1.$$AlbumUserEntityTableOrderingComposer($db: db, $table: table),
|
|
||||||
createComputedFieldComposer: () => i1
|
|
||||||
.$$AlbumUserEntityTableAnnotationComposer($db: db, $table: table),
|
|
||||||
updateCompanionCallback: ({
|
|
||||||
i0.Value<String> albumId = const i0.Value.absent(),
|
|
||||||
i0.Value<String> userId = const i0.Value.absent(),
|
|
||||||
i0.Value<i2.AlbumUserRole> role = const i0.Value.absent(),
|
|
||||||
}) =>
|
|
||||||
i1.AlbumUserEntityCompanion(
|
|
||||||
albumId: albumId,
|
|
||||||
userId: userId,
|
|
||||||
role: role,
|
|
||||||
),
|
|
||||||
createCompanionCallback: ({
|
|
||||||
required String albumId,
|
|
||||||
required String userId,
|
|
||||||
required i2.AlbumUserRole role,
|
|
||||||
}) =>
|
|
||||||
i1.AlbumUserEntityCompanion.insert(
|
|
||||||
albumId: albumId,
|
|
||||||
userId: userId,
|
|
||||||
role: role,
|
|
||||||
),
|
|
||||||
withReferenceMapper: (p0) => p0
|
|
||||||
.map((e) => (
|
|
||||||
e.readTable(table),
|
|
||||||
i1.$$AlbumUserEntityTableReferences(db, table, e)
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
prefetchHooksCallback: ({albumId = false, userId = false}) {
|
|
||||||
return i0.PrefetchHooks(
|
|
||||||
db: db,
|
|
||||||
explicitlyWatchedTables: [],
|
|
||||||
addJoins: <
|
|
||||||
T extends i0.TableManagerState<
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic>>(state) {
|
|
||||||
if (albumId) {
|
|
||||||
state = state.withJoin(
|
|
||||||
currentTable: table,
|
|
||||||
currentColumn: table.albumId,
|
|
||||||
referencedTable:
|
|
||||||
i1.$$AlbumUserEntityTableReferences._albumIdTable(db),
|
|
||||||
referencedColumn: i1.$$AlbumUserEntityTableReferences
|
|
||||||
._albumIdTable(db)
|
|
||||||
.id,
|
|
||||||
) as T;
|
|
||||||
}
|
|
||||||
if (userId) {
|
|
||||||
state = state.withJoin(
|
|
||||||
currentTable: table,
|
|
||||||
currentColumn: table.userId,
|
|
||||||
referencedTable:
|
|
||||||
i1.$$AlbumUserEntityTableReferences._userIdTable(db),
|
|
||||||
referencedColumn:
|
|
||||||
i1.$$AlbumUserEntityTableReferences._userIdTable(db).id,
|
|
||||||
) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
getPrefetchedDataCallback: (items) async {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef $$AlbumUserEntityTableProcessedTableManager = i0.ProcessedTableManager<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$AlbumUserEntityTable,
|
|
||||||
i1.AlbumUserEntityData,
|
|
||||||
i1.$$AlbumUserEntityTableFilterComposer,
|
|
||||||
i1.$$AlbumUserEntityTableOrderingComposer,
|
|
||||||
i1.$$AlbumUserEntityTableAnnotationComposer,
|
|
||||||
$$AlbumUserEntityTableCreateCompanionBuilder,
|
|
||||||
$$AlbumUserEntityTableUpdateCompanionBuilder,
|
|
||||||
(i1.AlbumUserEntityData, i1.$$AlbumUserEntityTableReferences),
|
|
||||||
i1.AlbumUserEntityData,
|
|
||||||
i0.PrefetchHooks Function({bool albumId, bool userId})>;
|
|
||||||
|
|
||||||
class $AlbumUserEntityTable extends i3.AlbumUserEntity
|
|
||||||
with i0.TableInfo<$AlbumUserEntityTable, i1.AlbumUserEntityData> {
|
|
||||||
@override
|
|
||||||
final i0.GeneratedDatabase attachedDatabase;
|
|
||||||
final String? _alias;
|
|
||||||
$AlbumUserEntityTable(this.attachedDatabase, [this._alias]);
|
|
||||||
static const i0.VerificationMeta _albumIdMeta =
|
|
||||||
const i0.VerificationMeta('albumId');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> albumId = i0.GeneratedColumn<String>(
|
|
||||||
'album_id', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.string,
|
|
||||||
requiredDuringInsert: true,
|
|
||||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
|
||||||
'REFERENCES remote_album_entity (id) ON DELETE CASCADE'));
|
|
||||||
static const i0.VerificationMeta _userIdMeta =
|
|
||||||
const i0.VerificationMeta('userId');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> userId = i0.GeneratedColumn<String>(
|
|
||||||
'user_id', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.string,
|
|
||||||
requiredDuringInsert: true,
|
|
||||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
|
||||||
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumnWithTypeConverter<i2.AlbumUserRole, int> role =
|
|
||||||
i0.GeneratedColumn<int>('role', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.int, requiredDuringInsert: true)
|
|
||||||
.withConverter<i2.AlbumUserRole>(
|
|
||||||
i1.$AlbumUserEntityTable.$converterrole);
|
|
||||||
@override
|
|
||||||
List<i0.GeneratedColumn> get $columns => [albumId, userId, role];
|
|
||||||
@override
|
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
|
||||||
@override
|
|
||||||
String get actualTableName => $name;
|
|
||||||
static const String $name = 'album_user_entity';
|
|
||||||
@override
|
|
||||||
i0.VerificationContext validateIntegrity(
|
|
||||||
i0.Insertable<i1.AlbumUserEntityData> instance,
|
|
||||||
{bool isInserting = false}) {
|
|
||||||
final context = i0.VerificationContext();
|
|
||||||
final data = instance.toColumns(true);
|
|
||||||
if (data.containsKey('album_id')) {
|
|
||||||
context.handle(_albumIdMeta,
|
|
||||||
albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_albumIdMeta);
|
|
||||||
}
|
|
||||||
if (data.containsKey('user_id')) {
|
|
||||||
context.handle(_userIdMeta,
|
|
||||||
userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_userIdMeta);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Set<i0.GeneratedColumn> get $primaryKey => {albumId, userId};
|
|
||||||
@override
|
|
||||||
i1.AlbumUserEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
|
||||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
|
||||||
return i1.AlbumUserEntityData(
|
|
||||||
albumId: attachedDatabase.typeMapping
|
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!,
|
|
||||||
userId: attachedDatabase.typeMapping
|
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!,
|
|
||||||
role: i1.$AlbumUserEntityTable.$converterrole.fromSql(attachedDatabase
|
|
||||||
.typeMapping
|
|
||||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}role'])!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
$AlbumUserEntityTable createAlias(String alias) {
|
|
||||||
return $AlbumUserEntityTable(attachedDatabase, alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
static i0.JsonTypeConverter2<i2.AlbumUserRole, int, int> $converterrole =
|
|
||||||
const i0.EnumIndexConverter<i2.AlbumUserRole>(i2.AlbumUserRole.values);
|
|
||||||
@override
|
|
||||||
bool get withoutRowId => true;
|
|
||||||
@override
|
|
||||||
bool get isStrict => true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumUserEntityData extends i0.DataClass
|
|
||||||
implements i0.Insertable<i1.AlbumUserEntityData> {
|
|
||||||
final String albumId;
|
|
||||||
final String userId;
|
|
||||||
final i2.AlbumUserRole role;
|
|
||||||
const AlbumUserEntityData(
|
|
||||||
{required this.albumId, required this.userId, required this.role});
|
|
||||||
@override
|
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, i0.Expression>{};
|
|
||||||
map['album_id'] = i0.Variable<String>(albumId);
|
|
||||||
map['user_id'] = i0.Variable<String>(userId);
|
|
||||||
{
|
|
||||||
map['role'] =
|
|
||||||
i0.Variable<int>(i1.$AlbumUserEntityTable.$converterrole.toSql(role));
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AlbumUserEntityData.fromJson(Map<String, dynamic> json,
|
|
||||||
{i0.ValueSerializer? serializer}) {
|
|
||||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
|
||||||
return AlbumUserEntityData(
|
|
||||||
albumId: serializer.fromJson<String>(json['albumId']),
|
|
||||||
userId: serializer.fromJson<String>(json['userId']),
|
|
||||||
role: i1.$AlbumUserEntityTable.$converterrole
|
|
||||||
.fromJson(serializer.fromJson<int>(json['role'])),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
|
||||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
|
||||||
return <String, dynamic>{
|
|
||||||
'albumId': serializer.toJson<String>(albumId),
|
|
||||||
'userId': serializer.toJson<String>(userId),
|
|
||||||
'role': serializer
|
|
||||||
.toJson<int>(i1.$AlbumUserEntityTable.$converterrole.toJson(role)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
i1.AlbumUserEntityData copyWith(
|
|
||||||
{String? albumId, String? userId, i2.AlbumUserRole? role}) =>
|
|
||||||
i1.AlbumUserEntityData(
|
|
||||||
albumId: albumId ?? this.albumId,
|
|
||||||
userId: userId ?? this.userId,
|
|
||||||
role: role ?? this.role,
|
|
||||||
);
|
|
||||||
AlbumUserEntityData copyWithCompanion(i1.AlbumUserEntityCompanion data) {
|
|
||||||
return AlbumUserEntityData(
|
|
||||||
albumId: data.albumId.present ? data.albumId.value : this.albumId,
|
|
||||||
userId: data.userId.present ? data.userId.value : this.userId,
|
|
||||||
role: data.role.present ? data.role.value : this.role,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('AlbumUserEntityData(')
|
|
||||||
..write('albumId: $albumId, ')
|
|
||||||
..write('userId: $userId, ')
|
|
||||||
..write('role: $role')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(albumId, userId, role);
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is i1.AlbumUserEntityData &&
|
|
||||||
other.albumId == this.albumId &&
|
|
||||||
other.userId == this.userId &&
|
|
||||||
other.role == this.role);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumUserEntityCompanion
|
|
||||||
extends i0.UpdateCompanion<i1.AlbumUserEntityData> {
|
|
||||||
final i0.Value<String> albumId;
|
|
||||||
final i0.Value<String> userId;
|
|
||||||
final i0.Value<i2.AlbumUserRole> role;
|
|
||||||
const AlbumUserEntityCompanion({
|
|
||||||
this.albumId = const i0.Value.absent(),
|
|
||||||
this.userId = const i0.Value.absent(),
|
|
||||||
this.role = const i0.Value.absent(),
|
|
||||||
});
|
|
||||||
AlbumUserEntityCompanion.insert({
|
|
||||||
required String albumId,
|
|
||||||
required String userId,
|
|
||||||
required i2.AlbumUserRole role,
|
|
||||||
}) : albumId = i0.Value(albumId),
|
|
||||||
userId = i0.Value(userId),
|
|
||||||
role = i0.Value(role);
|
|
||||||
static i0.Insertable<i1.AlbumUserEntityData> custom({
|
|
||||||
i0.Expression<String>? albumId,
|
|
||||||
i0.Expression<String>? userId,
|
|
||||||
i0.Expression<int>? role,
|
|
||||||
}) {
|
|
||||||
return i0.RawValuesInsertable({
|
|
||||||
if (albumId != null) 'album_id': albumId,
|
|
||||||
if (userId != null) 'user_id': userId,
|
|
||||||
if (role != null) 'role': role,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
i1.AlbumUserEntityCompanion copyWith(
|
|
||||||
{i0.Value<String>? albumId,
|
|
||||||
i0.Value<String>? userId,
|
|
||||||
i0.Value<i2.AlbumUserRole>? role}) {
|
|
||||||
return i1.AlbumUserEntityCompanion(
|
|
||||||
albumId: albumId ?? this.albumId,
|
|
||||||
userId: userId ?? this.userId,
|
|
||||||
role: role ?? this.role,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, i0.Expression>{};
|
|
||||||
if (albumId.present) {
|
|
||||||
map['album_id'] = i0.Variable<String>(albumId.value);
|
|
||||||
}
|
|
||||||
if (userId.present) {
|
|
||||||
map['user_id'] = i0.Variable<String>(userId.value);
|
|
||||||
}
|
|
||||||
if (role.present) {
|
|
||||||
map['role'] = i0.Variable<int>(
|
|
||||||
i1.$AlbumUserEntityTable.$converterrole.toSql(role.value));
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('AlbumUserEntityCompanion(')
|
|
||||||
..write('albumId: $albumId, ')
|
|
||||||
..write('userId: $userId, ')
|
|
||||||
..write('role: $role')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
@@ -12,6 +14,28 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
|||||||
// Only used during backup to mirror the favorite status of the asset in the server
|
// Only used during backup to mirror the favorite status of the asset in the server
|
||||||
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
|
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
|
BoolColumn get isLivePhoto => boolean().withDefault(const Constant(false))();
|
||||||
|
BoolColumn get livePhotoImageUploaded =>
|
||||||
|
boolean().withDefault(const Constant(false))();
|
||||||
|
BoolColumn get livePhotoVideoUploaded =>
|
||||||
|
boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
|
||||||
|
LocalAsset toDto() => LocalAsset(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
checksum: checksum,
|
||||||
|
type: type,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
durationInSeconds: durationInSeconds,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
isLivePhoto: isLivePhoto,
|
||||||
|
livePhotoImageUploaded: livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded: livePhotoVideoUploaded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+211
-9
@@ -20,6 +20,9 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder
|
|||||||
required String id,
|
required String id,
|
||||||
i0.Value<String?> checksum,
|
i0.Value<String?> checksum,
|
||||||
i0.Value<bool> isFavorite,
|
i0.Value<bool> isFavorite,
|
||||||
|
i0.Value<bool> isLivePhoto,
|
||||||
|
i0.Value<bool> livePhotoImageUploaded,
|
||||||
|
i0.Value<bool> livePhotoVideoUploaded,
|
||||||
});
|
});
|
||||||
typedef $$LocalAssetEntityTableUpdateCompanionBuilder
|
typedef $$LocalAssetEntityTableUpdateCompanionBuilder
|
||||||
= i1.LocalAssetEntityCompanion Function({
|
= i1.LocalAssetEntityCompanion Function({
|
||||||
@@ -33,6 +36,9 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder
|
|||||||
i0.Value<String> id,
|
i0.Value<String> id,
|
||||||
i0.Value<String?> checksum,
|
i0.Value<String?> checksum,
|
||||||
i0.Value<bool> isFavorite,
|
i0.Value<bool> isFavorite,
|
||||||
|
i0.Value<bool> isLivePhoto,
|
||||||
|
i0.Value<bool> livePhotoImageUploaded,
|
||||||
|
i0.Value<bool> livePhotoVideoUploaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
class $$LocalAssetEntityTableFilterComposer
|
class $$LocalAssetEntityTableFilterComposer
|
||||||
@@ -76,6 +82,18 @@ class $$LocalAssetEntityTableFilterComposer
|
|||||||
|
|
||||||
i0.ColumnFilters<bool> get isFavorite => $composableBuilder(
|
i0.ColumnFilters<bool> get isFavorite => $composableBuilder(
|
||||||
column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column));
|
column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<bool> get isLivePhoto => $composableBuilder(
|
||||||
|
column: $table.isLivePhoto,
|
||||||
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<bool> get livePhotoImageUploaded => $composableBuilder(
|
||||||
|
column: $table.livePhotoImageUploaded,
|
||||||
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<bool> get livePhotoVideoUploaded => $composableBuilder(
|
||||||
|
column: $table.livePhotoVideoUploaded,
|
||||||
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAssetEntityTableOrderingComposer
|
class $$LocalAssetEntityTableOrderingComposer
|
||||||
@@ -120,6 +138,18 @@ class $$LocalAssetEntityTableOrderingComposer
|
|||||||
i0.ColumnOrderings<bool> get isFavorite => $composableBuilder(
|
i0.ColumnOrderings<bool> get isFavorite => $composableBuilder(
|
||||||
column: $table.isFavorite,
|
column: $table.isFavorite,
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<bool> get isLivePhoto => $composableBuilder(
|
||||||
|
column: $table.isLivePhoto,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<bool> get livePhotoImageUploaded => $composableBuilder(
|
||||||
|
column: $table.livePhotoImageUploaded,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<bool> get livePhotoVideoUploaded => $composableBuilder(
|
||||||
|
column: $table.livePhotoVideoUploaded,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAssetEntityTableAnnotationComposer
|
class $$LocalAssetEntityTableAnnotationComposer
|
||||||
@@ -160,6 +190,15 @@ class $$LocalAssetEntityTableAnnotationComposer
|
|||||||
|
|
||||||
i0.GeneratedColumn<bool> get isFavorite => $composableBuilder(
|
i0.GeneratedColumn<bool> get isFavorite => $composableBuilder(
|
||||||
column: $table.isFavorite, builder: (column) => column);
|
column: $table.isFavorite, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<bool> get isLivePhoto => $composableBuilder(
|
||||||
|
column: $table.isLivePhoto, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<bool> get livePhotoImageUploaded => $composableBuilder(
|
||||||
|
column: $table.livePhotoImageUploaded, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<bool> get livePhotoVideoUploaded => $composableBuilder(
|
||||||
|
column: $table.livePhotoVideoUploaded, builder: (column) => column);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
||||||
@@ -201,6 +240,9 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i0.Value<String> id = const i0.Value.absent(),
|
i0.Value<String> id = const i0.Value.absent(),
|
||||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isLivePhoto = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> livePhotoImageUploaded = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> livePhotoVideoUploaded = const i0.Value.absent(),
|
||||||
}) =>
|
}) =>
|
||||||
i1.LocalAssetEntityCompanion(
|
i1.LocalAssetEntityCompanion(
|
||||||
name: name,
|
name: name,
|
||||||
@@ -213,6 +255,9 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
id: id,
|
id: id,
|
||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
|
isLivePhoto: isLivePhoto,
|
||||||
|
livePhotoImageUploaded: livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded: livePhotoVideoUploaded,
|
||||||
),
|
),
|
||||||
createCompanionCallback: ({
|
createCompanionCallback: ({
|
||||||
required String name,
|
required String name,
|
||||||
@@ -225,6 +270,9 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
required String id,
|
required String id,
|
||||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isLivePhoto = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> livePhotoImageUploaded = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> livePhotoVideoUploaded = const i0.Value.absent(),
|
||||||
}) =>
|
}) =>
|
||||||
i1.LocalAssetEntityCompanion.insert(
|
i1.LocalAssetEntityCompanion.insert(
|
||||||
name: name,
|
name: name,
|
||||||
@@ -237,6 +285,9 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
id: id,
|
id: id,
|
||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
|
isLivePhoto: isLivePhoto,
|
||||||
|
livePhotoImageUploaded: livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded: livePhotoVideoUploaded,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||||
@@ -337,6 +388,36 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
'CHECK ("is_favorite" IN (0, 1))'),
|
'CHECK ("is_favorite" IN (0, 1))'),
|
||||||
defaultValue: const i4.Constant(false));
|
defaultValue: const i4.Constant(false));
|
||||||
|
static const i0.VerificationMeta _isLivePhotoMeta =
|
||||||
|
const i0.VerificationMeta('isLivePhoto');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<bool> isLivePhoto = i0.GeneratedColumn<bool>(
|
||||||
|
'is_live_photo', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_live_photo" IN (0, 1))'),
|
||||||
|
defaultValue: const i4.Constant(false));
|
||||||
|
static const i0.VerificationMeta _livePhotoImageUploadedMeta =
|
||||||
|
const i0.VerificationMeta('livePhotoImageUploaded');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<bool> livePhotoImageUploaded =
|
||||||
|
i0.GeneratedColumn<bool>('live_photo_image_uploaded', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("live_photo_image_uploaded" IN (0, 1))'),
|
||||||
|
defaultValue: const i4.Constant(false));
|
||||||
|
static const i0.VerificationMeta _livePhotoVideoUploadedMeta =
|
||||||
|
const i0.VerificationMeta('livePhotoVideoUploaded');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<bool> livePhotoVideoUploaded =
|
||||||
|
i0.GeneratedColumn<bool>('live_photo_video_uploaded', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("live_photo_video_uploaded" IN (0, 1))'),
|
||||||
|
defaultValue: const i4.Constant(false));
|
||||||
@override
|
@override
|
||||||
List<i0.GeneratedColumn> get $columns => [
|
List<i0.GeneratedColumn> get $columns => [
|
||||||
name,
|
name,
|
||||||
@@ -348,7 +429,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
id,
|
id,
|
||||||
checksum,
|
checksum,
|
||||||
isFavorite
|
isFavorite,
|
||||||
|
isLivePhoto,
|
||||||
|
livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@@ -404,6 +488,24 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
isFavorite.isAcceptableOrUnknown(
|
isFavorite.isAcceptableOrUnknown(
|
||||||
data['is_favorite']!, _isFavoriteMeta));
|
data['is_favorite']!, _isFavoriteMeta));
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('is_live_photo')) {
|
||||||
|
context.handle(
|
||||||
|
_isLivePhotoMeta,
|
||||||
|
isLivePhoto.isAcceptableOrUnknown(
|
||||||
|
data['is_live_photo']!, _isLivePhotoMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('live_photo_image_uploaded')) {
|
||||||
|
context.handle(
|
||||||
|
_livePhotoImageUploadedMeta,
|
||||||
|
livePhotoImageUploaded.isAcceptableOrUnknown(
|
||||||
|
data['live_photo_image_uploaded']!, _livePhotoImageUploadedMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('live_photo_video_uploaded')) {
|
||||||
|
context.handle(
|
||||||
|
_livePhotoVideoUploadedMeta,
|
||||||
|
livePhotoVideoUploaded.isAcceptableOrUnknown(
|
||||||
|
data['live_photo_video_uploaded']!, _livePhotoVideoUploadedMeta));
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +537,14 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']),
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']),
|
||||||
isFavorite: attachedDatabase.typeMapping
|
isFavorite: attachedDatabase.typeMapping
|
||||||
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!,
|
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!,
|
||||||
|
isLivePhoto: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_live_photo'])!,
|
||||||
|
livePhotoImageUploaded: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}live_photo_image_uploaded'])!,
|
||||||
|
livePhotoVideoUploaded: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}live_photo_video_uploaded'])!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +573,9 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
final String id;
|
final String id;
|
||||||
final String? checksum;
|
final String? checksum;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
|
final bool isLivePhoto;
|
||||||
|
final bool livePhotoImageUploaded;
|
||||||
|
final bool livePhotoVideoUploaded;
|
||||||
const LocalAssetEntityData(
|
const LocalAssetEntityData(
|
||||||
{required this.name,
|
{required this.name,
|
||||||
required this.type,
|
required this.type,
|
||||||
@@ -473,7 +586,10 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
this.durationInSeconds,
|
this.durationInSeconds,
|
||||||
required this.id,
|
required this.id,
|
||||||
this.checksum,
|
this.checksum,
|
||||||
required this.isFavorite});
|
required this.isFavorite,
|
||||||
|
required this.isLivePhoto,
|
||||||
|
required this.livePhotoImageUploaded,
|
||||||
|
required this.livePhotoVideoUploaded});
|
||||||
@override
|
@override
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
final map = <String, i0.Expression>{};
|
final map = <String, i0.Expression>{};
|
||||||
@@ -498,6 +614,11 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
map['checksum'] = i0.Variable<String>(checksum);
|
map['checksum'] = i0.Variable<String>(checksum);
|
||||||
}
|
}
|
||||||
map['is_favorite'] = i0.Variable<bool>(isFavorite);
|
map['is_favorite'] = i0.Variable<bool>(isFavorite);
|
||||||
|
map['is_live_photo'] = i0.Variable<bool>(isLivePhoto);
|
||||||
|
map['live_photo_image_uploaded'] =
|
||||||
|
i0.Variable<bool>(livePhotoImageUploaded);
|
||||||
|
map['live_photo_video_uploaded'] =
|
||||||
|
i0.Variable<bool>(livePhotoVideoUploaded);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,6 +637,11 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
id: serializer.fromJson<String>(json['id']),
|
id: serializer.fromJson<String>(json['id']),
|
||||||
checksum: serializer.fromJson<String?>(json['checksum']),
|
checksum: serializer.fromJson<String?>(json['checksum']),
|
||||||
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
||||||
|
isLivePhoto: serializer.fromJson<bool>(json['isLivePhoto']),
|
||||||
|
livePhotoImageUploaded:
|
||||||
|
serializer.fromJson<bool>(json['livePhotoImageUploaded']),
|
||||||
|
livePhotoVideoUploaded:
|
||||||
|
serializer.fromJson<bool>(json['livePhotoVideoUploaded']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
@@ -533,6 +659,9 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
'id': serializer.toJson<String>(id),
|
'id': serializer.toJson<String>(id),
|
||||||
'checksum': serializer.toJson<String?>(checksum),
|
'checksum': serializer.toJson<String?>(checksum),
|
||||||
'isFavorite': serializer.toJson<bool>(isFavorite),
|
'isFavorite': serializer.toJson<bool>(isFavorite),
|
||||||
|
'isLivePhoto': serializer.toJson<bool>(isLivePhoto),
|
||||||
|
'livePhotoImageUploaded': serializer.toJson<bool>(livePhotoImageUploaded),
|
||||||
|
'livePhotoVideoUploaded': serializer.toJson<bool>(livePhotoVideoUploaded),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +675,10 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
||||||
String? id,
|
String? id,
|
||||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||||
bool? isFavorite}) =>
|
bool? isFavorite,
|
||||||
|
bool? isLivePhoto,
|
||||||
|
bool? livePhotoImageUploaded,
|
||||||
|
bool? livePhotoVideoUploaded}) =>
|
||||||
i1.LocalAssetEntityData(
|
i1.LocalAssetEntityData(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
@@ -560,6 +692,11 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
checksum: checksum.present ? checksum.value : this.checksum,
|
checksum: checksum.present ? checksum.value : this.checksum,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
isLivePhoto: isLivePhoto ?? this.isLivePhoto,
|
||||||
|
livePhotoImageUploaded:
|
||||||
|
livePhotoImageUploaded ?? this.livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded:
|
||||||
|
livePhotoVideoUploaded ?? this.livePhotoVideoUploaded,
|
||||||
);
|
);
|
||||||
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
|
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
|
||||||
return LocalAssetEntityData(
|
return LocalAssetEntityData(
|
||||||
@@ -576,6 +713,14 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
checksum: data.checksum.present ? data.checksum.value : this.checksum,
|
checksum: data.checksum.present ? data.checksum.value : this.checksum,
|
||||||
isFavorite:
|
isFavorite:
|
||||||
data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
|
data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
|
||||||
|
isLivePhoto:
|
||||||
|
data.isLivePhoto.present ? data.isLivePhoto.value : this.isLivePhoto,
|
||||||
|
livePhotoImageUploaded: data.livePhotoImageUploaded.present
|
||||||
|
? data.livePhotoImageUploaded.value
|
||||||
|
: this.livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded: data.livePhotoVideoUploaded.present
|
||||||
|
? data.livePhotoVideoUploaded.value
|
||||||
|
: this.livePhotoVideoUploaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,14 +736,29 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
..write('durationInSeconds: $durationInSeconds, ')
|
..write('durationInSeconds: $durationInSeconds, ')
|
||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('checksum: $checksum, ')
|
..write('checksum: $checksum, ')
|
||||||
..write('isFavorite: $isFavorite')
|
..write('isFavorite: $isFavorite, ')
|
||||||
|
..write('isLivePhoto: $isLivePhoto, ')
|
||||||
|
..write('livePhotoImageUploaded: $livePhotoImageUploaded, ')
|
||||||
|
..write('livePhotoVideoUploaded: $livePhotoVideoUploaded')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(name, type, createdAt, updatedAt, width,
|
int get hashCode => Object.hash(
|
||||||
height, durationInSeconds, id, checksum, isFavorite);
|
name,
|
||||||
|
type,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
durationInSeconds,
|
||||||
|
id,
|
||||||
|
checksum,
|
||||||
|
isFavorite,
|
||||||
|
isLivePhoto,
|
||||||
|
livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -612,7 +772,10 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
other.durationInSeconds == this.durationInSeconds &&
|
other.durationInSeconds == this.durationInSeconds &&
|
||||||
other.id == this.id &&
|
other.id == this.id &&
|
||||||
other.checksum == this.checksum &&
|
other.checksum == this.checksum &&
|
||||||
other.isFavorite == this.isFavorite);
|
other.isFavorite == this.isFavorite &&
|
||||||
|
other.isLivePhoto == this.isLivePhoto &&
|
||||||
|
other.livePhotoImageUploaded == this.livePhotoImageUploaded &&
|
||||||
|
other.livePhotoVideoUploaded == this.livePhotoVideoUploaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalAssetEntityCompanion
|
class LocalAssetEntityCompanion
|
||||||
@@ -627,6 +790,9 @@ class LocalAssetEntityCompanion
|
|||||||
final i0.Value<String> id;
|
final i0.Value<String> id;
|
||||||
final i0.Value<String?> checksum;
|
final i0.Value<String?> checksum;
|
||||||
final i0.Value<bool> isFavorite;
|
final i0.Value<bool> isFavorite;
|
||||||
|
final i0.Value<bool> isLivePhoto;
|
||||||
|
final i0.Value<bool> livePhotoImageUploaded;
|
||||||
|
final i0.Value<bool> livePhotoVideoUploaded;
|
||||||
const LocalAssetEntityCompanion({
|
const LocalAssetEntityCompanion({
|
||||||
this.name = const i0.Value.absent(),
|
this.name = const i0.Value.absent(),
|
||||||
this.type = const i0.Value.absent(),
|
this.type = const i0.Value.absent(),
|
||||||
@@ -638,6 +804,9 @@ class LocalAssetEntityCompanion
|
|||||||
this.id = const i0.Value.absent(),
|
this.id = const i0.Value.absent(),
|
||||||
this.checksum = const i0.Value.absent(),
|
this.checksum = const i0.Value.absent(),
|
||||||
this.isFavorite = const i0.Value.absent(),
|
this.isFavorite = const i0.Value.absent(),
|
||||||
|
this.isLivePhoto = const i0.Value.absent(),
|
||||||
|
this.livePhotoImageUploaded = const i0.Value.absent(),
|
||||||
|
this.livePhotoVideoUploaded = const i0.Value.absent(),
|
||||||
});
|
});
|
||||||
LocalAssetEntityCompanion.insert({
|
LocalAssetEntityCompanion.insert({
|
||||||
required String name,
|
required String name,
|
||||||
@@ -650,6 +819,9 @@ class LocalAssetEntityCompanion
|
|||||||
required String id,
|
required String id,
|
||||||
this.checksum = const i0.Value.absent(),
|
this.checksum = const i0.Value.absent(),
|
||||||
this.isFavorite = const i0.Value.absent(),
|
this.isFavorite = const i0.Value.absent(),
|
||||||
|
this.isLivePhoto = const i0.Value.absent(),
|
||||||
|
this.livePhotoImageUploaded = const i0.Value.absent(),
|
||||||
|
this.livePhotoVideoUploaded = const i0.Value.absent(),
|
||||||
}) : name = i0.Value(name),
|
}) : name = i0.Value(name),
|
||||||
type = i0.Value(type),
|
type = i0.Value(type),
|
||||||
id = i0.Value(id);
|
id = i0.Value(id);
|
||||||
@@ -664,6 +836,9 @@ class LocalAssetEntityCompanion
|
|||||||
i0.Expression<String>? id,
|
i0.Expression<String>? id,
|
||||||
i0.Expression<String>? checksum,
|
i0.Expression<String>? checksum,
|
||||||
i0.Expression<bool>? isFavorite,
|
i0.Expression<bool>? isFavorite,
|
||||||
|
i0.Expression<bool>? isLivePhoto,
|
||||||
|
i0.Expression<bool>? livePhotoImageUploaded,
|
||||||
|
i0.Expression<bool>? livePhotoVideoUploaded,
|
||||||
}) {
|
}) {
|
||||||
return i0.RawValuesInsertable({
|
return i0.RawValuesInsertable({
|
||||||
if (name != null) 'name': name,
|
if (name != null) 'name': name,
|
||||||
@@ -676,6 +851,11 @@ class LocalAssetEntityCompanion
|
|||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
if (checksum != null) 'checksum': checksum,
|
if (checksum != null) 'checksum': checksum,
|
||||||
if (isFavorite != null) 'is_favorite': isFavorite,
|
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||||
|
if (isLivePhoto != null) 'is_live_photo': isLivePhoto,
|
||||||
|
if (livePhotoImageUploaded != null)
|
||||||
|
'live_photo_image_uploaded': livePhotoImageUploaded,
|
||||||
|
if (livePhotoVideoUploaded != null)
|
||||||
|
'live_photo_video_uploaded': livePhotoVideoUploaded,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,7 +869,10 @@ class LocalAssetEntityCompanion
|
|||||||
i0.Value<int?>? durationInSeconds,
|
i0.Value<int?>? durationInSeconds,
|
||||||
i0.Value<String>? id,
|
i0.Value<String>? id,
|
||||||
i0.Value<String?>? checksum,
|
i0.Value<String?>? checksum,
|
||||||
i0.Value<bool>? isFavorite}) {
|
i0.Value<bool>? isFavorite,
|
||||||
|
i0.Value<bool>? isLivePhoto,
|
||||||
|
i0.Value<bool>? livePhotoImageUploaded,
|
||||||
|
i0.Value<bool>? livePhotoVideoUploaded}) {
|
||||||
return i1.LocalAssetEntityCompanion(
|
return i1.LocalAssetEntityCompanion(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
@@ -701,6 +884,11 @@ class LocalAssetEntityCompanion
|
|||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
checksum: checksum ?? this.checksum,
|
checksum: checksum ?? this.checksum,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
isLivePhoto: isLivePhoto ?? this.isLivePhoto,
|
||||||
|
livePhotoImageUploaded:
|
||||||
|
livePhotoImageUploaded ?? this.livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded:
|
||||||
|
livePhotoVideoUploaded ?? this.livePhotoVideoUploaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,6 +926,17 @@ class LocalAssetEntityCompanion
|
|||||||
if (isFavorite.present) {
|
if (isFavorite.present) {
|
||||||
map['is_favorite'] = i0.Variable<bool>(isFavorite.value);
|
map['is_favorite'] = i0.Variable<bool>(isFavorite.value);
|
||||||
}
|
}
|
||||||
|
if (isLivePhoto.present) {
|
||||||
|
map['is_live_photo'] = i0.Variable<bool>(isLivePhoto.value);
|
||||||
|
}
|
||||||
|
if (livePhotoImageUploaded.present) {
|
||||||
|
map['live_photo_image_uploaded'] =
|
||||||
|
i0.Variable<bool>(livePhotoImageUploaded.value);
|
||||||
|
}
|
||||||
|
if (livePhotoVideoUploaded.present) {
|
||||||
|
map['live_photo_video_uploaded'] =
|
||||||
|
i0.Variable<bool>(livePhotoVideoUploaded.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +952,10 @@ class LocalAssetEntityCompanion
|
|||||||
..write('durationInSeconds: $durationInSeconds, ')
|
..write('durationInSeconds: $durationInSeconds, ')
|
||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('checksum: $checksum, ')
|
..write('checksum: $checksum, ')
|
||||||
..write('isFavorite: $isFavorite')
|
..write('isFavorite: $isFavorite, ')
|
||||||
|
..write('isLivePhoto: $isLivePhoto, ')
|
||||||
|
..write('livePhotoImageUploaded: $livePhotoImageUploaded, ')
|
||||||
|
..write('livePhotoVideoUploaded: $livePhotoVideoUploaded')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import 'remote_asset.entity.dart';
|
||||||
|
import 'local_asset.entity.dart';
|
||||||
|
|
||||||
|
mergedAsset: SELECT * FROM
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
rae.id as remote_id,
|
||||||
|
lae.id as local_id,
|
||||||
|
rae.name,
|
||||||
|
rae."type",
|
||||||
|
rae.created_at,
|
||||||
|
rae.updated_at,
|
||||||
|
rae.width,
|
||||||
|
rae.height,
|
||||||
|
rae.duration_in_seconds,
|
||||||
|
rae.is_favorite,
|
||||||
|
rae.thumb_hash,
|
||||||
|
rae.checksum,
|
||||||
|
rae.owner_id
|
||||||
|
FROM
|
||||||
|
remote_asset_entity rae
|
||||||
|
LEFT JOIN
|
||||||
|
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||||
|
WHERE
|
||||||
|
rae.visibility = 0 AND rae.owner_id in ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
NULL as remote_id,
|
||||||
|
lae.id as local_id,
|
||||||
|
lae.name,
|
||||||
|
lae."type",
|
||||||
|
lae.created_at,
|
||||||
|
lae.updated_at,
|
||||||
|
lae.width,
|
||||||
|
lae.height,
|
||||||
|
lae.duration_in_seconds,
|
||||||
|
lae.is_favorite,
|
||||||
|
NULL as thumb_hash,
|
||||||
|
lae.checksum,
|
||||||
|
NULL as owner_id
|
||||||
|
FROM
|
||||||
|
local_asset_entity lae
|
||||||
|
LEFT JOIN
|
||||||
|
remote_asset_entity rae ON rae.checksum = lae.checksum
|
||||||
|
WHERE
|
||||||
|
rae.id IS NULL
|
||||||
|
)
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $limit;
|
||||||
|
|
||||||
|
mergedBucket(:group_by AS INTEGER):
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as asset_count,
|
||||||
|
CASE
|
||||||
|
WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', created_at) -- day
|
||||||
|
WHEN :group_by = 1 THEN STRFTIME('%Y-%m', created_at) -- month
|
||||||
|
END AS bucket_date
|
||||||
|
FROM
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
rae.name,
|
||||||
|
rae.created_at
|
||||||
|
FROM
|
||||||
|
remote_asset_entity rae
|
||||||
|
LEFT JOIN
|
||||||
|
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||||
|
WHERE
|
||||||
|
rae.visibility = 0 AND rae.owner_id in ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
lae.name,
|
||||||
|
lae.created_at
|
||||||
|
FROM
|
||||||
|
local_asset_entity lae
|
||||||
|
LEFT JOIN
|
||||||
|
remote_asset_entity rae ON rae.checksum = lae.checksum
|
||||||
|
WHERE
|
||||||
|
rae.id IS NULL
|
||||||
|
)
|
||||||
|
GROUP BY bucket_date
|
||||||
|
ORDER BY bucket_date DESC;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:drift/internal/modular.dart' as i1;
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||||
|
as i3;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||||
|
as i4;
|
||||||
|
|
||||||
|
class MergedAssetDrift extends i1.ModularAccessor {
|
||||||
|
MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
|
||||||
|
i0.Selectable<MergedAssetResult> mergedAsset(List<String> var1,
|
||||||
|
{required i0.Limit limit}) {
|
||||||
|
var $arrayStartIndex = 1;
|
||||||
|
final expandedvar1 = $expandVar($arrayStartIndex, var1.length);
|
||||||
|
$arrayStartIndex += var1.length;
|
||||||
|
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
|
||||||
|
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||||
|
return customSelect(
|
||||||
|
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||||
|
variables: [
|
||||||
|
for (var $ in var1) i0.Variable<String>($),
|
||||||
|
...generatedlimit.introducedVariables
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
remoteAssetEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
...generatedlimit.watchedTables,
|
||||||
|
}).map((i0.QueryRow row) => MergedAssetResult(
|
||||||
|
remoteId: row.readNullable<String>('remote_id'),
|
||||||
|
localId: row.readNullable<String>('local_id'),
|
||||||
|
name: row.read<String>('name'),
|
||||||
|
type: i3.$RemoteAssetEntityTable.$convertertype
|
||||||
|
.fromSql(row.read<int>('type')),
|
||||||
|
createdAt: row.read<DateTime>('created_at'),
|
||||||
|
updatedAt: row.read<DateTime>('updated_at'),
|
||||||
|
width: row.readNullable<int>('width'),
|
||||||
|
height: row.readNullable<int>('height'),
|
||||||
|
durationInSeconds: row.readNullable<int>('duration_in_seconds'),
|
||||||
|
isFavorite: row.read<bool>('is_favorite'),
|
||||||
|
thumbHash: row.readNullable<String>('thumb_hash'),
|
||||||
|
checksum: row.readNullable<String>('checksum'),
|
||||||
|
ownerId: row.readNullable<String>('owner_id'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
i0.Selectable<MergedBucketResult> mergedBucket(List<String> var2,
|
||||||
|
{required int groupBy}) {
|
||||||
|
var $arrayStartIndex = 2;
|
||||||
|
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
|
||||||
|
$arrayStartIndex += var2.length;
|
||||||
|
return customSelect(
|
||||||
|
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at) WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at) END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
|
||||||
|
variables: [
|
||||||
|
i0.Variable<int>(groupBy),
|
||||||
|
for (var $ in var2) i0.Variable<String>($)
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
remoteAssetEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
}).map((i0.QueryRow row) => MergedBucketResult(
|
||||||
|
assetCount: row.read<int>('asset_count'),
|
||||||
|
bucketDate: row.read<String>('bucket_date'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
i3.$RemoteAssetEntityTable get remoteAssetEntity =>
|
||||||
|
i1.ReadDatabaseContainer(attachedDatabase)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity');
|
||||||
|
i4.$LocalAssetEntityTable get localAssetEntity =>
|
||||||
|
i1.ReadDatabaseContainer(attachedDatabase)
|
||||||
|
.resultSet<i4.$LocalAssetEntityTable>('local_asset_entity');
|
||||||
|
}
|
||||||
|
|
||||||
|
class MergedAssetResult {
|
||||||
|
final String? remoteId;
|
||||||
|
final String? localId;
|
||||||
|
final String name;
|
||||||
|
final i2.AssetType type;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final int? width;
|
||||||
|
final int? height;
|
||||||
|
final int? durationInSeconds;
|
||||||
|
final bool isFavorite;
|
||||||
|
final String? thumbHash;
|
||||||
|
final String? checksum;
|
||||||
|
final String? ownerId;
|
||||||
|
MergedAssetResult({
|
||||||
|
this.remoteId,
|
||||||
|
this.localId,
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.durationInSeconds,
|
||||||
|
required this.isFavorite,
|
||||||
|
this.thumbHash,
|
||||||
|
this.checksum,
|
||||||
|
this.ownerId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MergedBucketResult {
|
||||||
|
final int assetCount;
|
||||||
|
final String bucketDate;
|
||||||
|
MergedBucketResult({
|
||||||
|
required this.assetCount,
|
||||||
|
required this.bucketDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
|
||||||
|
|
||||||
class RemoteAlbumEntity extends Table with DriftDefaultsMixin {
|
|
||||||
const RemoteAlbumEntity();
|
|
||||||
|
|
||||||
TextColumn get id => text()();
|
|
||||||
|
|
||||||
TextColumn get name => text()();
|
|
||||||
|
|
||||||
TextColumn get description => text()();
|
|
||||||
|
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
|
||||||
|
|
||||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
|
||||||
|
|
||||||
TextColumn get ownerId =>
|
|
||||||
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
|
||||||
|
|
||||||
TextColumn get thumbnailAssetId => text()
|
|
||||||
.references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull)
|
|
||||||
.nullable()();
|
|
||||||
|
|
||||||
BoolColumn get isActivityEnabled =>
|
|
||||||
boolean().withDefault(const Constant(true))();
|
|
||||||
|
|
||||||
IntColumn get order => intEnum<AssetOrder>()();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Set<Column> get primaryKey => {id};
|
|
||||||
}
|
|
||||||
@@ -1,946 +0,0 @@
|
|||||||
// dart format width=80
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
import 'package:drift/drift.dart' as i0;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
|
||||||
as i1;
|
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart' as i2;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'
|
|
||||||
as i3;
|
|
||||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
|
||||||
as i5;
|
|
||||||
import 'package:drift/internal/modular.dart' as i6;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
|
||||||
as i7;
|
|
||||||
|
|
||||||
typedef $$RemoteAlbumEntityTableCreateCompanionBuilder
|
|
||||||
= i1.RemoteAlbumEntityCompanion Function({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String description,
|
|
||||||
i0.Value<DateTime> createdAt,
|
|
||||||
i0.Value<DateTime> updatedAt,
|
|
||||||
required String ownerId,
|
|
||||||
i0.Value<String?> thumbnailAssetId,
|
|
||||||
i0.Value<bool> isActivityEnabled,
|
|
||||||
required i2.AssetOrder order,
|
|
||||||
});
|
|
||||||
typedef $$RemoteAlbumEntityTableUpdateCompanionBuilder
|
|
||||||
= i1.RemoteAlbumEntityCompanion Function({
|
|
||||||
i0.Value<String> id,
|
|
||||||
i0.Value<String> name,
|
|
||||||
i0.Value<String> description,
|
|
||||||
i0.Value<DateTime> createdAt,
|
|
||||||
i0.Value<DateTime> updatedAt,
|
|
||||||
i0.Value<String> ownerId,
|
|
||||||
i0.Value<String?> thumbnailAssetId,
|
|
||||||
i0.Value<bool> isActivityEnabled,
|
|
||||||
i0.Value<i2.AssetOrder> order,
|
|
||||||
});
|
|
||||||
|
|
||||||
final class $$RemoteAlbumEntityTableReferences extends i0.BaseReferences<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$RemoteAlbumEntityTable,
|
|
||||||
i1.RemoteAlbumEntityData> {
|
|
||||||
$$RemoteAlbumEntityTableReferences(
|
|
||||||
super.$_db, super.$_table, super.$_typedResult);
|
|
||||||
|
|
||||||
static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
|
|
||||||
i6.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i5.$UserEntityTable>('user_entity')
|
|
||||||
.createAlias(i0.$_aliasNameGenerator(
|
|
||||||
i6.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i1.$RemoteAlbumEntityTable>('remote_album_entity')
|
|
||||||
.ownerId,
|
|
||||||
i6.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i5.$UserEntityTable>('user_entity')
|
|
||||||
.id));
|
|
||||||
|
|
||||||
i5.$$UserEntityTableProcessedTableManager get ownerId {
|
|
||||||
final $_column = $_itemColumn<String>('owner_id')!;
|
|
||||||
|
|
||||||
final manager = i5
|
|
||||||
.$$UserEntityTableTableManager(
|
|
||||||
$_db,
|
|
||||||
i6.ReadDatabaseContainer($_db)
|
|
||||||
.resultSet<i5.$UserEntityTable>('user_entity'))
|
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
|
||||||
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
|
|
||||||
if (item == null) return manager;
|
|
||||||
return i0.ProcessedTableManager(
|
|
||||||
manager.$state.copyWith(prefetchedData: [item]));
|
|
||||||
}
|
|
||||||
|
|
||||||
static i7.$RemoteAssetEntityTable _thumbnailAssetIdTable(
|
|
||||||
i0.GeneratedDatabase db) =>
|
|
||||||
i6.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity')
|
|
||||||
.createAlias(i0.$_aliasNameGenerator(
|
|
||||||
i6.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i1.$RemoteAlbumEntityTable>('remote_album_entity')
|
|
||||||
.thumbnailAssetId,
|
|
||||||
i6.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity')
|
|
||||||
.id));
|
|
||||||
|
|
||||||
i7.$$RemoteAssetEntityTableProcessedTableManager? get thumbnailAssetId {
|
|
||||||
final $_column = $_itemColumn<String>('thumbnail_asset_id');
|
|
||||||
if ($_column == null) return null;
|
|
||||||
final manager = i7
|
|
||||||
.$$RemoteAssetEntityTableTableManager(
|
|
||||||
$_db,
|
|
||||||
i6.ReadDatabaseContainer($_db)
|
|
||||||
.resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'))
|
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
|
||||||
final item = $_typedResult.readTableOrNull(_thumbnailAssetIdTable($_db));
|
|
||||||
if (item == null) return manager;
|
|
||||||
return i0.ProcessedTableManager(
|
|
||||||
manager.$state.copyWith(prefetchedData: [item]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$RemoteAlbumEntityTableFilterComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$RemoteAlbumEntityTable> {
|
|
||||||
$$RemoteAlbumEntityTableFilterComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i0.ColumnFilters<String> get id => $composableBuilder(
|
|
||||||
column: $table.id, builder: (column) => i0.ColumnFilters(column));
|
|
||||||
|
|
||||||
i0.ColumnFilters<String> get name => $composableBuilder(
|
|
||||||
column: $table.name, builder: (column) => i0.ColumnFilters(column));
|
|
||||||
|
|
||||||
i0.ColumnFilters<String> get description => $composableBuilder(
|
|
||||||
column: $table.description,
|
|
||||||
builder: (column) => i0.ColumnFilters(column));
|
|
||||||
|
|
||||||
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
|
||||||
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
|
|
||||||
|
|
||||||
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
|
||||||
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
|
|
||||||
|
|
||||||
i0.ColumnFilters<bool> get isActivityEnabled => $composableBuilder(
|
|
||||||
column: $table.isActivityEnabled,
|
|
||||||
builder: (column) => i0.ColumnFilters(column));
|
|
||||||
|
|
||||||
i0.ColumnWithTypeConverterFilters<i2.AssetOrder, i2.AssetOrder, int>
|
|
||||||
get order => $composableBuilder(
|
|
||||||
column: $table.order,
|
|
||||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
|
|
||||||
|
|
||||||
i5.$$UserEntityTableFilterComposer get ownerId {
|
|
||||||
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.ownerId,
|
|
||||||
referencedTable: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$UserEntityTable>('user_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i5.$$UserEntityTableFilterComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$UserEntityTable>('user_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i7.$$RemoteAssetEntityTableFilterComposer get thumbnailAssetId {
|
|
||||||
final i7.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.thumbnailAssetId,
|
|
||||||
referencedTable: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i7.$$RemoteAssetEntityTableFilterComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$RemoteAlbumEntityTableOrderingComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$RemoteAlbumEntityTable> {
|
|
||||||
$$RemoteAlbumEntityTableOrderingComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i0.ColumnOrderings<String> get id => $composableBuilder(
|
|
||||||
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
|
|
||||||
|
|
||||||
i0.ColumnOrderings<String> get name => $composableBuilder(
|
|
||||||
column: $table.name, builder: (column) => i0.ColumnOrderings(column));
|
|
||||||
|
|
||||||
i0.ColumnOrderings<String> get description => $composableBuilder(
|
|
||||||
column: $table.description,
|
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
|
||||||
|
|
||||||
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
|
||||||
column: $table.createdAt,
|
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
|
||||||
|
|
||||||
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
|
||||||
column: $table.updatedAt,
|
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
|
||||||
|
|
||||||
i0.ColumnOrderings<bool> get isActivityEnabled => $composableBuilder(
|
|
||||||
column: $table.isActivityEnabled,
|
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
|
||||||
|
|
||||||
i0.ColumnOrderings<int> get order => $composableBuilder(
|
|
||||||
column: $table.order, builder: (column) => i0.ColumnOrderings(column));
|
|
||||||
|
|
||||||
i5.$$UserEntityTableOrderingComposer get ownerId {
|
|
||||||
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.ownerId,
|
|
||||||
referencedTable: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$UserEntityTable>('user_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i5.$$UserEntityTableOrderingComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$UserEntityTable>('user_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i7.$$RemoteAssetEntityTableOrderingComposer get thumbnailAssetId {
|
|
||||||
final i7.$$RemoteAssetEntityTableOrderingComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.thumbnailAssetId,
|
|
||||||
referencedTable: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i7.$$RemoteAssetEntityTableOrderingComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i7.$RemoteAssetEntityTable>(
|
|
||||||
'remote_asset_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$RemoteAlbumEntityTableAnnotationComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$RemoteAlbumEntityTable> {
|
|
||||||
$$RemoteAlbumEntityTableAnnotationComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i0.GeneratedColumn<String> get id =>
|
|
||||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
|
||||||
|
|
||||||
i0.GeneratedColumn<String> get name =>
|
|
||||||
$composableBuilder(column: $table.name, builder: (column) => column);
|
|
||||||
|
|
||||||
i0.GeneratedColumn<String> get description => $composableBuilder(
|
|
||||||
column: $table.description, builder: (column) => column);
|
|
||||||
|
|
||||||
i0.GeneratedColumn<DateTime> get createdAt =>
|
|
||||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
|
||||||
|
|
||||||
i0.GeneratedColumn<DateTime> get updatedAt =>
|
|
||||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
|
||||||
|
|
||||||
i0.GeneratedColumn<bool> get isActivityEnabled => $composableBuilder(
|
|
||||||
column: $table.isActivityEnabled, builder: (column) => column);
|
|
||||||
|
|
||||||
i0.GeneratedColumnWithTypeConverter<i2.AssetOrder, int> get order =>
|
|
||||||
$composableBuilder(column: $table.order, builder: (column) => column);
|
|
||||||
|
|
||||||
i5.$$UserEntityTableAnnotationComposer get ownerId {
|
|
||||||
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.ownerId,
|
|
||||||
referencedTable: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$UserEntityTable>('user_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i5.$$UserEntityTableAnnotationComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$UserEntityTable>('user_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i7.$$RemoteAssetEntityTableAnnotationComposer get thumbnailAssetId {
|
|
||||||
final i7.$$RemoteAssetEntityTableAnnotationComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.thumbnailAssetId,
|
|
||||||
referencedTable: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i7.$$RemoteAssetEntityTableAnnotationComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i6.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i7.$RemoteAssetEntityTable>(
|
|
||||||
'remote_asset_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$RemoteAlbumEntityTableTableManager extends i0.RootTableManager<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$RemoteAlbumEntityTable,
|
|
||||||
i1.RemoteAlbumEntityData,
|
|
||||||
i1.$$RemoteAlbumEntityTableFilterComposer,
|
|
||||||
i1.$$RemoteAlbumEntityTableOrderingComposer,
|
|
||||||
i1.$$RemoteAlbumEntityTableAnnotationComposer,
|
|
||||||
$$RemoteAlbumEntityTableCreateCompanionBuilder,
|
|
||||||
$$RemoteAlbumEntityTableUpdateCompanionBuilder,
|
|
||||||
(i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences),
|
|
||||||
i1.RemoteAlbumEntityData,
|
|
||||||
i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})> {
|
|
||||||
$$RemoteAlbumEntityTableTableManager(
|
|
||||||
i0.GeneratedDatabase db, i1.$RemoteAlbumEntityTable table)
|
|
||||||
: super(i0.TableManagerState(
|
|
||||||
db: db,
|
|
||||||
table: table,
|
|
||||||
createFilteringComposer: () =>
|
|
||||||
i1.$$RemoteAlbumEntityTableFilterComposer($db: db, $table: table),
|
|
||||||
createOrderingComposer: () => i1
|
|
||||||
.$$RemoteAlbumEntityTableOrderingComposer($db: db, $table: table),
|
|
||||||
createComputedFieldComposer: () =>
|
|
||||||
i1.$$RemoteAlbumEntityTableAnnotationComposer(
|
|
||||||
$db: db, $table: table),
|
|
||||||
updateCompanionCallback: ({
|
|
||||||
i0.Value<String> id = const i0.Value.absent(),
|
|
||||||
i0.Value<String> name = const i0.Value.absent(),
|
|
||||||
i0.Value<String> description = const i0.Value.absent(),
|
|
||||||
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
|
||||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
|
||||||
i0.Value<String> ownerId = const i0.Value.absent(),
|
|
||||||
i0.Value<String?> thumbnailAssetId = const i0.Value.absent(),
|
|
||||||
i0.Value<bool> isActivityEnabled = const i0.Value.absent(),
|
|
||||||
i0.Value<i2.AssetOrder> order = const i0.Value.absent(),
|
|
||||||
}) =>
|
|
||||||
i1.RemoteAlbumEntityCompanion(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
description: description,
|
|
||||||
createdAt: createdAt,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
ownerId: ownerId,
|
|
||||||
thumbnailAssetId: thumbnailAssetId,
|
|
||||||
isActivityEnabled: isActivityEnabled,
|
|
||||||
order: order,
|
|
||||||
),
|
|
||||||
createCompanionCallback: ({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String description,
|
|
||||||
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
|
||||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
|
||||||
required String ownerId,
|
|
||||||
i0.Value<String?> thumbnailAssetId = const i0.Value.absent(),
|
|
||||||
i0.Value<bool> isActivityEnabled = const i0.Value.absent(),
|
|
||||||
required i2.AssetOrder order,
|
|
||||||
}) =>
|
|
||||||
i1.RemoteAlbumEntityCompanion.insert(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
description: description,
|
|
||||||
createdAt: createdAt,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
ownerId: ownerId,
|
|
||||||
thumbnailAssetId: thumbnailAssetId,
|
|
||||||
isActivityEnabled: isActivityEnabled,
|
|
||||||
order: order,
|
|
||||||
),
|
|
||||||
withReferenceMapper: (p0) => p0
|
|
||||||
.map((e) => (
|
|
||||||
e.readTable(table),
|
|
||||||
i1.$$RemoteAlbumEntityTableReferences(db, table, e)
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
prefetchHooksCallback: ({ownerId = false, thumbnailAssetId = false}) {
|
|
||||||
return i0.PrefetchHooks(
|
|
||||||
db: db,
|
|
||||||
explicitlyWatchedTables: [],
|
|
||||||
addJoins: <
|
|
||||||
T extends i0.TableManagerState<
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic>>(state) {
|
|
||||||
if (ownerId) {
|
|
||||||
state = state.withJoin(
|
|
||||||
currentTable: table,
|
|
||||||
currentColumn: table.ownerId,
|
|
||||||
referencedTable:
|
|
||||||
i1.$$RemoteAlbumEntityTableReferences._ownerIdTable(db),
|
|
||||||
referencedColumn: i1.$$RemoteAlbumEntityTableReferences
|
|
||||||
._ownerIdTable(db)
|
|
||||||
.id,
|
|
||||||
) as T;
|
|
||||||
}
|
|
||||||
if (thumbnailAssetId) {
|
|
||||||
state = state.withJoin(
|
|
||||||
currentTable: table,
|
|
||||||
currentColumn: table.thumbnailAssetId,
|
|
||||||
referencedTable: i1.$$RemoteAlbumEntityTableReferences
|
|
||||||
._thumbnailAssetIdTable(db),
|
|
||||||
referencedColumn: i1.$$RemoteAlbumEntityTableReferences
|
|
||||||
._thumbnailAssetIdTable(db)
|
|
||||||
.id,
|
|
||||||
) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
getPrefetchedDataCallback: (items) async {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef $$RemoteAlbumEntityTableProcessedTableManager
|
|
||||||
= i0.ProcessedTableManager<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$RemoteAlbumEntityTable,
|
|
||||||
i1.RemoteAlbumEntityData,
|
|
||||||
i1.$$RemoteAlbumEntityTableFilterComposer,
|
|
||||||
i1.$$RemoteAlbumEntityTableOrderingComposer,
|
|
||||||
i1.$$RemoteAlbumEntityTableAnnotationComposer,
|
|
||||||
$$RemoteAlbumEntityTableCreateCompanionBuilder,
|
|
||||||
$$RemoteAlbumEntityTableUpdateCompanionBuilder,
|
|
||||||
(i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences),
|
|
||||||
i1.RemoteAlbumEntityData,
|
|
||||||
i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})>;
|
|
||||||
|
|
||||||
class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
|
|
||||||
with i0.TableInfo<$RemoteAlbumEntityTable, i1.RemoteAlbumEntityData> {
|
|
||||||
@override
|
|
||||||
final i0.GeneratedDatabase attachedDatabase;
|
|
||||||
final String? _alias;
|
|
||||||
$RemoteAlbumEntityTable(this.attachedDatabase, [this._alias]);
|
|
||||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
|
|
||||||
'id', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
|
||||||
static const i0.VerificationMeta _nameMeta =
|
|
||||||
const i0.VerificationMeta('name');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
|
|
||||||
'name', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
|
||||||
static const i0.VerificationMeta _descriptionMeta =
|
|
||||||
const i0.VerificationMeta('description');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> description =
|
|
||||||
i0.GeneratedColumn<String>('description', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
|
||||||
static const i0.VerificationMeta _createdAtMeta =
|
|
||||||
const i0.VerificationMeta('createdAt');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<DateTime> createdAt =
|
|
||||||
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.dateTime,
|
|
||||||
requiredDuringInsert: false,
|
|
||||||
defaultValue: i4.currentDateAndTime);
|
|
||||||
static const i0.VerificationMeta _updatedAtMeta =
|
|
||||||
const i0.VerificationMeta('updatedAt');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<DateTime> updatedAt =
|
|
||||||
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.dateTime,
|
|
||||||
requiredDuringInsert: false,
|
|
||||||
defaultValue: i4.currentDateAndTime);
|
|
||||||
static const i0.VerificationMeta _ownerIdMeta =
|
|
||||||
const i0.VerificationMeta('ownerId');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
|
|
||||||
'owner_id', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.string,
|
|
||||||
requiredDuringInsert: true,
|
|
||||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
|
||||||
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
|
||||||
static const i0.VerificationMeta _thumbnailAssetIdMeta =
|
|
||||||
const i0.VerificationMeta('thumbnailAssetId');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> thumbnailAssetId =
|
|
||||||
i0.GeneratedColumn<String>('thumbnail_asset_id', aliasedName, true,
|
|
||||||
type: i0.DriftSqlType.string,
|
|
||||||
requiredDuringInsert: false,
|
|
||||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
|
||||||
'REFERENCES remote_asset_entity (id) ON DELETE SET NULL'));
|
|
||||||
static const i0.VerificationMeta _isActivityEnabledMeta =
|
|
||||||
const i0.VerificationMeta('isActivityEnabled');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<bool> isActivityEnabled =
|
|
||||||
i0.GeneratedColumn<bool>('is_activity_enabled', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.bool,
|
|
||||||
requiredDuringInsert: false,
|
|
||||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
|
||||||
'CHECK ("is_activity_enabled" IN (0, 1))'),
|
|
||||||
defaultValue: const i4.Constant(true));
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumnWithTypeConverter<i2.AssetOrder, int> order =
|
|
||||||
i0.GeneratedColumn<int>('order', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.int, requiredDuringInsert: true)
|
|
||||||
.withConverter<i2.AssetOrder>(
|
|
||||||
i1.$RemoteAlbumEntityTable.$converterorder);
|
|
||||||
@override
|
|
||||||
List<i0.GeneratedColumn> get $columns => [
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
createdAt,
|
|
||||||
updatedAt,
|
|
||||||
ownerId,
|
|
||||||
thumbnailAssetId,
|
|
||||||
isActivityEnabled,
|
|
||||||
order
|
|
||||||
];
|
|
||||||
@override
|
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
|
||||||
@override
|
|
||||||
String get actualTableName => $name;
|
|
||||||
static const String $name = 'remote_album_entity';
|
|
||||||
@override
|
|
||||||
i0.VerificationContext validateIntegrity(
|
|
||||||
i0.Insertable<i1.RemoteAlbumEntityData> instance,
|
|
||||||
{bool isInserting = false}) {
|
|
||||||
final context = i0.VerificationContext();
|
|
||||||
final data = instance.toColumns(true);
|
|
||||||
if (data.containsKey('id')) {
|
|
||||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_idMeta);
|
|
||||||
}
|
|
||||||
if (data.containsKey('name')) {
|
|
||||||
context.handle(
|
|
||||||
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_nameMeta);
|
|
||||||
}
|
|
||||||
if (data.containsKey('description')) {
|
|
||||||
context.handle(
|
|
||||||
_descriptionMeta,
|
|
||||||
description.isAcceptableOrUnknown(
|
|
||||||
data['description']!, _descriptionMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_descriptionMeta);
|
|
||||||
}
|
|
||||||
if (data.containsKey('created_at')) {
|
|
||||||
context.handle(_createdAtMeta,
|
|
||||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
|
||||||
}
|
|
||||||
if (data.containsKey('updated_at')) {
|
|
||||||
context.handle(_updatedAtMeta,
|
|
||||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
|
|
||||||
}
|
|
||||||
if (data.containsKey('owner_id')) {
|
|
||||||
context.handle(_ownerIdMeta,
|
|
||||||
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_ownerIdMeta);
|
|
||||||
}
|
|
||||||
if (data.containsKey('thumbnail_asset_id')) {
|
|
||||||
context.handle(
|
|
||||||
_thumbnailAssetIdMeta,
|
|
||||||
thumbnailAssetId.isAcceptableOrUnknown(
|
|
||||||
data['thumbnail_asset_id']!, _thumbnailAssetIdMeta));
|
|
||||||
}
|
|
||||||
if (data.containsKey('is_activity_enabled')) {
|
|
||||||
context.handle(
|
|
||||||
_isActivityEnabledMeta,
|
|
||||||
isActivityEnabled.isAcceptableOrUnknown(
|
|
||||||
data['is_activity_enabled']!, _isActivityEnabledMeta));
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
|
||||||
@override
|
|
||||||
i1.RemoteAlbumEntityData map(Map<String, dynamic> data,
|
|
||||||
{String? tablePrefix}) {
|
|
||||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
|
||||||
return i1.RemoteAlbumEntityData(
|
|
||||||
id: attachedDatabase.typeMapping
|
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
|
|
||||||
name: attachedDatabase.typeMapping
|
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
|
|
||||||
description: attachedDatabase.typeMapping
|
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}description'])!,
|
|
||||||
createdAt: attachedDatabase.typeMapping.read(
|
|
||||||
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
|
||||||
updatedAt: attachedDatabase.typeMapping.read(
|
|
||||||
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
|
|
||||||
ownerId: attachedDatabase.typeMapping
|
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!,
|
|
||||||
thumbnailAssetId: attachedDatabase.typeMapping.read(
|
|
||||||
i0.DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']),
|
|
||||||
isActivityEnabled: attachedDatabase.typeMapping.read(
|
|
||||||
i0.DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!,
|
|
||||||
order: i1.$RemoteAlbumEntityTable.$converterorder.fromSql(attachedDatabase
|
|
||||||
.typeMapping
|
|
||||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}order'])!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
$RemoteAlbumEntityTable createAlias(String alias) {
|
|
||||||
return $RemoteAlbumEntityTable(attachedDatabase, alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
static i0.JsonTypeConverter2<i2.AssetOrder, int, int> $converterorder =
|
|
||||||
const i0.EnumIndexConverter<i2.AssetOrder>(i2.AssetOrder.values);
|
|
||||||
@override
|
|
||||||
bool get withoutRowId => true;
|
|
||||||
@override
|
|
||||||
bool get isStrict => true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemoteAlbumEntityData extends i0.DataClass
|
|
||||||
implements i0.Insertable<i1.RemoteAlbumEntityData> {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final DateTime createdAt;
|
|
||||||
final DateTime updatedAt;
|
|
||||||
final String ownerId;
|
|
||||||
final String? thumbnailAssetId;
|
|
||||||
final bool isActivityEnabled;
|
|
||||||
final i2.AssetOrder order;
|
|
||||||
const RemoteAlbumEntityData(
|
|
||||||
{required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.description,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.updatedAt,
|
|
||||||
required this.ownerId,
|
|
||||||
this.thumbnailAssetId,
|
|
||||||
required this.isActivityEnabled,
|
|
||||||
required this.order});
|
|
||||||
@override
|
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, i0.Expression>{};
|
|
||||||
map['id'] = i0.Variable<String>(id);
|
|
||||||
map['name'] = i0.Variable<String>(name);
|
|
||||||
map['description'] = i0.Variable<String>(description);
|
|
||||||
map['created_at'] = i0.Variable<DateTime>(createdAt);
|
|
||||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
|
||||||
map['owner_id'] = i0.Variable<String>(ownerId);
|
|
||||||
if (!nullToAbsent || thumbnailAssetId != null) {
|
|
||||||
map['thumbnail_asset_id'] = i0.Variable<String>(thumbnailAssetId);
|
|
||||||
}
|
|
||||||
map['is_activity_enabled'] = i0.Variable<bool>(isActivityEnabled);
|
|
||||||
{
|
|
||||||
map['order'] = i0.Variable<int>(
|
|
||||||
i1.$RemoteAlbumEntityTable.$converterorder.toSql(order));
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory RemoteAlbumEntityData.fromJson(Map<String, dynamic> json,
|
|
||||||
{i0.ValueSerializer? serializer}) {
|
|
||||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
|
||||||
return RemoteAlbumEntityData(
|
|
||||||
id: serializer.fromJson<String>(json['id']),
|
|
||||||
name: serializer.fromJson<String>(json['name']),
|
|
||||||
description: serializer.fromJson<String>(json['description']),
|
|
||||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
|
||||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
|
||||||
ownerId: serializer.fromJson<String>(json['ownerId']),
|
|
||||||
thumbnailAssetId: serializer.fromJson<String?>(json['thumbnailAssetId']),
|
|
||||||
isActivityEnabled: serializer.fromJson<bool>(json['isActivityEnabled']),
|
|
||||||
order: i1.$RemoteAlbumEntityTable.$converterorder
|
|
||||||
.fromJson(serializer.fromJson<int>(json['order'])),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
|
||||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
|
||||||
return <String, dynamic>{
|
|
||||||
'id': serializer.toJson<String>(id),
|
|
||||||
'name': serializer.toJson<String>(name),
|
|
||||||
'description': serializer.toJson<String>(description),
|
|
||||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
|
||||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
|
||||||
'ownerId': serializer.toJson<String>(ownerId),
|
|
||||||
'thumbnailAssetId': serializer.toJson<String?>(thumbnailAssetId),
|
|
||||||
'isActivityEnabled': serializer.toJson<bool>(isActivityEnabled),
|
|
||||||
'order': serializer.toJson<int>(
|
|
||||||
i1.$RemoteAlbumEntityTable.$converterorder.toJson(order)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
i1.RemoteAlbumEntityData copyWith(
|
|
||||||
{String? id,
|
|
||||||
String? name,
|
|
||||||
String? description,
|
|
||||||
DateTime? createdAt,
|
|
||||||
DateTime? updatedAt,
|
|
||||||
String? ownerId,
|
|
||||||
i0.Value<String?> thumbnailAssetId = const i0.Value.absent(),
|
|
||||||
bool? isActivityEnabled,
|
|
||||||
i2.AssetOrder? order}) =>
|
|
||||||
i1.RemoteAlbumEntityData(
|
|
||||||
id: id ?? this.id,
|
|
||||||
name: name ?? this.name,
|
|
||||||
description: description ?? this.description,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
|
||||||
ownerId: ownerId ?? this.ownerId,
|
|
||||||
thumbnailAssetId: thumbnailAssetId.present
|
|
||||||
? thumbnailAssetId.value
|
|
||||||
: this.thumbnailAssetId,
|
|
||||||
isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
|
|
||||||
order: order ?? this.order,
|
|
||||||
);
|
|
||||||
RemoteAlbumEntityData copyWithCompanion(i1.RemoteAlbumEntityCompanion data) {
|
|
||||||
return RemoteAlbumEntityData(
|
|
||||||
id: data.id.present ? data.id.value : this.id,
|
|
||||||
name: data.name.present ? data.name.value : this.name,
|
|
||||||
description:
|
|
||||||
data.description.present ? data.description.value : this.description,
|
|
||||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
|
||||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
|
||||||
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
|
|
||||||
thumbnailAssetId: data.thumbnailAssetId.present
|
|
||||||
? data.thumbnailAssetId.value
|
|
||||||
: this.thumbnailAssetId,
|
|
||||||
isActivityEnabled: data.isActivityEnabled.present
|
|
||||||
? data.isActivityEnabled.value
|
|
||||||
: this.isActivityEnabled,
|
|
||||||
order: data.order.present ? data.order.value : this.order,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('RemoteAlbumEntityData(')
|
|
||||||
..write('id: $id, ')
|
|
||||||
..write('name: $name, ')
|
|
||||||
..write('description: $description, ')
|
|
||||||
..write('createdAt: $createdAt, ')
|
|
||||||
..write('updatedAt: $updatedAt, ')
|
|
||||||
..write('ownerId: $ownerId, ')
|
|
||||||
..write('thumbnailAssetId: $thumbnailAssetId, ')
|
|
||||||
..write('isActivityEnabled: $isActivityEnabled, ')
|
|
||||||
..write('order: $order')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(id, name, description, createdAt, updatedAt,
|
|
||||||
ownerId, thumbnailAssetId, isActivityEnabled, order);
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is i1.RemoteAlbumEntityData &&
|
|
||||||
other.id == this.id &&
|
|
||||||
other.name == this.name &&
|
|
||||||
other.description == this.description &&
|
|
||||||
other.createdAt == this.createdAt &&
|
|
||||||
other.updatedAt == this.updatedAt &&
|
|
||||||
other.ownerId == this.ownerId &&
|
|
||||||
other.thumbnailAssetId == this.thumbnailAssetId &&
|
|
||||||
other.isActivityEnabled == this.isActivityEnabled &&
|
|
||||||
other.order == this.order);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemoteAlbumEntityCompanion
|
|
||||||
extends i0.UpdateCompanion<i1.RemoteAlbumEntityData> {
|
|
||||||
final i0.Value<String> id;
|
|
||||||
final i0.Value<String> name;
|
|
||||||
final i0.Value<String> description;
|
|
||||||
final i0.Value<DateTime> createdAt;
|
|
||||||
final i0.Value<DateTime> updatedAt;
|
|
||||||
final i0.Value<String> ownerId;
|
|
||||||
final i0.Value<String?> thumbnailAssetId;
|
|
||||||
final i0.Value<bool> isActivityEnabled;
|
|
||||||
final i0.Value<i2.AssetOrder> order;
|
|
||||||
const RemoteAlbumEntityCompanion({
|
|
||||||
this.id = const i0.Value.absent(),
|
|
||||||
this.name = const i0.Value.absent(),
|
|
||||||
this.description = const i0.Value.absent(),
|
|
||||||
this.createdAt = const i0.Value.absent(),
|
|
||||||
this.updatedAt = const i0.Value.absent(),
|
|
||||||
this.ownerId = const i0.Value.absent(),
|
|
||||||
this.thumbnailAssetId = const i0.Value.absent(),
|
|
||||||
this.isActivityEnabled = const i0.Value.absent(),
|
|
||||||
this.order = const i0.Value.absent(),
|
|
||||||
});
|
|
||||||
RemoteAlbumEntityCompanion.insert({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String description,
|
|
||||||
this.createdAt = const i0.Value.absent(),
|
|
||||||
this.updatedAt = const i0.Value.absent(),
|
|
||||||
required String ownerId,
|
|
||||||
this.thumbnailAssetId = const i0.Value.absent(),
|
|
||||||
this.isActivityEnabled = const i0.Value.absent(),
|
|
||||||
required i2.AssetOrder order,
|
|
||||||
}) : id = i0.Value(id),
|
|
||||||
name = i0.Value(name),
|
|
||||||
description = i0.Value(description),
|
|
||||||
ownerId = i0.Value(ownerId),
|
|
||||||
order = i0.Value(order);
|
|
||||||
static i0.Insertable<i1.RemoteAlbumEntityData> custom({
|
|
||||||
i0.Expression<String>? id,
|
|
||||||
i0.Expression<String>? name,
|
|
||||||
i0.Expression<String>? description,
|
|
||||||
i0.Expression<DateTime>? createdAt,
|
|
||||||
i0.Expression<DateTime>? updatedAt,
|
|
||||||
i0.Expression<String>? ownerId,
|
|
||||||
i0.Expression<String>? thumbnailAssetId,
|
|
||||||
i0.Expression<bool>? isActivityEnabled,
|
|
||||||
i0.Expression<int>? order,
|
|
||||||
}) {
|
|
||||||
return i0.RawValuesInsertable({
|
|
||||||
if (id != null) 'id': id,
|
|
||||||
if (name != null) 'name': name,
|
|
||||||
if (description != null) 'description': description,
|
|
||||||
if (createdAt != null) 'created_at': createdAt,
|
|
||||||
if (updatedAt != null) 'updated_at': updatedAt,
|
|
||||||
if (ownerId != null) 'owner_id': ownerId,
|
|
||||||
if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId,
|
|
||||||
if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled,
|
|
||||||
if (order != null) 'order': order,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
i1.RemoteAlbumEntityCompanion copyWith(
|
|
||||||
{i0.Value<String>? id,
|
|
||||||
i0.Value<String>? name,
|
|
||||||
i0.Value<String>? description,
|
|
||||||
i0.Value<DateTime>? createdAt,
|
|
||||||
i0.Value<DateTime>? updatedAt,
|
|
||||||
i0.Value<String>? ownerId,
|
|
||||||
i0.Value<String?>? thumbnailAssetId,
|
|
||||||
i0.Value<bool>? isActivityEnabled,
|
|
||||||
i0.Value<i2.AssetOrder>? order}) {
|
|
||||||
return i1.RemoteAlbumEntityCompanion(
|
|
||||||
id: id ?? this.id,
|
|
||||||
name: name ?? this.name,
|
|
||||||
description: description ?? this.description,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
|
||||||
ownerId: ownerId ?? this.ownerId,
|
|
||||||
thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
|
|
||||||
isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
|
|
||||||
order: order ?? this.order,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, i0.Expression>{};
|
|
||||||
if (id.present) {
|
|
||||||
map['id'] = i0.Variable<String>(id.value);
|
|
||||||
}
|
|
||||||
if (name.present) {
|
|
||||||
map['name'] = i0.Variable<String>(name.value);
|
|
||||||
}
|
|
||||||
if (description.present) {
|
|
||||||
map['description'] = i0.Variable<String>(description.value);
|
|
||||||
}
|
|
||||||
if (createdAt.present) {
|
|
||||||
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
|
|
||||||
}
|
|
||||||
if (updatedAt.present) {
|
|
||||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
|
||||||
}
|
|
||||||
if (ownerId.present) {
|
|
||||||
map['owner_id'] = i0.Variable<String>(ownerId.value);
|
|
||||||
}
|
|
||||||
if (thumbnailAssetId.present) {
|
|
||||||
map['thumbnail_asset_id'] = i0.Variable<String>(thumbnailAssetId.value);
|
|
||||||
}
|
|
||||||
if (isActivityEnabled.present) {
|
|
||||||
map['is_activity_enabled'] = i0.Variable<bool>(isActivityEnabled.value);
|
|
||||||
}
|
|
||||||
if (order.present) {
|
|
||||||
map['order'] = i0.Variable<int>(
|
|
||||||
i1.$RemoteAlbumEntityTable.$converterorder.toSql(order.value));
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('RemoteAlbumEntityCompanion(')
|
|
||||||
..write('id: $id, ')
|
|
||||||
..write('name: $name, ')
|
|
||||||
..write('description: $description, ')
|
|
||||||
..write('createdAt: $createdAt, ')
|
|
||||||
..write('updatedAt: $updatedAt, ')
|
|
||||||
..write('ownerId: $ownerId, ')
|
|
||||||
..write('thumbnailAssetId: $thumbnailAssetId, ')
|
|
||||||
..write('isActivityEnabled: $isActivityEnabled, ')
|
|
||||||
..write('order: $order')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
|
||||||
|
|
||||||
class RemoteAlbumAssetEntity extends Table with DriftDefaultsMixin {
|
|
||||||
const RemoteAlbumAssetEntity();
|
|
||||||
|
|
||||||
TextColumn get assetId =>
|
|
||||||
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
|
|
||||||
|
|
||||||
TextColumn get albumId =>
|
|
||||||
text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Set<Column> get primaryKey => {assetId, albumId};
|
|
||||||
}
|
|
||||||
@@ -1,565 +0,0 @@
|
|||||||
// dart format width=80
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
import 'package:drift/drift.dart' as i0;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
|
||||||
as i1;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'
|
|
||||||
as i2;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
|
||||||
as i3;
|
|
||||||
import 'package:drift/internal/modular.dart' as i4;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
|
||||||
as i5;
|
|
||||||
|
|
||||||
typedef $$RemoteAlbumAssetEntityTableCreateCompanionBuilder
|
|
||||||
= i1.RemoteAlbumAssetEntityCompanion Function({
|
|
||||||
required String assetId,
|
|
||||||
required String albumId,
|
|
||||||
});
|
|
||||||
typedef $$RemoteAlbumAssetEntityTableUpdateCompanionBuilder
|
|
||||||
= i1.RemoteAlbumAssetEntityCompanion Function({
|
|
||||||
i0.Value<String> assetId,
|
|
||||||
i0.Value<String> albumId,
|
|
||||||
});
|
|
||||||
|
|
||||||
final class $$RemoteAlbumAssetEntityTableReferences extends i0.BaseReferences<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$RemoteAlbumAssetEntityTable,
|
|
||||||
i1.RemoteAlbumAssetEntityData> {
|
|
||||||
$$RemoteAlbumAssetEntityTableReferences(
|
|
||||||
super.$_db, super.$_table, super.$_typedResult);
|
|
||||||
|
|
||||||
static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
|
||||||
i4.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
|
|
||||||
.createAlias(i0.$_aliasNameGenerator(
|
|
||||||
i4.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i1.$RemoteAlbumAssetEntityTable>(
|
|
||||||
'remote_album_asset_entity')
|
|
||||||
.assetId,
|
|
||||||
i4.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
|
|
||||||
.id));
|
|
||||||
|
|
||||||
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
|
||||||
final $_column = $_itemColumn<String>('asset_id')!;
|
|
||||||
|
|
||||||
final manager = i3
|
|
||||||
.$$RemoteAssetEntityTableTableManager(
|
|
||||||
$_db,
|
|
||||||
i4.ReadDatabaseContainer($_db)
|
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'))
|
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
|
||||||
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
|
||||||
if (item == null) return manager;
|
|
||||||
return i0.ProcessedTableManager(
|
|
||||||
manager.$state.copyWith(prefetchedData: [item]));
|
|
||||||
}
|
|
||||||
|
|
||||||
static i5.$RemoteAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) =>
|
|
||||||
i4.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity')
|
|
||||||
.createAlias(i0.$_aliasNameGenerator(
|
|
||||||
i4.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i1.$RemoteAlbumAssetEntityTable>(
|
|
||||||
'remote_album_asset_entity')
|
|
||||||
.albumId,
|
|
||||||
i4.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity')
|
|
||||||
.id));
|
|
||||||
|
|
||||||
i5.$$RemoteAlbumEntityTableProcessedTableManager get albumId {
|
|
||||||
final $_column = $_itemColumn<String>('album_id')!;
|
|
||||||
|
|
||||||
final manager = i5
|
|
||||||
.$$RemoteAlbumEntityTableTableManager(
|
|
||||||
$_db,
|
|
||||||
i4.ReadDatabaseContainer($_db)
|
|
||||||
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'))
|
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
|
||||||
final item = $_typedResult.readTableOrNull(_albumIdTable($_db));
|
|
||||||
if (item == null) return manager;
|
|
||||||
return i0.ProcessedTableManager(
|
|
||||||
manager.$state.copyWith(prefetchedData: [item]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$RemoteAlbumAssetEntityTableFilterComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$RemoteAlbumAssetEntityTable> {
|
|
||||||
$$RemoteAlbumAssetEntityTableFilterComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i3.$$RemoteAssetEntityTableFilterComposer get assetId {
|
|
||||||
final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.assetId,
|
|
||||||
referencedTable: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i3.$$RemoteAssetEntityTableFilterComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i5.$$RemoteAlbumEntityTableFilterComposer get albumId {
|
|
||||||
final i5.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.albumId,
|
|
||||||
referencedTable: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i5.$$RemoteAlbumEntityTableFilterComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$RemoteAlbumAssetEntityTableOrderingComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$RemoteAlbumAssetEntityTable> {
|
|
||||||
$$RemoteAlbumAssetEntityTableOrderingComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i3.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
|
||||||
final i3.$$RemoteAssetEntityTableOrderingComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.assetId,
|
|
||||||
referencedTable: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i3.$$RemoteAssetEntityTableOrderingComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>(
|
|
||||||
'remote_asset_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i5.$$RemoteAlbumEntityTableOrderingComposer get albumId {
|
|
||||||
final i5.$$RemoteAlbumEntityTableOrderingComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.albumId,
|
|
||||||
referencedTable: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i5.$$RemoteAlbumEntityTableOrderingComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$RemoteAlbumEntityTable>(
|
|
||||||
'remote_album_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$RemoteAlbumAssetEntityTableAnnotationComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$RemoteAlbumAssetEntityTable> {
|
|
||||||
$$RemoteAlbumAssetEntityTableAnnotationComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i3.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
|
||||||
final i3.$$RemoteAssetEntityTableAnnotationComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.assetId,
|
|
||||||
referencedTable: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i3.$$RemoteAssetEntityTableAnnotationComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>(
|
|
||||||
'remote_asset_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i5.$$RemoteAlbumEntityTableAnnotationComposer get albumId {
|
|
||||||
final i5.$$RemoteAlbumEntityTableAnnotationComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.albumId,
|
|
||||||
referencedTable: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i5.$$RemoteAlbumEntityTableAnnotationComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i4.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i5.$RemoteAlbumEntityTable>(
|
|
||||||
'remote_album_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$RemoteAlbumAssetEntityTableTableManager extends i0.RootTableManager<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$RemoteAlbumAssetEntityTable,
|
|
||||||
i1.RemoteAlbumAssetEntityData,
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableFilterComposer,
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableOrderingComposer,
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableAnnotationComposer,
|
|
||||||
$$RemoteAlbumAssetEntityTableCreateCompanionBuilder,
|
|
||||||
$$RemoteAlbumAssetEntityTableUpdateCompanionBuilder,
|
|
||||||
(i1.RemoteAlbumAssetEntityData, i1.$$RemoteAlbumAssetEntityTableReferences),
|
|
||||||
i1.RemoteAlbumAssetEntityData,
|
|
||||||
i0.PrefetchHooks Function({bool assetId, bool albumId})> {
|
|
||||||
$$RemoteAlbumAssetEntityTableTableManager(
|
|
||||||
i0.GeneratedDatabase db, i1.$RemoteAlbumAssetEntityTable table)
|
|
||||||
: super(i0.TableManagerState(
|
|
||||||
db: db,
|
|
||||||
table: table,
|
|
||||||
createFilteringComposer: () =>
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableFilterComposer(
|
|
||||||
$db: db, $table: table),
|
|
||||||
createOrderingComposer: () =>
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableOrderingComposer(
|
|
||||||
$db: db, $table: table),
|
|
||||||
createComputedFieldComposer: () =>
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableAnnotationComposer(
|
|
||||||
$db: db, $table: table),
|
|
||||||
updateCompanionCallback: ({
|
|
||||||
i0.Value<String> assetId = const i0.Value.absent(),
|
|
||||||
i0.Value<String> albumId = const i0.Value.absent(),
|
|
||||||
}) =>
|
|
||||||
i1.RemoteAlbumAssetEntityCompanion(
|
|
||||||
assetId: assetId,
|
|
||||||
albumId: albumId,
|
|
||||||
),
|
|
||||||
createCompanionCallback: ({
|
|
||||||
required String assetId,
|
|
||||||
required String albumId,
|
|
||||||
}) =>
|
|
||||||
i1.RemoteAlbumAssetEntityCompanion.insert(
|
|
||||||
assetId: assetId,
|
|
||||||
albumId: albumId,
|
|
||||||
),
|
|
||||||
withReferenceMapper: (p0) => p0
|
|
||||||
.map((e) => (
|
|
||||||
e.readTable(table),
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableReferences(db, table, e)
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
prefetchHooksCallback: ({assetId = false, albumId = false}) {
|
|
||||||
return i0.PrefetchHooks(
|
|
||||||
db: db,
|
|
||||||
explicitlyWatchedTables: [],
|
|
||||||
addJoins: <
|
|
||||||
T extends i0.TableManagerState<
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic,
|
|
||||||
dynamic>>(state) {
|
|
||||||
if (assetId) {
|
|
||||||
state = state.withJoin(
|
|
||||||
currentTable: table,
|
|
||||||
currentColumn: table.assetId,
|
|
||||||
referencedTable: i1.$$RemoteAlbumAssetEntityTableReferences
|
|
||||||
._assetIdTable(db),
|
|
||||||
referencedColumn: i1.$$RemoteAlbumAssetEntityTableReferences
|
|
||||||
._assetIdTable(db)
|
|
||||||
.id,
|
|
||||||
) as T;
|
|
||||||
}
|
|
||||||
if (albumId) {
|
|
||||||
state = state.withJoin(
|
|
||||||
currentTable: table,
|
|
||||||
currentColumn: table.albumId,
|
|
||||||
referencedTable: i1.$$RemoteAlbumAssetEntityTableReferences
|
|
||||||
._albumIdTable(db),
|
|
||||||
referencedColumn: i1.$$RemoteAlbumAssetEntityTableReferences
|
|
||||||
._albumIdTable(db)
|
|
||||||
.id,
|
|
||||||
) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
getPrefetchedDataCallback: (items) async {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef $$RemoteAlbumAssetEntityTableProcessedTableManager
|
|
||||||
= i0.ProcessedTableManager<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$RemoteAlbumAssetEntityTable,
|
|
||||||
i1.RemoteAlbumAssetEntityData,
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableFilterComposer,
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableOrderingComposer,
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableAnnotationComposer,
|
|
||||||
$$RemoteAlbumAssetEntityTableCreateCompanionBuilder,
|
|
||||||
$$RemoteAlbumAssetEntityTableUpdateCompanionBuilder,
|
|
||||||
(
|
|
||||||
i1.RemoteAlbumAssetEntityData,
|
|
||||||
i1.$$RemoteAlbumAssetEntityTableReferences
|
|
||||||
),
|
|
||||||
i1.RemoteAlbumAssetEntityData,
|
|
||||||
i0.PrefetchHooks Function({bool assetId, bool albumId})>;
|
|
||||||
|
|
||||||
class $RemoteAlbumAssetEntityTable extends i2.RemoteAlbumAssetEntity
|
|
||||||
with
|
|
||||||
i0.TableInfo<$RemoteAlbumAssetEntityTable,
|
|
||||||
i1.RemoteAlbumAssetEntityData> {
|
|
||||||
@override
|
|
||||||
final i0.GeneratedDatabase attachedDatabase;
|
|
||||||
final String? _alias;
|
|
||||||
$RemoteAlbumAssetEntityTable(this.attachedDatabase, [this._alias]);
|
|
||||||
static const i0.VerificationMeta _assetIdMeta =
|
|
||||||
const i0.VerificationMeta('assetId');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
|
|
||||||
'asset_id', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.string,
|
|
||||||
requiredDuringInsert: true,
|
|
||||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
|
||||||
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE'));
|
|
||||||
static const i0.VerificationMeta _albumIdMeta =
|
|
||||||
const i0.VerificationMeta('albumId');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> albumId = i0.GeneratedColumn<String>(
|
|
||||||
'album_id', aliasedName, false,
|
|
||||||
type: i0.DriftSqlType.string,
|
|
||||||
requiredDuringInsert: true,
|
|
||||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
|
||||||
'REFERENCES remote_album_entity (id) ON DELETE CASCADE'));
|
|
||||||
@override
|
|
||||||
List<i0.GeneratedColumn> get $columns => [assetId, albumId];
|
|
||||||
@override
|
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
|
||||||
@override
|
|
||||||
String get actualTableName => $name;
|
|
||||||
static const String $name = 'remote_album_asset_entity';
|
|
||||||
@override
|
|
||||||
i0.VerificationContext validateIntegrity(
|
|
||||||
i0.Insertable<i1.RemoteAlbumAssetEntityData> instance,
|
|
||||||
{bool isInserting = false}) {
|
|
||||||
final context = i0.VerificationContext();
|
|
||||||
final data = instance.toColumns(true);
|
|
||||||
if (data.containsKey('asset_id')) {
|
|
||||||
context.handle(_assetIdMeta,
|
|
||||||
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_assetIdMeta);
|
|
||||||
}
|
|
||||||
if (data.containsKey('album_id')) {
|
|
||||||
context.handle(_albumIdMeta,
|
|
||||||
albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_albumIdMeta);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Set<i0.GeneratedColumn> get $primaryKey => {assetId, albumId};
|
|
||||||
@override
|
|
||||||
i1.RemoteAlbumAssetEntityData map(Map<String, dynamic> data,
|
|
||||||
{String? tablePrefix}) {
|
|
||||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
|
||||||
return i1.RemoteAlbumAssetEntityData(
|
|
||||||
assetId: attachedDatabase.typeMapping
|
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!,
|
|
||||||
albumId: attachedDatabase.typeMapping
|
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
$RemoteAlbumAssetEntityTable createAlias(String alias) {
|
|
||||||
return $RemoteAlbumAssetEntityTable(attachedDatabase, alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get withoutRowId => true;
|
|
||||||
@override
|
|
||||||
bool get isStrict => true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemoteAlbumAssetEntityData extends i0.DataClass
|
|
||||||
implements i0.Insertable<i1.RemoteAlbumAssetEntityData> {
|
|
||||||
final String assetId;
|
|
||||||
final String albumId;
|
|
||||||
const RemoteAlbumAssetEntityData(
|
|
||||||
{required this.assetId, required this.albumId});
|
|
||||||
@override
|
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, i0.Expression>{};
|
|
||||||
map['asset_id'] = i0.Variable<String>(assetId);
|
|
||||||
map['album_id'] = i0.Variable<String>(albumId);
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory RemoteAlbumAssetEntityData.fromJson(Map<String, dynamic> json,
|
|
||||||
{i0.ValueSerializer? serializer}) {
|
|
||||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
|
||||||
return RemoteAlbumAssetEntityData(
|
|
||||||
assetId: serializer.fromJson<String>(json['assetId']),
|
|
||||||
albumId: serializer.fromJson<String>(json['albumId']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
|
||||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
|
||||||
return <String, dynamic>{
|
|
||||||
'assetId': serializer.toJson<String>(assetId),
|
|
||||||
'albumId': serializer.toJson<String>(albumId),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
i1.RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) =>
|
|
||||||
i1.RemoteAlbumAssetEntityData(
|
|
||||||
assetId: assetId ?? this.assetId,
|
|
||||||
albumId: albumId ?? this.albumId,
|
|
||||||
);
|
|
||||||
RemoteAlbumAssetEntityData copyWithCompanion(
|
|
||||||
i1.RemoteAlbumAssetEntityCompanion data) {
|
|
||||||
return RemoteAlbumAssetEntityData(
|
|
||||||
assetId: data.assetId.present ? data.assetId.value : this.assetId,
|
|
||||||
albumId: data.albumId.present ? data.albumId.value : this.albumId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('RemoteAlbumAssetEntityData(')
|
|
||||||
..write('assetId: $assetId, ')
|
|
||||||
..write('albumId: $albumId')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(assetId, albumId);
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is i1.RemoteAlbumAssetEntityData &&
|
|
||||||
other.assetId == this.assetId &&
|
|
||||||
other.albumId == this.albumId);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemoteAlbumAssetEntityCompanion
|
|
||||||
extends i0.UpdateCompanion<i1.RemoteAlbumAssetEntityData> {
|
|
||||||
final i0.Value<String> assetId;
|
|
||||||
final i0.Value<String> albumId;
|
|
||||||
const RemoteAlbumAssetEntityCompanion({
|
|
||||||
this.assetId = const i0.Value.absent(),
|
|
||||||
this.albumId = const i0.Value.absent(),
|
|
||||||
});
|
|
||||||
RemoteAlbumAssetEntityCompanion.insert({
|
|
||||||
required String assetId,
|
|
||||||
required String albumId,
|
|
||||||
}) : assetId = i0.Value(assetId),
|
|
||||||
albumId = i0.Value(albumId);
|
|
||||||
static i0.Insertable<i1.RemoteAlbumAssetEntityData> custom({
|
|
||||||
i0.Expression<String>? assetId,
|
|
||||||
i0.Expression<String>? albumId,
|
|
||||||
}) {
|
|
||||||
return i0.RawValuesInsertable({
|
|
||||||
if (assetId != null) 'asset_id': assetId,
|
|
||||||
if (albumId != null) 'album_id': albumId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
i1.RemoteAlbumAssetEntityCompanion copyWith(
|
|
||||||
{i0.Value<String>? assetId, i0.Value<String>? albumId}) {
|
|
||||||
return i1.RemoteAlbumAssetEntityCompanion(
|
|
||||||
assetId: assetId ?? this.assetId,
|
|
||||||
albumId: albumId ?? this.albumId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, i0.Expression>{};
|
|
||||||
if (assetId.present) {
|
|
||||||
map['asset_id'] = i0.Variable<String>(assetId.value);
|
|
||||||
}
|
|
||||||
if (albumId.present) {
|
|
||||||
map['album_id'] = i0.Variable<String>(albumId.value);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('RemoteAlbumAssetEntityCompanion(')
|
|
||||||
..write('assetId: $assetId, ')
|
|
||||||
..write('albumId: $albumId')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
|||||||
columns: {#checksum, #ownerId},
|
columns: {#checksum, #ownerId},
|
||||||
unique: true,
|
unique: true,
|
||||||
)
|
)
|
||||||
|
@TableIndex(name: 'idx_remote_asset_checksum', columns: {#checksum})
|
||||||
class RemoteAssetEntity extends Table
|
class RemoteAssetEntity extends Table
|
||||||
with DriftDefaultsMixin, AssetEntityMixin {
|
with DriftDefaultsMixin, AssetEntityMixin {
|
||||||
const RemoteAssetEntity();
|
const RemoteAssetEntity();
|
||||||
|
|||||||
@@ -1178,3 +1178,6 @@ class RemoteAssetEntityCompanion
|
|||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i0.Index get idxRemoteAssetChecksum => i0.Index('idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)');
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/domain/interfaces/asset_media.interface.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class AssetMediaRepository implements IAssetMediaRepository {
|
||||||
|
const AssetMediaRepository();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List?> getThumbnail(
|
||||||
|
String id, {
|
||||||
|
int quality = 80,
|
||||||
|
Size size = const Size.square(256),
|
||||||
|
}) =>
|
||||||
|
AssetEntity(
|
||||||
|
id: id,
|
||||||
|
// The below fields are not used in thumbnailDataWithSize but are required
|
||||||
|
// to create an AssetEntity instance. It is faster to create a dummy AssetEntity
|
||||||
|
// instance than to fetch the asset from the device first.
|
||||||
|
typeInt: AssetType.image.index,
|
||||||
|
width: size.width.toInt(),
|
||||||
|
height: size.height.toInt(),
|
||||||
|
).thumbnailDataWithSize(
|
||||||
|
ThumbnailSize(size.width.toInt(), size.height.toInt()),
|
||||||
|
quality: quality,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,15 +3,12 @@ import 'dart:async';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift_flutter/drift_flutter.dart';
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/album_user.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
@@ -41,12 +38,12 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
|||||||
LocalAlbumEntity,
|
LocalAlbumEntity,
|
||||||
LocalAssetEntity,
|
LocalAssetEntity,
|
||||||
LocalAlbumAssetEntity,
|
LocalAlbumAssetEntity,
|
||||||
RemoteExifEntity,
|
|
||||||
RemoteAssetEntity,
|
RemoteAssetEntity,
|
||||||
RemoteAlbumEntity,
|
RemoteExifEntity,
|
||||||
RemoteAlbumAssetEntity,
|
|
||||||
AlbumUserEntity,
|
|
||||||
],
|
],
|
||||||
|
include: {
|
||||||
|
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||||
|
},
|
||||||
)
|
)
|
||||||
class Drift extends $Drift implements IDatabaseRepository {
|
class Drift extends $Drift implements IDatabaseRepository {
|
||||||
Drift([QueryExecutor? executor])
|
Drift([QueryExecutor? executor])
|
||||||
|
|||||||
+50
-110
@@ -3,74 +3,72 @@
|
|||||||
import 'package:drift/drift.dart' as i0;
|
import 'package:drift/drift.dart' as i0;
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||||
as i1;
|
as i1;
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
|
||||||
as i2;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
|
||||||
as i3;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
|
||||||
as i4;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
|
||||||
as i5;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
|
||||||
as i6;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||||
|
as i2;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||||
|
as i3;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||||
|
as i4;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||||
|
as i5;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||||
|
as i6;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||||
as i7;
|
as i7;
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||||
as i8;
|
as i8;
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||||
as i9;
|
as i9;
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
import 'package:drift/internal/modular.dart' as i10;
|
||||||
as i10;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/album_user.entity.drift.dart'
|
|
||||||
as i11;
|
|
||||||
|
|
||||||
abstract class $Drift extends i0.GeneratedDatabase {
|
abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
$Drift(i0.QueryExecutor e) : super(e);
|
$Drift(i0.QueryExecutor e) : super(e);
|
||||||
$DriftManager get managers => $DriftManager(this);
|
$DriftManager get managers => $DriftManager(this);
|
||||||
late final i1.$UserEntityTable userEntity = i1.$UserEntityTable(this);
|
late final i1.$UserEntityTable userEntity = i1.$UserEntityTable(this);
|
||||||
late final i2.$UserMetadataEntityTable userMetadataEntity =
|
late final i2.$RemoteAssetEntityTable remoteAssetEntity =
|
||||||
i2.$UserMetadataEntityTable(this);
|
i2.$RemoteAssetEntityTable(this);
|
||||||
late final i3.$PartnerEntityTable partnerEntity =
|
late final i3.$LocalAssetEntityTable localAssetEntity =
|
||||||
i3.$PartnerEntityTable(this);
|
i3.$LocalAssetEntityTable(this);
|
||||||
late final i4.$LocalAlbumEntityTable localAlbumEntity =
|
late final i4.$UserMetadataEntityTable userMetadataEntity =
|
||||||
i4.$LocalAlbumEntityTable(this);
|
i4.$UserMetadataEntityTable(this);
|
||||||
late final i5.$LocalAssetEntityTable localAssetEntity =
|
late final i5.$PartnerEntityTable partnerEntity =
|
||||||
i5.$LocalAssetEntityTable(this);
|
i5.$PartnerEntityTable(this);
|
||||||
late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
|
late final i6.$LocalAlbumEntityTable localAlbumEntity =
|
||||||
i6.$LocalAlbumAssetEntityTable(this);
|
i6.$LocalAlbumEntityTable(this);
|
||||||
late final i7.$RemoteAssetEntityTable remoteAssetEntity =
|
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
|
||||||
i7.$RemoteAssetEntityTable(this);
|
i7.$LocalAlbumAssetEntityTable(this);
|
||||||
late final i8.$RemoteExifEntityTable remoteExifEntity =
|
late final i8.$RemoteExifEntityTable remoteExifEntity =
|
||||||
i8.$RemoteExifEntityTable(this);
|
i8.$RemoteExifEntityTable(this);
|
||||||
late final i9.$RemoteAlbumEntityTable remoteAlbumEntity =
|
i9.MergedAssetDrift get mergedAssetDrift => i10.ReadDatabaseContainer(this)
|
||||||
i9.$RemoteAlbumEntityTable(this);
|
.accessor<i9.MergedAssetDrift>(i9.MergedAssetDrift.new);
|
||||||
late final i10.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity =
|
|
||||||
i10.$RemoteAlbumAssetEntityTable(this);
|
|
||||||
late final i11.$AlbumUserEntityTable albumUserEntity =
|
|
||||||
i11.$AlbumUserEntityTable(this);
|
|
||||||
@override
|
@override
|
||||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||||
@override
|
@override
|
||||||
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
|
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
|
||||||
userEntity,
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
i3.idxLocalAssetChecksum,
|
||||||
|
i2.uQRemoteAssetOwnerChecksum,
|
||||||
|
i2.idxRemoteAssetChecksum,
|
||||||
userMetadataEntity,
|
userMetadataEntity,
|
||||||
partnerEntity,
|
partnerEntity,
|
||||||
localAlbumEntity,
|
localAlbumEntity,
|
||||||
localAssetEntity,
|
|
||||||
localAlbumAssetEntity,
|
localAlbumAssetEntity,
|
||||||
remoteAssetEntity,
|
remoteExifEntity
|
||||||
remoteExifEntity,
|
|
||||||
remoteAlbumEntity,
|
|
||||||
remoteAlbumAssetEntity,
|
|
||||||
albumUserEntity,
|
|
||||||
i5.idxLocalAssetChecksum,
|
|
||||||
i7.uQRemoteAssetOwnerChecksum
|
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
||||||
const i0.StreamQueryUpdateRules(
|
const i0.StreamQueryUpdateRules(
|
||||||
[
|
[
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
|
||||||
|
],
|
||||||
|
),
|
||||||
i0.WritePropagation(
|
i0.WritePropagation(
|
||||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
@@ -109,13 +107,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
kind: i0.UpdateKind.delete),
|
kind: i0.UpdateKind.delete),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
i0.WritePropagation(
|
|
||||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
|
||||||
result: [
|
|
||||||
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
i0.WritePropagation(
|
i0.WritePropagation(
|
||||||
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
|
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
@@ -123,50 +114,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete),
|
i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
i0.WritePropagation(
|
|
||||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
|
||||||
result: [
|
|
||||||
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
i0.WritePropagation(
|
|
||||||
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
|
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
|
||||||
result: [
|
|
||||||
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
i0.WritePropagation(
|
|
||||||
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
|
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
|
||||||
result: [
|
|
||||||
i0.TableUpdate('remote_album_asset_entity',
|
|
||||||
kind: i0.UpdateKind.delete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
i0.WritePropagation(
|
|
||||||
on: i0.TableUpdateQuery.onTableName('remote_album_entity',
|
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
|
||||||
result: [
|
|
||||||
i0.TableUpdate('remote_album_asset_entity',
|
|
||||||
kind: i0.UpdateKind.delete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
i0.WritePropagation(
|
|
||||||
on: i0.TableUpdateQuery.onTableName('remote_album_entity',
|
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
|
||||||
result: [
|
|
||||||
i0.TableUpdate('album_user_entity', kind: i0.UpdateKind.delete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
i0.WritePropagation(
|
|
||||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
|
||||||
result: [
|
|
||||||
i0.TableUpdate('album_user_entity', kind: i0.UpdateKind.delete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
@@ -179,25 +126,18 @@ class $DriftManager {
|
|||||||
$DriftManager(this._db);
|
$DriftManager(this._db);
|
||||||
i1.$$UserEntityTableTableManager get userEntity =>
|
i1.$$UserEntityTableTableManager get userEntity =>
|
||||||
i1.$$UserEntityTableTableManager(_db, _db.userEntity);
|
i1.$$UserEntityTableTableManager(_db, _db.userEntity);
|
||||||
i2.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
i2.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>
|
||||||
i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
|
||||||
i3.$$PartnerEntityTableTableManager get partnerEntity =>
|
i3.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||||
i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||||
i4.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
i4.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||||
i4.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
i4.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||||
i5.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
i5.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||||
i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
i5.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||||
i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6
|
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||||
|
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||||
|
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
|
||||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||||
i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>
|
|
||||||
i7.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
|
|
||||||
i8.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
i8.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||||
i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||||
i9.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
|
||||||
i9.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
|
||||||
i10.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
|
||||||
i10.$$RemoteAlbumAssetEntityTableTableManager(
|
|
||||||
_db, _db.remoteAlbumAssetEntity);
|
|
||||||
i11.$$AlbumUserEntityTableTableManager get albumUserEntity =>
|
|
||||||
i11.$$AlbumUserEntityTableTableManager(_db, _db.albumUserEntity);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,10 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
|||||||
durationInSeconds: Value(asset.durationInSeconds),
|
durationInSeconds: Value(asset.durationInSeconds),
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
checksum: const Value(null),
|
checksum: const Value(null),
|
||||||
|
isFavorite: Value(asset.isFavorite),
|
||||||
|
isLivePhoto: Value(asset.isLivePhoto),
|
||||||
|
livePhotoImageUploaded: Value(asset.livePhotoImageUploaded),
|
||||||
|
livePhotoVideoUploaded: Value(asset.livePhotoVideoUploaded),
|
||||||
);
|
);
|
||||||
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
|
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
|
||||||
_db.localAssetEntity,
|
_db.localAssetEntity,
|
||||||
@@ -395,6 +399,9 @@ extension on LocalAssetEntityData {
|
|||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
|
isLivePhoto: isLivePhoto,
|
||||||
|
livePhotoImageUploaded: livePhotoImageUploaded,
|
||||||
|
livePhotoVideoUploaded: livePhotoVideoUploaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ class SyncApiRepository implements ISyncApiRepository {
|
|||||||
SyncRequestType.partnerAssetsV1,
|
SyncRequestType.partnerAssetsV1,
|
||||||
SyncRequestType.assetExifsV1,
|
SyncRequestType.assetExifsV1,
|
||||||
SyncRequestType.partnerAssetExifsV1,
|
SyncRequestType.partnerAssetExifsV1,
|
||||||
SyncRequestType.albumsV1,
|
|
||||||
// SyncRequestType.albumAssetsV1,
|
|
||||||
SyncRequestType.albumUsersV1,
|
|
||||||
],
|
],
|
||||||
).toJson(),
|
).toJson(),
|
||||||
);
|
);
|
||||||
@@ -143,10 +140,4 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
|
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
|
||||||
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,
|
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||||
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.albumV1: SyncAlbumV1.fromJson,
|
|
||||||
SyncEntityType.albumDeleteV1: SyncAlbumDeleteV1.fromJson,
|
|
||||||
// SyncEntityType.albumAssetV1: SyncAlbumAssetV1.fromJson,
|
|
||||||
// SyncEntityType.albumAssetDeleteV1: SyncAlbumAssetDeleteV1.fromJson,
|
|
||||||
SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson,
|
|
||||||
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,19 +3,12 @@ import 'package:immich_mobile/domain/interfaces/sync_stream.interface.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/infrastructure/entities/exif.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/album_user.model.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/album_user.entity.drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
|
||||||
// import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart' as api
|
import 'package:openapi/api.dart' as api show AssetVisibility;
|
||||||
show AssetVisibility, AssetOrder, AlbumUserRole;
|
import 'package:openapi/api.dart' hide AssetVisibility;
|
||||||
import 'package:openapi/api.dart'
|
|
||||||
hide AssetVisibility, AssetOrder, AlbumUserRole;
|
|
||||||
|
|
||||||
class DriftSyncStreamRepository extends DriftDatabaseRepository
|
class DriftSyncStreamRepository extends DriftDatabaseRepository
|
||||||
implements ISyncStreamRepository {
|
implements ISyncStreamRepository {
|
||||||
@@ -168,135 +161,6 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> updateAlbumsV1(Iterable<SyncAlbumV1> data) async {
|
|
||||||
try {
|
|
||||||
await _db.batch((batch) {
|
|
||||||
for (final album in data) {
|
|
||||||
final companion = RemoteAlbumEntityCompanion(
|
|
||||||
name: Value(album.name),
|
|
||||||
description: Value(album.description),
|
|
||||||
ownerId: Value(album.ownerId),
|
|
||||||
thumbnailAssetId: Value(album.thumbnailAssetId),
|
|
||||||
createdAt: Value(album.createdAt),
|
|
||||||
updatedAt: Value(album.updatedAt),
|
|
||||||
isActivityEnabled: Value(album.isActivityEnabled),
|
|
||||||
order: Value(album.order.toAssetOrder()),
|
|
||||||
);
|
|
||||||
|
|
||||||
batch.insert(
|
|
||||||
_db.remoteAlbumEntity,
|
|
||||||
companion.copyWith(id: Value(album.id)),
|
|
||||||
onConflict: DoUpdate((_) => companion),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe('Error while processing updateAlbumsV1', e, s);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
|
|
||||||
try {
|
|
||||||
_db.batch((batch) {
|
|
||||||
for (final album in data) {
|
|
||||||
batch.delete(
|
|
||||||
_db.remoteAlbumEntity,
|
|
||||||
RemoteAlbumEntityCompanion(id: Value(album.albumId)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe('Error while processing deleteAlbumsV1', e, s);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Future<void> updateAlbumAssetsV1(Iterable<SyncAlbumAssetV1> data) async {
|
|
||||||
// try {
|
|
||||||
// await _db.remoteAlbumAssetEntity.insertAll(
|
|
||||||
// data.map(
|
|
||||||
// (albumAsset) => RemoteAlbumAssetEntityCompanion.insert(
|
|
||||||
// albumId: albumAsset.albumId,
|
|
||||||
// assetId: albumAsset.assetId,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// mode: InsertMode.insertOrIgnore,
|
|
||||||
// );
|
|
||||||
// } catch (e, s) {
|
|
||||||
// _logger.severe('Error while processing updateAlbumAssetsV1', e, s);
|
|
||||||
// rethrow;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Future<void> deleteAlbumAssetsV1(Iterable<SyncAlbumAssetDeleteV1> data) async {
|
|
||||||
// try {
|
|
||||||
// await _db.batch((batch) {
|
|
||||||
// for (final albumAsset in data) {
|
|
||||||
// batch.delete(
|
|
||||||
// _db.remoteAlbumAssetEntity,
|
|
||||||
// RemoteAlbumAssetEntityCompanion(
|
|
||||||
// albumId: Value(albumAsset.albumId),
|
|
||||||
// assetId: Value(albumAsset.assetId),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// } catch (e, s) {
|
|
||||||
// _logger.severe('Error while processing deleteAlbumAssetsV1', e, s);
|
|
||||||
// rethrow;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> updateAlbumUsersV1(Iterable<SyncAlbumUserV1> data) async {
|
|
||||||
try {
|
|
||||||
await _db.batch((batch) {
|
|
||||||
for (final albumUser in data) {
|
|
||||||
final companion = AlbumUserEntityCompanion(
|
|
||||||
role: Value(albumUser.role.toAlbumUserRole()),
|
|
||||||
);
|
|
||||||
|
|
||||||
batch.insert(
|
|
||||||
_db.albumUserEntity,
|
|
||||||
companion.copyWith(
|
|
||||||
albumId: Value(albumUser.albumId),
|
|
||||||
userId: Value(albumUser.userId),
|
|
||||||
),
|
|
||||||
onConflict: DoUpdate((_) => companion),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe('Error while processing updateAlbumUsersV1', e, s);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> deleteAlbumUsersV1(Iterable<SyncAlbumUserDeleteV1> data) async {
|
|
||||||
try {
|
|
||||||
await _db.batch((batch) {
|
|
||||||
for (final albumUser in data) {
|
|
||||||
batch.delete(
|
|
||||||
_db.albumUserEntity,
|
|
||||||
AlbumUserEntityCompanion(
|
|
||||||
albumId: Value(albumUser.albumId),
|
|
||||||
userId: Value(albumUser.userId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe('Error while processing deleteAlbumUsersV1', e, s);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateAssetsV1(Iterable<SyncAssetV1> data) =>
|
Future<void> _updateAssetsV1(Iterable<SyncAssetV1> data) =>
|
||||||
_db.batch((batch) {
|
_db.batch((batch) {
|
||||||
for (final asset in data) {
|
for (final asset in data) {
|
||||||
@@ -387,19 +251,3 @@ extension on api.AssetVisibility {
|
|||||||
_ => throw Exception('Unknown AssetVisibility value: $this'),
|
_ => throw Exception('Unknown AssetVisibility value: $this'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on api.AssetOrder {
|
|
||||||
AssetOrder toAssetOrder() => switch (this) {
|
|
||||||
api.AssetOrder.asc => AssetOrder.asc,
|
|
||||||
api.AssetOrder.desc => AssetOrder.desc,
|
|
||||||
_ => throw Exception('Unknown AssetOrder value: $this'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on api.AlbumUserRole {
|
|
||||||
AlbumUserRole toAlbumUserRole() => switch (this) {
|
|
||||||
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
|
||||||
api.AlbumUserRole.viewer => AlbumUserRole.viewer,
|
|
||||||
_ => throw Exception('Unknown AlbumUserRole value: $this'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/timeline.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:stream_transform/stream_transform.dart';
|
||||||
|
|
||||||
|
class DriftTimelineRepository extends DriftDatabaseRepository
|
||||||
|
implements ITimelineRepository {
|
||||||
|
final Drift _db;
|
||||||
|
|
||||||
|
const DriftTimelineRepository(super._db) : _db = _db;
|
||||||
|
|
||||||
|
List<Bucket> _generateBuckets(int count) {
|
||||||
|
final numBuckets = (count / kTimelineNoneSegmentSize).floor();
|
||||||
|
final buckets = List.generate(
|
||||||
|
numBuckets,
|
||||||
|
(_) => const Bucket(assetCount: kTimelineNoneSegmentSize),
|
||||||
|
);
|
||||||
|
if (count % kTimelineNoneSegmentSize != 0) {
|
||||||
|
buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize));
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<Bucket>> watchMainBucket(
|
||||||
|
List<String> userIds, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
throw UnsupportedError(
|
||||||
|
"GroupAssetsBy.none is not supported for watchMainBucket",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _db.mergedAssetDrift
|
||||||
|
.mergedBucket(userIds, groupBy: groupBy.index)
|
||||||
|
.map((row) {
|
||||||
|
final date = row.bucketDate.dateFmt(groupBy);
|
||||||
|
return TimeBucket(date: date, assetCount: row.assetCount);
|
||||||
|
})
|
||||||
|
.watch()
|
||||||
|
.throttle(const Duration(seconds: 3), trailing: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<BaseAsset>> getMainBucketAssets(
|
||||||
|
List<String> userIds, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
return _db.mergedAssetDrift
|
||||||
|
.mergedAsset(userIds, limit: Limit(count, offset))
|
||||||
|
.map(
|
||||||
|
(row) => row.remoteId != null
|
||||||
|
? Asset(
|
||||||
|
id: row.remoteId!,
|
||||||
|
localId: row.localId,
|
||||||
|
name: row.name,
|
||||||
|
checksum: row.checksum,
|
||||||
|
type: row.type,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
updatedAt: row.updatedAt,
|
||||||
|
thumbHash: row.thumbHash,
|
||||||
|
width: row.width,
|
||||||
|
height: row.height,
|
||||||
|
isFavorite: row.isFavorite,
|
||||||
|
durationInSeconds: row.durationInSeconds,
|
||||||
|
)
|
||||||
|
: LocalAsset(
|
||||||
|
id: row.localId!,
|
||||||
|
remoteId: row.remoteId,
|
||||||
|
name: row.name,
|
||||||
|
checksum: row.checksum,
|
||||||
|
type: row.type,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
updatedAt: row.updatedAt,
|
||||||
|
width: row.width,
|
||||||
|
height: row.height,
|
||||||
|
isFavorite: row.isFavorite,
|
||||||
|
durationInSeconds: row.durationInSeconds,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<Bucket>> watchLocalBucket(
|
||||||
|
String albumId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.localAlbumAssetEntity
|
||||||
|
.count(where: (row) => row.albumId.equals(albumId))
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.localAssetEntity.id.count();
|
||||||
|
final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.localAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<BaseAsset>> getLocalBucketAssets(
|
||||||
|
String albumId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.localAssetEntity.select().join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
|
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
return query
|
||||||
|
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Expression<DateTime> {
|
||||||
|
Expression<String> dateFmt(GroupAssetsBy groupBy) {
|
||||||
|
// DateTimes are stored in UTC, so we need to convert them to local time inside the query before formatting
|
||||||
|
// to create the correct time bucket
|
||||||
|
final localTimeExp = modify(const DateTimeModifier.localTime());
|
||||||
|
return switch (groupBy) {
|
||||||
|
GroupAssetsBy.day => localTimeExp.date,
|
||||||
|
GroupAssetsBy.month => localTimeExp.strftime("%Y-%m"),
|
||||||
|
GroupAssetsBy.none => throw ArgumentError(
|
||||||
|
"GroupAssetsBy.none is not supported for date formatting",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on String {
|
||||||
|
DateTime dateFmt(GroupAssetsBy groupBy) {
|
||||||
|
final format = switch (groupBy) {
|
||||||
|
GroupAssetsBy.day => "y-M-d",
|
||||||
|
GroupAssetsBy.month => "y-M",
|
||||||
|
GroupAssetsBy.none => throw ArgumentError(
|
||||||
|
"GroupAssetsBy.none is not supported for date formatting",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
return DateFormat(format).parse(this);
|
||||||
|
} catch (e) {
|
||||||
|
throw FormatException("Invalid date format: $this", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/translation.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||||
@@ -230,11 +230,17 @@ class AlbumsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
subtitle: sorted[index].ownerId != null
|
subtitle: sorted[index].ownerId != null
|
||||||
? Text(
|
? Text(
|
||||||
'${t('items_count', {
|
'${'items_count'.t(
|
||||||
'count': sorted[index].assetCount,
|
context: context,
|
||||||
})} • ${sorted[index].ownerId != userId ? t('shared_by_user', {
|
args: {
|
||||||
'user': sorted[index].ownerName!,
|
'count': sorted[index].assetCount,
|
||||||
}) : 'owned'.tr()}',
|
},
|
||||||
|
)} • ${sorted[index].ownerId != userId ? 'shared_by_user'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'user': sorted[index].ownerName!,
|
||||||
|
},
|
||||||
|
) : 'owned'.t(context: context)}',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style:
|
style:
|
||||||
context.textTheme.bodyMedium?.copyWith(
|
context.textTheme.bodyMedium?.copyWith(
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/utils/translation.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@@ -46,7 +46,10 @@ class LocalAlbumsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
t('items_count', {'count': albums[index].assetCount}),
|
'items_count'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': albums[index].assetCount},
|
||||||
|
),
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
),
|
),
|
||||||
|
|||||||
+5
@@ -40,6 +40,7 @@ class PlatformAsset {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
required this.durationInSeconds,
|
required this.durationInSeconds,
|
||||||
|
required this.isLivePhoto,
|
||||||
});
|
});
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
@@ -58,6 +59,8 @@ class PlatformAsset {
|
|||||||
|
|
||||||
int durationInSeconds;
|
int durationInSeconds;
|
||||||
|
|
||||||
|
bool isLivePhoto;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[
|
return <Object?>[
|
||||||
id,
|
id,
|
||||||
@@ -68,6 +71,7 @@ class PlatformAsset {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
|
isLivePhoto,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +90,7 @@ class PlatformAsset {
|
|||||||
width: result[5] as int?,
|
width: result[5] as int?,
|
||||||
height: result[6] as int?,
|
height: result[6] as int?,
|
||||||
durationInSeconds: result[7]! as int,
|
durationInSeconds: result[7]! as int,
|
||||||
|
isLivePhoto: result[8]! as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,10 +61,8 @@ final _features = [
|
|||||||
icon: Icons.delete_sweep_rounded,
|
icon: Icons.delete_sweep_rounded,
|
||||||
onTap: (_, ref) async {
|
onTap: (_, ref) async {
|
||||||
final db = ref.read(driftProvider);
|
final db = ref.read(driftProvider);
|
||||||
await db.remoteExifEntity.deleteAll();
|
|
||||||
await db.remoteAssetEntity.deleteAll();
|
await db.remoteAssetEntity.deleteAll();
|
||||||
await db.remoteAlbumEntity.deleteAll();
|
await db.remoteExifEntity.deleteAll();
|
||||||
await db.remoteAlbumAssetEntity.deleteAll();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
@@ -90,6 +88,11 @@ final _features = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Main Timeline',
|
||||||
|
icon: Icons.timeline_rounded,
|
||||||
|
onTap: (ctx, _) => ctx.pushRoute(const MainTimelineRoute()),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class LocalTimelinePage extends StatelessWidget {
|
||||||
|
final String albumId;
|
||||||
|
|
||||||
|
const LocalTimelinePage({super.key, required this.albumId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).localAlbum(albumId: albumId);
|
||||||
|
ref.onDispose(() => unawaited(timelineService.dispose()));
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class MainTimelinePage extends StatelessWidget {
|
||||||
|
const MainTimelinePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final timelineService = ref
|
||||||
|
.watch(timelineFactoryProvider)
|
||||||
|
.main(ref.watch(timelineUsersIdsProvider));
|
||||||
|
ref.onDispose(() => unawaited(timelineService.dispose()));
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
class _Stat {
|
class _Stat {
|
||||||
const _Stat({required this.name, required this.load});
|
const _Stat({required this.name, required this.load});
|
||||||
@@ -16,9 +17,16 @@ class _Stat {
|
|||||||
|
|
||||||
class _Summary extends StatelessWidget {
|
class _Summary extends StatelessWidget {
|
||||||
final String name;
|
final String name;
|
||||||
|
final Widget? leading;
|
||||||
final Future<int> countFuture;
|
final Future<int> countFuture;
|
||||||
|
final void Function()? onTap;
|
||||||
|
|
||||||
const _Summary({required this.name, required this.countFuture});
|
const _Summary({
|
||||||
|
required this.name,
|
||||||
|
required this.countFuture,
|
||||||
|
this.leading,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -34,7 +42,12 @@ class _Summary extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
subtitle = Text('${snapshot.data ?? 0}');
|
subtitle = Text('${snapshot.data ?? 0}');
|
||||||
}
|
}
|
||||||
return ListTile(title: Text(name), trailing: subtitle);
|
return ListTile(
|
||||||
|
leading: leading,
|
||||||
|
title: Text(name),
|
||||||
|
trailing: subtitle,
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,8 +118,12 @@ class LocalMediaSummaryPage extends StatelessWidget {
|
|||||||
.filter((f) => f.albumId.id.equals(album.id))
|
.filter((f) => f.albumId.id.equals(album.id))
|
||||||
.count();
|
.count();
|
||||||
return _Summary(
|
return _Summary(
|
||||||
|
leading: const Icon(Icons.photo_album_rounded),
|
||||||
name: album.name,
|
name: album.name,
|
||||||
countFuture: countFuture,
|
countFuture: countFuture,
|
||||||
|
onTap: () => context.router.push(
|
||||||
|
LocalTimelineRoute(albumId: album.id),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemCount: albums.length,
|
itemCount: albums.length,
|
||||||
@@ -130,10 +147,6 @@ final _remoteStats = [
|
|||||||
name: 'Exif Entities',
|
name: 'Exif Entities',
|
||||||
load: (db) => db.managers.remoteExifEntity.count(),
|
load: (db) => db.managers.remoteExifEntity.count(),
|
||||||
),
|
),
|
||||||
_Stat(
|
|
||||||
name: 'Remote Albums',
|
|
||||||
load: (db) => db.managers.remoteAlbumEntity.count(),
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/asset_media.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||||
|
|
||||||
|
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
||||||
|
final IAssetMediaRepository _assetMediaRepository =
|
||||||
|
const AssetMediaRepository();
|
||||||
|
final CacheManager? cacheManager;
|
||||||
|
|
||||||
|
final LocalAsset asset;
|
||||||
|
final double height;
|
||||||
|
final double width;
|
||||||
|
|
||||||
|
LocalThumbProvider({
|
||||||
|
required this.asset,
|
||||||
|
this.height = kTimelineFixedTileExtent,
|
||||||
|
this.width = kTimelineFixedTileExtent,
|
||||||
|
this.cacheManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LocalThumbProvider> obtainKey(
|
||||||
|
ImageConfiguration configuration,
|
||||||
|
) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter loadImage(
|
||||||
|
LocalThumbProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) {
|
||||||
|
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
||||||
|
return MultiFrameImageStreamCompleter(
|
||||||
|
codec: _codec(key, cache, decode),
|
||||||
|
scale: 1.0,
|
||||||
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
|
DiagnosticsProperty<LocalAsset>('Asset', key.asset),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Codec> _codec(
|
||||||
|
LocalThumbProvider key,
|
||||||
|
CacheManager cache,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) async {
|
||||||
|
final cacheKey = '${key.asset.id}-${key.asset.updatedAt}-${width}x$height';
|
||||||
|
|
||||||
|
final fileFromCache = await cache.getFileFromCache(cacheKey);
|
||||||
|
if (fileFromCache != null) {
|
||||||
|
try {
|
||||||
|
final buffer =
|
||||||
|
await ImmutableBuffer.fromFilePath(fileFromCache.file.path);
|
||||||
|
return await decode(buffer);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final thumbnailBytes = await _assetMediaRepository.getThumbnail(
|
||||||
|
key.asset.id,
|
||||||
|
size: Size(key.width, key.height),
|
||||||
|
);
|
||||||
|
if (thumbnailBytes == null) {
|
||||||
|
PaintingBinding.instance.imageCache.evict(key);
|
||||||
|
throw StateError(
|
||||||
|
"Loading thumb for local photo ${key.asset.name} failed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||||
|
unawaited(cache.putFile(cacheKey, thumbnailBytes));
|
||||||
|
return decode(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other is LocalThumbProvider) {
|
||||||
|
return asset.id == other.asset.id &&
|
||||||
|
asset.updatedAt == other.asset.updatedAt;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
|
class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
||||||
|
final String assetId;
|
||||||
|
final double height;
|
||||||
|
final double width;
|
||||||
|
final CacheManager? cacheManager;
|
||||||
|
|
||||||
|
RemoteThumbProvider({
|
||||||
|
required this.assetId,
|
||||||
|
this.height = kTimelineFixedTileExtent,
|
||||||
|
this.width = kTimelineFixedTileExtent,
|
||||||
|
this.cacheManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RemoteThumbProvider> obtainKey(
|
||||||
|
ImageConfiguration configuration,
|
||||||
|
) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter loadImage(
|
||||||
|
RemoteThumbProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) {
|
||||||
|
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
||||||
|
final chunkController = StreamController<ImageChunkEvent>();
|
||||||
|
return MultiFrameImageStreamCompleter(
|
||||||
|
codec: _codec(key, cache, decode, chunkController),
|
||||||
|
scale: 1.0,
|
||||||
|
chunkEvents: chunkController.stream,
|
||||||
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
|
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Codec> _codec(
|
||||||
|
RemoteThumbProvider key,
|
||||||
|
CacheManager cache,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
StreamController<ImageChunkEvent> chunkController,
|
||||||
|
) async {
|
||||||
|
final preview = getThumbnailUrlForRemoteId(
|
||||||
|
key.assetId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ImageLoader.loadImageFromCache(
|
||||||
|
preview,
|
||||||
|
cache: cache,
|
||||||
|
decode: decode,
|
||||||
|
chunkEvents: chunkController,
|
||||||
|
).whenComplete(chunkController.close);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other is RemoteThumbProvider) {
|
||||||
|
return assetId == other.assetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => assetId.hashCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'dart:convert' hide Codec;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:thumbhash/thumbhash.dart';
|
||||||
|
|
||||||
|
class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
|
||||||
|
final String thumbHash;
|
||||||
|
|
||||||
|
ThumbHashProvider({
|
||||||
|
required this.thumbHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ThumbHashProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter loadImage(
|
||||||
|
ThumbHashProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) {
|
||||||
|
return MultiFrameImageStreamCompleter(
|
||||||
|
codec: _loadCodec(key, decode),
|
||||||
|
scale: 1.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Codec> _loadCodec(
|
||||||
|
ThumbHashProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) async {
|
||||||
|
final image = thumbHashToRGBA(base64Decode(key.thumbHash));
|
||||||
|
return decode(await ImmutableBuffer.fromUint8List(rgbaToBmp(image)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other is ThumbHashProvider) {
|
||||||
|
return thumbHash == other.thumbHash;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => thumbHash.hashCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/local_thumb_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/remote_thumb_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
|
class Thumbnail extends StatelessWidget {
|
||||||
|
const Thumbnail({
|
||||||
|
required this.asset,
|
||||||
|
this.size = const Size.square(256),
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BaseAsset asset;
|
||||||
|
final Size size;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
|
static ImageProvider imageProvider({
|
||||||
|
required BaseAsset asset,
|
||||||
|
Size size = const Size.square(256),
|
||||||
|
}) {
|
||||||
|
if (asset is LocalAsset) {
|
||||||
|
return LocalThumbProvider(
|
||||||
|
asset: asset,
|
||||||
|
height: size.height,
|
||||||
|
width: size.width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset is Asset) {
|
||||||
|
return RemoteThumbProvider(
|
||||||
|
assetId: asset.id,
|
||||||
|
height: size.height,
|
||||||
|
width: size.width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final thumbHash = asset is Asset ? (asset as Asset).thumbHash : null;
|
||||||
|
final provider = imageProvider(asset: asset, size: size);
|
||||||
|
|
||||||
|
return OctoImage.fromSet(
|
||||||
|
image: provider,
|
||||||
|
octoSet: OctoSet(
|
||||||
|
placeholderBuilder: _blurHashPlaceholderBuilder(thumbHash, fit: fit),
|
||||||
|
errorBuilder: _blurHashErrorBuilder(
|
||||||
|
thumbHash,
|
||||||
|
provider: provider,
|
||||||
|
fit: fit,
|
||||||
|
asset: asset,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
fadeOutDuration: const Duration(milliseconds: 100),
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
fit: fit,
|
||||||
|
placeholderFadeInDuration: Duration.zero,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(
|
||||||
|
String? thumbHash, {
|
||||||
|
BoxFit? fit,
|
||||||
|
}) {
|
||||||
|
return (context) => thumbHash == null
|
||||||
|
? const ThumbnailPlaceholder()
|
||||||
|
: FadeInPlaceholderImage(
|
||||||
|
placeholder: const ThumbnailPlaceholder(),
|
||||||
|
image: ThumbHashProvider(thumbHash: thumbHash),
|
||||||
|
fit: fit ?? BoxFit.cover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OctoErrorBuilder _blurHashErrorBuilder(
|
||||||
|
String? blurhash, {
|
||||||
|
BaseAsset? asset,
|
||||||
|
ImageProvider? provider,
|
||||||
|
BoxFit? fit,
|
||||||
|
}) =>
|
||||||
|
(context, e, s) {
|
||||||
|
Logger("ImThumbnail")
|
||||||
|
.warning("Error loading thumbnail for ${asset?.name}", e, s);
|
||||||
|
provider?.evict();
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
_blurHashPlaceholderBuilder(blurhash, fit: fit)(context),
|
||||||
|
const Opacity(
|
||||||
|
opacity: 0.75,
|
||||||
|
child: Icon(Icons.error_outline_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
|
||||||
|
class ThumbnailTile extends StatelessWidget {
|
||||||
|
const ThumbnailTile(
|
||||||
|
this.asset, {
|
||||||
|
this.size = const Size.square(256),
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
this.showStorageIndicator = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BaseAsset asset;
|
||||||
|
final Size size;
|
||||||
|
final BoxFit fit;
|
||||||
|
final bool showStorageIndicator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: Thumbnail(asset: asset, fit: fit, size: size)),
|
||||||
|
if (asset.isVideo)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10.0, top: 6.0),
|
||||||
|
child: _VideoIndicator(asset.durationInSeconds ?? 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showStorageIndicator)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10.0, bottom: 6.0),
|
||||||
|
child: _TileOverlayIcon(
|
||||||
|
switch (asset.storage) {
|
||||||
|
AssetState.local => Icons.cloud_off_outlined,
|
||||||
|
AssetState.remote => Icons.cloud_outlined,
|
||||||
|
AssetState.merged => Icons.cloud_done_outlined,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (asset.isFavorite)
|
||||||
|
const Align(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 10.0, bottom: 6.0),
|
||||||
|
child: _TileOverlayIcon(Icons.favorite_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoIndicator extends StatelessWidget {
|
||||||
|
final int durationInSeconds;
|
||||||
|
const _VideoIndicator(this.durationInSeconds);
|
||||||
|
|
||||||
|
String _formatDuration(int durationInSec) {
|
||||||
|
final int hours = durationInSec ~/ 3600;
|
||||||
|
final int minutes = (durationInSec % 3600) ~/ 60;
|
||||||
|
final int seconds = durationInSec % 60;
|
||||||
|
|
||||||
|
final String minutesPadded = minutes.toString().padLeft(2, '0');
|
||||||
|
final String secondsPadded = seconds.toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return "$hours:$minutesPadded:$secondsPadded"; // H:MM:SS
|
||||||
|
} else {
|
||||||
|
return "$minutesPadded:$secondsPadded"; // MM:SS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
spacing: 3,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
// CrossAxisAlignment.end looks more centered vertically than CrossAxisAlignment.center
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatDuration(durationInSeconds),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
blurRadius: 5.0,
|
||||||
|
color: Colors.black.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _TileOverlayIcon(Icons.play_circle_outline_rounded),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TileOverlayIcon extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
const _TileOverlayIcon(this.icon);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Icon(
|
||||||
|
icon,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
blurRadius: 5.0,
|
||||||
|
color: Colors.black.withValues(alpha: 0.6),
|
||||||
|
offset: const Offset(0.0, 0.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const double kTimelineHeaderExtent = 80.0;
|
||||||
|
const double kTimelineFixedTileExtent = 256;
|
||||||
|
const double kTimelineSpacing = 2.0;
|
||||||
|
const int kTimelineColumnCount = 3;
|
||||||
|
|
||||||
|
const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300);
|
||||||
|
const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800);
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class FixedTimelineRow extends MultiChildRenderObjectWidget {
|
||||||
|
final double dimension;
|
||||||
|
final double spacing;
|
||||||
|
final TextDirection textDirection;
|
||||||
|
|
||||||
|
const FixedTimelineRow({
|
||||||
|
super.key,
|
||||||
|
required this.dimension,
|
||||||
|
required this.spacing,
|
||||||
|
required this.textDirection,
|
||||||
|
required super.children,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
return RenderFixedRow(
|
||||||
|
dimension: dimension,
|
||||||
|
spacing: spacing,
|
||||||
|
textDirection: textDirection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(BuildContext context, RenderFixedRow renderObject) {
|
||||||
|
renderObject.dimension = dimension;
|
||||||
|
renderObject.spacing = spacing;
|
||||||
|
renderObject.textDirection = textDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DoubleProperty('dimension', dimension));
|
||||||
|
properties.add(DoubleProperty('spacing', spacing));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RowParentData extends ContainerBoxParentData<RenderBox> {}
|
||||||
|
|
||||||
|
class RenderFixedRow extends RenderBox
|
||||||
|
with
|
||||||
|
ContainerRenderObjectMixin<RenderBox, _RowParentData>,
|
||||||
|
RenderBoxContainerDefaultsMixin<RenderBox, _RowParentData> {
|
||||||
|
RenderFixedRow({
|
||||||
|
List<RenderBox>? children,
|
||||||
|
required double dimension,
|
||||||
|
required double spacing,
|
||||||
|
required TextDirection textDirection,
|
||||||
|
}) : _dimension = dimension,
|
||||||
|
_spacing = spacing,
|
||||||
|
_textDirection = textDirection {
|
||||||
|
addAll(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
double get dimension => _dimension;
|
||||||
|
double _dimension;
|
||||||
|
|
||||||
|
set dimension(double value) {
|
||||||
|
if (_dimension == value) return;
|
||||||
|
_dimension = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get spacing => _spacing;
|
||||||
|
double _spacing;
|
||||||
|
|
||||||
|
set spacing(double value) {
|
||||||
|
if (_spacing == value) return;
|
||||||
|
_spacing = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextDirection get textDirection => _textDirection;
|
||||||
|
TextDirection _textDirection;
|
||||||
|
|
||||||
|
set textDirection(TextDirection value) {
|
||||||
|
if (_textDirection == value) return;
|
||||||
|
_textDirection = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setupParentData(RenderBox child) {
|
||||||
|
if (child.parentData is! _RowParentData) {
|
||||||
|
child.parentData = _RowParentData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double get intrinsicWidth =>
|
||||||
|
dimension * childCount + spacing * (childCount - 1);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicWidth(double height) => intrinsicWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicWidth(double height) => intrinsicWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicHeight(double width) => dimension;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicHeight(double width) => dimension;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? computeDistanceToActualBaseline(TextBaseline baseline) {
|
||||||
|
return defaultComputeDistanceToHighestActualBaseline(baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||||
|
return defaultHitTestChildren(result, position: position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
defaultPaint(context, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DoubleProperty('dimension', dimension));
|
||||||
|
properties.add(DoubleProperty('spacing', spacing));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
RenderBox? child = firstChild;
|
||||||
|
if (child == null) {
|
||||||
|
size = constraints.smallest;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Use the entire width of the parent for the row.
|
||||||
|
size = Size(constraints.maxWidth, dimension);
|
||||||
|
// Each tile is forced to be dimension x dimension.
|
||||||
|
final childConstraints = BoxConstraints.tight(Size(dimension, dimension));
|
||||||
|
final flipMainAxis = textDirection == TextDirection.rtl;
|
||||||
|
Offset offset = Offset(flipMainAxis ? size.width - dimension : 0, 0);
|
||||||
|
final dx = (flipMainAxis ? -1 : 1) * (dimension + spacing);
|
||||||
|
// Layout each child horizontally.
|
||||||
|
while (child != null) {
|
||||||
|
child.layout(childConstraints, parentUsesSize: false);
|
||||||
|
final childParentData = child.parentData! as _RowParentData;
|
||||||
|
childParentData.offset = offset;
|
||||||
|
offset += Offset(dx, 0);
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
|
class FixedSegment extends Segment {
|
||||||
|
final double tileHeight;
|
||||||
|
final int columnCount;
|
||||||
|
final double mainAxisExtend;
|
||||||
|
|
||||||
|
const FixedSegment({
|
||||||
|
required super.firstIndex,
|
||||||
|
required super.lastIndex,
|
||||||
|
required super.startOffset,
|
||||||
|
required super.endOffset,
|
||||||
|
required super.firstAssetIndex,
|
||||||
|
required super.bucket,
|
||||||
|
required this.tileHeight,
|
||||||
|
required this.columnCount,
|
||||||
|
required super.headerExtent,
|
||||||
|
required super.spacing,
|
||||||
|
required super.header,
|
||||||
|
}) : assert(tileHeight != 0),
|
||||||
|
mainAxisExtend = tileHeight + spacing;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double indexToLayoutOffset(int index) {
|
||||||
|
index -= gridIndex;
|
||||||
|
if (index < 0) {
|
||||||
|
return startOffset;
|
||||||
|
}
|
||||||
|
return gridOffset + (mainAxisExtend * index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int getMinChildIndexForScrollOffset(double scrollOffset) {
|
||||||
|
scrollOffset -= gridOffset;
|
||||||
|
if (!scrollOffset.isFinite || scrollOffset < 0) {
|
||||||
|
return firstIndex;
|
||||||
|
}
|
||||||
|
final rowsAbove = (scrollOffset / mainAxisExtend).floor();
|
||||||
|
return gridIndex + rowsAbove;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int getMaxChildIndexForScrollOffset(double scrollOffset) {
|
||||||
|
scrollOffset -= gridOffset;
|
||||||
|
if (!scrollOffset.isFinite || scrollOffset < 0) {
|
||||||
|
return firstIndex;
|
||||||
|
}
|
||||||
|
final firstRowBelow = (scrollOffset / mainAxisExtend).ceil();
|
||||||
|
return gridIndex + firstRowBelow - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget builder(BuildContext context, int index) {
|
||||||
|
if (index == firstIndex) {
|
||||||
|
return TimelineHeader(
|
||||||
|
bucket: bucket,
|
||||||
|
header: header,
|
||||||
|
height: headerExtent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final rowIndexInSegment = index - (firstIndex + 1);
|
||||||
|
final assetIndex = rowIndexInSegment * columnCount;
|
||||||
|
final assetCount = bucket.assetCount;
|
||||||
|
final numberOfAssets = math.min(columnCount, assetCount - assetIndex);
|
||||||
|
|
||||||
|
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRow(int assetIndex, int count) => Consumer(
|
||||||
|
builder: (ctx, ref, _) {
|
||||||
|
final isScrubbing =
|
||||||
|
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||||
|
final timelineService = ref.read(timelineServiceProvider);
|
||||||
|
|
||||||
|
// Timeline is being scrubbed, show placeholders
|
||||||
|
if (isScrubbing) {
|
||||||
|
return SegmentBuilder.buildPlaceholder(
|
||||||
|
ctx,
|
||||||
|
count,
|
||||||
|
size: Size.square(tileHeight),
|
||||||
|
spacing: spacing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket is already loaded, show the assets
|
||||||
|
if (timelineService.hasRange(assetIndex, count)) {
|
||||||
|
final assets = timelineService.getAssets(assetIndex, count);
|
||||||
|
return _buildAssetRow(ctx, assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket is not loaded, show placeholders and load the bucket
|
||||||
|
return FutureBuilder(
|
||||||
|
future: timelineService.loadAssets(assetIndex, count),
|
||||||
|
builder: (ctxx, snap) {
|
||||||
|
if (snap.connectionState != ConnectionState.done) {
|
||||||
|
return SegmentBuilder.buildPlaceholder(
|
||||||
|
ctx,
|
||||||
|
count,
|
||||||
|
size: Size.square(tileHeight),
|
||||||
|
spacing: spacing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildAssetRow(ctxx, snap.requireData);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets) =>
|
||||||
|
FixedTimelineRow(
|
||||||
|
dimension: tileHeight,
|
||||||
|
spacing: spacing,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
children: List.generate(
|
||||||
|
assets.length,
|
||||||
|
(i) => RepaintBoundary(child: ThumbnailTile(assets[i])),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
||||||
|
|
||||||
|
class FixedSegmentBuilder extends SegmentBuilder {
|
||||||
|
final double tileHeight;
|
||||||
|
final int columnCount;
|
||||||
|
|
||||||
|
const FixedSegmentBuilder({
|
||||||
|
required super.buckets,
|
||||||
|
required this.tileHeight,
|
||||||
|
required this.columnCount,
|
||||||
|
super.spacing,
|
||||||
|
super.groupBy,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Segment> generate() {
|
||||||
|
final segments = <Segment>[];
|
||||||
|
int firstIndex = 0;
|
||||||
|
double startOffset = 0;
|
||||||
|
int assetIndex = 0;
|
||||||
|
DateTime? previousDate;
|
||||||
|
|
||||||
|
for (int i = 0; i < buckets.length; i++) {
|
||||||
|
final bucket = buckets[i];
|
||||||
|
|
||||||
|
final assetCount = bucket.assetCount;
|
||||||
|
final numberOfRows = (assetCount / columnCount).ceil();
|
||||||
|
final segmentCount = numberOfRows + 1;
|
||||||
|
|
||||||
|
final segmentFirstIndex = firstIndex;
|
||||||
|
firstIndex += segmentCount;
|
||||||
|
final segmentLastIndex = firstIndex - 1;
|
||||||
|
|
||||||
|
final timelineHeader = switch (groupBy) {
|
||||||
|
GroupAssetsBy.month => HeaderType.month,
|
||||||
|
GroupAssetsBy.day =>
|
||||||
|
bucket is TimeBucket && bucket.date.month != previousDate?.month
|
||||||
|
? HeaderType.monthAndDay
|
||||||
|
: HeaderType.day,
|
||||||
|
GroupAssetsBy.none => HeaderType.none,
|
||||||
|
};
|
||||||
|
final headerExtent = SegmentBuilder.headerExtent(timelineHeader);
|
||||||
|
|
||||||
|
final segmentStartOffset = startOffset;
|
||||||
|
startOffset += headerExtent +
|
||||||
|
(tileHeight * numberOfRows) +
|
||||||
|
spacing * (numberOfRows - 1);
|
||||||
|
final segmentEndOffset = startOffset;
|
||||||
|
|
||||||
|
segments.add(
|
||||||
|
FixedSegment(
|
||||||
|
firstIndex: segmentFirstIndex,
|
||||||
|
lastIndex: segmentLastIndex,
|
||||||
|
startOffset: segmentStartOffset,
|
||||||
|
endOffset: segmentEndOffset,
|
||||||
|
firstAssetIndex: assetIndex,
|
||||||
|
bucket: bucket,
|
||||||
|
tileHeight: tileHeight,
|
||||||
|
columnCount: columnCount,
|
||||||
|
headerExtent: headerExtent,
|
||||||
|
spacing: spacing,
|
||||||
|
header: timelineHeader,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assetIndex += assetCount;
|
||||||
|
if (bucket is TimeBucket) {
|
||||||
|
previousDate = bucket.date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
|
class TimelineHeader extends StatelessWidget {
|
||||||
|
final Bucket bucket;
|
||||||
|
final HeaderType header;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
const TimelineHeader({
|
||||||
|
super.key,
|
||||||
|
required this.bucket,
|
||||||
|
required this.header,
|
||||||
|
required this.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
String _formatMonth(BuildContext context, DateTime date) {
|
||||||
|
final formatter = date.year == DateTime.now().year
|
||||||
|
? DateFormat.MMMM(context.locale.toLanguageTag())
|
||||||
|
: DateFormat.yMMMM(context.locale.toLanguageTag());
|
||||||
|
return formatter.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDay(BuildContext context, DateTime date) {
|
||||||
|
final formatter = DateFormat.yMMMEd(context.locale.toLanguageTag());
|
||||||
|
return formatter.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (bucket is! TimeBucket || header == HeaderType.none) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final date = (bucket as TimeBucket).date;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.only(left: 10, top: 30, bottom: 10),
|
||||||
|
height: height,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
if (header == HeaderType.month || header == HeaderType.monthAndDay)
|
||||||
|
Text(
|
||||||
|
_formatMonth(context, date),
|
||||||
|
style: context.textTheme.labelLarge
|
||||||
|
?.copyWith(fontSize: 24, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
if (header == HeaderType.day || header == HeaderType.monthAndDay)
|
||||||
|
Text(
|
||||||
|
_formatDay(context, date),
|
||||||
|
style: context.textTheme.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
|
import 'package:intl/intl.dart' hide TextDirection;
|
||||||
|
|
||||||
|
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||||
|
/// for quick navigation of the BoxScrollView.
|
||||||
|
class Scrubber extends StatefulWidget {
|
||||||
|
/// The view that will be scrolled with the scroll thumb
|
||||||
|
final CustomScrollView child;
|
||||||
|
|
||||||
|
/// The segments of the timeline
|
||||||
|
final List<Segment> layoutSegments;
|
||||||
|
|
||||||
|
final double timelineHeight;
|
||||||
|
|
||||||
|
final double topPadding;
|
||||||
|
|
||||||
|
final double bottomPadding;
|
||||||
|
|
||||||
|
Scrubber({
|
||||||
|
super.key,
|
||||||
|
Key? scrollThumbKey,
|
||||||
|
required this.layoutSegments,
|
||||||
|
required this.timelineHeight,
|
||||||
|
this.topPadding = 0,
|
||||||
|
this.bottomPadding = 0,
|
||||||
|
required this.child,
|
||||||
|
}) : assert(child.scrollDirection == Axis.vertical);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => ScrubberState();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_Segment> _buildSegments({
|
||||||
|
required List<Segment> layoutSegments,
|
||||||
|
required double timelineHeight,
|
||||||
|
}) {
|
||||||
|
final segments = <_Segment>[];
|
||||||
|
if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final formatter = DateFormat.yMMM();
|
||||||
|
for (final layoutSegment in layoutSegments) {
|
||||||
|
final scrollPercentage =
|
||||||
|
layoutSegment.startOffset / layoutSegments.last.endOffset;
|
||||||
|
final startOffset = scrollPercentage * timelineHeight;
|
||||||
|
|
||||||
|
final date = (layoutSegment.bucket as TimeBucket).date;
|
||||||
|
final label = formatter.format(date);
|
||||||
|
|
||||||
|
segments.add(
|
||||||
|
_Segment(
|
||||||
|
date: date,
|
||||||
|
startOffset: startOffset,
|
||||||
|
scrollLabel: label,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
|
||||||
|
double _thumbTopOffset = 0.0;
|
||||||
|
bool _isDragging = false;
|
||||||
|
List<_Segment> _segments = [];
|
||||||
|
|
||||||
|
late AnimationController _thumbAnimationController;
|
||||||
|
Timer? _fadeOutTimer;
|
||||||
|
late Animation<double> _thumbAnimation;
|
||||||
|
|
||||||
|
late AnimationController _labelAnimationController;
|
||||||
|
late Animation<double> _labelAnimation;
|
||||||
|
|
||||||
|
double get _scrubberHeight =>
|
||||||
|
widget.timelineHeight - widget.topPadding - widget.bottomPadding;
|
||||||
|
|
||||||
|
late final ScrollController _scrollController;
|
||||||
|
|
||||||
|
double get _currentOffset =>
|
||||||
|
_scrollController.offset *
|
||||||
|
_scrubberHeight /
|
||||||
|
_scrollController.position.maxScrollExtent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_isDragging = false;
|
||||||
|
_segments = _buildSegments(
|
||||||
|
layoutSegments: widget.layoutSegments,
|
||||||
|
timelineHeight: _scrubberHeight,
|
||||||
|
);
|
||||||
|
_thumbAnimationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: kTimelineScrubberFadeInDuration,
|
||||||
|
);
|
||||||
|
_thumbAnimation = CurvedAnimation(
|
||||||
|
parent: _thumbAnimationController,
|
||||||
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
|
);
|
||||||
|
_labelAnimationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: kTimelineScrubberFadeInDuration,
|
||||||
|
);
|
||||||
|
|
||||||
|
_labelAnimation = CurvedAnimation(
|
||||||
|
parent: _labelAnimationController,
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_scrollController = PrimaryScrollController.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant Scrubber oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (oldWidget.layoutSegments.lastOrNull?.endOffset !=
|
||||||
|
widget.layoutSegments.lastOrNull?.endOffset) {
|
||||||
|
_segments = _buildSegments(
|
||||||
|
layoutSegments: widget.layoutSegments,
|
||||||
|
timelineHeight: _scrubberHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_thumbAnimationController.dispose();
|
||||||
|
_labelAnimationController.dispose();
|
||||||
|
_fadeOutTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetThumbTimer() {
|
||||||
|
_fadeOutTimer?.cancel();
|
||||||
|
_fadeOutTimer = Timer(kTimelineScrubberFadeOutDuration, () {
|
||||||
|
_thumbAnimationController.reverse();
|
||||||
|
_fadeOutTimer = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _onScrollNotification(ScrollNotification notification) {
|
||||||
|
if (_isDragging) {
|
||||||
|
// If the user is dragging the thumb, we don't want to update the position
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (notification is ScrollUpdateNotification) {
|
||||||
|
_thumbTopOffset = _currentOffset;
|
||||||
|
if (_labelAnimation.status != AnimationStatus.reverse) {
|
||||||
|
_labelAnimationController.reverse();
|
||||||
|
}
|
||||||
|
if (_thumbAnimationController.status != AnimationStatus.forward) {
|
||||||
|
_thumbAnimationController.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_resetThumbTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragStart(WidgetRef ref) {
|
||||||
|
ref.read(timelineStateProvider.notifier).setScrubbing(true);
|
||||||
|
setState(() {
|
||||||
|
_isDragging = true;
|
||||||
|
_labelAnimationController.forward();
|
||||||
|
_fadeOutTimer?.cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragUpdate(DragUpdateDetails details) {
|
||||||
|
if (!_isDragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_thumbAnimationController.status != AnimationStatus.forward) {
|
||||||
|
_thumbAnimationController.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
final newOffset =
|
||||||
|
details.globalPosition.dy - widget.topPadding - widget.bottomPadding;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_thumbTopOffset = newOffset.clamp(0, _scrubberHeight);
|
||||||
|
final scrollPercentage = _thumbTopOffset / _scrubberHeight;
|
||||||
|
final maxScrollExtent = _scrollController.position.maxScrollExtent;
|
||||||
|
_scrollController.jumpTo(maxScrollExtent * scrollPercentage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragEnd(WidgetRef ref) {
|
||||||
|
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
||||||
|
_labelAnimationController.reverse();
|
||||||
|
_isDragging = false;
|
||||||
|
_resetThumbTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext ctx) {
|
||||||
|
Text? label;
|
||||||
|
if (_scrollController.hasClients) {
|
||||||
|
// Cache to avoid multiple calls to [_currentOffset]
|
||||||
|
final scrollOffset = _currentOffset;
|
||||||
|
final labelText = _segments
|
||||||
|
.lastWhereOrNull(
|
||||||
|
(segment) => segment.startOffset <= scrollOffset,
|
||||||
|
)
|
||||||
|
?.scrollLabel ??
|
||||||
|
_segments.firstOrNull?.scrollLabel;
|
||||||
|
label = labelText != null
|
||||||
|
? Text(
|
||||||
|
labelText,
|
||||||
|
style: ctx.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: _onScrollNotification,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
RepaintBoundary(child: widget.child),
|
||||||
|
PositionedDirectional(
|
||||||
|
top: _thumbTopOffset + widget.topPadding,
|
||||||
|
end: 0,
|
||||||
|
child: Consumer(
|
||||||
|
builder: (_, ref, child) => GestureDetector(
|
||||||
|
onVerticalDragStart: (_) => _onDragStart(ref),
|
||||||
|
onVerticalDragUpdate: _onDragUpdate,
|
||||||
|
onVerticalDragEnd: (_) => _onDragEnd(ref),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: _Scrubber(
|
||||||
|
thumbAnimation: _thumbAnimation,
|
||||||
|
labelAnimation: _labelAnimation,
|
||||||
|
label: label,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScrollLabel extends StatelessWidget {
|
||||||
|
final Text label;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Animation<double> animation;
|
||||||
|
|
||||||
|
const _ScrollLabel({
|
||||||
|
required this.label,
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.animation,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IgnorePointer(
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(right: 12.0),
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 28),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: label,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Scrubber extends StatelessWidget {
|
||||||
|
final Text? label;
|
||||||
|
final Animation<double> thumbAnimation;
|
||||||
|
final Animation<double> labelAnimation;
|
||||||
|
|
||||||
|
const _Scrubber({
|
||||||
|
this.label,
|
||||||
|
required this.thumbAnimation,
|
||||||
|
required this.labelAnimation,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final backgroundColor = context.isDarkTheme
|
||||||
|
? context.colorScheme.primary.darken(amount: .5)
|
||||||
|
: context.colorScheme.primary;
|
||||||
|
|
||||||
|
return _SlideFadeTransition(
|
||||||
|
animation: thumbAnimation,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (label != null)
|
||||||
|
_ScrollLabel(
|
||||||
|
label: label!,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
animation: labelAnimation,
|
||||||
|
),
|
||||||
|
_CircularThumb(backgroundColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CircularThumb extends StatelessWidget {
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
const _CircularThumb(this.backgroundColor);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CustomPaint(
|
||||||
|
foregroundPainter: const _ArrowPainter(Colors.white),
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(48.0),
|
||||||
|
bottomLeft: Radius.circular(48.0),
|
||||||
|
topRight: Radius.circular(4.0),
|
||||||
|
bottomRight: Radius.circular(4.0),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints.tight(const Size(48.0 * 0.6, 48.0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ArrowPainter extends CustomPainter {
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const _ArrowPainter(this.color);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()..color = color;
|
||||||
|
const width = 12.0;
|
||||||
|
const height = 8.0;
|
||||||
|
final baseX = size.width / 2;
|
||||||
|
final baseY = size.height / 2;
|
||||||
|
|
||||||
|
canvas.drawPath(
|
||||||
|
_trianglePath(Offset(baseX, baseY - 2.0), width, height, true),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
canvas.drawPath(
|
||||||
|
_trianglePath(Offset(baseX, baseY + 2.0), width, height, false),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Path _trianglePath(Offset o, double width, double height, bool isUp) {
|
||||||
|
return Path()
|
||||||
|
..moveTo(o.dx, o.dy)
|
||||||
|
..lineTo(o.dx + width, o.dy)
|
||||||
|
..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height)
|
||||||
|
..close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SlideFadeTransition extends StatelessWidget {
|
||||||
|
final Animation<double> _animation;
|
||||||
|
final Widget _child;
|
||||||
|
|
||||||
|
const _SlideFadeTransition({
|
||||||
|
required Animation<double> animation,
|
||||||
|
required Widget child,
|
||||||
|
}) : _animation = animation,
|
||||||
|
_child = child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _animation,
|
||||||
|
builder: (context, child) =>
|
||||||
|
_animation.value == 0.0 ? const SizedBox() : child!,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween(
|
||||||
|
begin: const Offset(0.3, 0.0),
|
||||||
|
end: const Offset(0.0, 0.0),
|
||||||
|
).animate(_animation),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _animation,
|
||||||
|
child: _child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Segment {
|
||||||
|
final DateTime date;
|
||||||
|
final double startOffset;
|
||||||
|
final String scrollLabel;
|
||||||
|
|
||||||
|
const _Segment({
|
||||||
|
required this.date,
|
||||||
|
required this.startOffset,
|
||||||
|
required this.scrollLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
_Segment copyWith({
|
||||||
|
DateTime? date,
|
||||||
|
double? startOffset,
|
||||||
|
String? scrollLabel,
|
||||||
|
}) {
|
||||||
|
return _Segment(
|
||||||
|
date: date ?? this.date,
|
||||||
|
startOffset: startOffset ?? this.startOffset,
|
||||||
|
scrollLabel: scrollLabel ?? this.scrollLabel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Segment(scrollLabel: $scrollLabel, date: $date)';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
|
||||||
|
// Segments are the time groups buckets in the timeline view.
|
||||||
|
// Each segment contains a header and a list of asset rows.
|
||||||
|
abstract class Segment {
|
||||||
|
// The index of the first row of the segment, usually the header, but if not, it can be any asset.
|
||||||
|
final int firstIndex;
|
||||||
|
// The index of the last asset of the segment.
|
||||||
|
final int lastIndex;
|
||||||
|
// The offset of the first row from beginning of the timeline.
|
||||||
|
final double startOffset;
|
||||||
|
// The offset of the last row from beginning of the timeline.
|
||||||
|
final double endOffset;
|
||||||
|
// The spacing between the header and the first row of the segment.
|
||||||
|
final double spacing;
|
||||||
|
final double headerExtent;
|
||||||
|
// the start index of the asset of this segment from the beginning of the timeline.
|
||||||
|
final int firstAssetIndex;
|
||||||
|
final Bucket bucket;
|
||||||
|
|
||||||
|
// The index of the row after the header
|
||||||
|
final int gridIndex;
|
||||||
|
// The offset of the row after the header
|
||||||
|
final double gridOffset;
|
||||||
|
// The type of the header
|
||||||
|
final HeaderType header;
|
||||||
|
|
||||||
|
const Segment({
|
||||||
|
required this.firstIndex,
|
||||||
|
required this.lastIndex,
|
||||||
|
required this.startOffset,
|
||||||
|
required this.endOffset,
|
||||||
|
required this.firstAssetIndex,
|
||||||
|
required this.bucket,
|
||||||
|
required this.headerExtent,
|
||||||
|
required this.spacing,
|
||||||
|
required this.header,
|
||||||
|
}) : gridIndex = firstIndex + 1,
|
||||||
|
gridOffset = startOffset + headerExtent + spacing;
|
||||||
|
|
||||||
|
bool containsIndex(int index) => firstIndex <= index && index <= lastIndex;
|
||||||
|
|
||||||
|
bool isWithinOffset(double offset) =>
|
||||||
|
startOffset <= offset && offset <= endOffset;
|
||||||
|
|
||||||
|
int getMinChildIndexForScrollOffset(double scrollOffset);
|
||||||
|
int getMaxChildIndexForScrollOffset(double scrollOffset);
|
||||||
|
double indexToLayoutOffset(int index);
|
||||||
|
|
||||||
|
Widget builder(BuildContext context, int index);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is Segment &&
|
||||||
|
other.firstIndex == firstIndex &&
|
||||||
|
other.lastIndex == lastIndex &&
|
||||||
|
other.startOffset == startOffset &&
|
||||||
|
other.endOffset == endOffset &&
|
||||||
|
other.spacing == spacing &&
|
||||||
|
other.firstAssetIndex == firstAssetIndex &&
|
||||||
|
other.headerExtent == headerExtent &&
|
||||||
|
other.gridIndex == gridIndex &&
|
||||||
|
other.gridOffset == gridOffset &&
|
||||||
|
other.header == header;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
firstIndex.hashCode ^
|
||||||
|
lastIndex.hashCode ^
|
||||||
|
startOffset.hashCode ^
|
||||||
|
endOffset.hashCode ^
|
||||||
|
spacing.hashCode ^
|
||||||
|
headerExtent.hashCode ^
|
||||||
|
firstAssetIndex.hashCode ^
|
||||||
|
gridIndex.hashCode ^
|
||||||
|
gridOffset.hashCode ^
|
||||||
|
header.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Segment(firstIndex: $firstIndex, lastIndex: $lastIndex)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SegmentListExtension on List<Segment> {
|
||||||
|
bool equals(List<Segment> other) =>
|
||||||
|
length == other.length &&
|
||||||
|
lastOrNull?.endOffset == other.lastOrNull?.endOffset;
|
||||||
|
|
||||||
|
Segment? findByIndex(int index) =>
|
||||||
|
firstWhereOrNull((s) => s.containsIndex(index));
|
||||||
|
|
||||||
|
Segment? findByOffset(double offset) =>
|
||||||
|
firstWhereOrNull((s) => s.isWithinOffset(offset)) ?? lastOrNull;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||||
|
|
||||||
|
abstract class SegmentBuilder {
|
||||||
|
final List<Bucket> buckets;
|
||||||
|
final double spacing;
|
||||||
|
final GroupAssetsBy groupBy;
|
||||||
|
|
||||||
|
const SegmentBuilder({
|
||||||
|
required this.buckets,
|
||||||
|
this.spacing = kTimelineSpacing,
|
||||||
|
this.groupBy = GroupAssetsBy.day,
|
||||||
|
});
|
||||||
|
|
||||||
|
static double headerExtent(HeaderType header) {
|
||||||
|
switch (header) {
|
||||||
|
case HeaderType.month:
|
||||||
|
return kTimelineHeaderExtent;
|
||||||
|
case HeaderType.day:
|
||||||
|
return kTimelineHeaderExtent * 0.90;
|
||||||
|
case HeaderType.monthAndDay:
|
||||||
|
return kTimelineHeaderExtent * 1.5;
|
||||||
|
case HeaderType.none:
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget buildPlaceholder(
|
||||||
|
BuildContext context,
|
||||||
|
int count, {
|
||||||
|
Size size = const Size.square(kTimelineFixedTileExtent),
|
||||||
|
double spacing = kTimelineSpacing,
|
||||||
|
}) =>
|
||||||
|
RepaintBoundary(
|
||||||
|
child: FixedTimelineRow(
|
||||||
|
dimension: size.height,
|
||||||
|
spacing: spacing,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
children: List.generate(
|
||||||
|
count,
|
||||||
|
(_) => ThumbnailPlaceholder(width: size.width, height: size.height),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builder.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
|
class TimelineArgs {
|
||||||
|
final double maxWidth;
|
||||||
|
final double maxHeight;
|
||||||
|
final double spacing;
|
||||||
|
final int columnCount;
|
||||||
|
|
||||||
|
const TimelineArgs({
|
||||||
|
required this.maxWidth,
|
||||||
|
required this.maxHeight,
|
||||||
|
this.spacing = kTimelineSpacing,
|
||||||
|
this.columnCount = kTimelineColumnCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant TimelineArgs other) {
|
||||||
|
return spacing == other.spacing &&
|
||||||
|
maxWidth == other.maxWidth &&
|
||||||
|
maxHeight == other.maxHeight &&
|
||||||
|
columnCount == other.columnCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
maxWidth.hashCode ^
|
||||||
|
maxHeight.hashCode ^
|
||||||
|
spacing.hashCode ^
|
||||||
|
columnCount.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineState {
|
||||||
|
final bool isScrubbing;
|
||||||
|
|
||||||
|
const TimelineState({this.isScrubbing = false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant TimelineState other) {
|
||||||
|
return isScrubbing == other.isScrubbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => isScrubbing.hashCode;
|
||||||
|
|
||||||
|
TimelineState copyWith({bool? isScrubbing}) {
|
||||||
|
return TimelineState(isScrubbing: isScrubbing ?? this.isScrubbing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineStateNotifier extends Notifier<TimelineState> {
|
||||||
|
TimelineStateNotifier();
|
||||||
|
|
||||||
|
void setScrubbing(bool isScrubbing) {
|
||||||
|
state = state.copyWith(isScrubbing: isScrubbing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelineState build() => const TimelineState(isScrubbing: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This provider watches the buckets from the timeline service & args and serves the segments.
|
||||||
|
// It should be used only after the timeline service and timeline args provider is overridden
|
||||||
|
final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>(
|
||||||
|
(ref) async* {
|
||||||
|
final args = ref.watch(timelineArgsProvider);
|
||||||
|
final columnCount = args.columnCount;
|
||||||
|
final spacing = args.spacing;
|
||||||
|
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
|
||||||
|
final tileExtent = math.max(0, availableTileWidth) / columnCount;
|
||||||
|
|
||||||
|
final groupBy = GroupAssetsBy
|
||||||
|
.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)];
|
||||||
|
|
||||||
|
final timelineService = ref.watch(timelineServiceProvider);
|
||||||
|
yield* timelineService.watchBuckets().map((buckets) {
|
||||||
|
return FixedSegmentBuilder(
|
||||||
|
buckets: buckets,
|
||||||
|
tileHeight: tileExtent,
|
||||||
|
columnCount: columnCount,
|
||||||
|
spacing: spacing,
|
||||||
|
groupBy: groupBy,
|
||||||
|
).generate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
dependencies: [timelineServiceProvider, timelineArgsProvider],
|
||||||
|
);
|
||||||
|
|
||||||
|
final timelineStateProvider =
|
||||||
|
NotifierProvider<TimelineStateNotifier, TimelineState>(
|
||||||
|
TimelineStateNotifier.new,
|
||||||
|
);
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
|
class Timeline extends StatelessWidget {
|
||||||
|
const Timeline({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: LayoutBuilder(
|
||||||
|
builder: (_, constraints) => ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineArgsProvider.overrideWith(
|
||||||
|
(ref) => TimelineArgs(
|
||||||
|
maxWidth: constraints.maxWidth,
|
||||||
|
maxHeight: constraints.maxHeight,
|
||||||
|
columnCount: ref.watch(
|
||||||
|
settingsProvider.select((s) => s.get(Setting.tilesPerRow)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const _SliverTimeline(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliverTimeline extends StatefulWidget {
|
||||||
|
const _SliverTimeline();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => _SliverTimelineState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliverTimelineState extends State<_SliverTimeline> {
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext _) {
|
||||||
|
return Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final asyncSegments = ref.watch(timelineSegmentProvider);
|
||||||
|
final maxHeight =
|
||||||
|
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
||||||
|
return asyncSegments.widgetWhen(
|
||||||
|
onData: (segments) {
|
||||||
|
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
||||||
|
|
||||||
|
return PrimaryScrollController(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: Scrubber(
|
||||||
|
layoutSegments: segments,
|
||||||
|
timelineHeight: maxHeight,
|
||||||
|
topPadding: context.padding.top + 10,
|
||||||
|
bottomPadding: context.padding.bottom + 10,
|
||||||
|
child: CustomScrollView(
|
||||||
|
primary: true,
|
||||||
|
cacheExtent: maxHeight * 2,
|
||||||
|
slivers: [
|
||||||
|
_SliverSegmentedList(
|
||||||
|
segments: segments,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(ctx, index) {
|
||||||
|
if (index >= childCount) return null;
|
||||||
|
final segment = segments.findByIndex(index);
|
||||||
|
return segment?.builder(ctx, index) ??
|
||||||
|
const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
childCount: childCount,
|
||||||
|
addAutomaticKeepAlives: false,
|
||||||
|
// We add repaint boundary around tiles, so skip the auto boundaries
|
||||||
|
addRepaintBoundaries: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliverSegmentedList extends SliverMultiBoxAdaptorWidget {
|
||||||
|
final List<Segment> _segments;
|
||||||
|
|
||||||
|
const _SliverSegmentedList({
|
||||||
|
required List<Segment> segments,
|
||||||
|
required super.delegate,
|
||||||
|
}) : _segments = segments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) =>
|
||||||
|
_RenderSliverTimelineBoxAdaptor(
|
||||||
|
childManager: context as SliverMultiBoxAdaptorElement,
|
||||||
|
segments: _segments,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(
|
||||||
|
BuildContext context,
|
||||||
|
_RenderSliverTimelineBoxAdaptor renderObject,
|
||||||
|
) {
|
||||||
|
renderObject.segments = _segments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modified version of [RenderSliverFixedExtentBoxAdaptor] to use precomputed offsets
|
||||||
|
class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
|
||||||
|
List<Segment> _segments;
|
||||||
|
|
||||||
|
set segments(List<Segment> updatedSegments) {
|
||||||
|
if (_segments.equals(updatedSegments)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_segments = updatedSegments;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
_RenderSliverTimelineBoxAdaptor({
|
||||||
|
required super.childManager,
|
||||||
|
required List<Segment> segments,
|
||||||
|
}) : _segments = segments;
|
||||||
|
|
||||||
|
int getMinChildIndexForScrollOffset(double offset) =>
|
||||||
|
_segments.findByOffset(offset)?.getMinChildIndexForScrollOffset(offset) ??
|
||||||
|
0;
|
||||||
|
|
||||||
|
int getMaxChildIndexForScrollOffset(double offset) =>
|
||||||
|
_segments.findByOffset(offset)?.getMaxChildIndexForScrollOffset(offset) ??
|
||||||
|
0;
|
||||||
|
|
||||||
|
double indexToLayoutOffset(int index) =>
|
||||||
|
(_segments.findByIndex(index) ?? _segments.lastOrNull)
|
||||||
|
?.indexToLayoutOffset(index) ??
|
||||||
|
0;
|
||||||
|
|
||||||
|
double estimateMaxScrollOffset() => _segments.lastOrNull?.endOffset ?? 0;
|
||||||
|
|
||||||
|
double computeMaxScrollOffset() => _segments.lastOrNull?.endOffset ?? 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
childManager.didStartLayout();
|
||||||
|
// Assume initially that we have enough children to fill the viewport/cache area.
|
||||||
|
childManager.setDidUnderflow(false);
|
||||||
|
|
||||||
|
final double scrollOffset =
|
||||||
|
constraints.scrollOffset + constraints.cacheOrigin;
|
||||||
|
assert(scrollOffset >= 0.0);
|
||||||
|
|
||||||
|
final double remainingExtent = constraints.remainingCacheExtent;
|
||||||
|
assert(remainingExtent >= 0.0);
|
||||||
|
|
||||||
|
final double targetScrollOffset = scrollOffset + remainingExtent;
|
||||||
|
|
||||||
|
// Find the index of the first child that should be visible or in the leading cache area.
|
||||||
|
final int firstRequiredChildIndex =
|
||||||
|
getMinChildIndexForScrollOffset(scrollOffset);
|
||||||
|
|
||||||
|
// Find the index of the last child that should be visible or in the trailing cache area.
|
||||||
|
final int? lastRequiredChildIndex = targetScrollOffset.isFinite
|
||||||
|
? getMaxChildIndexForScrollOffset(targetScrollOffset)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Remove children that are no longer visible or within the cache area.
|
||||||
|
if (firstChild == null) {
|
||||||
|
collectGarbage(0, 0);
|
||||||
|
} else {
|
||||||
|
final int leadingChildrenToRemove =
|
||||||
|
calculateLeadingGarbage(firstIndex: firstRequiredChildIndex);
|
||||||
|
final int trailingChildrenToRemove = lastRequiredChildIndex == null
|
||||||
|
? 0
|
||||||
|
: calculateTrailingGarbage(lastIndex: lastRequiredChildIndex);
|
||||||
|
collectGarbage(leadingChildrenToRemove, trailingChildrenToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are currently no children laid out (e.g., initial load),
|
||||||
|
// try to add the first child needed for the current scroll offset.
|
||||||
|
if (firstChild == null) {
|
||||||
|
final double firstChildLayoutOffset =
|
||||||
|
indexToLayoutOffset(firstRequiredChildIndex);
|
||||||
|
final bool childAdded = addInitialChild(
|
||||||
|
index: firstRequiredChildIndex,
|
||||||
|
layoutOffset: firstChildLayoutOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!childAdded) {
|
||||||
|
// There are either no children, or we are past the end of all our children.
|
||||||
|
final double max =
|
||||||
|
firstRequiredChildIndex <= 0 ? 0.0 : computeMaxScrollOffset();
|
||||||
|
geometry = SliverGeometry(scrollExtent: max, maxPaintExtent: max);
|
||||||
|
childManager.didFinishLayout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout children that might have scrolled into view from the top (before the current firstChild).
|
||||||
|
RenderBox? highestLaidOutChild;
|
||||||
|
final childConstraints = constraints.asBoxConstraints();
|
||||||
|
|
||||||
|
for (int currentIndex = indexOf(firstChild!) - 1;
|
||||||
|
currentIndex >= firstRequiredChildIndex;
|
||||||
|
--currentIndex) {
|
||||||
|
final RenderBox? newLeadingChild =
|
||||||
|
insertAndLayoutLeadingChild(childConstraints);
|
||||||
|
if (newLeadingChild == null) {
|
||||||
|
// If a child is missing where we expect one, it indicates
|
||||||
|
// an inconsistency in offset that needs correction.
|
||||||
|
final Segment? segment =
|
||||||
|
_segments.findByIndex(currentIndex) ?? _segments.firstOrNull;
|
||||||
|
geometry = SliverGeometry(
|
||||||
|
// Request a scroll correction based on where the missing child should have been.
|
||||||
|
scrollOffsetCorrection:
|
||||||
|
segment?.indexToLayoutOffset(currentIndex) ?? 0.0,
|
||||||
|
);
|
||||||
|
// Parent will re-layout everything.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final childParentData =
|
||||||
|
newLeadingChild.parentData! as SliverMultiBoxAdaptorParentData;
|
||||||
|
childParentData.layoutOffset = indexToLayoutOffset(currentIndex);
|
||||||
|
assert(childParentData.index == currentIndex);
|
||||||
|
highestLaidOutChild ??= newLeadingChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the loop above didn't run (meaning the firstChild was already the correct [firstRequiredChildIndex]),
|
||||||
|
// or even if it did, we need to ensure the first visible child is correctly laid out
|
||||||
|
// and establish our starting point for laying out trailing children.
|
||||||
|
|
||||||
|
// If [highestLaidOutChild] is still null, it means the loop above didn't add any new leading children.
|
||||||
|
// The [firstChild] that existed at the start of performLayout is still the first one we need.
|
||||||
|
if (highestLaidOutChild == null) {
|
||||||
|
firstChild!.layout(childConstraints);
|
||||||
|
final childParentData =
|
||||||
|
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
|
||||||
|
childParentData.layoutOffset =
|
||||||
|
indexToLayoutOffset(firstRequiredChildIndex);
|
||||||
|
highestLaidOutChild = firstChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderBox? mostRecentlyLaidOutChild = highestLaidOutChild;
|
||||||
|
|
||||||
|
// Starting from the child after [mostRecentlyLaidOutChild], layout subsequent children
|
||||||
|
// until we reach the [lastRequiredChildIndex] or run out of children.
|
||||||
|
double calculatedMaxScrollOffset = double.infinity;
|
||||||
|
|
||||||
|
for (int currentIndex = indexOf(mostRecentlyLaidOutChild!) + 1;
|
||||||
|
lastRequiredChildIndex == null ||
|
||||||
|
currentIndex <= lastRequiredChildIndex;
|
||||||
|
++currentIndex) {
|
||||||
|
RenderBox? child = childAfter(mostRecentlyLaidOutChild!);
|
||||||
|
|
||||||
|
if (child == null || indexOf(child) != currentIndex) {
|
||||||
|
child = insertAndLayoutChild(
|
||||||
|
childConstraints,
|
||||||
|
after: mostRecentlyLaidOutChild,
|
||||||
|
);
|
||||||
|
if (child == null) {
|
||||||
|
final Segment? segment =
|
||||||
|
_segments.findByIndex(currentIndex) ?? _segments.lastOrNull;
|
||||||
|
calculatedMaxScrollOffset =
|
||||||
|
segment?.indexToLayoutOffset(currentIndex) ??
|
||||||
|
computeMaxScrollOffset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
child.layout(childConstraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
mostRecentlyLaidOutChild = child;
|
||||||
|
final childParentData = mostRecentlyLaidOutChild.parentData!
|
||||||
|
as SliverMultiBoxAdaptorParentData;
|
||||||
|
assert(childParentData.index == currentIndex);
|
||||||
|
childParentData.layoutOffset = indexToLayoutOffset(currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int lastLaidOutChildIndex = indexOf(lastChild!);
|
||||||
|
final double leadingScrollOffset =
|
||||||
|
indexToLayoutOffset(firstRequiredChildIndex);
|
||||||
|
final double trailingScrollOffset =
|
||||||
|
indexToLayoutOffset(lastLaidOutChildIndex + 1);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
firstRequiredChildIndex == 0 ||
|
||||||
|
(childScrollOffset(firstChild!) ?? -1.0) - scrollOffset <=
|
||||||
|
precisionErrorTolerance,
|
||||||
|
);
|
||||||
|
assert(debugAssertChildListIsNonEmptyAndContiguous());
|
||||||
|
assert(indexOf(firstChild!) == firstRequiredChildIndex);
|
||||||
|
assert(
|
||||||
|
lastRequiredChildIndex == null ||
|
||||||
|
lastLaidOutChildIndex <= lastRequiredChildIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
calculatedMaxScrollOffset = math.min(
|
||||||
|
calculatedMaxScrollOffset,
|
||||||
|
estimateMaxScrollOffset(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final double paintExtent = calculatePaintOffset(
|
||||||
|
constraints,
|
||||||
|
from: leadingScrollOffset,
|
||||||
|
to: trailingScrollOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
final double cacheExtent = calculateCacheOffset(
|
||||||
|
constraints,
|
||||||
|
from: leadingScrollOffset,
|
||||||
|
to: trailingScrollOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
final double targetEndScrollOffsetForPaint =
|
||||||
|
constraints.scrollOffset + constraints.remainingPaintExtent;
|
||||||
|
final int? targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite
|
||||||
|
? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final maxPaintExtent = math.max(paintExtent, calculatedMaxScrollOffset);
|
||||||
|
|
||||||
|
geometry = SliverGeometry(
|
||||||
|
scrollExtent: calculatedMaxScrollOffset,
|
||||||
|
paintExtent: paintExtent,
|
||||||
|
maxPaintExtent: maxPaintExtent,
|
||||||
|
// Indicates if there's content scrolled off-screen.
|
||||||
|
// This is true if the last child needed for painting is actually laid out,
|
||||||
|
// or if the first child is partially visible.
|
||||||
|
hasVisualOverflow: (targetLastIndexForPaint != null &&
|
||||||
|
lastLaidOutChildIndex >= targetLastIndexForPaint) ||
|
||||||
|
constraints.scrollOffset > 0.0,
|
||||||
|
cacheExtent: cacheExtent,
|
||||||
|
);
|
||||||
|
|
||||||
|
// We may have started the layout while scrolled to the end, which would not
|
||||||
|
// expose a new child.
|
||||||
|
if (calculatedMaxScrollOffset == trailingScrollOffset) {
|
||||||
|
childManager.setDidUnderflow(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
childManager.didFinishLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-2
@@ -37,8 +37,7 @@ class ImageLoader {
|
|||||||
} else if (result is FileInfo) {
|
} else if (result is FileInfo) {
|
||||||
// We have the file
|
// We have the file
|
||||||
final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path);
|
final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path);
|
||||||
final decoded = await decode(buffer);
|
return decode(buffer);
|
||||||
return decoded;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
||||||
|
|
||||||
|
class SettingsNotifier extends Notifier<SettingsService> {
|
||||||
|
@override
|
||||||
|
SettingsService build() =>
|
||||||
|
SettingsService(storeService: ref.read(storeServiceProvider));
|
||||||
|
|
||||||
|
T get<T>(Setting<T> setting) => state.get(setting);
|
||||||
|
|
||||||
|
Future<void> set<T>(Setting<T> setting, T value) async {
|
||||||
|
await state.set(setting, value);
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<T> watch<T>(Setting<T> setting) => state.watch(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
final settingsProvider =
|
||||||
|
NotifierProvider<SettingsNotifier, SettingsService>(SettingsNotifier.new);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/timeline.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
|
|
||||||
|
final timelineRepositoryProvider = Provider<ITimelineRepository>(
|
||||||
|
(ref) => DriftTimelineRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final timelineArgsProvider = Provider.autoDispose<TimelineArgs>(
|
||||||
|
(ref) =>
|
||||||
|
throw UnimplementedError('Will be overridden through a ProviderScope.'),
|
||||||
|
);
|
||||||
|
|
||||||
|
final timelineServiceProvider = Provider.autoDispose<TimelineService>(
|
||||||
|
(ref) =>
|
||||||
|
throw UnimplementedError('Will be overridden through a ProviderScope.'),
|
||||||
|
);
|
||||||
|
|
||||||
|
final timelineFactoryProvider = Provider<TimelineFactory>(
|
||||||
|
(ref) => TimelineFactory(
|
||||||
|
timelineRepository: ref.watch(timelineRepositoryProvider),
|
||||||
|
settingsService: ref.watch(settingsProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -35,10 +35,8 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository {
|
|||||||
db.albums.clear(),
|
db.albums.clear(),
|
||||||
db.eTags.clear(),
|
db.eTags.clear(),
|
||||||
db.users.clear(),
|
db.users.clear(),
|
||||||
_drift.remoteExifEntity.deleteAll(),
|
|
||||||
_drift.remoteAssetEntity.deleteAll(),
|
_drift.remoteAssetEntity.deleteAll(),
|
||||||
_drift.remoteAlbumEntity.deleteAll(),
|
_drift.remoteExifEntity.deleteAll(),
|
||||||
_drift.remoteAlbumAssetEntity.deleteAll(),
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
|||||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||||
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
@@ -330,5 +332,13 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: RemoteMediaSummaryRoute.page,
|
page: RemoteMediaSummaryRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: LocalTimelineRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: MainTimelineRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -871,6 +871,43 @@ class LocalMediaSummaryRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [LocalTimelinePage]
|
||||||
|
class LocalTimelineRoute extends PageRouteInfo<LocalTimelineRouteArgs> {
|
||||||
|
LocalTimelineRoute({
|
||||||
|
Key? key,
|
||||||
|
required String albumId,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
LocalTimelineRoute.name,
|
||||||
|
args: LocalTimelineRouteArgs(key: key, albumId: albumId),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'LocalTimelineRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
final args = data.argsAs<LocalTimelineRouteArgs>();
|
||||||
|
return LocalTimelinePage(key: args.key, albumId: args.albumId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalTimelineRouteArgs {
|
||||||
|
const LocalTimelineRouteArgs({this.key, required this.albumId});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final String albumId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'LocalTimelineRouteArgs{key: $key, albumId: $albumId}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [LockedPage]
|
/// [LockedPage]
|
||||||
class LockedRoute extends PageRouteInfo<void> {
|
class LockedRoute extends PageRouteInfo<void> {
|
||||||
@@ -903,6 +940,22 @@ class LoginRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [MainTimelinePage]
|
||||||
|
class MainTimelineRoute extends PageRouteInfo<void> {
|
||||||
|
const MainTimelineRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(MainTimelineRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'MainTimelineRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const MainTimelinePage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [MapLocationPickerPage]
|
/// [MapLocationPickerPage]
|
||||||
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/translation.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
|
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
|
||||||
@@ -40,7 +40,11 @@ class MemoryService {
|
|||||||
.getAllByRemoteId(memory.assets.map((e) => e.id));
|
.getAllByRemoteId(memory.assets.map((e) => e.id));
|
||||||
final yearsAgo = now.year - memory.data.year;
|
final yearsAgo = now.year - memory.data.year;
|
||||||
if (dbAssets.isNotEmpty) {
|
if (dbAssets.isNotEmpty) {
|
||||||
final String title = t('years_ago', {'years': yearsAgo.toString()});
|
final String title = 'years_ago'.t(
|
||||||
|
args: {
|
||||||
|
'years': yearsAgo.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
memories.add(
|
memories.add(
|
||||||
Memory(
|
Memory(
|
||||||
title: title,
|
title: title,
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/services/asset.service.dart';
|
import 'package:immich_mobile/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/services/share.service.dart';
|
import 'package:immich_mobile/services/share.service.dart';
|
||||||
import 'package:immich_mobile/utils/translation.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||||
@@ -59,10 +59,11 @@ Future<void> handleArchiveAssets(
|
|||||||
await ref
|
await ref
|
||||||
.read(assetProvider.notifier)
|
.read(assetProvider.notifier)
|
||||||
.toggleArchive(selection, shouldArchive);
|
.toggleArchive(selection, shouldArchive);
|
||||||
|
|
||||||
final message = shouldArchive
|
final message = shouldArchive
|
||||||
? t('moved_to_archive', {'count': selection.length})
|
? 'moved_to_archive'
|
||||||
: t('moved_to_library', {'count': selection.length});
|
.t(context: context, args: {'count': selection.length})
|
||||||
|
: 'moved_to_library'
|
||||||
|
.t(context: context, args: {'count': selection.length});
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
|
||||||
|
String getAltText(
|
||||||
|
ExifInfo? exifInfo,
|
||||||
|
DateTime fileCreatedAt,
|
||||||
|
AssetType type,
|
||||||
|
List<String> peopleNames,
|
||||||
|
) {
|
||||||
|
if (exifInfo?.description != null && exifInfo!.description!.isNotEmpty) {
|
||||||
|
return exifInfo.description!;
|
||||||
|
}
|
||||||
|
final (template, args) =
|
||||||
|
getAltTextTemplate(exifInfo, fileCreatedAt, type, peopleNames);
|
||||||
|
return template.t(args: args);
|
||||||
|
}
|
||||||
|
|
||||||
|
(String, Map<String, String>) getAltTextTemplate(
|
||||||
|
ExifInfo? exifInfo,
|
||||||
|
DateTime fileCreatedAt,
|
||||||
|
AssetType type,
|
||||||
|
List<String> peopleNames,
|
||||||
|
) {
|
||||||
|
final isVideo = type == AssetType.video;
|
||||||
|
final hasLocation = exifInfo?.city != null && exifInfo?.country != null;
|
||||||
|
final date = DateFormat.yMMMMd().format(fileCreatedAt);
|
||||||
|
final args = {
|
||||||
|
"isVideo": isVideo.toString(),
|
||||||
|
"date": date,
|
||||||
|
"city": exifInfo?.city ?? "",
|
||||||
|
"country": exifInfo?.country ?? "",
|
||||||
|
"person1": peopleNames.elementAtOrNull(0) ?? "",
|
||||||
|
"person2": peopleNames.elementAtOrNull(1) ?? "",
|
||||||
|
"person3": peopleNames.elementAtOrNull(2) ?? "",
|
||||||
|
"additionalCount": (peopleNames.length - 3).toString(),
|
||||||
|
};
|
||||||
|
final template = hasLocation
|
||||||
|
? (switch (peopleNames.length) {
|
||||||
|
0 => "image_alt_text_date_place",
|
||||||
|
1 => "image_alt_text_date_place_1_person",
|
||||||
|
2 => "image_alt_text_date_place_2_people",
|
||||||
|
3 => "image_alt_text_date_place_3_people",
|
||||||
|
_ => "image_alt_text_date_place_4_or_more_people"
|
||||||
|
})
|
||||||
|
: (switch (peopleNames.length) {
|
||||||
|
0 => "image_alt_text_date",
|
||||||
|
1 => "image_alt_text_date_1_person",
|
||||||
|
2 => "image_alt_text_date_2_people",
|
||||||
|
3 => "image_alt_text_date_3_people",
|
||||||
|
_ => "image_alt_text_date_4_or_more_people"
|
||||||
|
});
|
||||||
|
return (template, args);
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:intl/message_format.dart';
|
|
||||||
|
|
||||||
String t(String key, [Map<String, Object>? args]) {
|
|
||||||
try {
|
|
||||||
String message = key.tr();
|
|
||||||
if (args != null) {
|
|
||||||
return MessageFormat(message, locale: Intl.defaultLocale ?? 'en')
|
|
||||||
.format(args);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
} catch (e) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/utils/translation.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||||
|
|
||||||
class AlbumThumbnailCard extends ConsumerWidget {
|
class AlbumThumbnailCard extends ConsumerWidget {
|
||||||
@@ -62,7 +62,12 @@ class AlbumThumbnailCard extends ConsumerWidget {
|
|||||||
if (album.ownerId == ref.read(currentUserProvider)?.id) {
|
if (album.ownerId == ref.read(currentUserProvider)?.id) {
|
||||||
owner = 'owned'.tr();
|
owner = 'owned'.tr();
|
||||||
} else if (album.ownerName != null) {
|
} else if (album.ownerName != null) {
|
||||||
owner = t('shared_by_user', {'user': album.ownerName!});
|
owner = 'shared_by_user'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'user': album.ownerName!,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +75,12 @@ class AlbumThumbnailCard extends ConsumerWidget {
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: t('items_count', {'count': album.assetCount}),
|
text: 'items_count'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'count': album.assetCount,
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (owner != null) const TextSpan(text: ' • '),
|
if (owner != null) const TextSpan(text: ' • '),
|
||||||
if (owner != null) TextSpan(text: owner),
|
if (owner != null) TextSpan(text: owner),
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:immich_mobile/utils/translation.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumThumbnailListTile extends StatelessWidget {
|
class AlbumThumbnailListTile extends StatelessWidget {
|
||||||
@@ -91,7 +91,12 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
t('items_count', {'count': album.assetCount}),
|
'items_count'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'count': album.assetCount,
|
||||||
|
},
|
||||||
|
),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -74,10 +75,14 @@ class GroupDividerTitle extends HookConsumerWidget {
|
|||||||
? Icon(
|
? Icon(
|
||||||
Icons.check_circle_rounded,
|
Icons.check_circle_rounded,
|
||||||
color: context.primaryColor,
|
color: context.primaryColor,
|
||||||
|
semanticLabel:
|
||||||
|
"unselect_all_in".tr(namedArgs: {"group": text}),
|
||||||
)
|
)
|
||||||
: Icon(
|
: Icon(
|
||||||
Icons.check_circle_outline_rounded,
|
Icons.check_circle_outline_rounded,
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
|
semanticLabel:
|
||||||
|
"select_all_in".tr(namedArgs: {"group": text}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/asset_selection_state.dart';
|
import 'package:immich_mobile/models/asset_selection_state.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
@@ -24,7 +25,6 @@ import 'package:immich_mobile/services/album.service.dart';
|
|||||||
import 'package:immich_mobile/services/stack.service.dart';
|
import 'package:immich_mobile/services/stack.service.dart';
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/utils/selection_handlers.dart';
|
import 'package:immich_mobile/utils/selection_handlers.dart';
|
||||||
import 'package:immich_mobile/utils/translation.dart';
|
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart';
|
import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart';
|
||||||
@@ -257,13 +257,19 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
final failedCount = totalCount - successCount;
|
final failedCount = totalCount - successCount;
|
||||||
|
|
||||||
final msg = failedCount > 0
|
final msg = failedCount > 0
|
||||||
? t('assets_downloaded_failed', {
|
? 'assets_downloaded_failed'.t(
|
||||||
'count': successCount,
|
context: context,
|
||||||
'error': failedCount,
|
args: {
|
||||||
})
|
'count': successCount,
|
||||||
: t('assets_downloaded_successfully', {
|
'error': failedCount,
|
||||||
'count': successCount,
|
},
|
||||||
});
|
)
|
||||||
|
: 'assets_downloaded_successfully'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'count': successCount,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
||||||
import 'package:immich_mobile/utils/translation.dart';
|
|
||||||
|
|
||||||
/// This is a simple debug widget which should be removed later on when we are
|
/// This is a simple debug widget which should be removed later on when we are
|
||||||
/// more confident about background sync
|
/// more confident about background sync
|
||||||
@@ -22,29 +22,28 @@ class IosDebugInfoTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
if (processes == 0) {
|
if (processes == 0) {
|
||||||
title = 'ios_debug_info_no_processes_queued'.tr();
|
title = 'ios_debug_info_no_processes_queued'.t(context: context);
|
||||||
} else {
|
} else {
|
||||||
title = t('ios_debug_info_processes_queued', {'count': processes});
|
title = 'ios_debug_info_processes_queued'
|
||||||
|
.t(context: context, args: {'count': processes});
|
||||||
}
|
}
|
||||||
|
|
||||||
final df = DateFormat.yMd().add_jm();
|
final df = DateFormat.yMd().add_jm();
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
if (fetch == null && processing == null) {
|
if (fetch == null && processing == null) {
|
||||||
subtitle = 'ios_debug_info_no_sync_yet'.tr();
|
subtitle = 'ios_debug_info_no_sync_yet'.t(context: context);
|
||||||
} else if (fetch != null && processing == null) {
|
} else if (fetch != null && processing == null) {
|
||||||
subtitle =
|
subtitle = 'ios_debug_info_fetch_ran_at'
|
||||||
t('ios_debug_info_fetch_ran_at', {'dateTime': df.format(fetch)});
|
.t(context: context, args: {'dateTime': df.format(fetch)});
|
||||||
} else if (processing != null && fetch == null) {
|
} else if (processing != null && fetch == null) {
|
||||||
subtitle = t(
|
subtitle = 'ios_debug_info_processing_ran_at'
|
||||||
'ios_debug_info_processing_ran_at',
|
.t(context: context, args: {'dateTime': df.format(processing)});
|
||||||
{'dateTime': df.format(processing)},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
final fetchOrProcessing =
|
final fetchOrProcessing =
|
||||||
fetch!.isAfter(processing!) ? fetch : processing;
|
fetch!.isAfter(processing!) ? fetch : processing;
|
||||||
subtitle = t(
|
subtitle = 'ios_debug_info_last_sync_at'.t(
|
||||||
'ios_debug_info_last_sync_at',
|
context: context,
|
||||||
{'dateTime': df.format(fetchOrProcessing)},
|
args: {'dateTime': df.format(fetchOrProcessing)},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user