Compare commits

..

3 Commits

Author SHA1 Message Date
Arno Wiest
01cca7328d chore: added no-undef oxlint rule 2025-06-13 14:46:24 +02:00
Arno Wiest
cfe7fc675d chore: trying out biome v2 beta 2025-06-13 14:21:29 +02:00
Arno Wiest
229ce04118 chore: trying out oxlint 2025-06-13 00:02:05 +02:00
127 changed files with 4199 additions and 2655 deletions

2
.devcontainer/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
library

16
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
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}

View File

@@ -1,67 +1,26 @@
{
"name": "Immich - Backend, Frontend and ML",
"service": "immich-server",
"runServices": [
"immich-server",
"redis",
"database",
"immich-machine-learning"
],
"name": "Immich",
"service": "immich-devcontainer",
"dockerComposeFile": [
"../docker/docker-compose.dev.yml",
"./server/container-compose-overrides.yml"
"docker-compose.yml",
"../docker/docker-compose.dev.yml"
],
"customizations": {
"vscode": {
"extensions": [
"Dart-Code.dart-code",
"Dart-Code.flutter",
"dbaeumer.vscode-eslint",
"dcmdev.dcm-vscode-extension",
"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"
"svelte.svelte-vscode"
]
}
},
"forwardPorts": [3000, 9231, 9230, 2283],
"portsAttributes": {
"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"
}
},
"forwardPorts": [],
"initializeCommand": "bash .devcontainer/scripts/initializeCommand.sh",
"onCreateCommand": "bash .devcontainer/scripts/onCreateCommand.sh",
"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}"
}
"workspaceFolder": "/immich",
"remoteUser": "node"
}

View File

@@ -0,0 +1,8 @@
services:
immich-devcontainer:
build:
dockerfile: Dockerfile
extra_hosts:
- 'host.docker.internal:host-gateway'
volumes:
- ..:/immich:cached

View File

@@ -1,34 +0,0 @@
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:

View File

@@ -1,52 +0,0 @@
{
"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}"
}
}

View File

@@ -0,0 +1,6 @@
#!/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

View File

@@ -0,0 +1,25 @@
#!/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

View File

@@ -1,57 +0,0 @@
#!/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
)
}

View File

@@ -1,44 +0,0 @@
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:

View File

@@ -1,17 +0,0 @@
#!/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

View File

@@ -1,22 +0,0 @@
#!/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

View File

@@ -1,7 +0,0 @@
#!/bin/bash
# shellcheck source=common.sh
# shellcheck disable=SC1091
source /immich-devcontainer/container-common.sh
fix_permissions
install_dependencies

72
.vscode/tasks.json vendored
View File

@@ -1,72 +0,0 @@
{
"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": []
}
]
}

View File

@@ -70,10 +70,7 @@ const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4));
}
await updateAlbums([...newAssets, ...duplicates], options);
await deleteFiles(
newAssets.map(({ filepath }) => filepath),
options,
);
await deleteFiles(newFiles, options);
};
export const startWatch = async (

View File

@@ -1,481 +0,0 @@
---
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.
[![Open in VSCode Containers](https://img.shields.io/static/v1?label=VSCode%20DevContainer&message=Immich&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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)

View File

@@ -123,7 +123,7 @@ The default configuration looks like this:
"buttonText": "Login with OAuth",
"clientId": "",
"clientSecret": "",
"defaultStorageQuota": null,
"defaultStorageQuota": 0,
"enabled": false,
"issuerUrl": "",
"mobileOverrideEnabled": false,

View File

@@ -11,24 +11,11 @@ describe('/people', () => {
let hiddenPerson: PersonResponseDto;
let multipleAssetsPerson: PersonResponseDto;
let nameAlicePerson: PersonResponseDto;
let nameBobPerson: PersonResponseDto;
let nameCharliePerson: PersonResponseDto;
let nameNullPerson: PersonResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
[
visiblePerson,
hiddenPerson,
multipleAssetsPerson,
nameCharliePerson,
nameBobPerson,
nameAlicePerson,
nameNullPerson,
] = await Promise.all([
[visiblePerson, hiddenPerson, multipleAssetsPerson] = await Promise.all([
utils.createPerson(admin.accessToken, {
name: 'visible_person',
}),
@@ -39,24 +26,10 @@ describe('/people', () => {
utils.createPerson(admin.accessToken, {
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 asset2 = await utils.createAsset(admin.accessToken);
const asset3 = await utils.createAsset(admin.accessToken);
await Promise.all([
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
@@ -64,15 +37,6 @@ describe('/people', () => {
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: 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
]);
});
@@ -87,53 +51,26 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 7,
total: 3,
hidden: 1,
people: [
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: '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 () => {
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 7,
total: 3,
hidden: 1,
people: [
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'Bob' }),
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Charlie' }),
expect.objectContaining({ name: 'visible_person' }),
],
});
@@ -143,12 +80,12 @@ describe('/people', () => {
const { status, body } = await request(app)
.get('/people')
.set('Authorization', `Bearer ${admin.accessToken}`)
.query({ withHidden: true, page: 5, size: 1 });
.query({ withHidden: true, page: 2, size: 1 });
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: true,
total: 7,
total: 3,
hidden: 1,
people: [expect.objectContaining({ name: 'visible_person' })],
});
@@ -191,7 +128,7 @@ describe('/people', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ assets: 3 }));
expect(body).toEqual(expect.objectContaining({ assets: 2 }));
});
});

View File

@@ -219,7 +219,7 @@ describe('/timeline', () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ timeBucket: '1970-02-01', isTrashed: true });
.query({ timeBucket: '1970-02-01T00:00:00.000Z', isTrashed: true });
expect(status).toBe(200);

View File

@@ -216,11 +216,7 @@ export const utils = {
websocket
.on('connect', () => resolve(websocket))
.on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'assetUpload', id: data.id }))
.on('on_asset_update', (assetId: string[]) => {
for (const id of assetId) {
onEvent({ event: 'assetUpdate', id });
}
})
.on('on_asset_update', (data: AssetResponseDto) => onEvent({ event: 'assetUpdate', id: data.id }))
.on('on_asset_hidden', (assetId: string) => onEvent({ event: 'assetHidden', id: assetId }))
.on('on_asset_delete', (assetId: string) => onEvent({ event: 'assetDelete', id: assetId }))
.on('on_user_delete', (userId: string) => onEvent({ event: 'userDelete', id: userId }))

View File

@@ -204,7 +204,7 @@
"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_default": "Default storage quota (GiB)",
"oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided.",
"oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).",
"oauth_timeout": "Request Timeout",
"oauth_timeout_description": "Timeout for requests in milliseconds",
"password_enable_description": "Login with email and password",
@@ -1150,7 +1150,6 @@
"locked_folder": "Locked Folder",
"log_out": "Log out",
"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_device": "Logged out device",
"login": "Login",
@@ -1608,7 +1607,6 @@
"select_album_cover": "Select album cover",
"select_all": "Select all",
"select_all_duplicates": "Select all duplicates",
"select_all_in": "Select all in {group}",
"select_avatar_color": "Select avatar color",
"select_face": "Select face",
"select_featured_photo": "Select featured photo",
@@ -1873,7 +1871,6 @@
"unsaved_change": "Unsaved change",
"unselect_all": "Unselect all",
"unselect_all_duplicates": "Unselect all duplicates",
"unselect_all_in": "Unselect all in {group}",
"unstack": "Un-stack",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"up_next": "Up next",

View File

@@ -15,4 +15,13 @@ abstract interface class ISyncStreamRepository implements IDatabaseRepository {
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data);
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> 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);
}

View File

@@ -0,0 +1,68 @@
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;
}
}

View File

@@ -0,0 +1,5 @@
enum AlbumUserRole {
// do not change this order!
editor,
viewer,
}

View File

@@ -81,6 +81,18 @@ class SyncStreamService {
return _syncStreamRepository.deletePartnerAssetsV1(data.cast());
case SyncEntityType.partnerAssetExifV1:
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:
_logger.warning("Unknown sync data type: $type");
}

View File

@@ -0,0 +1,20 @@
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};
}

View File

@@ -0,0 +1,602 @@
// 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();
}
}

View File

@@ -0,0 +1,34 @@
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};
}

View File

@@ -0,0 +1,946 @@
// 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();
}
}

View File

@@ -0,0 +1,17 @@
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};
}

View File

@@ -0,0 +1,565 @@
// 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();
}
}

View File

@@ -3,12 +3,15 @@ import 'dart:async';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.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/local_album.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/partner.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/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:isar/isar.dart';
@@ -38,8 +41,11 @@ class IsarDatabaseRepository implements IDatabaseRepository {
LocalAlbumEntity,
LocalAssetEntity,
LocalAlbumAssetEntity,
RemoteAssetEntity,
RemoteExifEntity,
RemoteAssetEntity,
RemoteAlbumEntity,
RemoteAlbumAssetEntity,
AlbumUserEntity,
],
)
class Drift extends $Drift implements IDatabaseRepository {

View File

@@ -17,6 +17,12 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
as i7;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i8;
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
as i9;
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
as i10;
import 'package:immich_mobile/infrastructure/entities/album_user.entity.drift.dart'
as i11;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -36,6 +42,12 @@ abstract class $Drift extends i0.GeneratedDatabase {
i7.$RemoteAssetEntityTable(this);
late final i8.$RemoteExifEntityTable remoteExifEntity =
i8.$RemoteExifEntityTable(this);
late final i9.$RemoteAlbumEntityTable remoteAlbumEntity =
i9.$RemoteAlbumEntityTable(this);
late final i10.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity =
i10.$RemoteAlbumAssetEntityTable(this);
late final i11.$AlbumUserEntityTable albumUserEntity =
i11.$AlbumUserEntityTable(this);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -49,6 +61,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
localAlbumAssetEntity,
remoteAssetEntity,
remoteExifEntity,
remoteAlbumEntity,
remoteAlbumAssetEntity,
albumUserEntity,
i5.idxLocalAssetChecksum,
i7.uQRemoteAssetOwnerChecksum
];
@@ -108,6 +123,50 @@ abstract class $Drift extends i0.GeneratedDatabase {
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
@@ -134,4 +193,11 @@ class $DriftManager {
i7.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
i8.$$RemoteExifEntityTableTableManager get 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);
}

View File

@@ -50,6 +50,9 @@ class SyncApiRepository implements ISyncApiRepository {
SyncRequestType.partnerAssetsV1,
SyncRequestType.assetExifsV1,
SyncRequestType.partnerAssetExifsV1,
SyncRequestType.albumsV1,
// SyncRequestType.albumAssetsV1,
SyncRequestType.albumUsersV1,
],
).toJson(),
);
@@ -140,4 +143,10 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.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,
};

View File

@@ -3,12 +3,19 @@ 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/infrastructure/entities/exif.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/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
import 'package:openapi/api.dart' as api
show AssetVisibility, AssetOrder, AlbumUserRole;
import 'package:openapi/api.dart'
hide AssetVisibility, AssetOrder, AlbumUserRole;
class DriftSyncStreamRepository extends DriftDatabaseRepository
implements ISyncStreamRepository {
@@ -161,6 +168,135 @@ 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) =>
_db.batch((batch) {
for (final asset in data) {
@@ -251,3 +387,19 @@ extension on api.AssetVisibility {
_ => 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'),
};
}

View File

@@ -61,8 +61,10 @@ final _features = [
icon: Icons.delete_sweep_rounded,
onTap: (_, ref) async {
final db = ref.read(driftProvider);
await db.remoteAssetEntity.deleteAll();
await db.remoteExifEntity.deleteAll();
await db.remoteAssetEntity.deleteAll();
await db.remoteAlbumEntity.deleteAll();
await db.remoteAlbumAssetEntity.deleteAll();
},
),
_Feature(

View File

@@ -130,6 +130,10 @@ final _remoteStats = [
name: 'Exif Entities',
load: (db) => db.managers.remoteExifEntity.count(),
),
_Stat(
name: 'Remote Albums',
load: (db) => db.managers.remoteAlbumEntity.count(),
),
];
@RoutePage()

View File

@@ -35,8 +35,10 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository {
db.albums.clear(),
db.eTags.clear(),
db.users.clear(),
_drift.remoteAssetEntity.deleteAll(),
_drift.remoteExifEntity.deleteAll(),
_drift.remoteAssetEntity.deleteAll(),
_drift.remoteAlbumEntity.deleteAll(),
_drift.remoteAlbumAssetEntity.deleteAll(),
]);
});
}

View File

@@ -1,55 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/utils/translation.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 t(template, 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);
}

View File

@@ -1,4 +1,3 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -75,14 +74,10 @@ class GroupDividerTitle extends HookConsumerWidget {
? Icon(
Icons.check_circle_rounded,
color: context.primaryColor,
semanticLabel:
"unselect_all_in".tr(namedArgs: {"group": text}),
)
: Icon(
Icons.check_circle_outline_rounded,
color: context.colorScheme.onSurfaceSecondary,
semanticLabel:
"select_all_in".tr(namedArgs: {"group": text}),
),
),
],

View File

@@ -66,13 +66,10 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
Icons.face_outlined,
size: widgetSize,
)
: Semantics(
label: "logged_in_as".tr(namedArgs: {"user": user.name}),
child: UserCircleAvatar(
radius: 17,
size: 31,
user: user,
),
: UserCircleAvatar(
radius: 17,
size: 31,
user: user,
),
),
);

View File

@@ -6,7 +6,6 @@ import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.da
import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
import 'package:immich_mobile/utils/thumbnail_utils.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart';
import 'package:octo_image/octo_image.dart';
@@ -78,13 +77,6 @@ class ImmichThumbnail extends HookConsumerWidget {
);
}
final assetAltText = getAltText(
asset!.exifInfo,
asset!.fileCreatedAt,
asset!.type,
[],
);
final thumbnailProviderInstance = ImmichThumbnail.imageProvider(
asset: asset,
userId: userId,
@@ -98,21 +90,18 @@ class ImmichThumbnail extends HookConsumerWidget {
return originalErrorWidgetBuilder(ctx, error, stackTrace);
}
return Semantics(
label: assetAltText,
child: OctoImage.fromSet(
placeholderFadeInDuration: Duration.zero,
fadeInDuration: Duration.zero,
fadeOutDuration: const Duration(milliseconds: 100),
octoSet: OctoSet(
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
errorBuilder: customErrorBuilder,
),
image: thumbnailProviderInstance,
width: width,
height: height,
fit: fit,
return OctoImage.fromSet(
placeholderFadeInDuration: Duration.zero,
fadeInDuration: Duration.zero,
fadeOutDuration: const Duration(milliseconds: 100),
octoSet: OctoSet(
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
errorBuilder: customErrorBuilder,
),
image: thumbnailProviderInstance,
width: width,
height: height,
fit: fit,
);
}
}

View File

@@ -122,8 +122,6 @@ Class | Method | HTTP request | Description
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} |
*DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates |
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates |
*FacesApi* | [**createFace**](doc//FacesApi.md#createface) | **POST** /faces |
*FacesApi* | [**deleteFace**](doc//FacesApi.md#deleteface) | **DELETE** /faces/{id} |

View File

@@ -542,9 +542,7 @@ class AssetsApi {
/// * [String] key:
///
/// * [String] duration:
///
/// * [String] filename:
Future<Response> replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, String? filename, }) async {
Future<Response> replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/{id}/original'
.replaceAll('{id}', id);
@@ -589,10 +587,6 @@ class AssetsApi {
hasFields = true;
mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt);
}
if (filename != null) {
hasFields = true;
mp.fields[r'filename'] = parameterToString(filename);
}
if (hasFields) {
postBody = mp;
}
@@ -629,10 +623,8 @@ class AssetsApi {
/// * [String] key:
///
/// * [String] duration:
///
/// * [String] filename:
Future<AssetMediaResponseDto?> replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, String? filename, }) async {
final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, duration: duration, filename: filename, );
Future<AssetMediaResponseDto?> replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, }) async {
final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, duration: duration, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -796,8 +788,6 @@ class AssetsApi {
///
/// * [String] duration:
///
/// * [String] filename:
///
/// * [bool] isFavorite:
///
/// * [String] livePhotoVideoId:
@@ -805,7 +795,7 @@ class AssetsApi {
/// * [MultipartFile] sidecarData:
///
/// * [AssetVisibility] visibility:
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets';
@@ -853,10 +843,6 @@ class AssetsApi {
hasFields = true;
mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt);
}
if (filename != null) {
hasFields = true;
mp.fields[r'filename'] = parameterToString(filename);
}
if (isFavorite != null) {
hasFields = true;
mp.fields[r'isFavorite'] = parameterToString(isFavorite);
@@ -908,8 +894,6 @@ class AssetsApi {
///
/// * [String] duration:
///
/// * [String] filename:
///
/// * [bool] isFavorite:
///
/// * [String] livePhotoVideoId:
@@ -917,8 +901,8 @@ class AssetsApi {
/// * [MultipartFile] sidecarData:
///
/// * [AssetVisibility] visibility:
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, );
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, xImmichChecksum: xImmichChecksum, duration: duration, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -16,85 +16,6 @@ class DuplicatesApi {
final ApiClient apiClient;
/// Performs an HTTP 'DELETE /duplicates/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> deleteDuplicateWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/duplicates/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> deleteDuplicate(String id,) async {
final response = await deleteDuplicateWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'DELETE /duplicates' operation and returns the [Response].
/// Parameters:
///
/// * [BulkIdsDto] bulkIdsDto (required):
Future<Response> deleteDuplicatesWithHttpInfo(BulkIdsDto bulkIdsDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/duplicates';
// ignore: prefer_final_locals
Object? postBody = bulkIdsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [BulkIdsDto] bulkIdsDto (required):
Future<void> deleteDuplicates(BulkIdsDto bulkIdsDto,) async {
final response = await deleteDuplicatesWithHttpInfo(bulkIdsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'GET /duplicates' operation and returns the [Response].
Future<Response> getAssetDuplicatesWithHttpInfo() async {
// ignore: prefer_const_declarations

View File

@@ -43,7 +43,7 @@ class SystemConfigOAuthDto {
String clientSecret;
/// Minimum value: 0
int? defaultStorageQuota;
num defaultStorageQuota;
bool enabled;
@@ -96,7 +96,7 @@ class SystemConfigOAuthDto {
(buttonText.hashCode) +
(clientId.hashCode) +
(clientSecret.hashCode) +
(defaultStorageQuota == null ? 0 : defaultStorageQuota!.hashCode) +
(defaultStorageQuota.hashCode) +
(enabled.hashCode) +
(issuerUrl.hashCode) +
(mobileOverrideEnabled.hashCode) +
@@ -119,11 +119,7 @@ class SystemConfigOAuthDto {
json[r'buttonText'] = this.buttonText;
json[r'clientId'] = this.clientId;
json[r'clientSecret'] = this.clientSecret;
if (this.defaultStorageQuota != null) {
json[r'defaultStorageQuota'] = this.defaultStorageQuota;
} else {
// json[r'defaultStorageQuota'] = null;
}
json[r'enabled'] = this.enabled;
json[r'issuerUrl'] = this.issuerUrl;
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
@@ -152,7 +148,7 @@ class SystemConfigOAuthDto {
buttonText: mapValueOfType<String>(json, r'buttonText')!,
clientId: mapValueOfType<String>(json, r'clientId')!,
clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
defaultStorageQuota: mapValueOfType<int>(json, r'defaultStorageQuota'),
defaultStorageQuota: num.parse('${json[r'defaultStorageQuota']}'),
enabled: mapValueOfType<bool>(json, r'enabled')!,
issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,

View File

@@ -1,78 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/utils/thumbnail_utils.dart';
void main() {
final dateTime = DateTime(2025, 04, 25, 12, 13, 14);
final dateTimeString = DateFormat.yMMMMd().format(dateTime);
test('returns description if it has one', () {
final result = getAltText(
const ExifInfo(description: 'description'),
dateTime,
AssetType.image,
[],
);
expect(result, 'description');
});
test('returns image alt text with date if no location', () {
final (template, args) = getAltTextTemplate(
const ExifInfo(),
dateTime,
AssetType.image,
[],
);
expect(template, "image_alt_text_date");
expect(args["isVideo"], "false");
expect(args["date"], dateTimeString);
});
test('returns image alt text with date and place', () {
final (template, args) = getAltTextTemplate(
const ExifInfo(city: 'city', country: 'country'),
dateTime,
AssetType.video,
[],
);
expect(template, "image_alt_text_date_place");
expect(args["isVideo"], "true");
expect(args["date"], dateTimeString);
expect(args["city"], "city");
expect(args["country"], "country");
});
test('returns image alt text with date and some people', () {
final (template, args) = getAltTextTemplate(
const ExifInfo(),
dateTime,
AssetType.image,
["Alice", "Bob"],
);
expect(template, "image_alt_text_date_2_people");
expect(args["isVideo"], "false");
expect(args["date"], dateTimeString);
expect(args["person1"], "Alice");
expect(args["person2"], "Bob");
});
test('returns image alt text with date and location and many people', () {
final (template, args) = getAltTextTemplate(
const ExifInfo(city: "city", country: 'country'),
dateTime,
AssetType.video,
["Alice", "Bob", "Carol", "David", "Eve"],
);
expect(template, "image_alt_text_date_place_4_or_more_people");
expect(args["isVideo"], "true");
expect(args["date"], dateTimeString);
expect(args["city"], "city");
expect(args["country"], "country");
expect(args["person1"], "Alice");
expect(args["person2"], "Bob");
expect(args["person3"], "Carol");
expect(args["additionalCount"], "2");
});
}

View File

@@ -2698,39 +2698,6 @@
}
},
"/duplicates": {
"delete": {
"operationId": "deleteDuplicates",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BulkIdsDto"
}
}
},
"required": true
},
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Duplicates"
]
},
"get": {
"operationId": "getAssetDuplicates",
"parameters": [],
@@ -2765,41 +2732,6 @@
]
}
},
"/duplicates/{id}": {
"delete": {
"operationId": "deleteDuplicate",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Duplicates"
]
}
},
"/faces": {
"get": {
"operationId": "getFaces",
@@ -9469,9 +9401,6 @@
"format": "date-time",
"type": "string"
},
"filename": {
"type": "string"
},
"isFavorite": {
"type": "boolean"
},
@@ -9522,9 +9451,6 @@
"fileModifiedAt": {
"format": "date-time",
"type": "string"
},
"filename": {
"type": "string"
}
},
"required": [
@@ -14344,10 +14270,8 @@
"type": "string"
},
"defaultStorageQuota": {
"format": "int64",
"minimum": 0,
"nullable": true,
"type": "integer"
"type": "number"
},
"enabled": {
"type": "boolean"

View File

@@ -444,7 +444,6 @@ export type AssetMediaCreateDto = {
duration?: string;
fileCreatedAt: string;
fileModifiedAt: string;
filename?: string;
isFavorite?: boolean;
livePhotoVideoId?: string;
sidecarData?: Blob;
@@ -511,7 +510,6 @@ export type AssetMediaReplaceDto = {
duration?: string;
fileCreatedAt: string;
fileModifiedAt: string;
filename?: string;
};
export type SignUpDto = {
email: string;
@@ -1392,7 +1390,7 @@ export type SystemConfigOAuthDto = {
buttonText: string;
clientId: string;
clientSecret: string;
defaultStorageQuota: number | null;
defaultStorageQuota: number;
enabled: boolean;
issuerUrl: string;
mobileOverrideEnabled: boolean;
@@ -2286,15 +2284,6 @@ export function getDownloadInfo({ key, downloadInfoDto }: {
body: downloadInfoDto
})));
}
export function deleteDuplicates({ bulkIdsDto }: {
bulkIdsDto: BulkIdsDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/duplicates", oazapfts.json({
...opts,
method: "DELETE",
body: bulkIdsDto
})));
}
export function getAssetDuplicates(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
@@ -2303,14 +2292,6 @@ export function getAssetDuplicates(opts?: Oazapfts.RequestOpts) {
...opts
}));
}
export function deleteDuplicate({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/duplicates/${encodeURIComponent(id)}`, {
...opts,
method: "DELETE"
}));
}
export function getFaces({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {

View File

@@ -16,58 +16,6 @@ ENV PATH="${PATH}:/usr/src/app/bin" \
NVIDIA_VISIBLE_DEVICES=all
ENTRYPOINT ["tini", "--", "/bin/sh"]
FROM dev AS dev-container-server
RUN apt-get update && \
apt-get install sudo inetutils-ping openjdk-11-jre-headless \
vim nano \
-y --no-install-recommends --fix-missing
RUN usermod -aG sudo node
RUN echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN mkdir -p /workspaces/immich
RUN chown node -R /workspaces
RUN mkdir /immich-devcontainer && chown node -R /immich-devcontainer
COPY --chmod=777 ../.devcontainer/server/*.sh /immich-devcontainer/
FROM dev-container-server AS dev-container-mobile
# Enable multiarch for arm64 if necessary
RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
dpkg --add-architecture amd64 && \
apt-get update && \
apt-get install -y --no-install-recommends \
qemu-user-static \
libc6:amd64 \
libstdc++6:amd64 \
libgcc1:amd64; \
fi
# 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 node ${FLUTTER_HOME}
USER node
RUN sudo apt-get update \
&& wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg \
&& 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 -y
COPY --chmod=777 ../.devcontainer/mobile/container-mobile-post-create.sh /immich-devcontainer/container-mobile-post-create.sh
RUN dart --disable-analytics
FROM dev AS prod

View File

@@ -89,7 +89,7 @@ export interface SystemConfig {
buttonText: string;
clientId: string;
clientSecret: string;
defaultStorageQuota: number | null;
defaultStorageQuota: number;
enabled: boolean;
issuerUrl: string;
mobileOverrideEnabled: boolean;
@@ -253,7 +253,7 @@ export const defaults = Object.freeze<SystemConfig>({
buttonText: 'Login with OAuth',
clientId: '',
clientSecret: '',
defaultStorageQuota: null,
defaultStorageQuota: 0,
enabled: false,
issuerUrl: '',
mobileOverrideEnabled: false,

View File

@@ -1,11 +1,9 @@
import { Body, Controller, Delete, Get, Param } from '@nestjs/common';
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { DuplicateService } from 'src/services/duplicate.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Duplicates')
@Controller('duplicates')
@@ -17,16 +15,4 @@ export class DuplicateController {
getAssetDuplicates(@Auth() auth: AuthDto): Promise<DuplicateResponseDto[]> {
return this.service.getDuplicates(auth);
}
@Delete()
@Authenticated()
deleteDuplicates(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.deleteAll(auth, dto);
}
@Delete(':id')
@Authenticated()
deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}
}

View File

@@ -46,10 +46,6 @@ class AssetMediaBase {
@IsString()
duration?: string;
@Optional()
@IsString()
filename?: string;
// The properties below are added to correctly generate the API docs
// and client SDKs. Validation should be handled in the controller.
@ApiProperty({ type: 'string', format: 'binary' })

View File

@@ -1,6 +1,14 @@
import { IsNotEmpty } from 'class-validator';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { ValidateUUID } from 'src/validation';
export class DuplicateResponseDto {
duplicateId!: string;
assets!: AssetResponseDto[];
}
export class ResolveDuplicatesDto {
@IsNotEmpty()
@ValidateUUID({ each: true })
assetIds!: string[];
}

View File

@@ -360,9 +360,7 @@ class SystemConfigOAuthDto {
@IsNumber()
@Min(0)
@Optional({ nullable: true })
@ApiProperty({ type: 'integer', format: 'int64' })
defaultStorageQuota!: number | null;
defaultStorageQuota!: number;
@ValidateBoolean()
enabled!: boolean;

View File

@@ -60,22 +60,6 @@ where
"unique"."duplicateId" = "duplicates"."duplicateId"
)
-- DuplicateRepository.delete
update "assets"
set
"duplicateId" = $1
where
"ownerId" = $2
and "duplicateId" = $3
-- DuplicateRepository.deleteAll
update "assets"
set
"duplicateId" = $1
where
"ownerId" = $2
and "duplicateId" in ($3)
-- DuplicateRepository.search
begin
set

View File

@@ -279,15 +279,6 @@ where
"asset_faces"."personId" = $1
and "asset_faces"."deletedAt" is null
-- PersonRepository.getAssetPersonByFaceId
select
"asset_faces"."assetId",
"asset_faces"."personId"
from
"asset_faces"
where
"asset_faces"."id" = $1
-- PersonRepository.getLatestFaceDate
select
max("asset_job_status"."facesRecognizedAt")::text as "latestDate"

View File

@@ -403,6 +403,8 @@ export class AssetRepository {
.$call((qb) => qb.select(withFacesAndPeople))
.executeTakeFirst();
}
return this.getById(asset.id, { exifInfo: true, faces: { person: true } });
}
async remove(asset: { id: string }): Promise<void> {

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { Kysely, NotNull, sql } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db';
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
import { DummyValue, GenerateSql } from 'src/decorators';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetType, VectorIndex } from 'src/enum';
import { probes } from 'src/repositories/database.repository';
@@ -78,31 +78,6 @@ export class DuplicateRepository {
);
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
async delete(userId: string, id: string): Promise<void> {
await this.db
.updateTable('assets')
.set({ duplicateId: null })
.where('ownerId', '=', userId)
.where('duplicateId', '=', id)
.execute();
}
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
@Chunked({ paramIndex: 1 })
async deleteAll(userId: string, ids: string[]): Promise<void> {
if (ids.length === 0) {
return;
}
await this.db
.updateTable('assets')
.set({ duplicateId: null })
.where('ownerId', '=', userId)
.where('duplicateId', 'in', ids)
.execute();
}
@GenerateSql({
params: [
{

View File

@@ -47,20 +47,11 @@ type EventMap = {
];
'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
// activity events
'activity.change': [{ recipientId: string[]; userId: string; albumId: string; assetId: string | null }];
// album events
'album.update': [
{ id: string; recipientId: string[]; assetId: string[]; userId: string; status: 'added' | 'removed' },
];
'album.update': [{ id: string; recipientId: string }];
'album.invite': [{ id: string; userId: string }];
// asset events
'asset.update': [{ assetIds: string[]; userId: string }];
'asset.person': [
{ assetId: string; userId: string; personId: string | undefined; status: 'created' | 'removed' | 'removed_soft' },
];
'asset.tag': [{ assetId: string }];
'asset.untag': [{ assetId: string }];
'asset.hide': [{ assetId: string; userId: string }];
@@ -106,12 +97,9 @@ export type ArgsOf<T extends EmitEvent> = EventMap[T];
export interface ClientEventMap {
on_upload_success: [AssetResponseDto];
on_user_delete: [string];
on_activity_change: [{ albumId: string; assetId: string | null }];
on_album_update: [{ albumId: string; assetId: string[]; status: 'added' | 'removed' }];
on_asset_person: [{ assetId: string; personId: string | undefined; status: 'created' | 'removed' | 'removed_soft' }];
on_asset_delete: [string];
on_asset_trash: [string[]];
on_asset_update: [string[]];
on_asset_update: [AssetResponseDto];
on_asset_hidden: [string];
on_asset_restore: [string[]];
on_asset_stack_update: string[];

View File

@@ -179,8 +179,9 @@ export class PersonRepository {
)
.$if(!options?.closestFaceAssetId, (qb) =>
qb
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
.orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
.orderBy(sql`NULLIF(person.name, '') asc nulls last`)
.orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`)
.orderBy('person.createdAt'),
)
.$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))
@@ -483,15 +484,6 @@ export class PersonRepository {
.executeTakeFirst();
}
@GenerateSql({ params: [DummyValue.UUID] })
async getAssetPersonByFaceId(id: string) {
return this.db
.selectFrom('asset_faces')
.select(['asset_faces.assetId', 'asset_faces.personId'])
.where('asset_faces.id', '=', id)
.executeTakeFirst();
}
@GenerateSql()
async getLatestFaceDate(): Promise<string | undefined> {
const result = (await this.db

View File

@@ -11,15 +11,6 @@ export class TrashRepository {
return this.db.selectFrom('assets').select(['id']).where('status', '=', AssetStatus.DELETED).stream();
}
getTrashedIds(userId: string): AsyncIterableIterator<{ id: string }> {
return this.db
.selectFrom('assets')
.select(['id'])
.where('ownerId', '=', userId)
.where('status', '=', AssetStatus.TRASHED)
.stream();
}
@GenerateSql({ params: [DummyValue.UUID] })
async restore(userId: string): Promise<number> {
const { numUpdatedRows } = await this.db

View File

@@ -1,7 +1,6 @@
import { BadRequestException } from '@nestjs/common';
import { ReactionType } from 'src/dtos/activity.dto';
import { ActivityService } from 'src/services/activity.service';
import { albumStub } from 'test/fixtures/album.stub';
import { factory, newUuid, newUuids } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -80,11 +79,6 @@ describe(ActivityService.name, () => {
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(activity);
mocks.album.getById.mockResolvedValue({
...albumStub.empty,
owner: factory.user({ id: userId }),
albumUsers: [],
});
await sut.create(factory.auth({ user: { id: userId } }), {
albumId,
@@ -121,11 +115,6 @@ describe(ActivityService.name, () => {
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(activity);
mocks.activity.search.mockResolvedValue([]);
mocks.album.getById.mockResolvedValue({
...albumStub.empty,
owner: factory.user({ id: userId }),
albumUsers: [],
});
await sut.create(factory.auth({ user: { id: userId } }), { albumId, assetId, type: ReactionType.LIKE });

View File

@@ -1,4 +1,4 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { Activity } from 'src/database';
import {
ActivityCreateDto,
@@ -58,24 +58,11 @@ export class ActivityService extends BaseService {
}
if (!activity) {
const album = await this.albumRepository.getById(common.albumId, { withAssets: false });
if (!album) {
throw new BadRequestException('Album not found');
}
activity = await this.activityRepository.create({
...common,
isLiked: dto.type === ReactionType.LIKE,
comment: dto.comment,
});
const allUsersExceptUs = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter(
(userId) => userId !== auth.user.id,
);
await this.eventRepository.emit('activity.change', {
recipientId: allUsersExceptUs,
userId: common.userId,
albumId: activity.albumId,
assetId: activity.assetId,
});
}
return { duplicate, value: mapActivity(activity) };

View File

@@ -664,10 +664,7 @@ describe(AlbumService.name, () => {
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
expect(mocks.event.emit).toHaveBeenCalledWith('album.update', {
id: 'album-123',
userId: 'user-id',
assetId: ['asset-1', 'asset-2', 'asset-3'],
recipientId: ['admin_id'],
status: 'added',
recipientId: 'admin_id',
});
});

View File

@@ -178,13 +178,9 @@ export class AlbumService extends BaseService {
(userId) => userId !== auth.user.id,
);
await this.eventRepository.emit('album.update', {
id,
userId: auth.user.id,
assetId: dto.ids,
recipientId: allUsersExceptUs,
status: 'added',
});
for (const recipientId of allUsersExceptUs) {
await this.eventRepository.emit('album.update', { id, recipientId });
}
}
return results;
@@ -204,16 +200,7 @@ export class AlbumService extends BaseService {
if (removedIds.length > 0 && album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
await this.albumRepository.updateThumbnails();
}
const allUsersExceptUs = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter(
(userId) => userId !== auth.user.id,
);
await this.eventRepository.emit('album.update', {
id,
userId: auth.user.id,
assetId: dto.ids,
recipientId: allUsersExceptUs,
status: 'removed',
});
return results;
}

View File

@@ -418,7 +418,7 @@ export class AssetMediaService extends BaseService {
duration: dto.duration || null,
visibility: dto.visibility ?? AssetVisibility.TIMELINE,
livePhotoVideoId: dto.livePhotoVideoId,
originalFileName: dto.filename || file.originalName,
originalFileName: file.originalName,
sidecarPath: sidecarFile?.originalPath,
});

View File

@@ -93,26 +93,9 @@ export class AssetService extends BaseService {
}
}
const metadataUpdated = await this.updateMetadata({
id,
description,
dateTimeOriginal,
latitude,
longitude,
rating,
});
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
const updatedAsset = await this.assetRepository.update({ id, ...rest });
// If update returned undefined (no changes), fetch the asset
// Match the relations that update() returns when it does update
const asset = updatedAsset ?? (await this.assetRepository.getById(id, { exifInfo: true, faces: { person: true } }));
if (!metadataUpdated && updatedAsset) {
// updateMetadata will send an event, but assetRepository.update() won't.
// to prevent doubles, only send an event if asset was updated
await this.eventRepository.emit('asset.update', { assetIds: [id], userId: auth.user.id });
}
const asset = await this.assetRepository.update({ id, ...rest });
if (previousMotion && asset) {
await onAfterUnlink(repos, {
@@ -130,27 +113,35 @@ export class AssetService extends BaseService {
}
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
const { ids, description, dateTimeOriginal, latitude, longitude, ...options } = dto;
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
const metadataUpdated = await this.updateAllMetadata(ids, {
description,
dateTimeOriginal,
latitude,
longitude,
rating,
});
if (
description !== undefined ||
dateTimeOriginal !== undefined ||
latitude !== undefined ||
longitude !== undefined
) {
await this.assetRepository.updateAllExif(ids, { description, dateTimeOriginal, latitude, longitude });
await this.jobRepository.queueAll(
ids.map((id) => ({
name: JobName.SIDECAR_WRITE,
data: { id, description, dateTimeOriginal, latitude, longitude },
})),
);
}
if (rest.visibility !== undefined || rest.isFavorite !== undefined || rest.duplicateId !== undefined) {
await this.assetRepository.updateAll(ids, rest);
if (
options.visibility !== undefined ||
options.isFavorite !== undefined ||
options.duplicateId !== undefined ||
options.rating !== undefined
) {
await this.assetRepository.updateAll(ids, options);
if (rest.visibility === AssetVisibility.LOCKED) {
if (options.visibility === AssetVisibility.LOCKED) {
await this.albumRepository.removeAssetsFromAll(ids);
}
if (!metadataUpdated) {
// If no metadata was updated, we still need to emit an event for the bulk update
await this.eventRepository.emit('asset.update', { assetIds: ids, userId: auth.user.id });
}
}
}
@@ -299,26 +290,6 @@ export class AssetService extends BaseService {
if (Object.keys(writes).length > 0) {
await this.assetRepository.upsertExif({ assetId: id, ...writes });
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } });
return true;
}
return false;
}
private async updateAllMetadata(
ids: string[],
dto: Pick<AssetBulkUpdateDto, 'description' | 'dateTimeOriginal' | 'latitude' | 'longitude' | 'rating'>,
) {
const { description, dateTimeOriginal, latitude, longitude, rating } = dto;
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined);
if (Object.keys(writes).length > 0) {
await this.assetRepository.updateAllExif(ids, writes);
const jobs: JobItem[] = ids.map((id) => ({
name: JobName.SIDECAR_WRITE,
data: { id, ...writes },
}));
await this.jobRepository.queueAll(jobs);
return true;
}
return false;
}
}

View File

@@ -704,7 +704,7 @@ describe(AuthService.name, () => {
expect(mocks.user.create).toHaveBeenCalledWith(expect.objectContaining({ quotaSizeInBytes: 1_073_741_824 }));
});
it('should set quota for 0 quota', async () => {
it('should not set quota for 0 quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
@@ -726,7 +726,7 @@ describe(AuthService.name, () => {
email: user.email,
name: ' ',
oauthId: user.oauthId,
quotaSizeInBytes: 0,
quotaSizeInBytes: null,
storageLabel: null,
});
});

View File

@@ -300,7 +300,7 @@ export class AuthService extends BaseService {
name: userName,
email: profile.email,
oauthId: profile.sub,
quotaSizeInBytes: storageQuota === null ? null : storageQuota * HumanReadableSize.GiB,
quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null,
storageLabel: storageLabel || null,
});
}

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
import { OnJob } from 'src/decorators';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
@@ -21,14 +20,6 @@ export class DuplicateService extends BaseService {
}));
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.duplicateRepository.delete(auth.user.id, id);
}
async deleteAll(auth: AuthDto, dto: BulkIdsDto) {
await this.duplicateRepository.deleteAll(auth.user.id, dto.ids);
}
@OnJob({ name: JobName.QUEUE_DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })
async handleQueueSearchDuplicates({ force }: JobOf<JobName.QUEUE_DUPLICATE_DETECTION>): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: false });
@@ -78,11 +69,6 @@ export class DuplicateService extends BaseService {
return JobStatus.SKIPPED;
}
if (asset.visibility === AssetVisibility.LOCKED) {
this.logger.debug(`Asset ${id} is locked, skipping`);
return JobStatus.SKIPPED;
}
if (!asset.embedding) {
this.logger.debug(`Asset ${id} is missing embedding`);
return JobStatus.FAILED;

View File

@@ -4,6 +4,7 @@ import { AlbumUser } from 'src/database';
import { SystemConfigDto } from 'src/dtos/system-config.dto';
import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum';
import { NotificationService } from 'src/services/notification.service';
import { INotifyAlbumUpdateJob } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub';
@@ -153,7 +154,7 @@ describe(NotificationService.name, () => {
describe('onAlbumUpdateEvent', () => {
it('should queue notify album update event', async () => {
await sut.onAlbumUpdate({ id: 'album', recipientId: ['42'], userId: '', assetId: [], status: 'added' });
await sut.onAlbumUpdate({ id: 'album', recipientId: '42' });
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.NOTIFY_ALBUM_UPDATE,
data: { id: 'album', recipientId: '42', delay: 300_000 },
@@ -498,13 +499,7 @@ describe(NotificationService.name, () => {
});
it('should add new recipients for new images if job is already queued', async () => {
await sut.onAlbumUpdate({
id: '1',
recipientId: ['2'],
userId: '',
assetId: [],
status: 'added',
});
await sut.onAlbumUpdate({ id: '1', recipientId: '2' } as INotifyAlbumUpdateJob);
expect(mocks.job.removeJob).toHaveBeenCalledWith(JobName.NOTIFY_ALBUM_UPDATE, '1/2');
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.NOTIFY_ALBUM_UPDATE,

View File

@@ -1,5 +1,6 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { OnEvent, OnJob } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import {
mapNotification,
@@ -127,20 +128,6 @@ export class NotificationService extends BaseService {
}
}
@OnEvent({ name: 'activity.change' })
onActivityChange({ recipientId, assetId, userId, albumId }: ArgOf<'activity.change'>) {
for (const recipient of recipientId) {
this.eventRepository.clientSend('on_activity_change', recipient, { albumId, assetId });
}
this.eventRepository.clientSend('on_activity_change', userId, { albumId, assetId });
}
@OnEvent({ name: 'asset.person' })
onAssetPerson({ assetId, userId, personId, status }: ArgOf<'asset.person'>) {
this.eventRepository.clientSend('on_asset_person', userId, { assetId, personId, status });
}
@OnEvent({ name: 'asset.hide' })
onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) {
this.eventRepository.clientSend('on_asset_hidden', userId, assetId);
@@ -166,17 +153,16 @@ export class NotificationService extends BaseService {
this.eventRepository.clientSend('on_asset_trash', userId, assetIds);
}
@OnEvent({ name: 'asset.update' })
onAssetUpdate({ assetIds, userId }: ArgOf<'asset.update'>) {
this.eventRepository.clientSend('on_asset_update', userId, assetIds);
}
@OnEvent({ name: 'asset.metadataExtracted' })
onAssetMetadataExtracted({ assetId, userId, source }: ArgOf<'asset.metadataExtracted'>) {
async onAssetMetadataExtracted({ assetId, userId, source }: ArgOf<'asset.metadataExtracted'>) {
if (source !== 'sidecar-write') {
return;
}
this.eventRepository.clientSend('on_asset_update', userId, [assetId]);
const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([assetId]);
if (asset) {
this.eventRepository.clientSend('on_asset_update', userId, mapAsset(asset));
}
}
@OnEvent({ name: 'assets.restore' })
@@ -212,23 +198,12 @@ export class NotificationService extends BaseService {
}
@OnEvent({ name: 'album.update' })
async onAlbumUpdate({ id, recipientId, userId, assetId, status }: ArgOf<'album.update'>) {
if (status === 'added') {
for (const recipient of recipientId) {
await this.jobRepository.removeJob(JobName.NOTIFY_ALBUM_UPDATE, `${id}/${recipientId}`);
await this.jobRepository.queue({
name: JobName.NOTIFY_ALBUM_UPDATE,
data: { id, recipientId: recipient, delay: NotificationService.albumUpdateEmailDelayMs },
});
this.eventRepository.clientSend('on_album_update', recipient, { albumId: id, assetId, status });
}
} else if (status === 'removed') {
for (const recipient of recipientId) {
this.eventRepository.clientSend('on_album_update', recipient, { albumId: id, assetId, status });
}
}
this.eventRepository.clientSend('on_album_update', userId, { albumId: id, assetId, status });
async onAlbumUpdate({ id, recipientId }: ArgOf<'album.update'>) {
await this.jobRepository.removeJob(JobName.NOTIFY_ALBUM_UPDATE, `${id}/${recipientId}`);
await this.jobRepository.queue({
name: JobName.NOTIFY_ALBUM_UPDATE,
data: { id, recipientId, delay: NotificationService.albumUpdateEmailDelayMs },
});
}
@OnEvent({ name: 'album.invite' })

View File

@@ -627,28 +627,11 @@ export class PersonService extends BaseService {
boundingBoxY2: dto.y + dto.height,
sourceType: SourceType.MANUAL,
});
await this.eventRepository.emit('asset.person', {
assetId: dto.assetId,
userId: auth.user.id,
personId: dto.personId,
status: 'created',
});
}
async deleteFace(auth: AuthDto, id: string, dto: AssetFaceDeleteDto): Promise<void> {
await this.requireAccess({ auth, permission: Permission.FACE_DELETE, ids: [id] });
const assetPerson = await this.personRepository.getAssetPersonByFaceId(id);
if (!assetPerson) {
throw new NotFoundException('Asset face not found');
}
await (dto.force ? this.personRepository.deleteAssetFace(id) : this.personRepository.softDeleteAssetFaces(id));
await this.eventRepository.emit('asset.person', {
userId: auth.user.id,
assetId: assetPerson.assetId,
personId: assetPerson.personId ?? undefined,
status: dto.force ? 'removed' : 'removed_soft',
});
return dto.force ? this.personRepository.deleteAssetFace(id) : this.personRepository.softDeleteAssetFaces(id);
}
}

View File

@@ -112,7 +112,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
buttonText: 'Login with OAuth',
clientId: '',
clientSecret: '',
defaultStorageQuota: null,
defaultStorageQuota: 0,
enabled: false,
issuerUrl: '',
mobileOverrideEnabled: false,

View File

@@ -50,28 +50,30 @@ describe(TrashService.name, () => {
describe('restore', () => {
it('should handle an empty trash', async () => {
mocks.trash.getTrashedIds.mockReturnValue(makeAssetIdStream(0));
mocks.trash.getDeletedIds.mockResolvedValue(makeAssetIdStream(0));
mocks.trash.restore.mockResolvedValue(0);
await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 0 });
expect(mocks.trash.restore).toHaveBeenCalledWith('user-id');
});
it('should restore', async () => {
mocks.trash.getTrashedIds.mockReturnValue(makeAssetIdStream(1));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
mocks.trash.restoreAll.mockResolvedValue(1);
mocks.trash.getDeletedIds.mockResolvedValue(makeAssetIdStream(1));
mocks.trash.restore.mockResolvedValue(1);
await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 1 });
expect(mocks.trash.restore).toHaveBeenCalledWith('user-id');
});
});
describe('empty', () => {
it('should handle an empty trash', async () => {
mocks.trash.getTrashedIds.mockReturnValue(makeAssetIdStream(0));
mocks.trash.getDeletedIds.mockResolvedValue(makeAssetIdStream(0));
mocks.trash.empty.mockResolvedValue(0);
await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 0 });
expect(mocks.job.queue).not.toHaveBeenCalled();
});
it('should empty the trash', async () => {
mocks.trash.getTrashedIds.mockReturnValue(makeAssetIdStream(1));
mocks.trash.getDeletedIds.mockResolvedValue(makeAssetIdStream(1));
mocks.trash.empty.mockResolvedValue(1);
await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 1 });
expect(mocks.trash.empty).toHaveBeenCalledWith('user-id');

View File

@@ -25,22 +25,11 @@ export class TrashService extends BaseService {
}
async restore(auth: AuthDto): Promise<TrashResponseDto> {
const assets = this.trashRepository.getTrashedIds(auth.user.id);
let total = 0;
let batch = new BulkIdsDto();
batch.ids = [];
for await (const { id } of assets) {
batch.ids.push(id);
if (batch.ids.length === JOBS_ASSET_PAGINATION_SIZE) {
const { count } = await this.restoreAssets(auth, batch);
total += count;
batch = new BulkIdsDto();
batch.ids = [];
}
const count = await this.trashRepository.restore(auth.user.id);
if (count > 0) {
this.logger.log(`Restored ${count} asset(s) from trash`);
}
const { count } = await this.restoreAssets(auth, batch);
total += count;
return { count: total };
return { count };
}
async empty(auth: AuthDto): Promise<TrashResponseDto> {

View File

@@ -33,7 +33,6 @@ export const newPersonRepositoryMock = (): Mocked<RepositoryInterface<PersonRepo
createAssetFace: vitest.fn(),
deleteAssetFace: vitest.fn(),
softDeleteAssetFaces: vitest.fn(),
getAssetPersonByFaceId: vitest.fn(),
vacuum: vitest.fn(),
};
};

View File

@@ -1,5 +0,0 @@
> 0.2% and last 4 major versions
> 0.5%
not dead
edge >= 135
not edge < 135

263
web/.oxlintrc.json Normal file
View File

@@ -0,0 +1,263 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"unicorn",
"typescript"
],
"categories": {
"correctness": "off"
},
"env": {
"builtin": true,
"browser": true,
"commonjs": true,
"es2025": true,
"node": true,
"shared-node-browser": true
},
"rules": {
"no-negated-condition": "off",
"no-nested-ternary": "off",
"unicorn/catch-error-name": "error",
"unicorn/consistent-assert": "error",
"unicorn/consistent-date-clone": "error",
"unicorn/consistent-empty-array-spread": "error",
"unicorn/consistent-existence-index-check": "error",
"unicorn/consistent-function-scoping": "off",
"unicorn/empty-brace-spaces": "error",
"unicorn/error-message": "error",
"unicorn/escape-case": "error",
"unicorn/explicit-length-check": "error",
"unicorn/filename-case": "off",
"unicorn/new-for-builtins": "error",
"unicorn/no-abusive-eslint-disable": "error",
"unicorn/no-accessor-recursion": "error",
"unicorn/no-anonymous-default-export": "error",
"unicorn/no-array-for-each": "error",
"unicorn/no-array-method-this-argument": "error",
"unicorn/no-array-reduce": "error",
"unicorn/no-await-expression-member": "error",
"unicorn/no-await-in-promise-methods": "error",
"unicorn/no-console-spaces": "error",
"unicorn/no-document-cookie": "error",
"unicorn/no-empty-file": "error",
"unicorn/no-hex-escape": "error",
"unicorn/no-instanceof-builtins": "error",
"unicorn/no-invalid-fetch-options": "error",
"unicorn/no-invalid-remove-event-listener": "error",
"unicorn/no-lonely-if": "error",
"unicorn/no-magic-array-flat-depth": "error",
"unicorn/no-negated-condition": "error",
"unicorn/no-negation-in-equality-check": "error",
"unicorn/no-nested-ternary": "off",
"unicorn/no-new-array": "error",
"unicorn/no-new-buffer": "error",
"unicorn/no-null": "off",
"unicorn/no-object-as-default-parameter": "error",
"unicorn/no-process-exit": "error",
"unicorn/no-single-promise-in-promise-methods": "error",
"unicorn/no-static-only-class": "error",
"unicorn/no-thenable": "error",
"unicorn/no-this-assignment": "error",
"unicorn/no-typeof-undefined": "error",
"unicorn/no-unnecessary-array-flat-depth": "error",
"unicorn/no-unnecessary-await": "error",
"unicorn/no-unnecessary-slice-end": "error",
"unicorn/no-unreadable-array-destructuring": "error",
"unicorn/no-unreadable-iife": "error",
"unicorn/no-useless-fallback-in-spread": "error",
"unicorn/no-useless-length-check": "error",
"unicorn/no-useless-promise-resolve-reject": "error",
"unicorn/no-useless-spread": "error",
"unicorn/no-useless-switch-case": "error",
"unicorn/no-useless-undefined": "off",
"unicorn/no-zero-fractions": "error",
"unicorn/number-literal-case": "error",
"unicorn/numeric-separators-style": "error",
"unicorn/prefer-add-event-listener": "error",
"unicorn/prefer-array-find": "error",
"unicorn/prefer-array-flat-map": "error",
"unicorn/prefer-array-flat": "error",
"unicorn/prefer-array-index-of": "error",
"unicorn/prefer-array-some": "error",
"unicorn/prefer-blob-reading-methods": "error",
"unicorn/prefer-code-point": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-dom-node-append": "error",
"unicorn/prefer-dom-node-dataset": "error",
"unicorn/prefer-dom-node-remove": "error",
"unicorn/prefer-dom-node-text-content": "error",
"unicorn/prefer-event-target": "error",
"unicorn/prefer-global-this": "error",
"unicorn/prefer-includes": "error",
"unicorn/prefer-logical-operator-over-ternary": "error",
"unicorn/prefer-math-min-max": "error",
"unicorn/prefer-math-trunc": "error",
"unicorn/prefer-modern-dom-apis": "error",
"unicorn/prefer-modern-math-apis": "error",
"unicorn/prefer-native-coercion-functions": "error",
"unicorn/prefer-negative-index": "error",
"unicorn/prefer-node-protocol": "error",
"unicorn/prefer-number-properties": "error",
"unicorn/prefer-object-from-entries": "error",
"unicorn/prefer-optional-catch-binding": "error",
"unicorn/prefer-prototype-methods": "error",
"unicorn/prefer-query-selector": "error",
"unicorn/prefer-reflect-apply": "error",
"unicorn/prefer-regexp-test": "error",
"unicorn/prefer-set-has": "error",
"unicorn/prefer-set-size": "error",
"unicorn/prefer-spread": "off",
"unicorn/prefer-string-raw": "error",
"unicorn/prefer-string-replace-all": "error",
"unicorn/prefer-string-slice": "error",
"unicorn/prefer-string-starts-ends-with": "error",
"unicorn/prefer-string-trim-start-end": "error",
"unicorn/prefer-structured-clone": "error",
"unicorn/prefer-type-error": "error",
"unicorn/require-array-join-separator": "error",
"unicorn/require-number-to-fixed-digits-argument": "error",
"unicorn/require-post-message-target-origin": "off",
"unicorn/switch-case-braces": "error",
"unicorn/text-encoding-identifier-case": "error",
"unicorn/throw-new-error": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-unexpected-multiline": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_$",
"varsIgnorePattern": "^_$"
}
],
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",
"@typescript-eslint/ban-ts-comment": "error",
"no-array-constructor": "error",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-declaration-merging": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"no-unused-expressions": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/triple-slash-reference": "error",
"curly": "warn",
"eslint/no-undef": "error"
},
"globals": {
"NodeJS": "writeable"
},
"ignorePatterns": [
"**/.DS_Store",
"**/node_modules",
"build",
".svelte-kit",
"package",
"**/.env",
"**/.env.*",
"!**/.env.example",
"**/pnpm-lock.yaml",
"**/package-lock.json",
"**/yarn.lock",
"**/svelte.config.js",
"eslint.config.js",
"tailwind.config.js",
"coverage",
"**/service-worker/**"
],
"overrides": [
{
"files": ["*.svelte", "**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
"rules": {
"eslint/no-undef": "off",
"no-inner-declarations": "off",
"no-self-assign": "off"
}
},
{
"files": [
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.cts"
],
"rules": {
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
}
]
}

View File

@@ -13,3 +13,5 @@ node_modules
pnpm-lock.yaml
package-lock.json
yarn.lock
biome.json
.oxlintrc.json

347
web/biome.json Normal file
View File

@@ -0,0 +1,347 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0-beta.6/schema.json",
"assist": {
"actions": { "source": { "organizeImports": "on" } },
"enabled": true
},
"files": { "ignoreUnknown": false },
"formatter": { "enabled": true, "indentStyle": "tab" },
"javascript": { "formatter": { "quoteStyle": "double" }, "globals": [] },
"linter": {
"enabled": true,
"includes": [
"**",
"!**/.DS_Store",
"!**/node_modules",
"!build",
"!.svelte-kit",
"!package",
"!**/.env",
"!**/.env.*",
"**/.env.example",
"!**/pnpm-lock.yaml",
"!**/package-lock.json",
"!**/yarn.lock",
"!**/svelte.config.js",
"!eslint.config.js",
"!tailwind.config.js",
"!coverage",
"**",
"!**/.DS_Store",
"!**/node_modules",
"!build",
"!.svelte-kit",
"!package",
"!**/.env",
"!**/.env.*",
"**/.env.example",
"!**/pnpm-lock.yaml",
"!**/package-lock.json",
"!**/yarn.lock",
"!**/svelte.config.js",
"!eslint.config.js",
"!tailwind.config.js",
"!coverage"
],
"rules": {
"complexity": {
"noAdjacentSpacesInRegex": "error",
"noExtraBooleanCast": "error",
"noForEach": "error",
"noStaticOnlyClass": "error",
"noUselessCatch": "error",
"noUselessEscapeInRegex": "error",
"noUselessSwitchCase": "error",
"noUselessThisAlias": "error",
"noUselessTypeConstraint": "error",
"useDateNow": "error",
"useFlatMap": "error"
},
"correctness": {
"noConstAssign": "error",
"noConstantCondition": "error",
"noEmptyCharacterClassInRegex": "error",
"noEmptyPattern": "error",
"noGlobalObjectCalls": "error",
"noInvalidBuiltinInstantiation": "error",
"noInvalidConstructorSuper": "error",
"noNonoctalDecimalEscape": "error",
"noPrecisionLoss": "error",
"noSelfAssign": "error",
"noSetterReturn": "error",
"noSwitchDeclarations": "error",
"noUndeclaredVariables": "error",
"noUnreachable": "error",
"noUnreachableSuper": "error",
"noUnsafeFinally": "error",
"noUnsafeOptionalChaining": "error",
"noUnusedLabels": "error",
"noUnusedPrivateClassMembers": "error",
"noUnusedVariables": "off",
"useIsNan": "error",
"useValidForDirection": "error",
"useValidTypeof": "error",
"useYield": "error"
},
"recommended": false,
"style": {
"noCommonJs": "error",
"noNamespace": "error",
"noNegationElse": "off",
"noNestedTernary": "off",
"noSubstr": "error",
"useArrayLiterals": "error",
"useAsConstAssertion": "error",
"useAtIndex": "error",
"useCollapsedIf": "error",
"useExplicitLengthCheck": "error",
"useFilenamingConvention": {
"level": "error",
"options": { "filenameCases": ["kebab-case"] }
},
"useForOf": "error",
"useNodejsImportProtocol": "error",
"useNumberNamespace": "error",
"useThrowNewError": "error",
"useTrimStartEnd": "error"
},
"suspicious": {
"noAssignInExpressions": "error",
"noAsyncPromiseExecutor": "error",
"noCatchAssign": "error",
"noClassAssign": "error",
"noCompareNegZero": "error",
"noControlCharactersInRegex": "error",
"noDebugger": "error",
"noDocumentCookie": "error",
"noDuplicateCase": "error",
"noDuplicateClassMembers": "error",
"noDuplicateElseIf": "error",
"noDuplicateObjectKeys": "error",
"noDuplicateParameters": "error",
"noEmptyBlockStatements": "error",
"noExplicitAny": "error",
"noExtraNonNullAssertion": "error",
"noFallthroughSwitchClause": "error",
"noFunctionAssign": "error",
"noGlobalAssign": "error",
"noImportAssign": "error",
"noIrregularWhitespace": "error",
"noMisleadingCharacterClass": "error",
"noMisleadingInstantiator": "error",
"noPrototypeBuiltins": "error",
"noRedeclare": "error",
"noShadowRestrictedNames": "error",
"noSparseArray": "error",
"noThenProperty": "error",
"noUnsafeDeclarationMerging": "error",
"noUnsafeNegation": "error",
"noWith": "error",
"useErrorMessage": "error",
"useGetterReturn": "error",
"useNamespaceKeyword": "error",
"useNumberToFixedDigitsArgument": "error"
}
}
},
"overrides": [
{
"includes": ["*.svelte.js", "*.svelte.ts", "**/*.svelte.js", "**/*.svelte.ts"],
"javascript": { "globals": [] }
},
{
"includes": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
"linter": {
"rules": {
"complexity": { "noArguments": "error" },
"correctness": {
"noConstAssign": "off",
"noGlobalObjectCalls": "off",
"noInvalidBuiltinInstantiation": "off",
"noInvalidConstructorSuper": "off",
"noSetterReturn": "off",
"noUndeclaredVariables": "off",
"noUnreachable": "off",
"noUnreachableSuper": "off"
},
"style": { "useConst": "error" },
"suspicious": {
"noClassAssign": "off",
"noDuplicateClassMembers": "off",
"noDuplicateObjectKeys": "off",
"noDuplicateParameters": "off",
"noFunctionAssign": "off",
"noImportAssign": "off",
"noRedeclare": "off",
"noUnsafeNegation": "off",
"noVar": "error",
"noWith": "off",
"useGetterReturn": "off"
}
}
}
},
{
"includes": ["**", "!**/service-worker/**"],
"javascript": {
"globals": [
"onanimationend",
"exports",
"ongamepadconnected",
"onlostpointercapture",
"onanimationiteration",
"onkeyup",
"onmousedown",
"onanimationstart",
"onslotchange",
"onprogress",
"ontransitionstart",
"onpause",
"onended",
"onpointerover",
"onscrollend",
"onformdata",
"ontransitionrun",
"onanimationcancel",
"ondrag",
"onchange",
"onbeforeinstallprompt",
"onbeforexrselect",
"onmessage",
"ontransitioncancel",
"onpointerdown",
"onabort",
"onpointerout",
"oncuechange",
"ongotpointercapture",
"onscrollsnapchanging",
"onsearch",
"onsubmit",
"onstalled",
"onsuspend",
"onreset",
"onerror",
"onmouseenter",
"ongamepaddisconnected",
"onresize",
"ondragover",
"onbeforetoggle",
"onmouseover",
"onpagehide",
"onmousemove",
"onratechange",
"oncommand",
"onmessageerror",
"onwheel",
"ondevicemotion",
"onauxclick",
"ontransitionend",
"onpaste",
"onpageswap",
"ononline",
"ondeviceorientationabsolute",
"onkeydown",
"onclose",
"onselect",
"onpageshow",
"onpointercancel",
"onbeforematch",
"onpointerrawupdate",
"ondragleave",
"onscrollsnapchange",
"onseeked",
"onwaiting",
"onbeforeunload",
"onplaying",
"onvolumechange",
"ondragend",
"onstorage",
"onloadeddata",
"onfocus",
"onoffline",
"onplay",
"onafterprint",
"onclick",
"oncut",
"onmouseout",
"ondblclick",
"oncanplay",
"onloadstart",
"onappinstalled",
"onpointermove",
"ontoggle",
"oncontextmenu",
"NodeJS",
"onblur",
"oncancel",
"onbeforeprint",
"oncontextrestored",
"onloadedmetadata",
"onpointerup",
"onlanguagechange",
"oncopy",
"onselectstart",
"onscroll",
"onload",
"ondragstart",
"onbeforeinput",
"oncanplaythrough",
"oninput",
"oninvalid",
"ontimeupdate",
"ondurationchange",
"onselectionchange",
"onmouseup",
"location",
"onkeypress",
"onpointerleave",
"oncontextlost",
"ondrop",
"onsecuritypolicyviolation",
"oncontentvisibilityautostatechange",
"ondeviceorientation",
"onseeking",
"onrejectionhandled",
"onunload",
"onmouseleave",
"onhashchange",
"onpointerenter",
"onmousewheel",
"onunhandledrejection",
"ondragenter",
"onpopstate",
"onpagereveal",
"onemptied"
]
},
"linter": {
"rules": {
"correctness": { "noUnusedVariables": "warn" },
"style": {
"useBlockStatements": "error",
"useFilenamingConvention": {
"level": "off",
"options": { "filenameCases": ["kebab-case"] }
}
},
"suspicious": { "useAwait": "error" }
}
}
},
{
"includes": ["*.svelte", "**/*.svelte"],
"javascript": { "globals": [] },
"linter": {
"rules": {
"correctness": {
"noInnerDeclarations": "off",
"noSelfAssign": "off",
"noUnusedImports": "off",
"noUnusedVariables": "off"
}
}
}
}
],
"vcs": { "clientKind": "git", "enabled": false, "useIgnoreFile": false }
}

View File

@@ -1,6 +1,4 @@
import js from '@eslint/js';
import tslintPluginCompat from '@koddsson/eslint-plugin-tscompat';
import eslintPluginCompat from 'eslint-plugin-compat';
import eslintPluginSvelte from 'eslint-plugin-svelte';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
@@ -16,37 +14,6 @@ export default typescriptEslint.config(
...eslintPluginSvelte.configs.recommended,
eslintPluginUnicorn.configs.recommended,
js.configs.recommended,
{
plugins: {
tscompat: tslintPluginCompat,
},
rules: {
'tscompat/tscompat': [
'error',
{ browserslist: ['> 0.2% and last 4 major versions', '> 0.5%', 'not dead', 'edge >= 135', 'not edge < 135'] },
],
},
languageOptions: {
parser,
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
},
ignores: ['**/service-worker/**'],
},
{
plugins: {
compat: eslintPluginCompat,
},
settings: {
polyfills: [],
lintAllEsApis: true,
},
rules: {
'compat/compat': 'error',
},
},
{
ignores: [
'**/.DS_Store',

403
web/package-lock.json generated
View File

@@ -11,7 +11,7 @@
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.22.7",
"@immich/ui": "^0.22.4",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.11.5",
@@ -39,10 +39,10 @@
"thumbhash": "^0.1.1"
},
"devDependencies": {
"@biomejs/biome": "2.0.0-beta.6",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.18.0",
"@faker-js/faker": "^9.3.0",
"@koddsson/eslint-plugin-tscompat": "^0.2.0",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.6.0",
@@ -64,11 +64,12 @@
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.0",
"eslint-p": "^0.23.0",
"eslint-plugin-compat": "^6.0.2",
"eslint-plugin-oxlint": "^1.1.0",
"eslint-plugin-svelte": "^3.9.0",
"eslint-plugin-unicorn": "^59.0.0",
"factory.ts": "^1.4.1",
"globals": "^16.0.0",
"oxlint": "^1.1.0",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-sort-json": "^4.1.1",
@@ -76,7 +77,6 @@
"rollup-plugin-visualizer": "^6.0.0",
"svelte": "^5.25.3",
"svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.2.0",
"tailwindcss": "^4.1.7",
"tslib": "^2.6.2",
"typescript": "^5.7.3",
@@ -205,6 +205,169 @@
"node": ">=18"
}
},
"node_modules/@biomejs/biome": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.0.0-beta.6.tgz",
"integrity": "sha512-14vw9b5QJxrcP7WLkCeRiB/fft9wNZwx6yEiikBDxFbN7IAp39Xtvt/gJPq4ifhZ5IS25CnQEAkLLwfBIDMjsA==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.0.0-beta.6",
"@biomejs/cli-darwin-x64": "2.0.0-beta.6",
"@biomejs/cli-linux-arm64": "2.0.0-beta.6",
"@biomejs/cli-linux-arm64-musl": "2.0.0-beta.6",
"@biomejs/cli-linux-x64": "2.0.0-beta.6",
"@biomejs/cli-linux-x64-musl": "2.0.0-beta.6",
"@biomejs/cli-win32-arm64": "2.0.0-beta.6",
"@biomejs/cli-win32-x64": "2.0.0-beta.6"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.6.tgz",
"integrity": "sha512-L7PBLJlGTz5anougOMJQvEbzgG9sT1wKIXvgjFhu0dIsDZ/px2caWFCnv7Q9L2K0+yF08EYRTTZVvoVO5D//sQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.6.tgz",
"integrity": "sha512-ekhOOyhcVJ1ZRqHjq+eUOv8/3XMRKQ9Qf0URuO/PvHgopejv+PEoix0RIyxholYELKc049M4J3IJgsX4q2pZzw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.0.0-beta.6.tgz",
"integrity": "sha512-pu+rCLI36ziPtwnJY53HRr154711uVeCt1i2KNXehvwNZZMK141wwg4yPkXkBdBvw7H7sez0HE/rCQR2fByJnQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.6.tgz",
"integrity": "sha512-70WOWJI1/vZ97OUAt6r9HpiP5+vlL7yAdIoVQzVLjQy1TArfltN38KKqp9fnhgX173liUh0gry//MrWkKHYrIQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.0.0-beta.6.tgz",
"integrity": "sha512-emqZAuAyRw4Ug4B+CTgozIxVg1QLol28oZyIWuIjWEDr7eOo6Ek9zSZGeusmbwIEPu6r6qon8JAV6OdukxEwIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.6.tgz",
"integrity": "sha512-G9ZIoaNs6q9+mOoMURoXvNRfCOs28jrS4R8+3/y0h9ttOXpd4VALPOAfjzBGPpMd/4RoEMHXw/1Ts4dKvrv9zw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.0.0-beta.6.tgz",
"integrity": "sha512-JijYVZC6R5qq94yLaElowLLzbZ4xR2qDiOVPQV8H1+ru3IqVOjQu5f/lIt4uuea1iRFbxS+mOaxOZM9tUl1pTQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.0.0-beta.6.tgz",
"integrity": "sha512-zs29t/nxon11dKV+ckQB1yUOmhYx17e2+cHGK8PCVamqVGSMbjrd5evjtlfbnVJXP0ar7nNKhcg4ZWYGJ6aR1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
@@ -1333,9 +1496,9 @@
"link": true
},
"node_modules/@immich/ui": {
"version": "0.22.7",
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.7.tgz",
"integrity": "sha512-FdA0RDSOO1IDSTQmCbW9u5yXFl59EHu++tYonDR/FEZUKrMwfmQEanePSW5g5KofdumKEuxBN1fWFym3NbB0jQ==",
"version": "0.22.4",
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.4.tgz",
"integrity": "sha512-l0H8G8XZ3YaP/pA8NsLhGsNZpTAwcOyEFmF88D5HZkK3nFTZOQFxvzcMfyOeMS6Nevv0CHdvJp3ns0zajfvNzw==",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@mdi/js": "^7.4.47",
@@ -1540,26 +1703,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@koddsson/eslint-plugin-tscompat": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@koddsson/eslint-plugin-tscompat/-/eslint-plugin-tscompat-0.2.0.tgz",
"integrity": "sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==",
"dev": true,
"license": "ISC",
"dependencies": {
"@mdn/browser-compat-data": "^6.0.17",
"@typescript-eslint/type-utils": "^8.0.1",
"@typescript-eslint/utils": "^8.0.0",
"browserslist": "^4.23.0"
}
},
"node_modules/@koddsson/eslint-plugin-tscompat/node_modules/@mdn/browser-compat-data": {
"version": "6.0.22",
"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-6.0.22.tgz",
"integrity": "sha512-zhgOBTouJOd8IbE5dEEcfzg83l+nxKL/7Ru2HPeCVbog9I0JGHg3QZab9IxZquKFTUsc+c7QqU4EVENeZzZWRg==",
"dev": true,
"license": "CC0-1.0"
},
"node_modules/@mapbox/geojson-rewind": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
@@ -1710,13 +1853,6 @@
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
"license": "Apache-2.0"
},
"node_modules/@mdn/browser-compat-data": {
"version": "5.7.6",
"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.7.6.tgz",
"integrity": "sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==",
"dev": true,
"license": "CC0-1.0"
},
"node_modules/@namnode/store": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@namnode/store/-/store-0.1.0.tgz",
@@ -1765,6 +1901,118 @@
"node": ">= 8"
}
},
"node_modules/@oxlint/darwin-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.1.0.tgz",
"integrity": "sha512-sSnR3SOxIU/QfaqXrcQ0UVUkzJO0bcInQ7dMhHa102gVAgWjp1fBeMVCM0adEY0UNmEXrRkgD/rQtQgn9YAU+w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@oxlint/darwin-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.1.0.tgz",
"integrity": "sha512-Jvd3fHnzY2OYbmsg9NSGPoBkGViDGHSFnBKyJQ9LOIw7lxAyQBG2Quxc3GYPFR/f9OYho9C3p4+dIaAJfKhnsw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@oxlint/linux-arm64-gnu": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.1.0.tgz",
"integrity": "sha512-MgW4iskOdXuoR+wDXIJUfbdnTg2eo2FnQRaD6ZqhnDTDa7LnV+06rp/Cg3aGj2X9jSEcKDv/bMbYQuot7WRs6Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxlint/linux-arm64-musl": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.1.0.tgz",
"integrity": "sha512-a+pkEKmDRdrW+y0gtZ/m68ElVW2VZgATGbMxDgDYFpdiMx9Y0pUPwTMZ2EX/17Aslop4c1BiDSFDK7aEBxKR2g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxlint/linux-x64-gnu": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.1.0.tgz",
"integrity": "sha512-wNBsXCKVZMvUTcFitrV1wTsdhUAv8l+XQxHxciZ2SO6dpNnWEb2YCxSAIOXeyzBLdO4pIODYcSy38CvGue7TwA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxlint/linux-x64-musl": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.1.0.tgz",
"integrity": "sha512-pZD0lt6A5j2Wp70fgIYk4GoPfKTZ8mHWamWIpKFT7aSkFkiOi6nhLWDFvMEIHWRTK3LgkWUNcnWPp4brvin4wQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxlint/win32-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.1.0.tgz",
"integrity": "sha512-rT6uXQvE80+B+L04HJf30uF26426FPI9i9DAY2AxBUhrpNwhqkDEhQdd9ilFWVC7SSbpHgAs50lo+ImSAAkHPQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@oxlint/win32-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.1.0.tgz",
"integrity": "sha512-x6r5yvM3wEty93Bx0NuNK+kutUyS/K55itkUrxdExoK6GcmVDboGGuhju9HyU2cM/IWLEWO8RHcXSyaxr9GR5g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@photo-sphere-viewer/core": {
"version": "5.13.2",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.13.2.tgz",
@@ -3535,16 +3783,6 @@
"node": ">=12"
}
},
"node_modules/ast-metadata-inferer": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.8.1.tgz",
"integrity": "sha512-ht3Dm6Zr7SXv6t1Ra6gFo0+kLDglHGrEbYihTkcycrbHw7WCcuhBzPlJYHEsIpycaUwzsJHje+vUcxXUX4ztTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdn/browser-compat-data": "^5.6.19"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -4757,40 +4995,14 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint-plugin-compat": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-6.0.2.tgz",
"integrity": "sha512-1ME+YfJjmOz1blH0nPZpHgjMGK4kjgEeoYqGCqoBPQ/mGu/dJzdoP0f1C8H2jcWZjzhZjAMccbM/VdXhPORIfA==",
"node_modules/eslint-plugin-oxlint": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-oxlint/-/eslint-plugin-oxlint-1.1.0.tgz",
"integrity": "sha512-spDWxcsAfoUDjSwxPrP2gfuOJ2Hrv8faqQ5Vkm90lURp4no5aWJQ09xRKmZroIPTuQCKYgG9nvnakdIbXGlijg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdn/browser-compat-data": "^5.5.35",
"ast-metadata-inferer": "^0.8.1",
"browserslist": "^4.24.2",
"caniuse-lite": "^1.0.30001687",
"find-up": "^5.0.0",
"globals": "^15.7.0",
"lodash.memoize": "^4.1.2",
"semver": "^7.6.2"
},
"engines": {
"node": ">=18.x"
},
"peerDependencies": {
"eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-compat/node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"jsonc-parser": "^3.3.1"
}
},
"node_modules/eslint-plugin-svelte": {
@@ -6218,6 +6430,13 @@
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
"license": "MIT"
},
"node_modules/jsonc-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
"dev": true,
"license": "MIT"
},
"node_modules/just-compare": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz",
@@ -6570,13 +6789,6 @@
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -7215,6 +7427,33 @@
"node": ">= 0.8.0"
}
},
"node_modules/oxlint": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.1.0.tgz",
"integrity": "sha512-OVNpaoaQCUHHhCv5sYMPJ7Ts5k7ziw0QteH1gBSwF3elf/8GAew2Uh/0S7HsU1iGtjhlFy80+A8nwIb3Tq6m1w==",
"dev": true,
"license": "MIT",
"bin": {
"oxc_language_server": "bin/oxc_language_server",
"oxlint": "bin/oxlint"
},
"engines": {
"node": ">=8.*"
},
"funding": {
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxlint/darwin-arm64": "1.1.0",
"@oxlint/darwin-x64": "1.1.0",
"@oxlint/linux-arm64-gnu": "1.1.0",
"@oxlint/linux-arm64-musl": "1.1.0",
"@oxlint/linux-x64-gnu": "1.1.0",
"@oxlint/linux-x64-musl": "1.1.0",
"@oxlint/win32-arm64": "1.1.0",
"@oxlint/win32-x64": "1.1.0"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",

View File

@@ -15,6 +15,8 @@
"check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript",
"check:all": "npm run check:code && npm run test:cov",
"lint": "eslint . --max-warnings 0",
"lint:ox": "oxlint .",
"lint:ox-full": "npm run lint:ox && npm run lint:p",
"lint:p": "eslint-p . --max-warnings 0 --concurrency=4",
"lint:fix": "npm run lint -- --fix",
"format": "prettier --check .",
@@ -28,7 +30,7 @@
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.22.7",
"@immich/ui": "^0.22.4",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.11.5",
@@ -56,10 +58,10 @@
"thumbhash": "^0.1.1"
},
"devDependencies": {
"@biomejs/biome": "2.0.0-beta.6",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.18.0",
"@faker-js/faker": "^9.3.0",
"@koddsson/eslint-plugin-tscompat": "^0.2.0",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.6.0",
@@ -81,11 +83,12 @@
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.0",
"eslint-p": "^0.23.0",
"eslint-plugin-compat": "^6.0.2",
"eslint-plugin-oxlint": "^1.1.0",
"eslint-plugin-svelte": "^3.9.0",
"eslint-plugin-unicorn": "^59.0.0",
"factory.ts": "^1.4.1",
"globals": "^16.0.0",
"oxlint": "^1.1.0",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-sort-json": "^4.1.1",
@@ -93,7 +96,6 @@
"rollup-plugin-visualizer": "^6.0.0",
"svelte": "^5.25.3",
"svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.2.0",
"tailwindcss": "^4.1.7",
"tslib": "^2.6.2",
"typescript": "^5.7.3",

View File

@@ -41,11 +41,17 @@
--color-immich-bg: rgb(var(--immich-bg));
--color-immich-fg: rgb(var(--immich-fg));
--color-immich-gray: rgb(var(--immich-gray));
--color-immich-error: rgb(var(--immich-error));
--color-immich-success: rgb(var(--immich-success));
--color-immich-warning: rgb(var(--immich-warning));
--color-immich-dark-primary: rgb(var(--immich-dark-primary));
--color-immich-dark-bg: rgb(var(--immich-dark-bg));
--color-immich-dark-fg: rgb(var(--immich-dark-fg));
--color-immich-dark-gray: rgb(var(--immich-dark-gray));
--color-immich-dark-error: rgb(var(--immich-dark-error));
--color-immich-dark-success: rgb(var(--immich-dark-success));
--color-immich-dark-warning: rgb(var(--immich-dark-warning));
}
@theme {
@@ -68,12 +74,18 @@
--immich-primary: 66 80 175;
--immich-bg: 255 255 255;
--immich-fg: 0 0 0;
--immich-error: 229 115 115;
--immich-success: 129 199 132;
--immich-warning: 255 183 77;
/* dark */
--immich-dark-primary: 172 203 250;
--immich-dark-bg: 10 10 10;
--immich-dark-fg: 229 231 235;
--immich-dark-gray: 33 33 33;
--immich-dark-error: 211 47 47;
--immich-dark-success: 56 142 60;
--immich-dark-warning: 245 124 0;
}
*,

View File

@@ -3,7 +3,6 @@
import Icon from '$lib/components/elements/icon.svelte';
import { locale } from '$lib/stores/preferences.store';
import { JobCommand, type JobCommandDto, type JobCountsDto, type QueueStatusDto } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import {
mdiAlertCircle,
mdiAllInclusive,
@@ -18,6 +17,7 @@
import { t } from 'svelte-i18n';
import JobTileButton from './job-tile-button.svelte';
import JobTileStatus from './job-tile-status.svelte';
import { IconButton } from '@immich/ui';
interface Props {
title: string;
@@ -71,7 +71,7 @@
</span>
<div class="flex gap-2">
{#if jobCounts.failed > 0}
<Badge>
<Badge color="primary">
<div class="flex flex-row gap-1">
<span class="text-sm">
{$t('admin.jobs_failed', { values: { jobCount: jobCounts.failed.toLocaleString($locale) } })}
@@ -88,7 +88,7 @@
</Badge>
{/if}
{#if jobCounts.delayed > 0}
<Badge>
<Badge color="secondary">
<span class="text-sm">
{$t('admin.jobs_delayed', { values: { jobCount: jobCounts.delayed.toLocaleString($locale) } })}
</span>

View File

@@ -182,7 +182,7 @@
label={$t('admin.oauth_storage_quota_default').toUpperCase()}
description={$t('admin.oauth_storage_quota_default_description')}
bind:value={config.oauth.defaultStorageQuota}
required={false}
required={true}
disabled={disabled || !config.oauth.enabled}
isEdited={!(config.oauth.defaultStorageQuota == savedConfig.oauth.defaultStorageQuota)}
/>

View File

@@ -4,7 +4,6 @@
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import EmailTemplatePreviewModal from '$lib/modals/EmailTemplatePreviewModal.svelte';
import { handleError } from '$lib/utils/handle-error';
import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplateAdmin } from '@immich/sdk';
@@ -22,11 +21,13 @@
let loadingPreview = $state(false);
const myCoolVariable = 'hello!';
const getTemplate = async (name: string, template: string) => {
try {
loadingPreview = true;
const { html } = await getNotificationTemplateAdmin({ name, templateDto: { template } });
await modalManager.show(EmailTemplatePreviewModal, { html });
modalManger.show(EmailTemplatePreviewModal, { html });
} catch (error) {
handleError(error, 'Could not load template.');
} finally {

View File

@@ -0,0 +1,202 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
import { handleError } from '$lib/utils/handle-error';
import {
AlbumUserRole,
AssetOrder,
removeUserFromAlbum,
updateAlbumInfo,
updateAlbumUser,
type AlbumResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { Modal, ModalBody } from '@immich/ui';
import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js';
import { findKey } from 'lodash-es';
import { t } from 'svelte-i18n';
import type { RenderedOption } from '../elements/dropdown.svelte';
import { notificationController, NotificationType } from '../shared-components/notification/notification';
import SettingDropdown from '../shared-components/settings/setting-dropdown.svelte';
interface Props {
album: AlbumResponseDto;
order: AssetOrder | undefined;
user: UserResponseDto;
onChangeOrder: (order: AssetOrder) => void;
onClose: () => void;
onToggleEnabledActivity: () => void;
onShowSelectSharedUser: () => void;
onRemove: (userId: string) => void;
onRefreshAlbum: () => void;
}
let {
album,
order,
user,
onChangeOrder,
onClose,
onToggleEnabledActivity,
onShowSelectSharedUser,
onRemove,
onRefreshAlbum,
}: Props = $props();
let selectedRemoveUser: UserResponseDto | null = $state(null);
const options: Record<AssetOrder, RenderedOption> = {
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') },
[AssetOrder.Desc]: { icon: mdiArrowDownThin, title: $t('newest_first') },
};
let selectedOption = $derived(order ? options[order] : options[AssetOrder.Desc]);
const handleToggle = async (returnedOption: RenderedOption): Promise<void> => {
if (selectedOption === returnedOption) {
return;
}
let order: AssetOrder = AssetOrder.Desc;
order = findKey(options, (option) => option === returnedOption) as AssetOrder;
try {
await updateAlbumInfo({
id: album.id,
updateAlbumDto: {
order,
},
});
onChangeOrder(order);
} catch (error) {
handleError(error, $t('errors.unable_to_save_album'));
}
};
const handleMenuRemove = (user: UserResponseDto): void => {
selectedRemoveUser = user;
};
const handleRemoveUser = async (): Promise<void> => {
if (!selectedRemoveUser) {
return;
}
try {
await removeUserFromAlbum({ id: album.id, userId: selectedRemoveUser.id });
onRemove(selectedRemoveUser.id);
notificationController.show({
type: NotificationType.Info,
message: $t('album_user_removed', { values: { user: selectedRemoveUser.name } }),
});
} catch (error) {
handleError(error, $t('errors.unable_to_remove_album_users'));
} finally {
selectedRemoveUser = null;
}
};
const handleUpdateSharedUserRole = async (user: UserResponseDto, role: AlbumUserRole) => {
try {
await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { role } });
const message = $t('user_role_set', {
values: { user: user.name, role: role == AlbumUserRole.Viewer ? $t('role_viewer') : $t('role_editor') },
});
onRefreshAlbum();
notificationController.show({ type: NotificationType.Info, message });
} catch (error) {
handleError(error, $t('errors.unable_to_change_album_user_role'));
} finally {
selectedRemoveUser = null;
}
};
</script>
{#if !selectedRemoveUser}
<Modal title={$t('options')} {onClose} size="small">
<ModalBody>
<div class="items-center justify-center">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
<div class="grid p-2 gap-y-2">
{#if order}
<SettingDropdown
title={$t('display_order')}
options={Object.values(options)}
selectedOption={options[order]}
onToggle={handleToggle}
/>
{/if}
<SettingSwitch
title={$t('comments_and_likes')}
subtitle={$t('let_others_respond')}
checked={album.isActivityEnabled}
onToggle={onToggleEnabledActivity}
/>
</div>
</div>
<div class="py-2">
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
<div class="p-2">
<button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div>
</div>
<div>{$t('invite_people')}</div>
</button>
<div class="flex items-center gap-2 py-2 mt-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
<div>{$t('owner')}</div>
</div>
{#each album.albumUsers as { user, role } (user.id)}
<div class="flex items-center gap-2 py-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
{#if role === AlbumUserRole.Viewer}
{$t('role_viewer')}
{:else}
{$t('role_editor')}
{/if}
{#if user.id !== album.ownerId}
<ButtonContextMenu icon={mdiDotsVertical} size="medium" title={$t('options')}>
{#if role === AlbumUserRole.Viewer}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Editor)}
text={$t('allow_edits')}
/>
{:else}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)}
text={$t('disallow_edits')}
/>
{/if}
<!-- Allow deletion for non-owners -->
<MenuOption onClick={() => handleMenuRemove(user)} text={$t('remove')} />
</ButtonContextMenu>
{/if}
</div>
{/each}
</div>
</div>
</div>
</ModalBody>
</Modal>
{/if}
{#if selectedRemoveUser}
<ConfirmModal
title={$t('album_remove_user')}
prompt={$t('album_remove_user_confirmation', { values: { user: selectedRemoveUser.name } })}
confirmText={$t('remove_user')}
onClose={(confirmed) => (confirmed ? handleRemoveUser() : (selectedRemoveUser = null))}
/>
{/if}

View File

@@ -8,9 +8,9 @@
import { AssetAction, ProjectionType } from '$lib/constants';
import { activityManager } from '$lib/managers/activity-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { isShowDetail } from '$lib/stores/preferences.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.store';
@@ -23,7 +23,6 @@
AssetJobName,
AssetTypeEnum,
getAllAlbums,
getAssetInfo,
getStack,
runAssetJobs,
type AlbumResponseDto,
@@ -139,20 +138,16 @@
}
};
const onAssetUpdate = async (assetId: string) => {
if (assetId === asset.id) {
asset = await getAssetInfo({ id: assetId, key: authManager.key });
const onAssetUpdate = ({ asset: assetUpdate }: { event: 'upload' | 'update'; asset: AssetResponseDto }) => {
if (assetUpdate.id === asset.id) {
asset = assetUpdate;
}
};
onMount(async () => {
unsubscribes.push(
websocketEvents.on('on_upload_success', (asset) => onAssetUpdate(asset.id)),
websocketEvents.on('on_asset_update', async (assetsIds) => {
for (const assetId of assetsIds) {
await onAssetUpdate(assetId);
}
}),
websocketEvents.on('on_upload_success', (asset) => onAssetUpdate({ event: 'upload', asset })),
websocketEvents.on('on_asset_update', (asset) => onAssetUpdate({ event: 'update', asset })),
);
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
@@ -305,10 +300,8 @@
const handleStopSlideshow = async () => {
try {
// eslint-disable-next-line tscompat/tscompat
if (document.fullscreenElement) {
document.body.style.cursor = '';
// eslint-disable-next-line tscompat/tscompat
await document.exitFullscreen();
}
} catch (error) {

View File

@@ -46,7 +46,7 @@
{#each tags as tag (tag.id)}
<div class="flex group transition-all">
<a
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-primary rounded-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary rounded-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
href={encodeURI(`${AppRoute.TAGS}/?path=${tag.value}`)}
>
<p class="text-sm">

View File

@@ -1,21 +1,18 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import { authManager } from '$lib/managers/auth-manager.svelte';
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
import { editTypes, showCancelConfirmDialog } from '$lib/stores/asset-editor.store';
import { websocketEvents } from '$lib/stores/websocket';
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
import { type AssetResponseDto } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiClose } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
onMount(() => {
return websocketEvents.on('on_asset_update', async (assetIds) => {
for (const assetId of assetIds) {
if (assetId === asset.id) {
asset = await getAssetInfo({ id: assetId, key: authManager.key });
}
return websocketEvents.on('on_asset_update', (assetUpdate) => {
if (assetUpdate.id === asset.id) {
asset = assetUpdate;
}
});
});

View File

@@ -57,7 +57,6 @@
canvas = new Canvas(canvasEl);
configureControlStyle();
// eslint-disable-next-line tscompat/tscompat
faceRect = new Rect({
fill: 'rgba(66,80,175,0.25)',
stroke: 'rgb(66,80,175)',

View File

@@ -101,9 +101,7 @@
};
const onShowSettings = async () => {
// eslint-disable-next-line tscompat/tscompat
if (document.fullscreenElement) {
// eslint-disable-next-line tscompat/tscompat
await document.exitFullscreen();
}
await modalManager.show(SlideshowSettingsModal);

View File

@@ -1,16 +1,29 @@
<script lang="ts" module>
export type Color = 'primary' | 'secondary';
export type Rounded = false | true | 'full';
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
rounded?: boolean | 'full';
color?: Color;
rounded?: Rounded;
children?: Snippet;
}
let { rounded = true, children }: Props = $props();
let { color = 'primary', rounded = true, children }: Props = $props();
const colorClasses: Record<Color, string> = {
primary: 'text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary',
secondary: 'text-immich-dark-bg dark:text-immich-gray dark:bg-gray-600 bg-gray-300 dark:text-immich-gray',
};
</script>
<span
class="bg-primary text-subtle inline-block h-min whitespace-nowrap px-3 py-1 text-center align-baseline text-xs leading-none"
class="inline-block h-min whitespace-nowrap px-3 py-1 text-center align-baseline text-xs leading-none {colorClasses[
color
]}"
class:rounded-md={rounded === true}
class:rounded-full={rounded === 'full'}
>

View File

@@ -169,9 +169,19 @@
>
<td class="w-1/8 text-ellipsis ps-8 text-sm">
{#if validatedPath.isValid}
<Icon path={mdiCheckCircleOutline} size="24" title={validatedPath.message} class="text-success" />
<Icon
path={mdiCheckCircleOutline}
size="24"
title={validatedPath.message}
class="text-immich-success dark:text-immich-dark-success"
/>
{:else}
<Icon path={mdiAlertOutline} size="24" title={validatedPath.message} class="text-warning" />
<Icon
path={mdiAlertOutline}
size="24"
title={validatedPath.message}
class="text-immich-warning dark:text-immich-dark-warning"
/>
{/if}
</td>

View File

@@ -51,7 +51,6 @@
const entries: FileSystemEntry[] = [];
const files: File[] = [];
for (const item of dataTransfer.items) {
// eslint-disable-next-line tscompat/tscompat
const entry = item.webkitGetAsEntry();
if (entry) {
entries.push(entry);
@@ -68,7 +67,6 @@
return handleFiles([...files, ...directoryFiles]);
};
// eslint-disable-next-line tscompat/tscompat
const browserSupportsDirectoryUpload = () => typeof DataTransferItem.prototype.webkitGetAsEntry === 'function';
const getAllFilesFromTransferEntries = async (transferEntries: FileSystemEntry[]): Promise<File[]> => {

View File

@@ -7,7 +7,7 @@
type ComponentNotification,
type Notification,
} from '$lib/components/shared-components/notification/notification';
import { Button, IconButton, type Color } from '@immich/ui';
import { IconButton } from '@immich/ui';
import { mdiCloseCircleOutline, mdiInformationOutline, mdiWindowClose } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -40,10 +40,10 @@
[NotificationType.Warning]: '#D08613',
};
const colors: Record<NotificationType, Color> = {
[NotificationType.Info]: 'primary',
[NotificationType.Error]: 'danger',
[NotificationType.Warning]: 'warning',
const buttonStyle: Record<NotificationType, string> = {
[NotificationType.Info]: 'text-white bg-immich-primary hover:bg-immich-primary/75',
[NotificationType.Error]: 'text-white bg-immich-error hover:bg-immich-error/75',
[NotificationType.Warning]: 'text-white bg-immich-warning hover:bg-immich-warning/75',
};
onMount(() => {
@@ -111,16 +111,16 @@
</p>
{#if notification.button}
<p class="ps-[28px] mt-2.5 light text-light">
<Button
size="small"
color={colors[notification.type]}
<p class="ps-[28px] mt-2.5 text-sm">
<button
type="button"
class="{buttonStyle[notification.type]} rounded px-3 pt-1.5 pb-1 transition-all duration-200"
onclick={handleButtonClick}
aria-hidden="true"
tabindex={-1}
>
{notification.button.text}
</Button>
</button>
</p>
{/if}
</div>

View File

@@ -310,7 +310,6 @@
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
};
/* eslint-disable tscompat/tscompat */
const getTouch = (event: TouchEvent) => {
if (event.touches.length === 1) {
return event.touches[0];
@@ -355,7 +354,6 @@
isHover = false;
}
};
/* eslint-enable tscompat/tscompat */
onMount(() => {
document.addEventListener('touchmove', onTouchMove, true);
return () => {
@@ -507,7 +505,10 @@
{/if}
<!-- Scroll Position Indicator Line -->
{#if !usingMobileDevice && !isDragging}
<div class="absolute end-0 h-[2px] w-10 bg-primary" style:top="{scrollY + PADDING_TOP - 2}px">
<div
class="absolute end-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
style:top="{scrollY + PADDING_TOP - 2}px"
>
{#if timelineManager.scrolling && scrollHoverLabel && !isHover}
<p
transition:fade={{ duration: 200 }}

Some files were not shown because too many files have changed in this diff Show More