Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fe8611ece | |||
| c1338d26a6 | |||
| f487f93441 | |||
| bfb68c3464 | |||
| a8e8d27492 |
@@ -11,8 +11,8 @@ services:
|
|||||||
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||||
- server_node_modules:/workspaces/immich/server/node_modules
|
- server_node_modules:/workspaces/immich/server/node_modules
|
||||||
- web_node_modules:/workspaces/immich/web/node_modules
|
- web_node_modules:/workspaces/immich/web/node_modules
|
||||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}/photos:/workspaces/immich/server/upload
|
||||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/workspaces/immich/server/upload/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
||||||
database:
|
database:
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ install_dependencies() {
|
|||||||
log "Installing dependencies"
|
log "Installing dependencies"
|
||||||
(
|
(
|
||||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||||
export CI=1 FROZEN=1 OFFLINE=1
|
run_cmd make ci-server
|
||||||
run_cmd make setup-web-dev setup-server-dev
|
run_cmd make ci-sdk
|
||||||
|
run_cmd make build-sdk
|
||||||
|
run_cmd make ci-web
|
||||||
)
|
)
|
||||||
log ""
|
log ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ services:
|
|||||||
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||||
- server_node_modules:/workspaces/immich/server/node_modules
|
- server_node_modules:/workspaces/immich/server/node_modules
|
||||||
- web_node_modules:/workspaces/immich/web/node_modules
|
- web_node_modules:/workspaces/immich/web/node_modules
|
||||||
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload
|
||||||
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/usr/src/app/upload/upload
|
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source /immich-devcontainer/container-common.sh
|
source /immich-devcontainer/container-common.sh
|
||||||
|
|
||||||
|
log "Setting up Immich dev container..."
|
||||||
|
fix_permissions
|
||||||
|
|
||||||
log "Installing npm dependencies (node_modules)..."
|
log "Installing npm dependencies (node_modules)..."
|
||||||
install_dependencies
|
install_dependencies
|
||||||
|
|
||||||
|
|||||||
+17
-17
@@ -1,41 +1,41 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
.github/
|
.github/
|
||||||
.git/
|
.git/
|
||||||
.env*
|
|
||||||
*.log
|
|
||||||
*.tmp
|
|
||||||
*.temp
|
|
||||||
|
|
||||||
**/Dockerfile
|
|
||||||
**/node_modules/
|
|
||||||
**/.pnpm-store/
|
|
||||||
**/dist/
|
|
||||||
**/coverage/
|
|
||||||
**/build/
|
|
||||||
|
|
||||||
design/
|
design/
|
||||||
docker/
|
docker/
|
||||||
|
Dockerfile
|
||||||
!docker/scripts
|
!docker/scripts
|
||||||
|
|
||||||
docs/
|
docs/
|
||||||
!docs/package.json
|
!docs/package.json
|
||||||
!docs/package-lock.json
|
!docs/package-lock.json
|
||||||
|
|
||||||
e2e/
|
e2e/
|
||||||
!e2e/package.json
|
!e2e/package.json
|
||||||
!e2e/package-lock.json
|
!e2e/package-lock.json
|
||||||
|
|
||||||
fastlane/
|
fastlane/
|
||||||
machine-learning/
|
machine-learning/
|
||||||
misc/
|
misc/
|
||||||
mobile/
|
mobile/
|
||||||
|
|
||||||
open-api/typescript-sdk/build/
|
cli/coverage/
|
||||||
!open-api/typescript-sdk/package.json
|
cli/dist/
|
||||||
!open-api/typescript-sdk/package-lock.json
|
cli/node_modules/
|
||||||
|
cli/Dockerfile
|
||||||
|
|
||||||
|
open-api/typescript-sdk/build/
|
||||||
|
open-api/typescript-sdk/node_modules/
|
||||||
|
|
||||||
|
server/coverage/
|
||||||
|
server/node_modules/
|
||||||
server/upload/
|
server/upload/
|
||||||
server/src/queries
|
server/src/queries
|
||||||
|
server/dist/
|
||||||
server/www/
|
server/www/
|
||||||
|
server/Dockerfile
|
||||||
|
|
||||||
|
web/node_modules/
|
||||||
|
web/coverage/
|
||||||
web/.svelte-kit
|
web/.svelte-kit
|
||||||
|
web/build/
|
||||||
|
web/.env
|
||||||
|
web/Dockerfile
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# Ignore files for PNPM, NPM and YARN
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
Generated
+3
-3
@@ -9,9 +9,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
|
||||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -122,17 +122,17 @@ jobs:
|
|||||||
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
|
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
|
||||||
run: |
|
run: |
|
||||||
if [[ $IS_MAIN == 'true' ]]; then
|
if [[ $IS_MAIN == 'true' ]]; then
|
||||||
flutter build apk --release --flavor production
|
flutter build apk --release
|
||||||
flutter build apk --release --flavor production --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||||
else
|
else
|
||||||
flutter build apk --debug --flavor production --split-per-abi --target-platform android-arm64
|
flutter build apk --debug --split-per-abi --target-platform android-arm64
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Publish Android Artifact
|
- name: Publish Android Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: release-apk-signed
|
name: release-apk-signed
|
||||||
path: mobile/build/app/outputs/flutter-apk/**/*.apk
|
path: mobile/build/app/outputs/flutter-apk/*.apk
|
||||||
|
|
||||||
- name: Save Gradle Cache
|
- name: Save Gradle Cache
|
||||||
id: cache-gradle-save
|
id: cache-gradle-save
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: Check Team Approval
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-approval:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check for team/admin review
|
||||||
|
id: check-review
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
|
||||||
|
console.log(`Checking reviews for PR #${prNumber}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the users.json file from immich-app/devtools repository
|
||||||
|
const { data: usersFile } = await github.rest.repos.getContent({
|
||||||
|
owner: 'immich-app',
|
||||||
|
repo: 'devtools',
|
||||||
|
path: 'tf/deployment/data/users.json'
|
||||||
|
});
|
||||||
|
|
||||||
|
const usersData = JSON.parse(Buffer.from(usersFile.content, 'base64').toString());
|
||||||
|
console.log(`Loaded ${usersData.length} users from devtools repo`);
|
||||||
|
|
||||||
|
// Create a map of GitHub IDs to user roles for efficient lookup
|
||||||
|
const userRoles = new Map();
|
||||||
|
for (const user of usersData) {
|
||||||
|
if (user.github && user.github.id && (user.role === 'team' || user.role === 'admin')) {
|
||||||
|
userRoles.set(user.github.id, {
|
||||||
|
username: user.github.username,
|
||||||
|
role: user.role
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${userRoles.size} team/admin users`);
|
||||||
|
|
||||||
|
// Get all reviews for the pull request
|
||||||
|
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: prNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${reviews.length} reviews`);
|
||||||
|
|
||||||
|
// Check if any review is from a team/admin member
|
||||||
|
let hasValidReview = false;
|
||||||
|
|
||||||
|
for (const review of reviews) {
|
||||||
|
console.log(`Review by ${review.user.login} (ID: ${review.user.id}): state=${review.state}`);
|
||||||
|
|
||||||
|
// Check if the reviewer is a team/admin member and the review is approved
|
||||||
|
const userInfo = userRoles.get(review.user.id);
|
||||||
|
if (userInfo && review.state === 'APPROVED') {
|
||||||
|
console.log(`✅ Found approved review from ${userInfo.role} member: ${review.user.login}`);
|
||||||
|
hasValidReview = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidReview) {
|
||||||
|
console.log('❌ No approved review from team/admin member found');
|
||||||
|
core.setFailed('This pull request requires an approved review from a team or admin member');
|
||||||
|
} else {
|
||||||
|
console.log('✅ Required team/admin member review found');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking reviews:', error);
|
||||||
|
core.setFailed(`Failed to check reviews: ${error.message}`);
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -76,6 +76,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ jobs:
|
|||||||
tag-suffix: '-rocm'
|
tag-suffix: '-rocm'
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
runner-mapping: '{"linux/amd64": "mich"}'
|
runner-mapping: '{"linux/amd64": "mich"}'
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
@@ -154,7 +154,7 @@ jobs:
|
|||||||
name: Build and Push Server
|
name: Build and Push Server
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
name: Org Checks
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_review:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-approvals:
|
|
||||||
name: Check for Team/Admin Review
|
|
||||||
uses: immich-app/devtools/.github/workflows/required-approval.yml@main
|
|
||||||
permissions:
|
|
||||||
pull-requests: read
|
|
||||||
contents: read
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name: Required Reviewers Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_review:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-member-review:
|
||||||
|
uses: ./.github/workflows/check-team-approval.yml
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
contents: read
|
||||||
@@ -42,9 +42,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./mobile
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@@ -59,22 +56,27 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dart pub get
|
run: dart pub get
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Install DCM
|
- name: Install DCM
|
||||||
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
|
run: |
|
||||||
with:
|
sudo apt-get update
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
||||||
version: auto
|
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
|
||||||
working-directory: ./mobile
|
sudo apt-get update
|
||||||
|
sudo apt-get install dcm
|
||||||
|
|
||||||
- name: Generate translation file
|
- name: Generate translation file
|
||||||
run: make translation
|
run: make translation
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Run Build Runner
|
- name: Run Build Runner
|
||||||
run: make build
|
run: make build
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Generate platform API
|
- name: Generate platform API
|
||||||
run: make pigeon
|
run: make pigeon
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
@@ -96,16 +98,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Run dart analyze
|
- name: Run dart analyze
|
||||||
run: dart analyze --fatal-infos
|
run: dart analyze --fatal-infos
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Run dart format
|
- name: Run dart format
|
||||||
run: dart format lib/ --set-exit-if-changed
|
run: dart format lib/ --set-exit-if-changed
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Run dart custom_lint
|
- name: Run dart custom_lint
|
||||||
run: dart run custom_lint
|
run: dart run custom_lint
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
# TODO: Use https://github.com/CQLabs/dcm-action
|
|
||||||
- name: Run DCM
|
- name: Run DCM
|
||||||
run: dcm analyze lib --fatal-style --fatal-warnings
|
run: dcm analyze lib --fatal-style --fatal-warnings
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
zizmor:
|
zizmor:
|
||||||
name: zizmor
|
name: zizmor
|
||||||
@@ -129,7 +134,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload SARIF file
|
- name: Upload SARIF file
|
||||||
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
category: zizmor
|
category: zizmor
|
||||||
|
|||||||
@@ -24,4 +24,3 @@ mobile/android/fastlane/report.xml
|
|||||||
mobile/ios/fastlane/report.xml
|
mobile/ios/fastlane/report.xml
|
||||||
|
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
.pnpm-store
|
|
||||||
|
|||||||
Vendored
-19
@@ -18,25 +18,6 @@
|
|||||||
"name": "Immich Workers",
|
"name": "Immich Workers",
|
||||||
"remoteRoot": "/usr/src/app",
|
"remoteRoot": "/usr/src/app",
|
||||||
"localRoot": "${workspaceFolder}/server"
|
"localRoot": "${workspaceFolder}/server"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Flavor - Production",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "dart",
|
|
||||||
"codeLens": {
|
|
||||||
"for": [
|
|
||||||
"run-test",
|
|
||||||
"run-test-file",
|
|
||||||
"run-file",
|
|
||||||
"debug-test",
|
|
||||||
"debug-test-file",
|
|
||||||
"debug-file",
|
|
||||||
],
|
|
||||||
"title": "${debugType}",
|
|
||||||
},
|
|
||||||
"args": [
|
|
||||||
"--flavor", "production"
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,5 +106,4 @@ clean:
|
|||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
||||||
|
|
||||||
setup-server-dev: install-server
|
setup-dev: install-server install-sdk build-sdk install-web
|
||||||
setup-web-dev: install-sdk build-sdk install-web
|
|
||||||
Generated
+103
-103
@@ -16,7 +16,7 @@
|
|||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"immich": "bin/immich"
|
"immich": "dist/index.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
"vite": "^7.0.0",
|
"vite": "^6.0.0",
|
||||||
"vite-tsconfig-paths": "^5.0.0",
|
"vite-tsconfig-paths": "^5.0.0",
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
"vitest-fetch-mock": "^0.4.0",
|
"vitest-fetch-mock": "^0.4.0",
|
||||||
@@ -607,9 +607,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array": {
|
"node_modules/@eslint/config-array": {
|
||||||
"version": "0.21.0",
|
"version": "0.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz",
|
||||||
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
|
"integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -622,9 +622,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-helpers": {
|
"node_modules/@eslint/config-helpers": {
|
||||||
"version": "0.3.0",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
|
||||||
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==",
|
"integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -682,9 +682,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.30.1",
|
"version": "9.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
|
||||||
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
|
"integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1365,17 +1365,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
|
||||||
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
|
"integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.36.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/type-utils": "8.36.0",
|
"@typescript-eslint/type-utils": "8.35.0",
|
||||||
"@typescript-eslint/utils": "8.36.0",
|
"@typescript-eslint/utils": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
"@typescript-eslint/visitor-keys": "8.35.0",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@@ -1389,7 +1389,7 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.36.0",
|
"@typescript-eslint/parser": "^8.35.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
@@ -1405,16 +1405,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
|
||||||
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
|
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.36.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.36.0",
|
"@typescript-eslint/typescript-estree": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
"@typescript-eslint/visitor-keys": "8.35.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1430,14 +1430,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
|
||||||
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
|
"integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.36.0",
|
"@typescript-eslint/tsconfig-utils": "^8.35.0",
|
||||||
"@typescript-eslint/types": "^8.36.0",
|
"@typescript-eslint/types": "^8.35.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1452,14 +1452,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
|
||||||
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
|
"integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.36.0"
|
"@typescript-eslint/visitor-keys": "8.35.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1470,9 +1470,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
|
||||||
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
|
"integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1487,14 +1487,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
|
||||||
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
|
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.36.0",
|
"@typescript-eslint/typescript-estree": "8.35.0",
|
||||||
"@typescript-eslint/utils": "8.36.0",
|
"@typescript-eslint/utils": "8.35.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -1511,9 +1511,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
|
||||||
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
|
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1525,16 +1525,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
|
||||||
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
|
"integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.36.0",
|
"@typescript-eslint/project-service": "8.35.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.36.0",
|
"@typescript-eslint/tsconfig-utils": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
"@typescript-eslint/visitor-keys": "8.35.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@@ -1580,16 +1580,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
|
||||||
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
|
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.36.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.36.0"
|
"@typescript-eslint/typescript-estree": "8.35.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1604,13 +1604,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
|
||||||
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
|
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2305,19 +2305,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.30.1",
|
"version": "9.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
|
||||||
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
|
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
"@eslint/config-array": "^0.21.0",
|
"@eslint/config-array": "^0.20.1",
|
||||||
"@eslint/config-helpers": "^0.3.0",
|
"@eslint/config-helpers": "^0.2.1",
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.14.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.30.1",
|
"@eslint/js": "9.29.0",
|
||||||
"@eslint/plugin-kit": "^0.3.1",
|
"@eslint/plugin-kit": "^0.3.1",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
@@ -2822,9 +2822,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "16.3.0",
|
"version": "16.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
|
||||||
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
|
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3466,9 +3466,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3486,7 +3486,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.8",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -3505,9 +3505,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
|
||||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4125,15 +4125,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz",
|
||||||
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==",
|
"integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.36.0",
|
"@typescript-eslint/eslint-plugin": "8.35.0",
|
||||||
"@typescript-eslint/parser": "8.36.0",
|
"@typescript-eslint/parser": "8.35.0",
|
||||||
"@typescript-eslint/utils": "8.36.0"
|
"@typescript-eslint/utils": "8.35.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -4196,24 +4196,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.0.4",
|
"version": "6.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||||
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
|
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.6",
|
"fdir": "^6.4.4",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.3",
|
||||||
"rollup": "^4.40.0",
|
"rollup": "^4.34.9",
|
||||||
"tinyglobby": "^0.2.14"
|
"tinyglobby": "^0.2.13"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
@@ -4222,14 +4222,14 @@
|
|||||||
"fsevents": "~2.3.3"
|
"fsevents": "~2.3.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "^4.0.0",
|
"less": "*",
|
||||||
"lightningcss": "^1.21.0",
|
"lightningcss": "^1.21.0",
|
||||||
"sass": "^1.70.0",
|
"sass": "*",
|
||||||
"sass-embedded": "^1.70.0",
|
"sass-embedded": "*",
|
||||||
"stylus": ">=0.54.8",
|
"stylus": "*",
|
||||||
"sugarss": "^5.0.0",
|
"sugarss": "*",
|
||||||
"terser": "^5.16.0",
|
"terser": "^5.16.0",
|
||||||
"tsx": "^4.8.1",
|
"tsx": "^4.8.1",
|
||||||
"yaml": "^2.4.2"
|
"yaml": "^2.4.2"
|
||||||
@@ -4314,9 +4314,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/fdir": {
|
"node_modules/vite/node_modules/fdir": {
|
||||||
"version": "6.4.6",
|
"version": "6.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
"vite": "^7.0.0",
|
"vite": "^6.0.0",
|
||||||
"vite-tsconfig-paths": "^5.0.0",
|
"vite-tsconfig-paths": "^5.0.0",
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
"vitest-fetch-mock": "^0.4.0",
|
"vitest-fetch-mock": "^0.4.0",
|
||||||
|
|||||||
@@ -16,25 +16,22 @@ name: immich-dev
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: ['immich-dev']
|
command: ['/usr/src/app/bin/immich-dev']
|
||||||
image: immich-server-dev:latest
|
image: immich-server-dev:latest
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||||
build:
|
build:
|
||||||
args:
|
|
||||||
- SERVER_USER=${SERVER_USER:-0}
|
|
||||||
- SERVER_GROUP=${SERVER_GROUP:-0}
|
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
target: dev
|
target: dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ../server:/usr/src/app/server
|
- ../server:/usr/src/app
|
||||||
- ../open-api:/usr/src/app/open-api
|
- ../open-api:/usr/src/open-api
|
||||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
||||||
- /usr/src/app/server/node_modules
|
- /usr/src/app/node_modules
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -72,23 +69,19 @@ services:
|
|||||||
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
||||||
# user: 0:0
|
# user: 0:0
|
||||||
build:
|
build:
|
||||||
args:
|
context: ../web
|
||||||
- WEB_USER=${WEB_USER:-1000}
|
command: ['/usr/src/app/bin/immich-web']
|
||||||
- WEB_GROUP=${WEB_GROUP:-1000}
|
|
||||||
context: ../
|
|
||||||
dockerfile: web/Dockerfile
|
|
||||||
command: ['immich-web']
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ../web:/usr/src/app/web
|
- ../web:/usr/src/app
|
||||||
- ../i18n:/usr/src/app/i18n
|
- ../i18n:/usr/src/i18n
|
||||||
- ../open-api/:/usr/src/app/open-api/
|
- ../open-api/:/usr/src/open-api/
|
||||||
# - ../../ui:/usr/ui
|
# - ../../ui:/usr/ui
|
||||||
- /usr/src/app/web/node_modules
|
- /usr/src/app/node_modules
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 1048576
|
soft: 1048576
|
||||||
@@ -123,7 +116,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
@@ -83,7 +83,7 @@ services:
|
|||||||
container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
|
image: prom/prometheus@sha256:7a34573f0b9c952286b33d537f233cd5b708e12263733aa646e50c33f598f16c
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
@@ -46,12 +46,6 @@ services:
|
|||||||
|
|
||||||
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
|
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
|
||||||
|
|
||||||
|
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
|
||||||
|
|
||||||
<img src={require('./img/admin-jobs.webp').default} width="60%" title="Admin jobs" />
|
<img src={require('./img/admin-jobs.webp').default} width="60%" title="Admin jobs" />
|
||||||
|
|
||||||
Additionally, some jobs (such as memories generation) run on a schedule, which is every night at midnight by default. To change when they run or enable/disable a job navigate to System Settings -> [Nightly Tasks Settings](https://my.immich.app/admin/system-settings?isOpen=nightly-tasks).
|
|
||||||
|
|
||||||
<img src={require('./img/admin-nightly-tasks.webp').default} width="60%" title="Admin nightly tasks" />
|
|
||||||
|
|
||||||
:::note
|
|
||||||
Some jobs ([External Libraries](/docs/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings.
|
|
||||||
:::
|
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||||
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
|
|
||||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
||||||
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
||||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||||
|
|||||||
@@ -2,17 +2,16 @@
|
|||||||
|
|
||||||
The `immich-server` docker image comes preinstalled with an administrative CLI (`immich-admin`) that supports the following commands:
|
The `immich-server` docker image comes preinstalled with an administrative CLI (`immich-admin`) that supports the following commands:
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------------------------ | ------------------------------------------------------------- |
|
| ------------------------ | ------------------------------------- |
|
||||||
| `help` | Display help |
|
| `help` | Display help |
|
||||||
| `reset-admin-password` | Reset the password for the admin user |
|
| `reset-admin-password` | Reset the password for the admin user |
|
||||||
| `disable-password-login` | Disable password login |
|
| `disable-password-login` | Disable password login |
|
||||||
| `enable-password-login` | Enable password login |
|
| `enable-password-login` | Enable password login |
|
||||||
| `enable-oauth-login` | Enable OAuth login |
|
| `enable-oauth-login` | Enable OAuth login |
|
||||||
| `disable-oauth-login` | Disable OAuth login |
|
| `disable-oauth-login` | Disable OAuth login |
|
||||||
| `list-users` | List Immich users |
|
| `list-users` | List Immich users |
|
||||||
| `version` | Print Immich version |
|
| `version` | Print Immich version |
|
||||||
| `change-media-location` | Change database file paths to align with a new media location |
|
|
||||||
|
|
||||||
## How to run a command
|
## How to run a command
|
||||||
|
|
||||||
@@ -89,24 +88,3 @@ Print Immich Version
|
|||||||
immich-admin version
|
immich-admin version
|
||||||
v1.129.0
|
v1.129.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Change media location
|
|
||||||
|
|
||||||
```
|
|
||||||
immich-admin change-media-location
|
|
||||||
? Enter the previous value of IMMICH_MEDIA_LOCATION: /usr/src/app/upload
|
|
||||||
? Enter the new value of IMMICH_MEDIA_LOCATION: /data
|
|
||||||
|
|
||||||
Previous value: /usr/src/app/upload
|
|
||||||
Current value: /data
|
|
||||||
|
|
||||||
Changing database paths from "/usr/src/app/upload/*" to "/data/*"
|
|
||||||
|
|
||||||
? Do you want to proceed? [Y/n] y
|
|
||||||
|
|
||||||
Database file paths updated successfully! 🎉
|
|
||||||
|
|
||||||
You may now set IMMICH_MEDIA_LOCATION=/data and restart!
|
|
||||||
|
|
||||||
(please remember to update applicable volume mounts e.g. ${UPLOAD_LOCATION}:/data)
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ sidebar_position: 3
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
Get started fast!
|
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/)
|
||||||
|
|
||||||
[](https://codespaces.new/immich-app/immich/)
|
[](https://codespaces.new/immich-app/immich/)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ cd immich
|
|||||||
|
|
||||||
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.
|
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 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.
|
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
|
```bash
|
||||||
@@ -88,10 +88,6 @@ source ~/.bashrc
|
|||||||
|
|
||||||
### Step 3: Launch the Dev Container
|
### Step 3: Launch the Dev Container
|
||||||
|
|
||||||
:::tip
|
|
||||||
Immich development makes extensive use of specialized [base images](https://github.com/immich-app/base-images) for its docker-compose based development. For this reason, you won't be able to use VSCode's **_Clone Repository in a Container Volume_** command.
|
|
||||||
:::
|
|
||||||
|
|
||||||
#### Using VS Code UI:
|
#### Using VS Code UI:
|
||||||
|
|
||||||
1. Open the cloned repository in VS Code
|
1. Open the cloned repository in VS Code
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
|
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
|
||||||
|
|
||||||
You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
||||||
|
|
||||||
## Enable folder view
|
## Enable folder view
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ Internally, Immich uses the [glob](https://www.npmjs.com/package/glob) package t
|
|||||||
|
|
||||||
### Automatic watching (EXPERIMENTAL)
|
### Automatic watching (EXPERIMENTAL)
|
||||||
|
|
||||||
This feature is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan.
|
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan.
|
||||||
|
|
||||||
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
|
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ _Remember to run `docker compose up -d` to register the changes. Make sure you c
|
|||||||
|
|
||||||
These actions must be performed by the Immich administrator.
|
These actions must be performed by the Immich administrator.
|
||||||
|
|
||||||
- Click on your avatar in the upper right corner
|
- Click on your avatar on the upper right corner
|
||||||
- Click on Administration -> External Libraries
|
- Click on Administration -> External Libraries
|
||||||
- Click on Create an external library…
|
- Click on Create an external library…
|
||||||
- Select which user owns the library, this can not be changed later
|
- Select which user owns the library, this can not be changed later
|
||||||
@@ -159,7 +159,9 @@ Within seconds, the assets from the old-pics and videos folders should show up i
|
|||||||
|
|
||||||
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
|
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
|
||||||
|
|
||||||
You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
||||||
|
|
||||||
|
The UI is currently only available for the web; mobile will come in a subsequent release.
|
||||||
|
|
||||||
<img src={require('./img/folder-view-1.webp').default} width="100%" title='Folder-view' />
|
<img src={require('./img/folder-view-1.webp').default} width="100%" title='Folder-view' />
|
||||||
|
|
||||||
@@ -169,7 +171,7 @@ You can enable this feature under [`Account Settings > Features > Folders`](http
|
|||||||
Only an admin can do this.
|
Only an admin can do this.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> External Library.
|
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library.
|
||||||
You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/).
|
You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/).
|
||||||
|
|
||||||
<img src={require('./img/library-custom-scan-interval.webp').default} width="75%" title='Set custom scan interval for external library' />
|
<img src={require('./img/library-custom-scan-interval.webp').default} width="75%" title='Set custom scan interval for external library' />
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
|||||||
| `HEIC` | `.heic` | :white_check_mark: | |
|
| `HEIC` | `.heic` | :white_check_mark: | |
|
||||||
| `HEIF` | `.heif` | :white_check_mark: | |
|
| `HEIF` | `.heif` | :white_check_mark: | |
|
||||||
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
||||||
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||||
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
||||||
| `PNG` | `.png` | :white_check_mark: | |
|
| `PNG` | `.png` | :white_check_mark: | |
|
||||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ In the Immich web UI:
|
|||||||
- Click Add path
|
- Click Add path
|
||||||
<img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" />
|
<img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" />
|
||||||
|
|
||||||
- Enter **/home/user/photos1** as the path and click Add
|
- Enter **/usr/src/app/external** as the path and click Add
|
||||||
<img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" />
|
<img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" />
|
||||||
|
|
||||||
- Save the new path
|
- Save the new path
|
||||||
|
|||||||
@@ -29,20 +29,20 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
| Variable | Description | Default | Containers | Workers |
|
| Variable | Description | Default | Containers | Workers |
|
||||||
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :---------------------------------: | :----------------------- | :----------------- |
|
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
||||||
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
||||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/usr/src/app/upload`<sup>\*3</sup> | server | api, microservices |
|
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `./upload`<sup>\*3</sup> | server | api, microservices |
|
||||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||||
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
||||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||||
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
||||||
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
|
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
|
||||||
|
|
||||||
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||||
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
||||||
@@ -72,25 +72,22 @@ Information on the current workers can be found [here](/docs/administration/jobs
|
|||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
| Variable | Description | Default | Containers |
|
| Variable | Description | Default | Containers |
|
||||||
| :---------------------------------- | :------------------------------------------------------------------------------------- | :--------: | :----------------------------- |
|
| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- |
|
||||||
| `DB_URL` | Database URL | | server |
|
| `DB_URL` | Database URL | | server |
|
||||||
| `DB_HOSTNAME` | Database host | `database` | server |
|
| `DB_HOSTNAME` | Database host | `database` | server |
|
||||||
| `DB_PORT` | Database port | `5432` | server |
|
| `DB_PORT` | Database port | `5432` | server |
|
||||||
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
||||||
| `DB_SSL_MODE` | Database SSL mode | | server |
|
| `DB_SSL_MODE` | Database SSL mode | | server |
|
||||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
||||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||||
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | server |
|
|
||||||
|
|
||||||
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
||||||
|
|
||||||
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector.
|
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector.
|
||||||
|
|
||||||
\*3: Uses either [`postgresql.ssd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.ssd.conf) or [`postgresql.hdd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.hdd.conf) which mainly controls the Postgres `effective_io_concurrency` setting to allow for concurrenct IO on SSDs and sequential IO on HDDs.
|
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|
||||||
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
|
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
|
||||||
|
|||||||
Generated
+3
-3
@@ -16459,9 +16459,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
|
||||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+10
-17
@@ -85,7 +85,6 @@ import React from 'react';
|
|||||||
import { Item, Timeline } from '../components/timeline';
|
import { Item, Timeline } from '../components/timeline';
|
||||||
|
|
||||||
const releases = {
|
const releases = {
|
||||||
'v1.135.0': new Date(2025, 5, 18),
|
|
||||||
'v1.133.0': new Date(2025, 4, 21),
|
'v1.133.0': new Date(2025, 4, 21),
|
||||||
'v1.130.0': new Date(2025, 2, 25),
|
'v1.130.0': new Date(2025, 2, 25),
|
||||||
'v1.127.0': new Date(2025, 1, 26),
|
'v1.127.0': new Date(2025, 1, 26),
|
||||||
@@ -197,6 +196,14 @@ const roadmap: Item[] = [
|
|||||||
description: 'Automate tasks with workflows',
|
description: 'Automate tasks with workflows',
|
||||||
getDateLabel: () => 'Planned for 2025',
|
getDateLabel: () => 'Planned for 2025',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
done: false,
|
||||||
|
icon: mdiTableKey,
|
||||||
|
iconColor: 'gray',
|
||||||
|
title: 'Fine grained access controls',
|
||||||
|
description: 'Granular access controls for users and api keys',
|
||||||
|
getDateLabel: () => 'Planned for 2025',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
done: false,
|
done: false,
|
||||||
icon: mdiImageEdit,
|
icon: mdiImageEdit,
|
||||||
@@ -232,26 +239,12 @@ const roadmap: Item[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const milestones: Item[] = [
|
const milestones: Item[] = [
|
||||||
{
|
|
||||||
icon: mdiStar,
|
|
||||||
iconColor: 'gold',
|
|
||||||
title: '70,000 Stars',
|
|
||||||
description: 'Reached 70K Stars on GitHub!',
|
|
||||||
getDateLabel: withLanguage(new Date(2025, 6, 9)),
|
|
||||||
},
|
|
||||||
withRelease({
|
|
||||||
icon: mdiTableKey,
|
|
||||||
iconColor: 'gray',
|
|
||||||
title: 'Fine grained access controls',
|
|
||||||
description: 'Granular access controls for api keys',
|
|
||||||
release: 'v1.135.0',
|
|
||||||
}),
|
|
||||||
withRelease({
|
withRelease({
|
||||||
icon: mdiCast,
|
icon: mdiCast,
|
||||||
iconColor: 'aqua',
|
iconColor: 'aqua',
|
||||||
title: 'Google Cast (web and mobile)',
|
title: 'Google Cast (web)',
|
||||||
description: 'Cast assets to Google Cast/Chromecast compatible devices',
|
description: 'Cast assets to Google Cast/Chromecast compatible devices',
|
||||||
release: 'v1.135.0',
|
release: 'v1.133.0',
|
||||||
}),
|
}),
|
||||||
withRelease({
|
withRelease({
|
||||||
icon: mdiLockOutline,
|
icon: mdiLockOutline,
|
||||||
|
|||||||
Vendored
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
/docs /docs/overview/welcome 307
|
/docs /docs/overview/introduction 307
|
||||||
/docs/ /docs/overview/welcome 307
|
|
||||||
/docs/mobile-app-beta-program /docs/features/mobile-app 307
|
/docs/mobile-app-beta-program /docs/features/mobile-app 307
|
||||||
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
|
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
|
||||||
/docs/install /docs/install/docker-compose 307
|
/docs/install /docs/install/docker-compose 307
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name: immich-e2e
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich-e2e-server
|
container_name: immich-e2e-server
|
||||||
|
command: ['./start.sh']
|
||||||
image: immich-server:latest
|
image: immich-server:latest
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
@@ -35,7 +36,7 @@ services:
|
|||||||
- 2285:2285
|
- 2285:2285
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine@sha256:03fd052257735b41cd19f3d8ae9782926bf9b704fb6a9dc5e29f9ccfbe8827f0
|
image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea
|
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea
|
||||||
|
|||||||
Generated
+212
-153
@@ -14,7 +14,6 @@
|
|||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@socket.io/component-emitter": "^3.1.2",
|
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.15.33",
|
"@types/node": "^22.15.33",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
@@ -57,7 +56,7 @@
|
|||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"immich": "bin/immich"
|
"immich": "dist/index.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
@@ -83,7 +82,7 @@
|
|||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
"vite": "^7.0.0",
|
"vite": "^6.0.0",
|
||||||
"vite-tsconfig-paths": "^5.0.0",
|
"vite-tsconfig-paths": "^5.0.0",
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
"vitest-fetch-mock": "^0.4.0",
|
"vitest-fetch-mock": "^0.4.0",
|
||||||
@@ -659,9 +658,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array": {
|
"node_modules/@eslint/config-array": {
|
||||||
"version": "0.21.0",
|
"version": "0.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz",
|
||||||
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
|
"integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -674,9 +673,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-helpers": {
|
"node_modules/@eslint/config-helpers": {
|
||||||
"version": "0.3.0",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz",
|
||||||
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==",
|
"integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -734,9 +733,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.30.1",
|
"version": "9.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
|
||||||
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
|
"integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1510,13 +1509,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.53.2",
|
"version": "1.53.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.2.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
|
||||||
"integrity": "sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==",
|
"integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.53.2"
|
"playwright": "1.53.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -2029,6 +2028,66 @@
|
|||||||
"pg-types": "^2.2.0"
|
"pg-types": "^2.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pg/node_modules/pg-types": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pg-int8": "1.0.1",
|
||||||
|
"postgres-array": "~2.0.0",
|
||||||
|
"postgres-bytea": "~1.0.0",
|
||||||
|
"postgres-date": "~1.0.4",
|
||||||
|
"postgres-interval": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/pg/node_modules/postgres-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/pg/node_modules/postgres-bytea": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/pg/node_modules/postgres-date": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/pg/node_modules/postgres-interval": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xtend": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/pngjs": {
|
"node_modules/@types/pngjs": {
|
||||||
"version": "6.0.5",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
|
||||||
@@ -2101,17 +2160,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
|
||||||
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
|
"integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.36.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/type-utils": "8.36.0",
|
"@typescript-eslint/type-utils": "8.35.0",
|
||||||
"@typescript-eslint/utils": "8.36.0",
|
"@typescript-eslint/utils": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
"@typescript-eslint/visitor-keys": "8.35.0",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@@ -2125,7 +2184,7 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.36.0",
|
"@typescript-eslint/parser": "^8.35.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
@@ -2141,16 +2200,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
|
||||||
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
|
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.36.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.36.0",
|
"@typescript-eslint/typescript-estree": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
"@typescript-eslint/visitor-keys": "8.35.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2166,14 +2225,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
|
||||||
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
|
"integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.36.0",
|
"@typescript-eslint/tsconfig-utils": "^8.35.0",
|
||||||
"@typescript-eslint/types": "^8.36.0",
|
"@typescript-eslint/types": "^8.35.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2188,14 +2247,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
|
||||||
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
|
"integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.36.0"
|
"@typescript-eslint/visitor-keys": "8.35.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -2206,9 +2265,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
|
||||||
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
|
"integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2223,14 +2282,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
|
||||||
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
|
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.36.0",
|
"@typescript-eslint/typescript-estree": "8.35.0",
|
||||||
"@typescript-eslint/utils": "8.36.0",
|
"@typescript-eslint/utils": "8.35.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -2247,9 +2306,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
|
||||||
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
|
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2261,16 +2320,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
|
||||||
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
|
"integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.36.0",
|
"@typescript-eslint/project-service": "8.35.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.36.0",
|
"@typescript-eslint/tsconfig-utils": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
"@typescript-eslint/visitor-keys": "8.35.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@@ -2316,16 +2375,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
|
||||||
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
|
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.36.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.36.0"
|
"@typescript-eslint/typescript-estree": "8.35.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -2340,13 +2399,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
|
||||||
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
|
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.36.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3439,19 +3498,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.30.1",
|
"version": "9.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
|
||||||
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
|
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
"@eslint/config-array": "^0.21.0",
|
"@eslint/config-array": "^0.20.1",
|
||||||
"@eslint/config-helpers": "^0.3.0",
|
"@eslint/config-helpers": "^0.2.1",
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.14.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.30.1",
|
"@eslint/js": "9.29.0",
|
||||||
"@eslint/plugin-kit": "^0.3.1",
|
"@eslint/plugin-kit": "^0.3.1",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
@@ -4220,9 +4279,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "16.3.0",
|
"version": "16.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
|
||||||
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
|
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4812,9 +4871,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/luxon": {
|
"node_modules/luxon": {
|
||||||
"version": "3.7.1",
|
"version": "3.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
|
||||||
"integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==",
|
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5371,15 +5430,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg": {
|
"node_modules/pg": {
|
||||||
"version": "8.16.3",
|
"version": "8.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz",
|
||||||
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
"integrity": "sha512-OtLWF0mKLmpxelOt9BqVq83QV6bTfsS0XLegIeAKqKjurRnRKie1Dc1iL89MugmSLhftxw6NNCyZhm1yQFLMEQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.9.1",
|
"pg-connection-string": "^2.9.1",
|
||||||
"pg-pool": "^3.10.1",
|
"pg-pool": "^3.10.1",
|
||||||
"pg-protocol": "^1.10.3",
|
"pg-protocol": "^1.10.2",
|
||||||
"pg-types": "2.2.0",
|
"pg-types": "2.2.0",
|
||||||
"pgpass": "1.0.5"
|
"pgpass": "1.0.5"
|
||||||
},
|
},
|
||||||
@@ -5387,7 +5446,7 @@
|
|||||||
"node": ">= 16.0.0"
|
"node": ">= 16.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"pg-cloudflare": "^1.2.7"
|
"pg-cloudflare": "^1.2.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"pg-native": ">=3.0.1"
|
"pg-native": ">=3.0.1"
|
||||||
@@ -5440,7 +5499,7 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pg-types": {
|
"node_modules/pg/node_modules/pg-types": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||||
@@ -5457,6 +5516,49 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pg/node_modules/postgres-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg/node_modules/postgres-bytea": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg/node_modules/postgres-date": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg/node_modules/postgres-interval": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xtend": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pgpass": {
|
"node_modules/pgpass": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||||
@@ -5488,13 +5590,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.53.2",
|
"version": "1.53.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
|
||||||
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
|
"integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.53.2"
|
"playwright-core": "1.53.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -5507,9 +5609,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.53.2",
|
"version": "1.53.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
|
||||||
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
|
"integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -5587,49 +5689,6 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postgres-array": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-bytea": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-date": {
|
|
||||||
"version": "1.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
|
||||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-interval": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"xtend": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -5641,9 +5700,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
|
||||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -6430,9 +6489,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/superagent": {
|
"node_modules/superagent": {
|
||||||
"version": "10.2.2",
|
"version": "10.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz",
|
||||||
"integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==",
|
"integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6451,14 +6510,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supertest": {
|
"node_modules/supertest": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz",
|
||||||
"integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==",
|
"integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"methods": "^1.1.2",
|
"methods": "^1.1.2",
|
||||||
"superagent": "^10.2.2"
|
"superagent": "^10.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.18.0"
|
"node": ">=14.18.0"
|
||||||
@@ -6778,15 +6837,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.36.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz",
|
||||||
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==",
|
"integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.36.0",
|
"@typescript-eslint/eslint-plugin": "8.35.0",
|
||||||
"@typescript-eslint/parser": "8.36.0",
|
"@typescript-eslint/parser": "8.35.0",
|
||||||
"@typescript-eslint/utils": "8.36.0"
|
"@typescript-eslint/utils": "8.35.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@socket.io/component-emitter": "^3.1.2",
|
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.15.33",
|
"@types/node": "^22.15.33",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
ReactionType,
|
ReactionType,
|
||||||
createActivity as create,
|
createActivity as create,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
removeAssetFromAlbum,
|
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
@@ -343,36 +342,5 @@ describe('/activities', () => {
|
|||||||
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty list when asset is removed', async () => {
|
|
||||||
const album3 = await createAlbum(
|
|
||||||
{
|
|
||||||
createAlbumDto: {
|
|
||||||
albumName: 'Album 3',
|
|
||||||
assetIds: [asset.id],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
|
|
||||||
await createActivity({ albumId: album3.id, assetId: asset.id, type: ReactionType.Like });
|
|
||||||
|
|
||||||
await removeAssetFromAlbum(
|
|
||||||
{
|
|
||||||
id: album3.id,
|
|
||||||
bulkIdsDto: {
|
|
||||||
ids: [asset.id],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/activities')
|
|
||||||
.query({ albumId: album.id })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toEqual(200);
|
|
||||||
expect(body).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase(['api_key']);
|
await utils.resetDatabase(['api_keys']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /api-keys', () => {
|
describe('POST /api-keys', () => {
|
||||||
|
|||||||
@@ -227,21 +227,6 @@ describe(`/oauth`, () => {
|
|||||||
expect(user.storageLabel).toBe('user-username');
|
expect(user.storageLabel).toBe('user-username');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the admin status from a role claim', async () => {
|
|
||||||
const callbackParams = await loginWithOAuth(OAuthUser.WITH_ROLE);
|
|
||||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
|
||||||
expect(status).toBe(201);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
accessToken: expect.any(String),
|
|
||||||
userId: expect.any(String),
|
|
||||||
userEmail: 'oauth-with-role@immich.app',
|
|
||||||
isAdmin: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await getMyUser({ headers: asBearerAuth(body.accessToken) });
|
|
||||||
expect(user.isAdmin).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with RS256 signed tokens', async () => {
|
it('should work with RS256 signed tokens', async () => {
|
||||||
await setupOAuth(admin.accessToken, {
|
await setupOAuth(admin.accessToken, {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ describe('/system-config', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /system-config', () => {
|
describe('PUT /system-config', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put('/system-config');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should always return the new config', async () => {
|
it('should always return the new config', async () => {
|
||||||
const config = await getSystemConfig(admin.accessToken);
|
const config = await getSystemConfig(admin.accessToken);
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe('/tags', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// tagging assets eventually triggers metadata extraction which can impact other tests
|
// tagging assets eventually triggers metadata extraction which can impact other tests
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
await utils.resetDatabase(['tag']);
|
await utils.resetDatabase(['tags']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /tags', () => {
|
describe('POST /tags', () => {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ describe(`immich upload`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase(['asset', 'album']);
|
await utils.resetDatabase(['assets', 'albums']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`immich upload /path/to/file.jpg`, () => {
|
describe(`immich upload /path/to/file.jpg`, () => {
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ export const deviceDto = {
|
|||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
updatedAt: expect.any(String),
|
updatedAt: expect.any(String),
|
||||||
current: true,
|
current: true,
|
||||||
isPendingSyncReset: false,
|
|
||||||
deviceOS: '',
|
deviceOS: '',
|
||||||
deviceType: '',
|
deviceType: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export enum OAuthUser {
|
|||||||
NO_NAME = 'no-name',
|
NO_NAME = 'no-name',
|
||||||
WITH_QUOTA = 'with-quota',
|
WITH_QUOTA = 'with-quota',
|
||||||
WITH_USERNAME = 'with-username',
|
WITH_USERNAME = 'with-username',
|
||||||
WITH_ROLE = 'with-role',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const claims = [
|
const claims = [
|
||||||
@@ -35,12 +34,6 @@ const claims = [
|
|||||||
preferred_username: 'user-quota',
|
preferred_username: 'user-quota',
|
||||||
immich_quota: 25,
|
immich_quota: 25,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
sub: OAuthUser.WITH_ROLE,
|
|
||||||
email: 'oauth-with-role@immich.app',
|
|
||||||
email_verified: true,
|
|
||||||
immich_role: 'admin',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const withDefaultClaims = (sub: string) => ({
|
const withDefaultClaims = (sub: string) => ({
|
||||||
@@ -71,15 +64,7 @@ const setup = async () => {
|
|||||||
claims: {
|
claims: {
|
||||||
openid: ['sub'],
|
openid: ['sub'],
|
||||||
email: ['email', 'email_verified'],
|
email: ['email', 'email_verified'],
|
||||||
profile: [
|
profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'],
|
||||||
'name',
|
|
||||||
'given_name',
|
|
||||||
'family_name',
|
|
||||||
'preferred_username',
|
|
||||||
'immich_quota',
|
|
||||||
'immich_username',
|
|
||||||
'immich_role',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
jwtUserinfo: {
|
jwtUserinfo: {
|
||||||
|
|||||||
+14
-15
@@ -60,7 +60,6 @@ import { io, type Socket } from 'socket.io-client';
|
|||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
import { makeRandomImage } from 'src/generators';
|
import { makeRandomImage } from 'src/generators';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
export type { Emitter } from '@socket.io/component-emitter';
|
|
||||||
|
|
||||||
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
|
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
|
||||||
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
|
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
|
||||||
@@ -85,10 +84,10 @@ export const immichAdmin = (args: string[]) =>
|
|||||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||||
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
const executeCommand = (command: string, args: string[], options?: { cwd?: string }) => {
|
const executeCommand = (command: string, args: string[]) => {
|
||||||
let _resolve: (value: CommandResponse) => void;
|
let _resolve: (value: CommandResponse) => void;
|
||||||
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
||||||
const child = spawn(command, args, { stdio: 'pipe', cwd: options?.cwd });
|
const child = spawn(command, args, { stdio: 'pipe' });
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
@@ -154,19 +153,19 @@ export const utils = {
|
|||||||
|
|
||||||
tables = tables || [
|
tables = tables || [
|
||||||
// TODO e2e test for deleting a stack, since it is quite complex
|
// TODO e2e test for deleting a stack, since it is quite complex
|
||||||
'stack',
|
'asset_stack',
|
||||||
'library',
|
'libraries',
|
||||||
'shared_link',
|
'shared_links',
|
||||||
'person',
|
'person',
|
||||||
'album',
|
'albums',
|
||||||
'asset',
|
'assets',
|
||||||
'asset_face',
|
'asset_faces',
|
||||||
'activity',
|
'activity',
|
||||||
'api_key',
|
'api_keys',
|
||||||
'session',
|
'sessions',
|
||||||
'user',
|
'users',
|
||||||
'system_metadata',
|
'system_metadata',
|
||||||
'tag',
|
'tags',
|
||||||
];
|
];
|
||||||
|
|
||||||
const sql: string[] = [];
|
const sql: string[] = [];
|
||||||
@@ -175,7 +174,7 @@ export const utils = {
|
|||||||
if (table === 'system_metadata') {
|
if (table === 'system_metadata') {
|
||||||
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
|
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
|
||||||
} else {
|
} else {
|
||||||
sql.push(`DELETE FROM "${table}" CASCADE;`);
|
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +450,7 @@ export const utils = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
|
await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
|
||||||
},
|
},
|
||||||
|
|
||||||
setPersonThumbnail: async (personId: string) => {
|
setPersonThumbnail: async (personId: string) => {
|
||||||
|
|||||||
+1
-32
@@ -166,20 +166,6 @@
|
|||||||
"metadata_settings_description": "Manage metadata settings",
|
"metadata_settings_description": "Manage metadata settings",
|
||||||
"migration_job": "Migration",
|
"migration_job": "Migration",
|
||||||
"migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure",
|
"migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure",
|
||||||
"nightly_tasks_cluster_faces_setting_description": "Run facial recognition on newly detected faces",
|
|
||||||
"nightly_tasks_cluster_new_faces_setting": "Cluster new faces",
|
|
||||||
"nightly_tasks_database_cleanup_setting": "Database cleanup tasks",
|
|
||||||
"nightly_tasks_database_cleanup_setting_description": "Clean up old, expired data from the database",
|
|
||||||
"nightly_tasks_generate_memories_setting": "Generate memories",
|
|
||||||
"nightly_tasks_generate_memories_setting_description": "Create new memories from assets",
|
|
||||||
"nightly_tasks_missing_thumbnails_setting": "Generate missing thumbnails",
|
|
||||||
"nightly_tasks_missing_thumbnails_setting_description": "Queue assets without thumbnails for thumbnail generation",
|
|
||||||
"nightly_tasks_settings": "Nightly Tasks Settings",
|
|
||||||
"nightly_tasks_settings_description": "Manage nightly tasks",
|
|
||||||
"nightly_tasks_start_time_setting": "Start time",
|
|
||||||
"nightly_tasks_start_time_setting_description": "The time at which the server starts running the nightly tasks",
|
|
||||||
"nightly_tasks_sync_quota_usage_setting": "Sync quota usage",
|
|
||||||
"nightly_tasks_sync_quota_usage_setting_description": "Update user storage quota, based on current usage",
|
|
||||||
"no_paths_added": "No paths added",
|
"no_paths_added": "No paths added",
|
||||||
"no_pattern_added": "No pattern added",
|
"no_pattern_added": "No pattern added",
|
||||||
"note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
"note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
||||||
@@ -210,8 +196,6 @@
|
|||||||
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
||||||
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
||||||
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
||||||
"oauth_role_claim": "Role Claim",
|
|
||||||
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
|
|
||||||
"oauth_settings": "OAuth",
|
"oauth_settings": "OAuth",
|
||||||
"oauth_settings_description": "Manage OAuth login settings",
|
"oauth_settings_description": "Manage OAuth login settings",
|
||||||
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
|
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
|
||||||
@@ -373,12 +357,10 @@
|
|||||||
"admin_password": "Admin Password",
|
"admin_password": "Admin Password",
|
||||||
"administration": "Administration",
|
"administration": "Administration",
|
||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
"advanced_settings_beta_timeline_subtitle": "Try the new app experience",
|
|
||||||
"advanced_settings_beta_timeline_title": "Beta Timeline",
|
|
||||||
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
|
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
|
||||||
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
|
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
|
||||||
"advanced_settings_log_level_title": "Log level: {level}",
|
"advanced_settings_log_level_title": "Log level: {level}",
|
||||||
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from local assets. Activate this setting to load remote images instead.",
|
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
|
||||||
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
||||||
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
||||||
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
||||||
@@ -737,7 +719,6 @@
|
|||||||
"default_locale": "Default Locale",
|
"default_locale": "Default Locale",
|
||||||
"default_locale_description": "Format dates and numbers based on your browser locale",
|
"default_locale_description": "Format dates and numbers based on your browser locale",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete_action_prompt": "{count} deleted permanently",
|
|
||||||
"delete_album": "Delete album",
|
"delete_album": "Delete album",
|
||||||
"delete_api_key_prompt": "Are you sure you want to delete this API key?",
|
"delete_api_key_prompt": "Are you sure you want to delete this API key?",
|
||||||
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
||||||
@@ -751,7 +732,6 @@
|
|||||||
"delete_key": "Delete key",
|
"delete_key": "Delete key",
|
||||||
"delete_library": "Delete Library",
|
"delete_library": "Delete Library",
|
||||||
"delete_link": "Delete link",
|
"delete_link": "Delete link",
|
||||||
"delete_local_action_prompt": "{count} deleted locally",
|
|
||||||
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_others": "Delete others",
|
"delete_others": "Delete others",
|
||||||
@@ -819,7 +799,6 @@
|
|||||||
"edit_key": "Edit key",
|
"edit_key": "Edit key",
|
||||||
"edit_link": "Edit link",
|
"edit_link": "Edit link",
|
||||||
"edit_location": "Edit location",
|
"edit_location": "Edit location",
|
||||||
"edit_location_action_prompt": "{count} location edited",
|
|
||||||
"edit_location_dialog_title": "Location",
|
"edit_location_dialog_title": "Location",
|
||||||
"edit_name": "Edit name",
|
"edit_name": "Edit name",
|
||||||
"edit_people": "Edit people",
|
"edit_people": "Edit people",
|
||||||
@@ -1149,7 +1128,6 @@
|
|||||||
"library_page_sort_created": "Created date",
|
"library_page_sort_created": "Created date",
|
||||||
"library_page_sort_last_modified": "Last modified",
|
"library_page_sort_last_modified": "Last modified",
|
||||||
"library_page_sort_title": "Album title",
|
"library_page_sort_title": "Album title",
|
||||||
"licenses": "Licenses",
|
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"like_deleted": "Like deleted",
|
"like_deleted": "Like deleted",
|
||||||
"link_motion_video": "Link motion video",
|
"link_motion_video": "Link motion video",
|
||||||
@@ -1519,7 +1497,6 @@
|
|||||||
"remove_custom_date_range": "Remove custom date range",
|
"remove_custom_date_range": "Remove custom date range",
|
||||||
"remove_deleted_assets": "Remove Deleted Assets",
|
"remove_deleted_assets": "Remove Deleted Assets",
|
||||||
"remove_from_album": "Remove from album",
|
"remove_from_album": "Remove from album",
|
||||||
"remove_from_album_action_prompt": "{count} removed from the album",
|
|
||||||
"remove_from_favorites": "Remove from favorites",
|
"remove_from_favorites": "Remove from favorites",
|
||||||
"remove_from_lock_folder_action_prompt": "{count} removed from the locked folder",
|
"remove_from_lock_folder_action_prompt": "{count} removed from the locked folder",
|
||||||
"remove_from_locked_folder": "Remove from locked folder",
|
"remove_from_locked_folder": "Remove from locked folder",
|
||||||
@@ -1693,7 +1670,6 @@
|
|||||||
"settings_saved": "Settings saved",
|
"settings_saved": "Settings saved",
|
||||||
"setup_pin_code": "Setup a PIN code",
|
"setup_pin_code": "Setup a PIN code",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"share_action_prompt": "Shared {count} assets",
|
|
||||||
"share_add_photos": "Add photos",
|
"share_add_photos": "Add photos",
|
||||||
"share_assets_selected": "{count} selected",
|
"share_assets_selected": "{count} selected",
|
||||||
"share_dialog_preparing": "Preparing...",
|
"share_dialog_preparing": "Preparing...",
|
||||||
@@ -1795,7 +1771,6 @@
|
|||||||
"sort_title": "Title",
|
"sort_title": "Title",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
"stack": "Stack",
|
"stack": "Stack",
|
||||||
"stack_action_prompt": "{count} stacked",
|
|
||||||
"stack_duplicates": "Stack duplicates",
|
"stack_duplicates": "Stack duplicates",
|
||||||
"stack_select_one_photo": "Select one main photo for the stack",
|
"stack_select_one_photo": "Select one main photo for the stack",
|
||||||
"stack_selected_photos": "Stack selected photos",
|
"stack_selected_photos": "Stack selected photos",
|
||||||
@@ -1866,7 +1841,6 @@
|
|||||||
"total": "Total",
|
"total": "Total",
|
||||||
"total_usage": "Total usage",
|
"total_usage": "Total usage",
|
||||||
"trash": "Trash",
|
"trash": "Trash",
|
||||||
"trash_action_prompt": "{count} moved to trash",
|
|
||||||
"trash_all": "Trash All",
|
"trash_all": "Trash All",
|
||||||
"trash_count": "Trash {count, number}",
|
"trash_count": "Trash {count, number}",
|
||||||
"trash_delete_asset": "Trash/Delete Asset",
|
"trash_delete_asset": "Trash/Delete Asset",
|
||||||
@@ -1884,11 +1858,9 @@
|
|||||||
"unable_to_change_pin_code": "Unable to change PIN code",
|
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||||
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
||||||
"unarchive": "Unarchive",
|
"unarchive": "Unarchive",
|
||||||
"unarchive_action_prompt": "{count} removed from Archive",
|
|
||||||
"unarchived_count": "{count, plural, other {Unarchived #}}",
|
"unarchived_count": "{count, plural, other {Unarchived #}}",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"unfavorite": "Unfavorite",
|
"unfavorite": "Unfavorite",
|
||||||
"unfavorite_action_prompt": "{count} removed from Favorites",
|
|
||||||
"unhide_person": "Unhide person",
|
"unhide_person": "Unhide person",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unknown_country": "Unknown Country",
|
"unknown_country": "Unknown Country",
|
||||||
@@ -1906,9 +1878,7 @@
|
|||||||
"unselect_all_duplicates": "Unselect all duplicates",
|
"unselect_all_duplicates": "Unselect all duplicates",
|
||||||
"unselect_all_in": "Unselect all in {group}",
|
"unselect_all_in": "Unselect all in {group}",
|
||||||
"unstack": "Un-stack",
|
"unstack": "Un-stack",
|
||||||
"unstack_action_prompt": "{count} unstacked",
|
|
||||||
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
||||||
"untagged": "Untagged",
|
|
||||||
"up_next": "Up next",
|
"up_next": "Up next",
|
||||||
"updated_at": "Updated",
|
"updated_at": "Updated",
|
||||||
"updated_password": "Updated password",
|
"updated_password": "Updated password",
|
||||||
@@ -1945,7 +1915,6 @@
|
|||||||
"user_usage_stats_description": "View account usage statistics",
|
"user_usage_stats_description": "View account usage statistics",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"users_added_to_album_count": "Added {count, plural, one {# user} other {# users}} to the album",
|
|
||||||
"utilities": "Utilities",
|
"utilities": "Utilities",
|
||||||
"validate": "Validate",
|
"validate": "Validate",
|
||||||
"validate_endpoint_error": "Please enter a valid URL",
|
"validate_endpoint_error": "Please enter a valid URL",
|
||||||
|
|||||||
@@ -4,12 +4,9 @@ import sys
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
port = os.getenv("IMMICH_PORT", 3003)
|
port = os.getenv("IMMICH_PORT", 3003)
|
||||||
host = os.getenv("IMMICH_HOST", "0.0.0.0")
|
|
||||||
|
|
||||||
host = "localhost" if host == "0.0.0.0" else host
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(f"http://{host}:{port}/ping", timeout=2)
|
response = requests.get(f"http://localhost:{port}/ping", timeout=2)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
Generated
+49
-49
@@ -517,16 +517,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.115.14"
|
version = "0.115.13"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "starlette" },
|
{ name = "starlette" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" },
|
{ url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -900,7 +900,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "huggingface-hub"
|
name = "huggingface-hub"
|
||||||
version = "0.33.2"
|
version = "0.33.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "filelock" },
|
{ name = "filelock" },
|
||||||
@@ -912,9 +912,9 @@ dependencies = [
|
|||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/42/8a95c5632080ae312c0498744b2b852195e10b05a20b1be11c5141092f4c/huggingface_hub-0.33.2.tar.gz", hash = "sha256:84221defaec8fa09c090390cd68c78b88e3c4c2b7befba68d3dc5aacbc3c2c5f", size = 426637, upload-time = "2025-07-02T06:26:05.156Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/91/8a/1362d565fefabaa4185cf3ae842a98dbc5b35146f5694f7080f043a6952f/huggingface_hub-0.33.0.tar.gz", hash = "sha256:aa31f70d29439d00ff7a33837c03f1f9dd83971ce4e29ad664d63ffb17d3bb97", size = 426179, upload-time = "2025-06-11T17:08:07.913Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/f4/5f3f22e762ad1965f01122b42dae5bf0e009286e2dba601ce1d0dba72424/huggingface_hub-0.33.2-py3-none-any.whl", hash = "sha256:3749498bfa91e8cde2ddc2c1db92c79981f40e66434c20133b39e5928ac9bcc5", size = 515373, upload-time = "2025-07-02T06:26:03.072Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/fb/53587a89fbc00799e4179796f51b3ad713c5de6bb680b2becb6d37c94649/huggingface_hub-0.33.0-py3-none-any.whl", hash = "sha256:e8668875b40c68f9929150d99727d39e5ebb8a05a98e4191b908dc7ded9074b3", size = 514799, upload-time = "2025-06-11T17:08:05.757Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1044,7 +1044,7 @@ requires-dist = [
|
|||||||
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" },
|
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" },
|
||||||
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" },
|
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" },
|
||||||
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" },
|
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" },
|
||||||
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" },
|
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" },
|
||||||
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" },
|
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" },
|
||||||
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
||||||
{ name = "orjson", specifier = ">=3.9.5" },
|
{ name = "orjson", specifier = ">=3.9.5" },
|
||||||
@@ -1568,7 +1568,7 @@ wheels = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime-gpu"
|
name = "onnxruntime-gpu"
|
||||||
version = "1.19.2"
|
version = "1.19.2"
|
||||||
source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" }
|
source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "coloredlogs" },
|
{ name = "coloredlogs" },
|
||||||
{ name = "flatbuffers" },
|
{ name = "flatbuffers" },
|
||||||
@@ -1936,16 +1936,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-settings"
|
name = "pydantic-settings"
|
||||||
version = "2.10.1"
|
version = "2.9.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
|
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2304,27 +2304,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.12.2"
|
version = "0.12.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
|
{ url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
|
{ url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
|
{ url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
|
{ url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
|
{ url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
|
{ url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
|
{ url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
|
{ url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
|
{ url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
|
{ url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
|
{ url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2504,27 +2504,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokenizers"
|
name = "tokenizers"
|
||||||
version = "0.21.2"
|
version = "0.21.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "huggingface-hub" },
|
{ name = "huggingface-hub" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload-time = "2025-03-13T10:51:18.189Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" },
|
{ url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload-time = "2025-03-13T10:51:09.459Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload-time = "2025-03-13T10:51:07.692Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" },
|
{ url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload-time = "2025-03-13T10:50:56.679Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" },
|
{ url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload-time = "2025-03-13T10:50:59.525Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload-time = "2025-03-13T10:51:04.678Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload-time = "2025-03-13T10:51:01.261Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload-time = "2025-03-13T10:51:03.243Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload-time = "2025-03-13T10:51:06.235Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload-time = "2025-03-13T10:51:10.927Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload-time = "2025-03-13T10:51:12.688Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" },
|
{ url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload-time = "2025-03-13T10:51:14.723Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload-time = "2025-03-13T10:51:16.526Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload-time = "2025-03-13T10:51:20.643Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" },
|
{ url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload-time = "2025-03-13T10:51:19.243Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2628,16 +2628,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.35.0"
|
version = "0.34.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "h11" },
|
{ name = "h11" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutter": "3.32.6"
|
"flutter": "3.29.3"
|
||||||
}
|
}
|
||||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.32.6",
|
"dart.flutterSdkPath": ".fvm/versions/3.29.3",
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.fvm": true
|
"**/.fvm": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ custom_lint:
|
|||||||
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
||||||
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
||||||
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database
|
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database
|
||||||
- lib/domain/services/search.service.dart
|
|
||||||
|
|
||||||
# refactor
|
# refactor
|
||||||
- lib/models/map/map_marker.model.dart
|
- lib/models/map/map_marker.model.dart
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ plugins {
|
|||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' // this version matches your Kotlin version
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
@@ -47,10 +45,6 @@ android {
|
|||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
compose true
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.alextran.immich"
|
applicationId "app.alextran.immich"
|
||||||
minSdkVersion 26
|
minSdkVersion 26
|
||||||
@@ -72,20 +66,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "default"
|
|
||||||
productFlavors {
|
|
||||||
production {
|
|
||||||
dimension "default"
|
|
||||||
applicationId "app.alextran.immich"
|
|
||||||
}
|
|
||||||
|
|
||||||
beta {
|
|
||||||
dimension "default"
|
|
||||||
applicationId "app.alextran.immich.beta"
|
|
||||||
versionNameSuffix "-BETA"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix '.debug'
|
applicationIdSuffix '.debug'
|
||||||
@@ -111,8 +91,6 @@ dependencies {
|
|||||||
def guava_version = '33.3.1-android'
|
def guava_version = '33.3.1-android'
|
||||||
def glide_version = '4.16.0'
|
def glide_version = '4.16.0'
|
||||||
def serialization_version = '1.8.1'
|
def serialization_version = '1.8.1'
|
||||||
def compose_version = '1.1.1'
|
|
||||||
def gson_version = '2.10.1'
|
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||||
@@ -124,17 +102,6 @@ dependencies {
|
|||||||
|
|
||||||
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
|
||||||
|
|
||||||
//Glance Widget
|
|
||||||
implementation "androidx.glance:glance-appwidget:$compose_version"
|
|
||||||
implementation "com.google.code.gson:gson:$gson_version"
|
|
||||||
|
|
||||||
// Glance Configure
|
|
||||||
implementation "androidx.activity:activity-compose:1.8.2"
|
|
||||||
implementation "androidx.compose.ui:ui:$compose_version"
|
|
||||||
implementation "androidx.compose.ui:ui-tooling:$compose_version"
|
|
||||||
implementation "androidx.compose.material3:material3:1.2.1"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is uncommented in F-Droid build script
|
// This is uncommented in F-Droid build script
|
||||||
|
|||||||
-7
@@ -25,15 +25,8 @@
|
|||||||
@com.google.gson.annotations.SerializedName <fields>;
|
@com.google.gson.annotations.SerializedName <fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
# TypeToken preventions
|
|
||||||
-keep class com.google.gson.reflect.TypeToken { *; }
|
|
||||||
-keep class * extends com.google.gson.reflect.TypeToken
|
|
||||||
|
|
||||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||||
|
|
||||||
##---------------End: proguard configuration for Gson ----------
|
##---------------End: proguard configuration for Gson ----------
|
||||||
|
|
||||||
# Keep all widget model classes and their fields for Gson
|
|
||||||
-keep class app.alextran.immich.widget.model.** { *; }
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<application android:label="Immich Beta" tools:replace="android:label" />
|
|
||||||
</manifest>
|
|
||||||
@@ -100,24 +100,24 @@
|
|||||||
|
|
||||||
<!-- my.immich.app deep link -->
|
<!-- my.immich.app deep link -->
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:path="/" />
|
android:path="/" />
|
||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:pathPrefix="/albums/" />
|
android:pathPrefix="/albums/" />
|
||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:pathPrefix="/memories/" />
|
android:pathPrefix="/memories/" />
|
||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:pathPrefix="/photos/" />
|
android:pathPrefix="/photos/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
@@ -141,41 +141,6 @@
|
|||||||
android:name="androidx.startup.InitializationProvider"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
|
|
||||||
<!-- Widgets -->
|
|
||||||
<receiver
|
|
||||||
android:name=".widget.RandomReceiver"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/random_widget_title">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
|
||||||
</intent-filter>
|
|
||||||
<meta-data
|
|
||||||
android:name="android.appwidget.provider"
|
|
||||||
android:resource="@xml/random_widget" />
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".widget.MemoryReceiver"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/memory_widget_title">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
|
||||||
</intent-filter>
|
|
||||||
<meta-data
|
|
||||||
android:name="android.appwidget.provider"
|
|
||||||
android:resource="@xml/memory_widget" />
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<activity android:name=".widget.configure.RandomConfigure"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -87,8 +87,7 @@ data class PlatformAsset (
|
|||||||
val updatedAt: Long? = null,
|
val updatedAt: Long? = null,
|
||||||
val width: Long? = null,
|
val width: Long? = null,
|
||||||
val height: Long? = null,
|
val height: Long? = null,
|
||||||
val durationInSeconds: Long,
|
val durationInSeconds: Long
|
||||||
val orientation: Long
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
@@ -101,8 +100,7 @@ data class PlatformAsset (
|
|||||||
val width = pigeonVar_list[5] as Long?
|
val width = pigeonVar_list[5] as Long?
|
||||||
val height = pigeonVar_list[6] as Long?
|
val height = pigeonVar_list[6] as Long?
|
||||||
val durationInSeconds = pigeonVar_list[7] as Long
|
val durationInSeconds = pigeonVar_list[7] as Long
|
||||||
val orientation = pigeonVar_list[8] as Long
|
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds)
|
||||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@@ -115,7 +113,6 @@ data class PlatformAsset (
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
orientation,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.content.Context
|
|||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.database.getStringOrNull
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -40,8 +39,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
MediaStore.MediaColumns.BUCKET_ID,
|
MediaStore.MediaColumns.BUCKET_ID,
|
||||||
MediaStore.MediaColumns.WIDTH,
|
MediaStore.MediaColumns.WIDTH,
|
||||||
MediaStore.MediaColumns.HEIGHT,
|
MediaStore.MediaColumns.HEIGHT,
|
||||||
MediaStore.MediaColumns.DURATION,
|
MediaStore.MediaColumns.DURATION
|
||||||
MediaStore.MediaColumns.ORIENTATION,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
||||||
@@ -75,8 +73,6 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
|
val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
|
||||||
val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
|
val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
|
||||||
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
||||||
val orientationColumn =
|
|
||||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
|
||||||
|
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
val id = c.getLong(idColumn).toString()
|
val id = c.getLong(idColumn).toString()
|
||||||
@@ -104,7 +100,6 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
|
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
|
||||||
else c.getLong(durationColumn) / 1000
|
else c.getLong(durationColumn) / 1000
|
||||||
val bucketId = c.getString(bucketIdColumn)
|
val bucketId = c.getString(bucketIdColumn)
|
||||||
val orientation = c.getInt(orientationColumn)
|
|
||||||
|
|
||||||
val asset = PlatformAsset(
|
val asset = PlatformAsset(
|
||||||
id,
|
id,
|
||||||
@@ -114,8 +109,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
modifiedAt,
|
modifiedAt,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
duration,
|
duration
|
||||||
orientation.toLong(),
|
|
||||||
)
|
)
|
||||||
yield(AssetResult.ValidAsset(asset, bucketId))
|
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||||
}
|
}
|
||||||
@@ -158,8 +152,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaStore might return null for bucket name (commonly for the Root Directory), so default to "Internal Storage"
|
val name = cursor.getString(bucketNameColumn)
|
||||||
val name = cursor.getStringOrNull(bucketNameColumn) ?: "Internal Storage"
|
|
||||||
val updatedAt = cursor.getLong(dateModified)
|
val updatedAt = cursor.getLong(dateModified)
|
||||||
albums.add(PlatformAlbum(id, name, updatedAt, false, 0))
|
albums.add(PlatformAlbum(id, name, updatedAt, false, 0))
|
||||||
albumsCount[id] = 1
|
albumsCount[id] = 1
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package app.alextran.immich.widget
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
fun loadScaledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap? {
|
|
||||||
val options = BitmapFactory.Options().apply {
|
|
||||||
inJustDecodeBounds = true
|
|
||||||
}
|
|
||||||
BitmapFactory.decodeFile(file.absolutePath, options)
|
|
||||||
|
|
||||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
|
|
||||||
options.inJustDecodeBounds = false
|
|
||||||
|
|
||||||
return BitmapFactory.decodeFile(file.absolutePath, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
|
||||||
val (height: Int, width: Int) = options.run { outHeight to outWidth }
|
|
||||||
var inSampleSize = 1
|
|
||||||
|
|
||||||
if (height > reqHeight || width > reqWidth) {
|
|
||||||
val halfHeight: Int = height / 2
|
|
||||||
val halfWidth: Int = width / 2
|
|
||||||
|
|
||||||
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
|
||||||
inSampleSize *= 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inSampleSize
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
package app.alextran.immich.widget
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.datastore.preferences.core.Preferences
|
|
||||||
import androidx.glance.*
|
|
||||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
|
||||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
|
||||||
import androidx.work.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.UUID
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import androidx.glance.appwidget.state.getAppWidgetState
|
|
||||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
|
||||||
import app.alextran.immich.widget.model.*
|
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
class ImageDownloadWorker(
|
|
||||||
private val context: Context,
|
|
||||||
workerParameters: WorkerParameters
|
|
||||||
) : CoroutineWorker(context, workerParameters) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName
|
|
||||||
|
|
||||||
private fun buildConstraints(): Constraints {
|
|
||||||
return Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildInputData(appWidgetId: Int, widgetType: WidgetType): Data {
|
|
||||||
return Data.Builder()
|
|
||||||
.putString(kWorkerWidgetType, widgetType.toString())
|
|
||||||
.putInt(kWorkerWidgetID, appWidgetId)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enqueuePeriodic(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
|
||||||
val manager = WorkManager.getInstance(context)
|
|
||||||
|
|
||||||
val workRequest = PeriodicWorkRequestBuilder<ImageDownloadWorker>(
|
|
||||||
20, TimeUnit.MINUTES
|
|
||||||
)
|
|
||||||
.setConstraints(buildConstraints())
|
|
||||||
.setInputData(buildInputData(appWidgetId, widgetType))
|
|
||||||
.addTag(appWidgetId.toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
manager.enqueueUniquePeriodicWork(
|
|
||||||
"$uniqueWorkName-$appWidgetId",
|
|
||||||
ExistingPeriodicWorkPolicy.UPDATE,
|
|
||||||
workRequest
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun singleShot(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
|
||||||
val manager = WorkManager.getInstance(context)
|
|
||||||
|
|
||||||
val workRequest = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
|
|
||||||
.setConstraints(buildConstraints())
|
|
||||||
.setInputData(buildInputData(appWidgetId, widgetType))
|
|
||||||
.addTag(appWidgetId.toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
manager.enqueueUniqueWork(
|
|
||||||
"$uniqueWorkName-$appWidgetId",
|
|
||||||
ExistingWorkPolicy.REPLACE,
|
|
||||||
workRequest
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun cancel(context: Context, appWidgetId: Int) {
|
|
||||||
WorkManager.getInstance(context).cancelAllWorkByTag("$uniqueWorkName-$appWidgetId")
|
|
||||||
|
|
||||||
// delete cached image
|
|
||||||
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
|
|
||||||
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
|
||||||
val currentImgUUID = widgetConfig[kImageUUID]
|
|
||||||
|
|
||||||
if (!currentImgUUID.isNullOrEmpty()) {
|
|
||||||
val file = File(context.cacheDir, imageFilename(currentImgUUID))
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
|
||||||
return try {
|
|
||||||
val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "")
|
|
||||||
val widgetId = inputData.getInt(kWorkerWidgetID, -1)
|
|
||||||
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
|
|
||||||
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
|
||||||
val currentImgUUID = widgetConfig[kImageUUID]
|
|
||||||
|
|
||||||
val serverConfig = ImmichAPI.getServerConfig(context)
|
|
||||||
|
|
||||||
// clear any image caches and go to "login" state if no credentials
|
|
||||||
if (serverConfig == null) {
|
|
||||||
if (!currentImgUUID.isNullOrEmpty()) {
|
|
||||||
deleteImage(currentImgUUID)
|
|
||||||
updateWidget(
|
|
||||||
glanceId,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"immich://",
|
|
||||||
WidgetState.LOG_IN
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch new image
|
|
||||||
val entry = when (widgetType) {
|
|
||||||
WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig)
|
|
||||||
WidgetType.MEMORIES -> fetchMemory(serverConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear current image if it exists
|
|
||||||
if (!currentImgUUID.isNullOrEmpty()) {
|
|
||||||
deleteImage(currentImgUUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// save a new image
|
|
||||||
val imgUUID = UUID.randomUUID().toString()
|
|
||||||
saveImage(entry.image, imgUUID)
|
|
||||||
|
|
||||||
// trigger the update routine with new image uuid
|
|
||||||
updateWidget(glanceId, imgUUID, entry.subtitle, entry.deeplink)
|
|
||||||
|
|
||||||
Result.success()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(uniqueWorkName, "Error while loading image", e)
|
|
||||||
if (runAttemptCount < 10) {
|
|
||||||
Result.retry()
|
|
||||||
} else {
|
|
||||||
Result.failure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateWidget(
|
|
||||||
glanceId: GlanceId,
|
|
||||||
imageUUID: String,
|
|
||||||
subtitle: String?,
|
|
||||||
deeplink: String?,
|
|
||||||
widgetState: WidgetState = WidgetState.SUCCESS
|
|
||||||
) {
|
|
||||||
updateAppWidgetState(context, glanceId) { prefs ->
|
|
||||||
prefs[kNow] = System.currentTimeMillis()
|
|
||||||
prefs[kImageUUID] = imageUUID
|
|
||||||
prefs[kWidgetState] = widgetState.toString()
|
|
||||||
prefs[kSubtitleText] = subtitle ?: ""
|
|
||||||
prefs[kDeeplinkURL] = deeplink ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
PhotoWidget().update(context,glanceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchRandom(
|
|
||||||
serverConfig: ServerConfig,
|
|
||||||
widgetConfig: Preferences
|
|
||||||
): WidgetEntry {
|
|
||||||
val api = ImmichAPI(serverConfig)
|
|
||||||
|
|
||||||
val filters = SearchFilters(AssetType.IMAGE)
|
|
||||||
val albumId = widgetConfig[kSelectedAlbum]
|
|
||||||
val showSubtitle = widgetConfig[kShowAlbumName]
|
|
||||||
val albumName = widgetConfig[kSelectedAlbumName]
|
|
||||||
var subtitle: String? = if (showSubtitle == true) albumName else ""
|
|
||||||
|
|
||||||
if (albumId != null) {
|
|
||||||
filters.albumIds = listOf(albumId)
|
|
||||||
}
|
|
||||||
|
|
||||||
var randomSearch = api.fetchSearchResults(filters)
|
|
||||||
|
|
||||||
// handle an empty album, fallback to random
|
|
||||||
if (randomSearch.isEmpty() && albumId != null) {
|
|
||||||
randomSearch = api.fetchSearchResults(SearchFilters(AssetType.IMAGE))
|
|
||||||
subtitle = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val random = randomSearch.first()
|
|
||||||
val image = api.fetchImage(random)
|
|
||||||
|
|
||||||
return WidgetEntry(
|
|
||||||
image,
|
|
||||||
subtitle,
|
|
||||||
assetDeeplink(random)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchMemory(
|
|
||||||
serverConfig: ServerConfig
|
|
||||||
): WidgetEntry {
|
|
||||||
val api = ImmichAPI(serverConfig)
|
|
||||||
|
|
||||||
val today = LocalDate.now()
|
|
||||||
val memories = api.fetchMemory(today)
|
|
||||||
val asset: Asset
|
|
||||||
var subtitle: String? = null
|
|
||||||
|
|
||||||
if (memories.isNotEmpty()) {
|
|
||||||
// pick a random asset from a random memory
|
|
||||||
val memory = memories.random()
|
|
||||||
asset = memory.assets.random()
|
|
||||||
|
|
||||||
val yearDiff = today.year - memory.data.year
|
|
||||||
subtitle = "$yearDiff ${if (yearDiff == 1) "year" else "years"} ago"
|
|
||||||
} else {
|
|
||||||
val filters = SearchFilters(AssetType.IMAGE, size=1)
|
|
||||||
asset = api.fetchSearchResults(filters).first()
|
|
||||||
}
|
|
||||||
|
|
||||||
val image = api.fetchImage(asset)
|
|
||||||
return WidgetEntry(
|
|
||||||
image,
|
|
||||||
subtitle,
|
|
||||||
assetDeeplink(asset)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) {
|
|
||||||
val file = File(context.cacheDir, imageFilename(uuid))
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveImage(bitmap: Bitmap, uuid: String) = withContext(Dispatchers.IO) {
|
|
||||||
val file = File(context.cacheDir, imageFilename(uuid))
|
|
||||||
FileOutputStream(file).use { out ->
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package app.alextran.immich.widget
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import app.alextran.immich.widget.model.*
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import es.antonborri.home_widget.HomeWidgetPlugin
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.OutputStreamWriter
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
class ImmichAPI(cfg: ServerConfig) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getServerConfig(context: Context): ServerConfig? {
|
|
||||||
val prefs = HomeWidgetPlugin.getData(context)
|
|
||||||
|
|
||||||
val serverURL = prefs.getString("widget_server_url", "") ?: ""
|
|
||||||
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
|
|
||||||
|
|
||||||
if (serverURL.isBlank() || sessionKey.isBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServerConfig(
|
|
||||||
serverURL,
|
|
||||||
sessionKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private val gson = Gson()
|
|
||||||
private val serverConfig = cfg
|
|
||||||
|
|
||||||
private fun buildRequestURL(endpoint: String, params: List<Pair<String, String>> = emptyList()): URL {
|
|
||||||
val urlString = StringBuilder("${serverConfig.serverEndpoint}$endpoint?sessionKey=${serverConfig.sessionKey}")
|
|
||||||
|
|
||||||
for ((key, value) in params) {
|
|
||||||
urlString.append("&${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}")
|
|
||||||
}
|
|
||||||
|
|
||||||
return URL(urlString.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchSearchResults(filters: SearchFilters): List<Asset> = withContext(Dispatchers.IO) {
|
|
||||||
val url = buildRequestURL("/search/random")
|
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
|
||||||
requestMethod = "POST"
|
|
||||||
setRequestProperty("Content-Type", "application/json")
|
|
||||||
doOutput = true
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.outputStream.use {
|
|
||||||
OutputStreamWriter(it).use { writer ->
|
|
||||||
writer.write(gson.toJson(filters))
|
|
||||||
writer.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = connection.inputStream.bufferedReader().readText()
|
|
||||||
val type = object : TypeToken<List<Asset>>() {}.type
|
|
||||||
gson.fromJson(response, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchMemory(date: LocalDate): List<MemoryResult> = withContext(Dispatchers.IO) {
|
|
||||||
val iso8601 = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
|
||||||
val url = buildRequestURL("/memories", listOf("for" to iso8601))
|
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
|
||||||
requestMethod = "GET"
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = connection.inputStream.bufferedReader().readText()
|
|
||||||
val type = object : TypeToken<List<MemoryResult>>() {}.type
|
|
||||||
gson.fromJson(response, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchImage(asset: Asset): Bitmap = withContext(Dispatchers.IO) {
|
|
||||||
val url = buildRequestURL("/assets/${asset.id}/thumbnail", listOf("size" to "preview"))
|
|
||||||
val connection = url.openConnection()
|
|
||||||
val data = connection.getInputStream().readBytes()
|
|
||||||
BitmapFactory.decodeByteArray(data, 0, data.size)
|
|
||||||
?: throw Exception("Invalid image data")
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchAlbums(): List<Album> = withContext(Dispatchers.IO) {
|
|
||||||
val url = buildRequestURL("/albums")
|
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
|
||||||
requestMethod = "GET"
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = connection.inputStream.bufferedReader().readText()
|
|
||||||
val type = object : TypeToken<List<Album>>() {}.type
|
|
||||||
gson.fromJson(response, type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package app.alextran.immich.widget
|
|
||||||
|
|
||||||
import android.appwidget.AppWidgetManager
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
|
||||||
import app.alextran.immich.widget.model.*
|
|
||||||
import es.antonborri.home_widget.HomeWidgetPlugin
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MemoryReceiver : GlanceAppWidgetReceiver() {
|
|
||||||
override val glanceAppWidget = PhotoWidget()
|
|
||||||
|
|
||||||
override fun onUpdate(
|
|
||||||
context: Context,
|
|
||||||
appWidgetManager: AppWidgetManager,
|
|
||||||
appWidgetIds: IntArray
|
|
||||||
) {
|
|
||||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
|
||||||
|
|
||||||
appWidgetIds.forEach { widgetID ->
|
|
||||||
ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.MEMORIES)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false)
|
|
||||||
|
|
||||||
// Launch coroutine to setup a single shot if the app requested the update
|
|
||||||
if (fromMainApp) {
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
val provider = ComponentName(context, MemoryReceiver::class.java)
|
|
||||||
val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider)
|
|
||||||
|
|
||||||
glanceIds.forEach { widgetID ->
|
|
||||||
ImageDownloadWorker.singleShot(context, widgetID, WidgetType.MEMORIES)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onReceive(context, intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
|
||||||
super.onDeleted(context, appWidgetIds)
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
appWidgetIds.forEach { id ->
|
|
||||||
ImageDownloadWorker.cancel(context, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package app.alextran.immich.widget
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.*
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.datastore.preferences.core.MutablePreferences
|
|
||||||
import androidx.glance.appwidget.*
|
|
||||||
import androidx.glance.*
|
|
||||||
import androidx.glance.action.clickable
|
|
||||||
import androidx.glance.layout.*
|
|
||||||
import androidx.glance.state.GlanceStateDefinition
|
|
||||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
|
||||||
import androidx.glance.text.Text
|
|
||||||
import androidx.glance.text.TextAlign
|
|
||||||
import androidx.glance.text.TextStyle
|
|
||||||
import androidx.glance.unit.ColorProvider
|
|
||||||
import app.alextran.immich.R
|
|
||||||
import app.alextran.immich.widget.model.*
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class PhotoWidget : GlanceAppWidget() {
|
|
||||||
override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
|
|
||||||
|
|
||||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
|
||||||
provideContent {
|
|
||||||
val prefs = currentState<MutablePreferences>()
|
|
||||||
|
|
||||||
val imageUUID = prefs[kImageUUID]
|
|
||||||
val subtitle = prefs[kSubtitleText]
|
|
||||||
val deeplinkURL = prefs[kDeeplinkURL]?.toUri()
|
|
||||||
val widgetState = prefs[kWidgetState]
|
|
||||||
var bitmap: Bitmap? = null
|
|
||||||
|
|
||||||
if (imageUUID != null) {
|
|
||||||
// fetch a random photo from server
|
|
||||||
val file = File(context.cacheDir, imageFilename(imageUUID))
|
|
||||||
|
|
||||||
if (file.exists()) {
|
|
||||||
bitmap = loadScaledBitmap(file, 500, 500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WIDGET CONTENT
|
|
||||||
Box(
|
|
||||||
modifier = GlanceModifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(GlanceTheme.colors.background)
|
|
||||||
.clickable {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, deeplinkURL ?: "immich://".toUri())
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
if (bitmap != null) {
|
|
||||||
Image(
|
|
||||||
provider = ImageProvider(bitmap),
|
|
||||||
contentDescription = "Widget Image",
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = GlanceModifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!subtitle.isNullOrBlank()) {
|
|
||||||
Column(
|
|
||||||
verticalAlignment = Alignment.Bottom,
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
modifier = GlanceModifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = subtitle,
|
|
||||||
style = TextStyle(
|
|
||||||
color = ColorProvider(Color.White),
|
|
||||||
fontSize = 16.sp
|
|
||||||
),
|
|
||||||
modifier = GlanceModifier
|
|
||||||
.background(ColorProvider(Color(0x99000000))) // 60% black
|
|
||||||
.padding(8.dp)
|
|
||||||
.cornerRadius(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Column(
|
|
||||||
modifier = GlanceModifier.fillMaxSize(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
provider = ImageProvider(R.drawable.splash),
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (widgetState == WidgetState.LOG_IN.toString()) {
|
|
||||||
Box(
|
|
||||||
modifier = GlanceModifier.fillMaxWidth().padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text("Log in to your Immich server", style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.primary))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = GlanceModifier.fillMaxWidth().padding(16.dp)
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = GlanceModifier.size(12.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = GlanceModifier.width(8.dp))
|
|
||||||
|
|
||||||
Text("Loading widget...", style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.primary))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package app.alextran.immich.widget
|
|
||||||
|
|
||||||
import android.appwidget.AppWidgetManager
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import es.antonborri.home_widget.HomeWidgetPlugin
|
|
||||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
|
||||||
import app.alextran.immich.widget.model.*
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class RandomReceiver : GlanceAppWidgetReceiver() {
|
|
||||||
override val glanceAppWidget = PhotoWidget()
|
|
||||||
|
|
||||||
override fun onUpdate(
|
|
||||||
context: Context,
|
|
||||||
appWidgetManager: AppWidgetManager,
|
|
||||||
appWidgetIds: IntArray
|
|
||||||
) {
|
|
||||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
|
||||||
|
|
||||||
appWidgetIds.forEach { widgetID ->
|
|
||||||
ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.RANDOM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false)
|
|
||||||
|
|
||||||
// Launch coroutine to setup a single shot if the app requested the update
|
|
||||||
if (fromMainApp) {
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
val provider = ComponentName(context, RandomReceiver::class.java)
|
|
||||||
val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider)
|
|
||||||
|
|
||||||
glanceIds.forEach { widgetID ->
|
|
||||||
ImageDownloadWorker.singleShot(context, widgetID, WidgetType.RANDOM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onReceive(context, intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
|
||||||
super.onDeleted(context, appWidgetIds)
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
appWidgetIds.forEach { id ->
|
|
||||||
ImageDownloadWorker.cancel(context, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package app.alextran.immich.widget.configure
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.*
|
|
||||||
|
|
||||||
|
|
||||||
data class DropdownItem (
|
|
||||||
val label: String,
|
|
||||||
val id: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Creating a composable to display a drop down menu
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun Dropdown(items: List<DropdownItem>,
|
|
||||||
selectedItem: DropdownItem?,
|
|
||||||
onItemSelected: (DropdownItem) -> Unit,
|
|
||||||
enabled: Boolean = true
|
|
||||||
) {
|
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
var selectedOption by remember { mutableStateOf(selectedItem?.label ?: items[0].label) }
|
|
||||||
|
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded,
|
|
||||||
onExpandedChange = { expanded = !expanded && enabled },
|
|
||||||
) {
|
|
||||||
|
|
||||||
TextField(
|
|
||||||
value = selectedOption,
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
enabled = enabled,
|
|
||||||
trailingIcon = {
|
|
||||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
|
||||||
},
|
|
||||||
colors = ExposedDropdownMenuDefaults.textFieldColors(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.menuAnchor()
|
|
||||||
)
|
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { expanded = false }
|
|
||||||
) {
|
|
||||||
items.forEach { option ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(option.label, color = MaterialTheme.colorScheme.onSurface) },
|
|
||||||
onClick = {
|
|
||||||
selectedOption = option.label
|
|
||||||
onItemSelected(option)
|
|
||||||
|
|
||||||
expanded = false
|
|
||||||
},
|
|
||||||
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
package app.alextran.immich.widget.configure
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LightDarkTheme(
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
|
|
||||||
val colorScheme = when {
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isDarkTheme ->
|
|
||||||
dynamicDarkColorScheme(context)
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isDarkTheme ->
|
|
||||||
dynamicLightColorScheme(context)
|
|
||||||
isDarkTheme -> darkColorScheme()
|
|
||||||
else -> lightColorScheme()
|
|
||||||
}
|
|
||||||
MaterialTheme(
|
|
||||||
colorScheme = colorScheme,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-210
@@ -1,210 +0,0 @@
|
|||||||
package app.alextran.immich.widget.configure
|
|
||||||
|
|
||||||
import android.appwidget.AppWidgetManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.Warning
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.glance.GlanceId
|
|
||||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
|
||||||
import androidx.glance.appwidget.state.getAppWidgetState
|
|
||||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
|
||||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
|
||||||
import app.alextran.immich.widget.ImageDownloadWorker
|
|
||||||
import app.alextran.immich.widget.ImmichAPI
|
|
||||||
import app.alextran.immich.widget.model.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
class RandomConfigure : ComponentActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
// Get widget ID from intent
|
|
||||||
val appWidgetId = intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
|
||||||
AppWidgetManager.INVALID_APPWIDGET_ID)
|
|
||||||
?: AppWidgetManager.INVALID_APPWIDGET_ID
|
|
||||||
|
|
||||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val glanceId = GlanceAppWidgetManager(applicationContext)
|
|
||||||
.getGlanceIdBy(appWidgetId)
|
|
||||||
|
|
||||||
setContent {
|
|
||||||
LightDarkTheme {
|
|
||||||
RandomConfiguration(applicationContext, appWidgetId, glanceId, onDone = {
|
|
||||||
finish()
|
|
||||||
Log.w("WIDGET_ACTIVITY", "SAVING")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, onDone: () -> Unit) {
|
|
||||||
|
|
||||||
var selectedAlbum by remember { mutableStateOf<DropdownItem?>(null) }
|
|
||||||
var showAlbumName by remember { mutableStateOf(false) }
|
|
||||||
var availableAlbums by remember { mutableStateOf<List<DropdownItem>>(listOf()) }
|
|
||||||
var state by remember { mutableStateOf(WidgetConfigState.LOADING) }
|
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
// get albums from server
|
|
||||||
val serverCfg = ImmichAPI.getServerConfig(context)
|
|
||||||
|
|
||||||
if (serverCfg == null) {
|
|
||||||
state = WidgetConfigState.LOG_IN
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
val api = ImmichAPI(serverCfg)
|
|
||||||
|
|
||||||
val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
|
||||||
val currentAlbumId = currentState[kSelectedAlbum] ?: "NONE"
|
|
||||||
val currentAlbumName = currentState[kSelectedAlbumName] ?: "None"
|
|
||||||
var albumItems: List<DropdownItem>
|
|
||||||
|
|
||||||
try {
|
|
||||||
albumItems = api.fetchAlbums().map {
|
|
||||||
DropdownItem(it.albumName, it.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
state = WidgetConfigState.SUCCESS
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
Log.e("WidgetWorker", "Error fetching albums: ${e.message}")
|
|
||||||
|
|
||||||
state = WidgetConfigState.NO_CONNECTION
|
|
||||||
albumItems = listOf(DropdownItem(currentAlbumName, currentAlbumId))
|
|
||||||
}
|
|
||||||
|
|
||||||
availableAlbums = listOf(DropdownItem("None", "NONE")) + albumItems
|
|
||||||
|
|
||||||
// load selected configuration
|
|
||||||
val albumEntity = availableAlbums.firstOrNull { it.id == currentAlbumId }
|
|
||||||
selectedAlbum = albumEntity ?: availableAlbums.first()
|
|
||||||
|
|
||||||
// load showAlbumName
|
|
||||||
showAlbumName = currentState[kShowAlbumName] == true
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun saveConfiguration() {
|
|
||||||
updateAppWidgetState(context, glanceId) { prefs ->
|
|
||||||
prefs[kSelectedAlbum] = selectedAlbum?.id ?: ""
|
|
||||||
prefs[kSelectedAlbumName] = selectedAlbum?.label ?: ""
|
|
||||||
prefs[kShowAlbumName] = showAlbumName
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageDownloadWorker.singleShot(context, appWidgetId, WidgetType.RANDOM)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar (
|
|
||||||
title = { Text("Widget Configuration") },
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
scope.launch {
|
|
||||||
saveConfiguration()
|
|
||||||
onDone()
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Default.Check, contentDescription = "Close", tint = MaterialTheme.colorScheme.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { innerPadding ->
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(innerPadding), // Respect the top bar
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
|
||||||
when (state) {
|
|
||||||
WidgetConfigState.LOADING -> CircularProgressIndicator(modifier = Modifier.size(48.dp))
|
|
||||||
WidgetConfigState.LOG_IN -> Text("You must log in inside the Immich App to configure this widget.")
|
|
||||||
else -> {
|
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text("View a random image from your library or a specific album.", style = MaterialTheme.typography.bodyMedium)
|
|
||||||
|
|
||||||
// no connection warning
|
|
||||||
if (state == WidgetConfigState.NO_CONNECTION) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.errorContainer)
|
|
||||||
.padding(12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Warning,
|
|
||||||
contentDescription = "Warning",
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Text(
|
|
||||||
text = "No connection to the server is available. Please try again later.",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
Text("Album")
|
|
||||||
Dropdown(
|
|
||||||
items = availableAlbums,
|
|
||||||
selectedItem = selectedAlbum,
|
|
||||||
onItemSelected = { selectedAlbum = it },
|
|
||||||
enabled = (state != WidgetConfigState.NO_CONNECTION)
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(text = "Show Album Name")
|
|
||||||
Switch(
|
|
||||||
checked = showAlbumName,
|
|
||||||
onCheckedChange = { showAlbumName = it },
|
|
||||||
enabled = (state != WidgetConfigState.NO_CONNECTION)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package app.alextran.immich.widget.model
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import androidx.datastore.preferences.core.*
|
|
||||||
|
|
||||||
// MARK: Immich Entities
|
|
||||||
|
|
||||||
enum class AssetType {
|
|
||||||
IMAGE, VIDEO, AUDIO, OTHER
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Asset(
|
|
||||||
val id: String,
|
|
||||||
val type: AssetType,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SearchFilters(
|
|
||||||
var type: AssetType = AssetType.IMAGE,
|
|
||||||
val size: Int = 1,
|
|
||||||
var albumIds: List<String> = listOf()
|
|
||||||
)
|
|
||||||
|
|
||||||
data class MemoryResult(
|
|
||||||
val id: String,
|
|
||||||
var assets: List<Asset>,
|
|
||||||
val type: String,
|
|
||||||
val data: MemoryData
|
|
||||||
) {
|
|
||||||
data class MemoryData(val year: Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Album(
|
|
||||||
val id: String,
|
|
||||||
val albumName: String
|
|
||||||
)
|
|
||||||
|
|
||||||
// MARK: Widget Specific
|
|
||||||
|
|
||||||
enum class WidgetType {
|
|
||||||
RANDOM, MEMORIES;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class WidgetState {
|
|
||||||
LOADING, SUCCESS, LOG_IN;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class WidgetConfigState {
|
|
||||||
LOADING, SUCCESS, LOG_IN, NO_CONNECTION
|
|
||||||
}
|
|
||||||
|
|
||||||
data class WidgetEntry (
|
|
||||||
val image: Bitmap,
|
|
||||||
val subtitle: String?,
|
|
||||||
val deeplink: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
|
|
||||||
|
|
||||||
// MARK: Widget State Keys
|
|
||||||
val kImageUUID = stringPreferencesKey("uuid")
|
|
||||||
val kSubtitleText = stringPreferencesKey("subtitle")
|
|
||||||
val kNow = longPreferencesKey("now")
|
|
||||||
val kWidgetState = stringPreferencesKey("state")
|
|
||||||
val kSelectedAlbum = stringPreferencesKey("albumID")
|
|
||||||
val kSelectedAlbumName = stringPreferencesKey("albumName")
|
|
||||||
val kShowAlbumName = booleanPreferencesKey("showAlbumName")
|
|
||||||
val kDeeplinkURL = stringPreferencesKey("deeplink")
|
|
||||||
|
|
||||||
const val kWorkerWidgetType = "widgetType"
|
|
||||||
const val kWorkerWidgetID = "widgetId"
|
|
||||||
const val kTriggeredFromApp = "triggeredFromApp"
|
|
||||||
|
|
||||||
fun imageFilename(id: String): String {
|
|
||||||
return "widget_image_$id.jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assetDeeplink(asset: Asset): String {
|
|
||||||
return "immich://asset?id=${asset.id}"
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 240 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 244 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="memory_widget_title">Memories</string>
|
|
||||||
<string name="random_widget_title">Random</string>
|
|
||||||
|
|
||||||
<string name="memory_widget_description">See memories from Immich.</string>
|
|
||||||
<string name="random_widget_description">View a random image from your library or a specific album.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:initialLayout="@layout/glance_default_loading_layout"
|
|
||||||
android:minWidth="110dp"
|
|
||||||
android:minHeight="110dp"
|
|
||||||
android:resizeMode="horizontal|vertical"
|
|
||||||
android:updatePeriodMillis="1200000"
|
|
||||||
android:description="@string/memory_widget_description"
|
|
||||||
android:previewImage="@drawable/memory_preview"
|
|
||||||
/>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:initialLayout="@layout/glance_default_loading_layout"
|
|
||||||
android:minWidth="110dp"
|
|
||||||
android:minHeight="110dp"
|
|
||||||
android:resizeMode="horizontal|vertical"
|
|
||||||
android:updatePeriodMillis="1200000"
|
|
||||||
android:configure="app.alextran.immich.widget.configure.RandomConfigure"
|
|
||||||
android:widgetFeatures="reconfigurable|configuration_optional"
|
|
||||||
tools:targetApi="28"
|
|
||||||
android:description="@string/random_widget_description"
|
|
||||||
android:previewImage="@drawable/random_preview"
|
|
||||||
/>
|
|
||||||
@@ -1 +1 @@
|
|||||||
version: '>=1.29.0 <=1.30.0'
|
version: '>=1.29.0 <1.30.0'
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
@@ -23,7 +23,7 @@ class ImmichLinter extends PluginBase {
|
|||||||
return rules;
|
return rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
static LintCode makeCode(String name, LintOptions options) => LintCode(
|
static makeCode(String name, LintOptions options) => LintCode(
|
||||||
name: name,
|
name: name,
|
||||||
problemMessage: options.json["message"] as String,
|
problemMessage: options.json["message"] as String,
|
||||||
errorSeverity: ErrorSeverity.WARNING,
|
errorSeverity: ErrorSeverity.WARNING,
|
||||||
|
|||||||
@@ -5,34 +5,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
|
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "82.0.0"
|
version: "80.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
|
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.4.5"
|
version: "7.3.0"
|
||||||
analyzer_plugin:
|
analyzer_plugin:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: analyzer_plugin
|
name: analyzer_plugin
|
||||||
sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef
|
sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.1"
|
version: "0.13.0"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.6.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -53,10 +53,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: checked_yaml
|
name: checked_yaml
|
||||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.3"
|
||||||
ci:
|
ci:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -125,10 +125,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_visitor
|
name: custom_lint_visitor
|
||||||
sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d
|
sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0+7.4.5"
|
version: "1.0.0+7.3.0"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -157,10 +157,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: freezed_annotation
|
name: freezed_annotation
|
||||||
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.0.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -213,18 +213,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.16.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_config
|
name: package_config
|
||||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.1.1"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -317,10 +317,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.4"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -341,18 +341,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.0"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -362,4 +362,4 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0 <4.0.0"
|
dart: ">=3.8.0-0 <4.0.0"
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
@@ -44,7 +43,6 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
|
||||||
disableMainThreadChecker = "YES"
|
disableMainThreadChecker = "YES"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ struct PlatformAsset: Hashable {
|
|||||||
var width: Int64? = nil
|
var width: Int64? = nil
|
||||||
var height: Int64? = nil
|
var height: Int64? = nil
|
||||||
var durationInSeconds: Int64
|
var durationInSeconds: Int64
|
||||||
var orientation: Int64
|
|
||||||
|
|
||||||
|
|
||||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
@@ -151,7 +150,6 @@ struct PlatformAsset: Hashable {
|
|||||||
let width: Int64? = nilOrValue(pigeonVar_list[5])
|
let width: Int64? = nilOrValue(pigeonVar_list[5])
|
||||||
let height: Int64? = nilOrValue(pigeonVar_list[6])
|
let height: Int64? = nilOrValue(pigeonVar_list[6])
|
||||||
let durationInSeconds = pigeonVar_list[7] as! Int64
|
let durationInSeconds = pigeonVar_list[7] as! Int64
|
||||||
let orientation = pigeonVar_list[8] as! Int64
|
|
||||||
|
|
||||||
return PlatformAsset(
|
return PlatformAsset(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -161,8 +159,7 @@ struct PlatformAsset: Hashable {
|
|||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds
|
||||||
orientation: orientation
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func toList() -> [Any?] {
|
func toList() -> [Any?] {
|
||||||
@@ -175,7 +172,6 @@ struct PlatformAsset: Hashable {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
orientation,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ extension PHAsset {
|
|||||||
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
|
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
|
||||||
width: Int64(pixelWidth),
|
width: Int64(pixelWidth),
|
||||||
height: Int64(pixelHeight),
|
height: Int64(pixelHeight),
|
||||||
durationInSeconds: Int64(duration),
|
durationInSeconds: Int64(duration)
|
||||||
orientation: 0
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,8 +169,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
id: asset.localIdentifier,
|
id: asset.localIdentifier,
|
||||||
name: "",
|
name: "",
|
||||||
type: 0,
|
type: 0,
|
||||||
durationInSeconds: 0,
|
durationInSeconds: 0
|
||||||
orientation: 0
|
|
||||||
)
|
)
|
||||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
|
func buildEntry(
|
||||||
|
api: ImmichAPI,
|
||||||
|
asset: Asset,
|
||||||
|
dateOffset: Int,
|
||||||
|
subtitle: String? = nil
|
||||||
|
)
|
||||||
|
async throws -> ImageEntry
|
||||||
|
{
|
||||||
|
let entryDate = Calendar.current.date(
|
||||||
|
byAdding: .minute,
|
||||||
|
value: dateOffset * 20,
|
||||||
|
to: Date.now
|
||||||
|
)!
|
||||||
|
let image = try await api.fetchImage(asset: asset)
|
||||||
|
|
||||||
|
return ImageEntry(date: entryDate, image: image, subtitle: subtitle, deepLink: asset.deepLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomEntries(
|
||||||
|
api: ImmichAPI,
|
||||||
|
now: Date,
|
||||||
|
count: Int,
|
||||||
|
albumId: String? = nil,
|
||||||
|
subtitle: String? = nil
|
||||||
|
)
|
||||||
|
async throws -> [ImageEntry]
|
||||||
|
{
|
||||||
|
|
||||||
|
var entries: [ImageEntry] = []
|
||||||
|
let albumIds = albumId != nil ? [albumId!] : []
|
||||||
|
|
||||||
|
let randomAssets = try await api.fetchSearchResults(
|
||||||
|
with: SearchFilters(size: count, albumIds: albumIds)
|
||||||
|
)
|
||||||
|
|
||||||
|
await withTaskGroup(of: ImageEntry?.self) { group in
|
||||||
|
for (dateOffset, asset) in randomAssets.enumerated() {
|
||||||
|
group.addTask {
|
||||||
|
return try? await buildEntry(
|
||||||
|
api: api,
|
||||||
|
asset: asset,
|
||||||
|
dateOffset: dateOffset,
|
||||||
|
subtitle: subtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await result in group {
|
||||||
|
if let entry = result {
|
||||||
|
entries.append(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import WidgetKit
|
|
||||||
|
|
||||||
typealias EntryMetadata = ImageEntry.Metadata
|
|
||||||
|
|
||||||
struct ImageEntry: TimelineEntry {
|
|
||||||
let date: Date
|
|
||||||
var image: UIImage?
|
|
||||||
var metadata: Metadata = Metadata()
|
|
||||||
|
|
||||||
struct Metadata: Codable {
|
|
||||||
var subtitle: String? = nil
|
|
||||||
var error: WidgetError? = nil
|
|
||||||
var deepLink: URL? = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
static func build(
|
|
||||||
api: ImmichAPI,
|
|
||||||
asset: Asset,
|
|
||||||
dateOffset: Int,
|
|
||||||
subtitle: String? = nil
|
|
||||||
)
|
|
||||||
async throws -> Self
|
|
||||||
{
|
|
||||||
let entryDate = Calendar.current.date(
|
|
||||||
byAdding: .minute,
|
|
||||||
value: dateOffset * 20,
|
|
||||||
to: Date.now
|
|
||||||
)!
|
|
||||||
let image = try await api.fetchImage(asset: asset)
|
|
||||||
|
|
||||||
return Self(
|
|
||||||
date: entryDate,
|
|
||||||
image: image,
|
|
||||||
metadata: EntryMetadata(
|
|
||||||
subtitle: subtitle,
|
|
||||||
deepLink: asset.deepLink
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cache(for key: String) throws {
|
|
||||||
if let containerURL = FileManager.default.containerURL(
|
|
||||||
forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP
|
|
||||||
) {
|
|
||||||
let imageURL = containerURL.appendingPathComponent("\(key)_image.png")
|
|
||||||
let metadataURL = containerURL.appendingPathComponent(
|
|
||||||
"\(key)_metadata.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
// build metadata JSON
|
|
||||||
let entryMetadata = try JSONEncoder().encode(self.metadata)
|
|
||||||
|
|
||||||
// write to disk
|
|
||||||
try self.image?.pngData()?.write(to: imageURL, options: .atomic)
|
|
||||||
try entryMetadata.write(to: metadataURL, options: .atomic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func loadCached(for key: String, at date: Date = Date.now)
|
|
||||||
-> ImageEntry?
|
|
||||||
{
|
|
||||||
if let containerURL = FileManager.default.containerURL(
|
|
||||||
forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP
|
|
||||||
) {
|
|
||||||
let imageURL = containerURL.appendingPathComponent("\(key)_image.png")
|
|
||||||
let metadataURL = containerURL.appendingPathComponent(
|
|
||||||
"\(key)_metadata.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
guard let imageData = try? Data(contentsOf: imageURL),
|
|
||||||
let metadataJSON = try? Data(contentsOf: metadataURL),
|
|
||||||
let decodedMetadata = try? JSONDecoder().decode(
|
|
||||||
Metadata.self,
|
|
||||||
from: metadataJSON
|
|
||||||
)
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ImageEntry(
|
|
||||||
date: date,
|
|
||||||
image: UIImage(data: imageData),
|
|
||||||
metadata: decodedMetadata
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
static func handleError(
|
|
||||||
for key: String,
|
|
||||||
error: WidgetError = .fetchFailed
|
|
||||||
) -> Timeline<ImageEntry> {
|
|
||||||
var timelineEntry = ImageEntry(
|
|
||||||
date: Date.now,
|
|
||||||
image: nil,
|
|
||||||
metadata: EntryMetadata(error: error)
|
|
||||||
)
|
|
||||||
|
|
||||||
// use cache if generic failed error
|
|
||||||
// we want to show the other errors to the user since without intervention,
|
|
||||||
// it will never succeed
|
|
||||||
if error == .fetchFailed, let cachedEntry = ImageEntry.loadCached(for: key)
|
|
||||||
{
|
|
||||||
timelineEntry = cachedEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
return Timeline(entries: [timelineEntry], policy: .atEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateRandomEntries(
|
|
||||||
api: ImmichAPI,
|
|
||||||
now: Date,
|
|
||||||
count: Int,
|
|
||||||
filter: SearchFilter = Album.NONE.filter,
|
|
||||||
subtitle: String? = nil
|
|
||||||
)
|
|
||||||
async throws -> [ImageEntry]
|
|
||||||
{
|
|
||||||
|
|
||||||
var entries: [ImageEntry] = []
|
|
||||||
|
|
||||||
let randomAssets = try await api.fetchSearchResults(with: filter)
|
|
||||||
|
|
||||||
await withTaskGroup(of: ImageEntry?.self) { group in
|
|
||||||
for (dateOffset, asset) in randomAssets.enumerated() {
|
|
||||||
group.addTask {
|
|
||||||
return try? await ImageEntry.build(
|
|
||||||
api: api,
|
|
||||||
asset: asset,
|
|
||||||
dateOffset: dateOffset,
|
|
||||||
subtitle: subtitle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for await result in group {
|
|
||||||
if let entry = result {
|
|
||||||
entries.append(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
extension Image {
|
struct ImageEntry: TimelineEntry {
|
||||||
@ViewBuilder
|
let date: Date
|
||||||
func tintedWidgetImageModifier() -> some View {
|
var image: UIImage?
|
||||||
if #available(iOS 18.0, *) {
|
var subtitle: String? = nil
|
||||||
self
|
var error: WidgetError? = nil
|
||||||
.widgetAccentedRenderingMode(.accentedDesaturated)
|
var deepLink: URL? = nil
|
||||||
} else {
|
|
||||||
self
|
// Resizes the stored image to a maximum width of 450 pixels
|
||||||
|
mutating func resize() {
|
||||||
|
if (image == nil || image!.size.height < 450 || image!.size.width < 450 ) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
image = image?.resized(toWidth: 450)
|
||||||
|
|
||||||
|
if image == nil {
|
||||||
|
error = .unableToResize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,8 +29,7 @@ struct ImmichWidgetView: View {
|
|||||||
if entry.image == nil {
|
if entry.image == nil {
|
||||||
VStack {
|
VStack {
|
||||||
Image("LaunchImage")
|
Image("LaunchImage")
|
||||||
.tintedWidgetImageModifier()
|
Text(entry.error?.errorDescription ?? "")
|
||||||
Text(entry.metadata.error?.errorDescription ?? "")
|
|
||||||
.minimumScaleFactor(0.25)
|
.minimumScaleFactor(0.25)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -32,13 +40,11 @@ struct ImmichWidgetView: View {
|
|||||||
Color.clear.overlay(
|
Color.clear.overlay(
|
||||||
Image(uiImage: entry.image!)
|
Image(uiImage: entry.image!)
|
||||||
.resizable()
|
.resizable()
|
||||||
.tintedWidgetImageModifier()
|
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
|
|
||||||
)
|
)
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
if let subtitle = entry.metadata.subtitle {
|
if let subtitle = entry.subtitle {
|
||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
@@ -49,7 +55,7 @@ struct ImmichWidgetView: View {
|
|||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
}
|
}
|
||||||
.widgetURL(entry.metadata.deepLink)
|
.widgetURL(entry.deepLink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,9 +70,7 @@ struct ImmichWidgetView: View {
|
|||||||
ImageEntry(
|
ImageEntry(
|
||||||
date: date,
|
date: date,
|
||||||
image: UIImage(named: "ImmichLogo"),
|
image: UIImage(named: "ImmichLogo"),
|
||||||
metadata: EntryMetadata(
|
subtitle: "1 year ago"
|
||||||
subtitle: "1 year ago"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,20 +2,14 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
enum WidgetError: Error {
|
||||||
|
|
||||||
enum WidgetError: Error, Codable {
|
|
||||||
case noLogin
|
case noLogin
|
||||||
case fetchFailed
|
case fetchFailed
|
||||||
|
case unknown
|
||||||
case albumNotFound
|
case albumNotFound
|
||||||
case noAssetsAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
enum FetchError: Error {
|
|
||||||
case unableToResize
|
case unableToResize
|
||||||
case invalidImage
|
case invalidImage
|
||||||
case invalidURL
|
case invalidURL
|
||||||
case fetchFailed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WidgetError: LocalizedError {
|
extension WidgetError: LocalizedError {
|
||||||
@@ -30,8 +24,14 @@ extension WidgetError: LocalizedError {
|
|||||||
case .albumNotFound:
|
case .albumNotFound:
|
||||||
return "Album not found"
|
return "Album not found"
|
||||||
|
|
||||||
case .noAssetsAvailable:
|
case .invalidURL:
|
||||||
return "No assets available"
|
return "An invalid URL was used"
|
||||||
|
|
||||||
|
case .invalidImage:
|
||||||
|
return "An invalid image was received"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "An unknown error occured"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,11 +52,10 @@ struct Asset: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchFilter: Codable {
|
struct SearchFilters: Codable {
|
||||||
var type = AssetType.image
|
var type: AssetType = .image
|
||||||
var size = 1
|
let size: Int
|
||||||
var albumIds: [String] = []
|
var albumIds: [String] = []
|
||||||
var isFavorite: Bool? = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MemoryResult: Codable {
|
struct MemoryResult: Codable {
|
||||||
@@ -71,34 +70,9 @@ struct MemoryResult: Codable {
|
|||||||
let data: MemoryData
|
let data: MemoryData
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Album: Codable, Equatable {
|
struct Album: Codable {
|
||||||
let id: String
|
let id: String
|
||||||
let albumName: String
|
let albumName: String
|
||||||
|
|
||||||
static let NONE = Album(id: "NONE", albumName: "None")
|
|
||||||
static let FAVORITES = Album(id: "FAVORITES", albumName: "Favorites")
|
|
||||||
|
|
||||||
var filter: SearchFilter {
|
|
||||||
switch self {
|
|
||||||
case Album.NONE:
|
|
||||||
return SearchFilter()
|
|
||||||
case Album.FAVORITES:
|
|
||||||
return SearchFilter(isFavorite: true)
|
|
||||||
|
|
||||||
// regular album
|
|
||||||
default:
|
|
||||||
return SearchFilter(albumIds: [id])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isVirtual: Bool {
|
|
||||||
switch self {
|
|
||||||
case Album.NONE, Album.FAVORITES:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: API
|
// MARK: API
|
||||||
@@ -112,7 +86,7 @@ class ImmichAPI {
|
|||||||
|
|
||||||
init() async throws {
|
init() async throws {
|
||||||
// fetch the credentials from the UserDefaults store that dart placed here
|
// fetch the credentials from the UserDefaults store that dart placed here
|
||||||
guard let defaults = UserDefaults(suiteName: IMMICH_SHARE_GROUP),
|
guard let defaults = UserDefaults(suiteName: "group.app.immich.share"),
|
||||||
let serverURL = defaults.string(forKey: "widget_server_url"),
|
let serverURL = defaults.string(forKey: "widget_server_url"),
|
||||||
let sessionKey = defaults.string(forKey: "widget_auth_token")
|
let sessionKey = defaults.string(forKey: "widget_auth_token")
|
||||||
else {
|
else {
|
||||||
@@ -156,8 +130,7 @@ class ImmichAPI {
|
|||||||
return components?.url
|
return components?.url
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
func fetchSearchResults(with filters: SearchFilters) async throws
|
||||||
async throws
|
|
||||||
-> [Asset]
|
-> [Asset]
|
||||||
{
|
{
|
||||||
// get URL
|
// get URL
|
||||||
@@ -203,7 +176,7 @@ class ImmichAPI {
|
|||||||
return try JSONDecoder().decode([MemoryResult].self, from: data)
|
return try JSONDecoder().decode([MemoryResult].self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchImage(asset: Asset) async throws(FetchError) -> UIImage {
|
func fetchImage(asset: Asset) async throws(WidgetError) -> UIImage {
|
||||||
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
|
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
|
||||||
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
||||||
|
|
||||||
@@ -217,24 +190,17 @@ class ImmichAPI {
|
|||||||
throw .invalidURL
|
throw .invalidURL
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil)
|
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) else {
|
||||||
else {
|
|
||||||
throw .invalidURL
|
throw .invalidURL
|
||||||
}
|
}
|
||||||
|
|
||||||
let decodeOptions: [NSString: Any] = [
|
let decodeOptions: [NSString: Any] = [
|
||||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||||
kCGImageSourceThumbnailMaxPixelSize: 512,
|
kCGImageSourceThumbnailMaxPixelSize: 400,
|
||||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
kCGImageSourceCreateThumbnailWithTransform: true
|
||||||
]
|
]
|
||||||
|
|
||||||
guard
|
guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions as CFDictionary) else {
|
||||||
let thumbnail = CGImageSourceCreateThumbnailAtIndex(
|
|
||||||
imageSource,
|
|
||||||
0,
|
|
||||||
decodeOptions as CFDictionary
|
|
||||||
)
|
|
||||||
else {
|
|
||||||
throw .fetchFailed
|
throw .fetchFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAppTransportSecurity</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
|||||||
@@ -7,17 +7,14 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension UIImage {
|
extension UIImage {
|
||||||
/// Crops the image to ensure width and height do not exceed maxSize.
|
/// Crops the image to ensure width and height do not exceed maxSize.
|
||||||
/// Keeps original aspect ratio and crops excess equally from edges (center crop).
|
/// Keeps original aspect ratio and crops excess equally from edges (center crop).
|
||||||
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
|
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
|
||||||
let canvas = CGSize(
|
let canvas = CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))
|
||||||
width: width,
|
let format = imageRendererFormat
|
||||||
height: CGFloat(ceil(width / size.width * size.height))
|
format.opaque = isOpaque
|
||||||
)
|
return UIGraphicsImageRenderer(size: canvas, format: format).image {
|
||||||
let format = imageRendererFormat
|
_ in draw(in: CGRect(origin: .zero, size: canvas))
|
||||||
format.opaque = isOpaque
|
}
|
||||||
return UIGraphicsImageRenderer(size: canvas, format: format).image {
|
|
||||||
_ in draw(in: CGRect(origin: .zero, size: canvas))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,31 +19,28 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
in context: Context,
|
in context: Context,
|
||||||
completion: @escaping @Sendable (ImageEntry) -> Void
|
completion: @escaping @Sendable (ImageEntry) -> Void
|
||||||
) {
|
) {
|
||||||
let cacheKey = "memory_\(context.family.rawValue)"
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
completion(
|
completion(ImageEntry(date: Date(), image: nil, error: .noLogin))
|
||||||
ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first!
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let memories = try? await api.fetchMemory(for: Date.now)
|
guard let memories = try? await api.fetchMemory(for: Date.now)
|
||||||
else {
|
else {
|
||||||
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for memory in memories {
|
for memory in memories {
|
||||||
if let asset = memory.assets.first(where: { $0.type == .image }),
|
if let asset = memory.assets.first(where: { $0.type == .image }),
|
||||||
let entry = try? await ImageEntry.build(
|
var entry = try? await buildEntry(
|
||||||
api: api,
|
api: api,
|
||||||
asset: asset,
|
asset: asset,
|
||||||
dateOffset: 0,
|
dateOffset: 0,
|
||||||
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
entry.resize()
|
||||||
completion(entry)
|
completion(entry)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -51,17 +48,26 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
|
|
||||||
// fallback to random image
|
// fallback to random image
|
||||||
guard
|
guard
|
||||||
let randomImage = try? await api.fetchSearchResults().first,
|
let randomImage = try? await api.fetchSearchResults(
|
||||||
let imageEntry = try? await ImageEntry.build(
|
with: SearchFilters(size: 1)
|
||||||
|
).first
|
||||||
|
else {
|
||||||
|
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard
|
||||||
|
var imageEntry = try? await buildEntry(
|
||||||
api: api,
|
api: api,
|
||||||
asset: randomImage,
|
asset: randomImage,
|
||||||
dateOffset: 0
|
dateOffset: 0
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageEntry.resize()
|
||||||
completion(imageEntry)
|
completion(imageEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,12 +80,9 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
var entries: [ImageEntry] = []
|
var entries: [ImageEntry] = []
|
||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
let cacheKey = "memory_\(context.family.rawValue)"
|
|
||||||
|
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
completion(
|
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
|
||||||
ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
completion(Timeline(entries: entries, policy: .atEnd))
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +95,7 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
for asset in memory.assets {
|
for asset in memory.assets {
|
||||||
if asset.type == .image && totalAssets < 12 {
|
if asset.type == .image && totalAssets < 12 {
|
||||||
group.addTask {
|
group.addTask {
|
||||||
try? await ImageEntry.build(
|
try? await buildEntry(
|
||||||
api: api,
|
api: api,
|
||||||
asset: asset,
|
asset: asset,
|
||||||
dateOffset: totalAssets,
|
dateOffset: totalAssets,
|
||||||
@@ -117,32 +120,25 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
// If we didnt add any memory images (some failure occured or no images in memory),
|
// If we didnt add any memory images (some failure occured or no images in memory),
|
||||||
// default to 12 hours of random photos
|
// default to 12 hours of random photos
|
||||||
if entries.count == 0 {
|
if entries.count == 0 {
|
||||||
// this must be a do/catch since we need to
|
entries.append(
|
||||||
// distinguish between a network fail and an empty search
|
contentsOf: (try? await generateRandomEntries(
|
||||||
do {
|
|
||||||
let search = try await generateRandomEntries(
|
|
||||||
api: api,
|
api: api,
|
||||||
now: now,
|
now: now,
|
||||||
count: 12
|
count: 12
|
||||||
)
|
)) ?? []
|
||||||
|
)
|
||||||
// Load or save a cached asset for when network conditions are bad
|
|
||||||
if search.count == 0 {
|
|
||||||
completion(
|
|
||||||
ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.append(contentsOf: search)
|
|
||||||
} catch {
|
|
||||||
completion(ImageEntry.handleError(for: cacheKey))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache the last image
|
// If we fail to fetch images, we still want to add an entry
|
||||||
try? entries.last!.cache(for: cacheKey)
|
// with a nil image and an error
|
||||||
|
if entries.count == 0 {
|
||||||
|
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize all images to something that can be stored by iOS
|
||||||
|
for i in entries.indices {
|
||||||
|
entries[i].resize()
|
||||||
|
}
|
||||||
|
|
||||||
completion(Timeline(entries: entries, policy: .atEnd))
|
completion(Timeline(entries: entries, policy: .atEnd))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,21 +8,20 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
|||||||
|
|
||||||
struct AlbumQuery: EntityQuery {
|
struct AlbumQuery: EntityQuery {
|
||||||
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
|
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
|
||||||
return await suggestedEntities().filter {
|
// use cached albums to search
|
||||||
|
var albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
||||||
|
albums.insert(NO_ALBUM, at: 0)
|
||||||
|
|
||||||
|
return albums.filter {
|
||||||
identifiers.contains($0.id)
|
identifiers.contains($0.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func suggestedEntities() async -> [Album] {
|
func suggestedEntities() async throws -> [Album] {
|
||||||
let albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? []
|
||||||
|
albums.insert(NO_ALBUM, at: 0)
|
||||||
|
|
||||||
let options =
|
return albums
|
||||||
[
|
|
||||||
NONE,
|
|
||||||
FAVORITES,
|
|
||||||
] + albums
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +35,8 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let NO_ALBUM = Album(id: "NONE", albumName: "None")
|
||||||
|
|
||||||
struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
||||||
static var title: LocalizedStringResource { "Select Album" }
|
static var title: LocalizedStringResource { "Select Album" }
|
||||||
static var description: IntentDescription {
|
static var description: IntentDescription {
|
||||||
@@ -53,7 +54,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
|||||||
|
|
||||||
struct ImmichRandomProvider: AppIntentTimelineProvider {
|
struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||||
func placeholder(in context: Context) -> ImageEntry {
|
func placeholder(in context: Context) -> ImageEntry {
|
||||||
ImageEntry(date: Date())
|
ImageEntry(date: Date(), image: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshot(
|
func snapshot(
|
||||||
@@ -62,26 +63,30 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
|||||||
) async
|
) async
|
||||||
-> ImageEntry
|
-> ImageEntry
|
||||||
{
|
{
|
||||||
let cacheKey = "random_none_\(context.family.rawValue)"
|
|
||||||
|
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries
|
return ImageEntry(date: Date(), image: nil, error: .noLogin)
|
||||||
.first!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let randomImage = try? await api.fetchSearchResults(
|
let randomImage = try? await api.fetchSearchResults(
|
||||||
with: Album.NONE.filter
|
with: SearchFilters(size: 1)
|
||||||
).first,
|
).first
|
||||||
let entry = try? await ImageEntry.build(
|
else {
|
||||||
|
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard
|
||||||
|
var entry = try? await buildEntry(
|
||||||
api: api,
|
api: api,
|
||||||
asset: randomImage,
|
asset: randomImage,
|
||||||
dateOffset: 0
|
dateOffset: 0
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
return ImageEntry.handleError(for: cacheKey).entries.first!
|
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.resize()
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,41 +99,50 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
|||||||
var entries: [ImageEntry] = []
|
var entries: [ImageEntry] = []
|
||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
// nil if album is NONE or nil
|
|
||||||
let album = configuration.album ?? Album.NONE
|
|
||||||
let albumName = album.isVirtual ? nil : album.albumName
|
|
||||||
|
|
||||||
let cacheKey = "random_\(album.id)_\(context.family.rawValue)"
|
|
||||||
|
|
||||||
// If we don't have a server config, return an entry with an error
|
// If we don't have a server config, return an entry with an error
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
return ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
|
||||||
|
return Timeline(entries: entries, policy: .atEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// build entries
|
// nil if album is NONE or nil
|
||||||
// this must be a do/catch since we need to
|
let albumId =
|
||||||
// distinguish between a network fail and an empty search
|
configuration.album?.id != "NONE" ? configuration.album?.id : nil
|
||||||
do {
|
var albumName: String? = albumId != nil ? configuration.album?.albumName : nil
|
||||||
let search = try await generateRandomEntries(
|
|
||||||
|
if albumId != nil {
|
||||||
|
// make sure the album exists on server, otherwise show error
|
||||||
|
guard let albums = try? await api.fetchAlbums() else {
|
||||||
|
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
|
||||||
|
return Timeline(entries: entries, policy: .atEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !albums.contains(where: { $0.id == albumId }) {
|
||||||
|
entries.append(ImageEntry(date: now, image: nil, error: .albumNotFound))
|
||||||
|
return Timeline(entries: entries, policy: .atEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.append(
|
||||||
|
contentsOf: (try? await generateRandomEntries(
|
||||||
api: api,
|
api: api,
|
||||||
now: now,
|
now: now,
|
||||||
count: 12,
|
count: 12,
|
||||||
filter: album.filter,
|
albumId: albumId,
|
||||||
subtitle: configuration.showAlbumName ? albumName : nil
|
subtitle: configuration.showAlbumName ? albumName : nil
|
||||||
)
|
))
|
||||||
|
?? []
|
||||||
|
)
|
||||||
|
|
||||||
// Load or save a cached asset for when network conditions are bad
|
// If we fail to fetch images, we still want to add an entry with a nil image and an error
|
||||||
if search.count == 0 {
|
if entries.count == 0 {
|
||||||
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
|
||||||
}
|
|
||||||
|
|
||||||
entries.append(contentsOf: search)
|
|
||||||
} catch {
|
|
||||||
return ImageEntry.handleError(for: cacheKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache the last image
|
// Resize all images to something that can be stored by iOS
|
||||||
try? entries.last!.cache(for: cacheKey)
|
for i in entries.indices {
|
||||||
|
entries[i].resize()
|
||||||
|
}
|
||||||
|
|
||||||
return Timeline(entries: entries, policy: .atEnd)
|
return Timeline(entries: entries, policy: .atEnd)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":["<all>"]},"commands":{"<all>":{"tool":"phony","inputs":["<WorkspaceHeaderMapVFSFilesWritten>"],"outputs":["<all>"]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":["<WorkspaceHeaderMapVFSFilesWritten>"]}}}
|
||||||
@@ -28,8 +28,7 @@ const String appShareGroupId = "group.app.immich.share";
|
|||||||
|
|
||||||
// add widget identifiers here for new widgets
|
// add widget identifiers here for new widgets
|
||||||
// these are used to force a widget refresh
|
// these are used to force a widget refresh
|
||||||
// (iOSName, androidFQDN)
|
const List<String> kWidgetNames = [
|
||||||
const List<(String, String)> kWidgetNames = [
|
'com.immich.widget.random',
|
||||||
('com.immich.widget.random', 'app.alextran.immich.widget.RandomReceiver'),
|
'com.immich.widget.memory',
|
||||||
('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ enum AlbumUserRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Model for an album stored in the server
|
// Model for an album stored in the server
|
||||||
class RemoteAlbum {
|
class Album {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
@@ -24,7 +24,7 @@ class RemoteAlbum {
|
|||||||
final int assetCount;
|
final int assetCount;
|
||||||
final String ownerName;
|
final String ownerName;
|
||||||
|
|
||||||
const RemoteAlbum({
|
const Album({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.ownerId,
|
required this.ownerId,
|
||||||
@@ -57,7 +57,7 @@ class RemoteAlbum {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! RemoteAlbum) return false;
|
if (other is! Album) return false;
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
return id == other.id &&
|
return id == other.id &&
|
||||||
name == other.name &&
|
name == other.name &&
|
||||||
@@ -86,32 +86,4 @@ class RemoteAlbum {
|
|||||||
assetCount.hashCode ^
|
assetCount.hashCode ^
|
||||||
ownerName.hashCode;
|
ownerName.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteAlbum copyWith({
|
|
||||||
String? id,
|
|
||||||
String? name,
|
|
||||||
String? ownerId,
|
|
||||||
String? description,
|
|
||||||
DateTime? createdAt,
|
|
||||||
DateTime? updatedAt,
|
|
||||||
String? thumbnailAssetId,
|
|
||||||
bool? isActivityEnabled,
|
|
||||||
AlbumAssetOrder? order,
|
|
||||||
int? assetCount,
|
|
||||||
String? ownerName,
|
|
||||||
}) {
|
|
||||||
return RemoteAlbum(
|
|
||||||
id: id ?? this.id,
|
|
||||||
name: name ?? this.name,
|
|
||||||
ownerId: ownerId ?? this.ownerId,
|
|
||||||
description: description ?? this.description,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
|
||||||
thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
|
|
||||||
isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
|
|
||||||
order: order ?? this.order,
|
|
||||||
assetCount: assetCount ?? this.assetCount,
|
|
||||||
ownerName: ownerName ?? this.ownerName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
part 'local_asset.model.dart';
|
|
||||||
part 'remote_asset.model.dart';
|
part 'remote_asset.model.dart';
|
||||||
|
part 'local_asset.model.dart';
|
||||||
|
|
||||||
enum AssetType {
|
enum AssetType {
|
||||||
// do not change this order!
|
// do not change this order!
|
||||||
@@ -25,7 +25,6 @@ sealed class BaseAsset {
|
|||||||
final int? height;
|
final int? height;
|
||||||
final int? durationInSeconds;
|
final int? durationInSeconds;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
final String? livePhotoVideoId;
|
|
||||||
|
|
||||||
const BaseAsset({
|
const BaseAsset({
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -37,32 +36,11 @@ sealed class BaseAsset {
|
|||||||
this.height,
|
this.height,
|
||||||
this.durationInSeconds,
|
this.durationInSeconds,
|
||||||
this.isFavorite = false,
|
this.isFavorite = false,
|
||||||
this.livePhotoVideoId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isImage => type == AssetType.image;
|
bool get isImage => type == AssetType.image;
|
||||||
bool get isVideo => type == AssetType.video;
|
bool get isVideo => type == AssetType.video;
|
||||||
|
|
||||||
bool get isMotionPhoto => livePhotoVideoId != null;
|
|
||||||
|
|
||||||
Duration get duration {
|
|
||||||
final durationInSeconds = this.durationInSeconds;
|
|
||||||
if (durationInSeconds != null) {
|
|
||||||
return Duration(seconds: durationInSeconds);
|
|
||||||
}
|
|
||||||
return const Duration();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasRemote =>
|
|
||||||
storage == AssetState.remote || storage == AssetState.merged;
|
|
||||||
bool get hasLocal =>
|
|
||||||
storage == AssetState.local || storage == AssetState.merged;
|
|
||||||
bool get isLocalOnly => storage == AssetState.local;
|
|
||||||
bool get isRemoteOnly => storage == AssetState.remote;
|
|
||||||
|
|
||||||
// Overridden in subclasses
|
|
||||||
AssetState get storage;
|
AssetState get storage;
|
||||||
String get heroTag;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ part of 'base_asset.model.dart';
|
|||||||
class LocalAsset extends BaseAsset {
|
class LocalAsset extends BaseAsset {
|
||||||
final String id;
|
final String id;
|
||||||
final String? remoteId;
|
final String? remoteId;
|
||||||
final int orientation;
|
|
||||||
|
|
||||||
const LocalAsset({
|
const LocalAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -17,17 +16,12 @@ class LocalAsset extends BaseAsset {
|
|||||||
super.height,
|
super.height,
|
||||||
super.durationInSeconds,
|
super.durationInSeconds,
|
||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
super.livePhotoVideoId,
|
|
||||||
this.orientation = 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AssetState get storage =>
|
AssetState get storage =>
|
||||||
remoteId == null ? AssetState.local : AssetState.merged;
|
remoteId == null ? AssetState.local : AssetState.merged;
|
||||||
|
|
||||||
@override
|
|
||||||
String get heroTag => '${id}_${remoteId ?? checksum}';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''LocalAsset {
|
return '''LocalAsset {
|
||||||
@@ -41,21 +35,18 @@ class LocalAsset extends BaseAsset {
|
|||||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||||
remoteId: ${remoteId ?? "<NA>"}
|
remoteId: ${remoteId ?? "<NA>"}
|
||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
orientation: $orientation,
|
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not checking for remoteId here
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! LocalAsset) return false;
|
if (other is! LocalAsset) return false;
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
return super == other && id == other.id && orientation == other.orientation;
|
return super == other && id == other.id && remoteId == other.remoteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode;
|
||||||
super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode;
|
|
||||||
|
|
||||||
LocalAsset copyWith({
|
LocalAsset copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
@@ -69,7 +60,6 @@ class LocalAsset extends BaseAsset {
|
|||||||
int? height,
|
int? height,
|
||||||
int? durationInSeconds,
|
int? durationInSeconds,
|
||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
int? orientation,
|
|
||||||
}) {
|
}) {
|
||||||
return LocalAsset(
|
return LocalAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -83,7 +73,6 @@ class LocalAsset extends BaseAsset {
|
|||||||
height: height ?? this.height,
|
height: height ?? this.height,
|
||||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
orientation: orientation ?? this.orientation,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ class RemoteAsset extends BaseAsset {
|
|||||||
final String? thumbHash;
|
final String? thumbHash;
|
||||||
final AssetVisibility visibility;
|
final AssetVisibility visibility;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
final String? stackId;
|
|
||||||
final int stackCount;
|
|
||||||
|
|
||||||
const RemoteAsset({
|
const RemoteAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -32,18 +30,12 @@ class RemoteAsset extends BaseAsset {
|
|||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
this.thumbHash,
|
this.thumbHash,
|
||||||
this.visibility = AssetVisibility.timeline,
|
this.visibility = AssetVisibility.timeline,
|
||||||
super.livePhotoVideoId,
|
|
||||||
this.stackId,
|
|
||||||
this.stackCount = 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AssetState get storage =>
|
AssetState get storage =>
|
||||||
localId == null ? AssetState.remote : AssetState.merged;
|
localId == null ? AssetState.remote : AssetState.merged;
|
||||||
|
|
||||||
@override
|
|
||||||
String get heroTag => '${localId ?? checksum}_$id';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Asset {
|
return '''Asset {
|
||||||
@@ -60,14 +52,9 @@ class RemoteAsset extends BaseAsset {
|
|||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
thumbHash: ${thumbHash ?? "<NA>"},
|
thumbHash: ${thumbHash ?? "<NA>"},
|
||||||
visibility: $visibility,
|
visibility: $visibility,
|
||||||
stackId: ${stackId ?? "<NA>"},
|
|
||||||
stackCount: $stackCount,
|
|
||||||
checksum: $checksum,
|
|
||||||
livePhotoVideoId: ${livePhotoVideoId ?? "<NA>"},
|
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not checking for localId here
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! RemoteAsset) return false;
|
if (other is! RemoteAsset) return false;
|
||||||
@@ -75,10 +62,9 @@ class RemoteAsset extends BaseAsset {
|
|||||||
return super == other &&
|
return super == other &&
|
||||||
id == other.id &&
|
id == other.id &&
|
||||||
ownerId == other.ownerId &&
|
ownerId == other.ownerId &&
|
||||||
|
localId == other.localId &&
|
||||||
thumbHash == other.thumbHash &&
|
thumbHash == other.thumbHash &&
|
||||||
visibility == other.visibility &&
|
visibility == other.visibility;
|
||||||
stackId == other.stackId &&
|
|
||||||
stackCount == other.stackCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -88,47 +74,5 @@ class RemoteAsset extends BaseAsset {
|
|||||||
ownerId.hashCode ^
|
ownerId.hashCode ^
|
||||||
localId.hashCode ^
|
localId.hashCode ^
|
||||||
thumbHash.hashCode ^
|
thumbHash.hashCode ^
|
||||||
visibility.hashCode ^
|
visibility.hashCode;
|
||||||
stackId.hashCode ^
|
|
||||||
stackCount.hashCode;
|
|
||||||
|
|
||||||
RemoteAsset copyWith({
|
|
||||||
String? id,
|
|
||||||
String? localId,
|
|
||||||
String? name,
|
|
||||||
String? ownerId,
|
|
||||||
String? checksum,
|
|
||||||
AssetType? type,
|
|
||||||
DateTime? createdAt,
|
|
||||||
DateTime? updatedAt,
|
|
||||||
int? width,
|
|
||||||
int? height,
|
|
||||||
int? durationInSeconds,
|
|
||||||
bool? isFavorite,
|
|
||||||
String? thumbHash,
|
|
||||||
AssetVisibility? visibility,
|
|
||||||
String? livePhotoVideoId,
|
|
||||||
String? stackId,
|
|
||||||
int? stackCount,
|
|
||||||
}) {
|
|
||||||
return RemoteAsset(
|
|
||||||
id: id ?? this.id,
|
|
||||||
localId: localId ?? this.localId,
|
|
||||||
name: name ?? this.name,
|
|
||||||
ownerId: ownerId ?? this.ownerId,
|
|
||||||
checksum: checksum ?? this.checksum,
|
|
||||||
type: type ?? this.type,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
|
||||||
width: width ?? this.width,
|
|
||||||
height: height ?? this.height,
|
|
||||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
|
||||||
thumbHash: thumbHash ?? this.thumbHash,
|
|
||||||
visibility: visibility ?? this.visibility,
|
|
||||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
|
||||||
stackId: stackId ?? this.stackId,
|
|
||||||
stackCount: stackCount ?? this.stackCount,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ class ExifInfo {
|
|||||||
final int? fileSize;
|
final int? fileSize;
|
||||||
final String? description;
|
final String? description;
|
||||||
final bool isFlipped;
|
final bool isFlipped;
|
||||||
final double? width;
|
|
||||||
final double? height;
|
|
||||||
final String? orientation;
|
final String? orientation;
|
||||||
final String? timeZone;
|
final String? timeZone;
|
||||||
final DateTime? dateTimeOriginal;
|
final DateTime? dateTimeOriginal;
|
||||||
@@ -47,8 +45,6 @@ class ExifInfo {
|
|||||||
this.fileSize,
|
this.fileSize,
|
||||||
this.description,
|
this.description,
|
||||||
this.orientation,
|
this.orientation,
|
||||||
this.width,
|
|
||||||
this.height,
|
|
||||||
this.timeZone,
|
this.timeZone,
|
||||||
this.dateTimeOriginal,
|
this.dateTimeOriginal,
|
||||||
this.isFlipped = false,
|
this.isFlipped = false,
|
||||||
@@ -72,9 +68,6 @@ class ExifInfo {
|
|||||||
|
|
||||||
return other.fileSize == fileSize &&
|
return other.fileSize == fileSize &&
|
||||||
other.description == description &&
|
other.description == description &&
|
||||||
other.isFlipped == isFlipped &&
|
|
||||||
other.width == width &&
|
|
||||||
other.height == height &&
|
|
||||||
other.orientation == orientation &&
|
other.orientation == orientation &&
|
||||||
other.timeZone == timeZone &&
|
other.timeZone == timeZone &&
|
||||||
other.dateTimeOriginal == dateTimeOriginal &&
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
@@ -98,9 +91,6 @@ class ExifInfo {
|
|||||||
return fileSize.hashCode ^
|
return fileSize.hashCode ^
|
||||||
description.hashCode ^
|
description.hashCode ^
|
||||||
orientation.hashCode ^
|
orientation.hashCode ^
|
||||||
isFlipped.hashCode ^
|
|
||||||
width.hashCode ^
|
|
||||||
height.hashCode ^
|
|
||||||
timeZone.hashCode ^
|
timeZone.hashCode ^
|
||||||
dateTimeOriginal.hashCode ^
|
dateTimeOriginal.hashCode ^
|
||||||
latitude.hashCode ^
|
latitude.hashCode ^
|
||||||
@@ -124,9 +114,6 @@ class ExifInfo {
|
|||||||
fileSize: ${fileSize ?? 'NA'},
|
fileSize: ${fileSize ?? 'NA'},
|
||||||
description: ${description ?? 'NA'},
|
description: ${description ?? 'NA'},
|
||||||
orientation: ${orientation ?? 'NA'},
|
orientation: ${orientation ?? 'NA'},
|
||||||
width: ${width ?? 'NA'},
|
|
||||||
height: ${height ?? 'NA'},
|
|
||||||
isFlipped: $isFlipped,
|
|
||||||
timeZone: ${timeZone ?? 'NA'},
|
timeZone: ${timeZone ?? 'NA'},
|
||||||
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
|
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
|
||||||
latitude: ${latitude ?? 'NA'},
|
latitude: ${latitude ?? 'NA'},
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
|
|
||||||
enum MemoryTypeEnum {
|
|
||||||
// do not change this order!
|
|
||||||
onThisDay,
|
|
||||||
}
|
|
||||||
|
|
||||||
class MemoryData {
|
|
||||||
final int year;
|
|
||||||
|
|
||||||
const MemoryData({
|
|
||||||
required this.year,
|
|
||||||
});
|
|
||||||
|
|
||||||
MemoryData copyWith({
|
|
||||||
int? year,
|
|
||||||
}) {
|
|
||||||
return MemoryData(
|
|
||||||
year: year ?? this.year,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return <String, dynamic>{
|
|
||||||
'year': year,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory MemoryData.fromMap(Map<String, dynamic> map) {
|
|
||||||
return MemoryData(
|
|
||||||
year: map['year'] as int,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory MemoryData.fromJson(String source) =>
|
|
||||||
MemoryData.fromMap(json.decode(source) as Map<String, dynamic>);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'MemoryData(year: $year)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(covariant MemoryData other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other.year == year;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => year.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model for a memory stored in the server
|
|
||||||
class DriftMemory {
|
|
||||||
final String id;
|
|
||||||
final DateTime createdAt;
|
|
||||||
final DateTime updatedAt;
|
|
||||||
final DateTime? deletedAt;
|
|
||||||
final String ownerId;
|
|
||||||
|
|
||||||
// enum
|
|
||||||
final MemoryTypeEnum type;
|
|
||||||
final MemoryData data;
|
|
||||||
final bool isSaved;
|
|
||||||
final DateTime memoryAt;
|
|
||||||
final DateTime? seenAt;
|
|
||||||
final DateTime? showAt;
|
|
||||||
final DateTime? hideAt;
|
|
||||||
final List<RemoteAsset> assets;
|
|
||||||
|
|
||||||
const DriftMemory({
|
|
||||||
required this.id,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.updatedAt,
|
|
||||||
this.deletedAt,
|
|
||||||
required this.ownerId,
|
|
||||||
required this.type,
|
|
||||||
required this.data,
|
|
||||||
required this.isSaved,
|
|
||||||
required this.memoryAt,
|
|
||||||
this.seenAt,
|
|
||||||
this.showAt,
|
|
||||||
this.hideAt,
|
|
||||||
required this.assets,
|
|
||||||
});
|
|
||||||
|
|
||||||
DriftMemory copyWith({
|
|
||||||
String? id,
|
|
||||||
DateTime? createdAt,
|
|
||||||
DateTime? updatedAt,
|
|
||||||
DateTime? deletedAt,
|
|
||||||
String? ownerId,
|
|
||||||
MemoryTypeEnum? type,
|
|
||||||
MemoryData? data,
|
|
||||||
bool? isSaved,
|
|
||||||
DateTime? memoryAt,
|
|
||||||
DateTime? seenAt,
|
|
||||||
DateTime? showAt,
|
|
||||||
DateTime? hideAt,
|
|
||||||
List<RemoteAsset>? assets,
|
|
||||||
}) {
|
|
||||||
return DriftMemory(
|
|
||||||
id: id ?? this.id,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
|
||||||
deletedAt: deletedAt ?? this.deletedAt,
|
|
||||||
ownerId: ownerId ?? this.ownerId,
|
|
||||||
type: type ?? this.type,
|
|
||||||
data: data ?? this.data,
|
|
||||||
isSaved: isSaved ?? this.isSaved,
|
|
||||||
memoryAt: memoryAt ?? this.memoryAt,
|
|
||||||
seenAt: seenAt ?? this.seenAt,
|
|
||||||
showAt: showAt ?? this.showAt,
|
|
||||||
hideAt: hideAt ?? this.hideAt,
|
|
||||||
assets: assets ?? this.assets,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return '''Memory {
|
|
||||||
id: $id,
|
|
||||||
createdAt: $createdAt,
|
|
||||||
updatedAt: $updatedAt,
|
|
||||||
deletedAt: ${deletedAt ?? "<NA>"},
|
|
||||||
ownerId: $ownerId,
|
|
||||||
type: $type,
|
|
||||||
data: $data,
|
|
||||||
isSaved: $isSaved,
|
|
||||||
memoryAt: $memoryAt,
|
|
||||||
seenAt: ${seenAt ?? "<NA>"},
|
|
||||||
showAt: ${showAt ?? "<NA>"},
|
|
||||||
hideAt: ${hideAt ?? "<NA>"},
|
|
||||||
assets: $assets
|
|
||||||
}''';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(covariant DriftMemory other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
final listEquals = const DeepCollectionEquality().equals;
|
|
||||||
|
|
||||||
return other.id == id &&
|
|
||||||
other.createdAt == createdAt &&
|
|
||||||
other.updatedAt == updatedAt &&
|
|
||||||
other.deletedAt == deletedAt &&
|
|
||||||
other.ownerId == ownerId &&
|
|
||||||
other.type == type &&
|
|
||||||
other.data == data &&
|
|
||||||
other.isSaved == isSaved &&
|
|
||||||
other.memoryAt == memoryAt &&
|
|
||||||
other.seenAt == seenAt &&
|
|
||||||
other.showAt == showAt &&
|
|
||||||
other.hideAt == hideAt &&
|
|
||||||
listEquals(other.assets, assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
createdAt.hashCode ^
|
|
||||||
updatedAt.hashCode ^
|
|
||||||
deletedAt.hashCode ^
|
|
||||||
ownerId.hashCode ^
|
|
||||||
type.hashCode ^
|
|
||||||
data.hashCode ^
|
|
||||||
isSaved.hashCode ^
|
|
||||||
memoryAt.hashCode ^
|
|
||||||
seenAt.hashCode ^
|
|
||||||
showAt.hashCode ^
|
|
||||||
hideAt.hashCode ^
|
|
||||||
assets.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
// TODO: Remove PersonDto once Isar is removed
|
class Person {
|
||||||
class PersonDto {
|
const Person({
|
||||||
const PersonDto({
|
|
||||||
required this.id,
|
required this.id,
|
||||||
this.birthDate,
|
this.birthDate,
|
||||||
required this.isHidden,
|
required this.isHidden,
|
||||||
@@ -23,7 +22,7 @@ class PersonDto {
|
|||||||
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
|
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
|
||||||
}
|
}
|
||||||
|
|
||||||
PersonDto copyWith({
|
Person copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
DateTime? birthDate,
|
DateTime? birthDate,
|
||||||
bool? isHidden,
|
bool? isHidden,
|
||||||
@@ -31,7 +30,7 @@ class PersonDto {
|
|||||||
String? thumbnailPath,
|
String? thumbnailPath,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
}) {
|
}) {
|
||||||
return PersonDto(
|
return Person(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
birthDate: birthDate ?? this.birthDate,
|
birthDate: birthDate ?? this.birthDate,
|
||||||
isHidden: isHidden ?? this.isHidden,
|
isHidden: isHidden ?? this.isHidden,
|
||||||
@@ -52,8 +51,8 @@ class PersonDto {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory PersonDto.fromMap(Map<String, dynamic> map) {
|
factory Person.fromMap(Map<String, dynamic> map) {
|
||||||
return PersonDto(
|
return Person(
|
||||||
id: map['id'] as String,
|
id: map['id'] as String,
|
||||||
birthDate: map['birthDate'] != null
|
birthDate: map['birthDate'] != null
|
||||||
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
|
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
|
||||||
@@ -69,11 +68,11 @@ class PersonDto {
|
|||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory PersonDto.fromJson(String source) =>
|
factory Person.fromJson(String source) =>
|
||||||
PersonDto.fromMap(json.decode(source) as Map<String, dynamic>);
|
Person.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant PersonDto other) {
|
bool operator ==(covariant Person other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other.id == id &&
|
return other.id == id &&
|
||||||
@@ -94,109 +93,3 @@ class PersonDto {
|
|||||||
updatedAt.hashCode;
|
updatedAt.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model for a person stored in the server
|
|
||||||
class Person {
|
|
||||||
final String id;
|
|
||||||
final DateTime createdAt;
|
|
||||||
final DateTime updatedAt;
|
|
||||||
final String ownerId;
|
|
||||||
final String name;
|
|
||||||
final String? faceAssetId;
|
|
||||||
final String thumbnailPath;
|
|
||||||
final bool isFavorite;
|
|
||||||
final bool isHidden;
|
|
||||||
final String? color;
|
|
||||||
final DateTime? birthDate;
|
|
||||||
|
|
||||||
const Person({
|
|
||||||
required this.id,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.updatedAt,
|
|
||||||
required this.ownerId,
|
|
||||||
required this.name,
|
|
||||||
this.faceAssetId,
|
|
||||||
required this.thumbnailPath,
|
|
||||||
required this.isFavorite,
|
|
||||||
required this.isHidden,
|
|
||||||
required this.color,
|
|
||||||
this.birthDate,
|
|
||||||
});
|
|
||||||
|
|
||||||
Person copyWith({
|
|
||||||
String? id,
|
|
||||||
DateTime? createdAt,
|
|
||||||
DateTime? updatedAt,
|
|
||||||
String? ownerId,
|
|
||||||
String? name,
|
|
||||||
String? faceAssetId,
|
|
||||||
String? thumbnailPath,
|
|
||||||
bool? isFavorite,
|
|
||||||
bool? isHidden,
|
|
||||||
String? color,
|
|
||||||
DateTime? birthDate,
|
|
||||||
}) {
|
|
||||||
return Person(
|
|
||||||
id: id ?? this.id,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
|
||||||
ownerId: ownerId ?? this.ownerId,
|
|
||||||
name: name ?? this.name,
|
|
||||||
faceAssetId: faceAssetId ?? this.faceAssetId,
|
|
||||||
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
|
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
|
||||||
isHidden: isHidden ?? this.isHidden,
|
|
||||||
color: color ?? this.color,
|
|
||||||
birthDate: birthDate ?? this.birthDate,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return '''Person {
|
|
||||||
id: $id,
|
|
||||||
createdAt: $createdAt,
|
|
||||||
updatedAt: $updatedAt,
|
|
||||||
ownerId: $ownerId,
|
|
||||||
name: $name,
|
|
||||||
faceAssetId: ${faceAssetId ?? "<NA>"},
|
|
||||||
thumbnailPath: $thumbnailPath,
|
|
||||||
isFavorite: $isFavorite,
|
|
||||||
isHidden: $isHidden,
|
|
||||||
color: ${color ?? "<NA>"},
|
|
||||||
birthDate: ${birthDate ?? "<NA>"}
|
|
||||||
}''';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(covariant Person other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other.id == id &&
|
|
||||||
other.createdAt == createdAt &&
|
|
||||||
other.updatedAt == updatedAt &&
|
|
||||||
other.ownerId == ownerId &&
|
|
||||||
other.name == name &&
|
|
||||||
other.faceAssetId == faceAssetId &&
|
|
||||||
other.thumbnailPath == thumbnailPath &&
|
|
||||||
other.isFavorite == isFavorite &&
|
|
||||||
other.isHidden == isHidden &&
|
|
||||||
other.color == color &&
|
|
||||||
other.birthDate == birthDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
createdAt.hashCode ^
|
|
||||||
updatedAt.hashCode ^
|
|
||||||
ownerId.hashCode ^
|
|
||||||
name.hashCode ^
|
|
||||||
faceAssetId.hashCode ^
|
|
||||||
thumbnailPath.hashCode ^
|
|
||||||
isFavorite.hashCode ^
|
|
||||||
isHidden.hashCode ^
|
|
||||||
color.hashCode ^
|
|
||||||
birthDate.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user