Compare commits

..

40 Commits

Author SHA1 Message Date
Zack Pollard
0fe8611ece chore: remove output 2025-07-08 15:32:35 +01:00
Zack Pollard
c1338d26a6 chore: remove subtype of reviews from triggers 2025-07-08 15:31:23 +01:00
Zack Pollard
f487f93441 chore: workflow call with no inputs 2025-07-08 15:22:19 +01:00
Zack Pollard
bfb68c3464 chore: workflow call 2025-07-08 15:19:18 +01:00
Zack Pollard
a8e8d27492 chore: required reviewers 2025-07-08 15:03:12 +01:00
shenlong
15be3437bf fix: timeline service uninitialised across routes (#19544) 2025-07-01 10:23:20 -05:00
Daimolean
f59b0bab5a refactor(mobile): action provider (#19669)
* refactor action provider

* fix lint
2025-07-01 10:18:23 -05:00
Alex
fa418d778b feat: lock folder action (#19634)
* feat: lock folder action

* refactor
2025-07-01 14:03:45 +00:00
bo0tzz
e0c4b8df6f chore: remove runner deps install step (#19527)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-01 14:18:14 +01:00
Min Idzelis
7f9689b4bc feat: bin for cli (#19648) 2025-07-01 08:00:41 -04:00
seifer44
e6f8bfdf5e chore(docs): add instruction for trusting self-signed certificates with Immich and an OAuth server (#18624) 2025-07-01 11:21:57 +00:00
Min Idzelis
8ccca04e27 fix(web): improve request cancellation handling in service worker cache (#19217) 2025-07-01 11:53:04 +01:00
Daniel Dietzler
53f80393bf chore: upgrade to cron v4 (#19664) 2025-07-01 12:47:04 +02:00
renovate[bot]
e5e857edc3 chore(deps): update prom/prometheus docker digest to 7a34573 (#19646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 11:33:39 +01:00
renovate[bot]
590f96246d chore(deps): update github-actions (#19654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 11:33:01 +01:00
renovate[bot]
38d73f2bc6 chore(deps): update dependency @types/node to ^22.15.33 (#19653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 11:32:11 +01:00
renovate[bot]
96e3b96d57 fix(deps): update dependency nestjs-otel to v7 (#19662)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 11:02:46 +01:00
renovate[bot]
36b018e355 fix(deps): update typescript-projects (#18898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2025-07-01 10:00:35 +00:00
renovate[bot]
214ca50406 chore(deps): update node.js to v22.17.0 (#19656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 10:59:31 +01:00
renovate[bot]
29b3981609 fix(deps): update dependency nestjs-kysely to v3 (#19660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 10:27:20 +01:00
Mert
a068a41c06 fix(server): prevent duplicate geodata temp table (#18580)
drop tmp table, create gist index first
2025-06-30 23:28:30 -04:00
bo0tzz
3c6e9e1191 feat: use request host as default SSR domain (#19485)
fix: hostname and domain confusion

chore: e2e test
2025-06-30 23:24:44 -04:00
Min Idzelis
db0415bbcc chore: undeclared versions/updates (#19649) 2025-06-30 23:23:41 -04:00
shenlong
a5c431fbf5 refactor: animate bottom sheet (#19655)
* refactor: animate bottom sheet

* rebase on main

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-30 22:23:38 -05:00
Min Idzelis
a3d588f6bd feat: makefile improvements (#19650) 2025-06-30 21:40:42 -05:00
shenlong
21f500191a refactor: actions provider (#19651)
* refactor: actions provider

* chore: rename error and stack

* remove empty checks

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-01 08:10:25 +05:30
shenlong
5011636d95 refactor: header - bulk select icon (#19652)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-30 21:33:37 -05:00
Alex
3f330c6476 feat: drift album page (#19564)
* feat: drift album page

* feat: page renderred

* feat: asset count

* refactor: use statefulwidget

* refactor: private widgets

* refactor: service layer

* refactor: import

* feat: get owner name

* pr feedback

* pr feedback

* pr feedback

* pr feedback
2025-07-01 07:54:50 +05:30
Daimolean
bb8755021d revert: timeout (#19639) 2025-06-30 17:02:50 -05:00
Jason Rasmussen
93f9e118ad refactor: timeline tests (#19641) 2025-06-30 17:43:45 -04:00
Jason Rasmussen
58ca1402ed feat: sync partner stacks (#19635) 2025-06-30 16:41:06 -04:00
Daimolean
32a7087883 feat(mobile): archive action (#19630)
* feat(mobile): archive action

* fix: lint

* Update i18n/en.json

Co-authored-by: Alex <alex.tran1502@gmail.com>

* fix: lint

* fix: lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-30 14:38:15 -05:00
Daimolean
53020852ec fix(web): modal race condition (#19625)
* fix(web): modal race condition

* fix: translation

* fix: translation
2025-06-30 14:33:47 -05:00
Jason Rasmussen
181a7e115f feat: sync stacks (#19629) 2025-06-30 14:26:41 -05:00
Alex
095ace8687 feat: shared link action (#19610) 2025-06-30 17:32:18 +00:00
Alex
4c3fcdc745 feat: favorite action (#19623) 2025-06-30 12:21:09 -05:00
Alex
fa5f30d9ca fix: timeline service mismatch state (#19612) 2025-06-30 12:20:13 -05:00
Jason Rasmussen
e60bc3c304 refactor: database types (#19624) 2025-06-30 13:19:16 -04:00
Jason Rasmussen
09cbc5d3f4 refactor: change password repository lookup (#19584) 2025-06-27 16:52:04 -04:00
Jason Rasmussen
a2a9797fab refactor: auth medium tests (#19583) 2025-06-27 15:35:19 -04:00
240 changed files with 7850 additions and 6776 deletions

View File

@@ -4,6 +4,7 @@
design/
docker/
Dockerfile
!docker/scripts
docs/
!docs/package.json
@@ -19,6 +20,7 @@ mobile/
cli/coverage/
cli/dist/
cli/node_modules/
cli/Dockerfile
open-api/typescript-sdk/build/
open-api/typescript-sdk/node_modules/
@@ -29,9 +31,11 @@ server/upload/
server/src/queries
server/dist/
server/www/
server/Dockerfile
web/node_modules/
web/coverage/
web/.svelte-kit
web/build/
web/.env
web/Dockerfile

2
.github/.nvmrc vendored
View File

@@ -1 +1 @@
22.16.0
22.17.0

6
.github/package-lock.json generated vendored
View File

@@ -9,9 +9,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
"dev": true,
"license": "MIT",
"bin": {

View File

@@ -66,12 +66,6 @@ jobs:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- name: Install missing deps
run: |
sudo add-apt-repository ppa:rmescandon/yq
sudo apt-get update
sudo apt-get install -y yq xz-utils ninja-build zstd
- name: Create the Keystore
env:
KEY_JKS: ${{ secrets.KEY_JKS }}
@@ -96,7 +90,7 @@ jobs:
key: build-mobile-gradle-${{ runner.os }}-main
- name: Setup Flutter SDK
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml

View File

@@ -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}`);
}

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Require PR to have a changelog label
uses: mheap/github-action-required-labels@fb29a14a076b0f74099f6198f77750e8fc236016 # v5.5.0
uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1
with:
mode: exactly
count: 1

View File

@@ -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

View File

@@ -49,7 +49,7 @@ jobs:
persist-credentials: false
- name: Setup Flutter SDK
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml

View File

@@ -516,7 +516,7 @@ jobs:
persist-credentials: false
- name: Setup Flutter SDK
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml

View File

@@ -1,27 +1,33 @@
dev:
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-down:
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
dev-update:
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
dev-scale:
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
.PHONY: e2e
e2e:
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
e2e-update:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
e2e-down:
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
prod:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
prod-down:
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
prod-scale:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
.PHONY: open-api
open-api:
@@ -93,9 +99,11 @@ hygiene-all: lint-all format-all check-all sql audit-all;
test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ;
clean:
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name "node_modules" -type d -prune -exec rm -rf {} +
find . -name "dist" -type d -prune -exec rm -rf '{}' +
find . -name "build" -type d -prune -exec rm -rf '{}' +
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
docker compose -f ./e2e/docker-compose.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
setup-dev: install-server install-sdk build-sdk install-web

View File

@@ -1 +1 @@
22.16.0
22.17.0

2
cli/bin/immich Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
import '../dist/index.js';

472
cli/package-lock.json generated
View File

@@ -27,7 +27,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.15.32",
"@types/node": "^22.15.33",
"@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
@@ -61,7 +61,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.15.32",
"@types/node": "^22.15.33",
"typescript": "^5.3.3"
}
},
@@ -607,9 +607,9 @@
}
},
"node_modules/@eslint/config-array": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
"version": "0.20.1",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz",
"integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -682,9 +682,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.27.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
"integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1276,6 +1276,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/chai": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
"integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/deep-eql": "*"
}
},
"node_modules/@types/cli-progress": {
"version": "3.11.6",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz",
@@ -1286,6 +1296,13 @@
"@types/node": "*"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -1338,9 +1355,9 @@
}
},
"node_modules/@types/node": {
"version": "22.15.32",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz",
"integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
"version": "22.15.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz",
"integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1348,17 +1365,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
"integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/type-utils": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/type-utils": "8.35.0",
"@typescript-eslint/utils": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -1372,7 +1389,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"@typescript-eslint/parser": "^8.35.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@@ -1388,16 +1405,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1412,15 +1429,37 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
"node_modules/@typescript-eslint/project-service": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
"integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1"
"@typescript-eslint/tsconfig-utils": "^8.35.0",
"@typescript-eslint/types": "^8.35.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
"integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1430,15 +1469,32 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
"integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/typescript-estree": "8.35.0",
"@typescript-eslint/utils": "8.35.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -1455,9 +1511,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1469,14 +1525,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
"integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/project-service": "8.35.0",
"@typescript-eslint/tsconfig-utils": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1496,9 +1554,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1522,16 +1580,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1"
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1546,14 +1604,14 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"eslint-visitor-keys": "^4.2.0"
"@typescript-eslint/types": "8.35.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1564,15 +1622,16 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz",
"integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
"integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@bcoe/v8-coverage": "^1.0.2",
"debug": "^4.4.0",
"ast-v8-to-istanbul": "^0.3.3",
"debug": "^4.4.1",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-lib-source-maps": "^5.0.6",
@@ -1587,8 +1646,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "3.1.4",
"vitest": "3.1.4"
"@vitest/browser": "3.2.4",
"vitest": "3.2.4"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -1597,14 +1656,15 @@
}
},
"node_modules/@vitest/expect": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz",
"integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.1.4",
"@vitest/utils": "3.1.4",
"@types/chai": "^5.2.2",
"@vitest/spy": "3.2.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
@@ -1613,13 +1673,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz",
"integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.1.4",
"@vitest/spy": "3.2.4",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@@ -1628,7 +1688,7 @@
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^5.0.0 || ^6.0.0"
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
"msw": {
@@ -1640,9 +1700,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz",
"integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1653,27 +1713,28 @@
}
},
"node_modules/@vitest/runner": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz",
"integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.1.4",
"pathe": "^2.0.3"
"@vitest/utils": "3.2.4",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz",
"integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.1.4",
"@vitest/pretty-format": "3.2.4",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
@@ -1682,27 +1743,27 @@
}
},
"node_modules/@vitest/spy": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz",
"integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyspy": "^3.0.2"
"tinyspy": "^4.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz",
"integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.1.4",
"loupe": "^3.1.3",
"@vitest/pretty-format": "3.2.4",
"loupe": "^3.1.4",
"tinyrainbow": "^2.0.0"
},
"funding": {
@@ -1710,9 +1771,9 @@
}
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -1792,6 +1853,18 @@
"node": ">=12"
}
},
"node_modules/ast-v8-to-istanbul": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz",
"integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"estree-walker": "^3.0.3",
"js-tokens": "^9.0.1"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2105,9 +2178,9 @@
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2232,19 +2305,19 @@
}
},
"node_modules/eslint": {
"version": "9.27.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
"integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.20.0",
"@eslint/config-array": "^0.20.1",
"@eslint/config-helpers": "^0.2.1",
"@eslint/core": "^0.14.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.27.0",
"@eslint/js": "9.29.0",
"@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -2256,9 +2329,9 @@
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.3.0",
"eslint-visitor-keys": "^4.2.0",
"espree": "^10.3.0",
"eslint-scope": "^8.4.0",
"eslint-visitor-keys": "^4.2.1",
"espree": "^10.4.0",
"esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -2309,14 +2382,14 @@
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz",
"integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==",
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz",
"integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
"synckit": "^0.11.0"
"synckit": "^0.11.7"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -2402,9 +2475,9 @@
}
},
"node_modules/eslint-scope": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -2419,9 +2492,9 @@
}
},
"node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -2432,15 +2505,15 @@
}
},
"node_modules/espree": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.0"
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2749,9 +2822,9 @@
}
},
"node_modules/globals": {
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2975,6 +3048,13 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -3076,9 +3156,9 @@
"license": "MIT"
},
"node_modules/loupe": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz",
"integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==",
"dev": true,
"license": "MIT"
},
@@ -3347,9 +3427,9 @@
"license": "MIT"
},
"node_modules/pathval": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3425,9 +3505,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3799,6 +3879,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-literal": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^9.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3813,14 +3906,13 @@
}
},
"node_modules/synckit": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz",
"integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==",
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
"integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.3",
"tslib": "^2.8.1"
"@pkgr/core": "^0.2.4"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -3885,9 +3977,9 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3930,9 +4022,9 @@
}
},
"node_modules/tinypool": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3950,9 +4042,9 @@
}
},
"node_modules/tinyspy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
"integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4005,13 +4097,6 @@
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -4040,15 +4125,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz",
"integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/utils": "8.32.1"
"@typescript-eslint/eslint-plugin": "8.35.0",
"@typescript-eslint/parser": "8.35.0",
"@typescript-eslint/utils": "8.35.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4186,17 +4271,17 @@
}
},
"node_modules/vite-node": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz",
"integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.4.0",
"debug": "^4.4.1",
"es-module-lexer": "^1.7.0",
"pathe": "^2.0.3",
"vite": "^5.0.0 || ^6.0.0"
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"bin": {
"vite-node": "vite-node.mjs"
@@ -4257,32 +4342,34 @@
}
},
"node_modules/vitest": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz",
"integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "3.1.4",
"@vitest/mocker": "3.1.4",
"@vitest/pretty-format": "^3.1.4",
"@vitest/runner": "3.1.4",
"@vitest/snapshot": "3.1.4",
"@vitest/spy": "3.1.4",
"@vitest/utils": "3.1.4",
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
"@vitest/mocker": "3.2.4",
"@vitest/pretty-format": "^3.2.4",
"@vitest/runner": "3.2.4",
"@vitest/snapshot": "3.2.4",
"@vitest/spy": "3.2.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0",
"debug": "^4.4.0",
"debug": "^4.4.1",
"expect-type": "^1.2.1",
"magic-string": "^0.30.17",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"std-env": "^3.9.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.13",
"tinypool": "^1.0.2",
"tinyglobby": "^0.2.14",
"tinypool": "^1.1.1",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.1.4",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
"vite-node": "3.2.4",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -4298,8 +4385,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.1.4",
"@vitest/ui": "3.1.4",
"@vitest/browser": "3.2.4",
"@vitest/ui": "3.2.4",
"happy-dom": "*",
"jsdom": "*"
},
@@ -4340,6 +4427,19 @@
"vitest": ">=2.0.0"
}
},
"node_modules/vitest/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -5,7 +5,7 @@
"type": "module",
"exports": "./dist/index.js",
"bin": {
"immich": "dist/index.js"
"immich": "./bin/immich"
},
"license": "GNU Affero General Public License version 3",
"keywords": [
@@ -21,7 +21,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.15.32",
"@types/node": "^22.15.33",
"@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
@@ -69,6 +69,6 @@
"micromatch": "^4.0.8"
},
"volta": {
"node": "22.16.0"
"node": "22.17.0"
}
}

View File

@@ -83,7 +83,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:9abc6cf6aea7710d163dbb28d8eeb7dc5baef01e38fa4cd146a406dd9f07f70d
image: prom/prometheus@sha256:7a34573f0b9c952286b33d537f233cd5b708e12263733aa646e50c33f598f16c
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus

View File

@@ -1 +1 @@
22.16.0
22.17.0

View File

@@ -150,12 +150,10 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele
- Preview images (small thumbnails and large previews) for each asset and thumbnails for recognized faces.
- Stored in `UPLOAD_LOCATION/thumbs/<userID>`.
- **Encoded Assets:**
- Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
- Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
- **Postgres**
- The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `DB_DATA_LOCATION`.
@@ -201,7 +199,6 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
- Temporarily located in `UPLOAD_LOCATION/upload/<userID>`.
- Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload.
- **Postgres**
- The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `DB_DATA_LOCATION`.

View File

@@ -20,7 +20,6 @@ Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an i
Before enabling OAuth in Immich, a new client application needs to be configured in the 3rd-party authentication server. While the specifics of this setup vary from provider to provider, the general approach should be the same.
1. Create a new (Client) Application
1. The **Provider** type should be `OpenID Connect` or `OAuth2`
2. The **Client type** should be `Confidential`
3. The **Application** type should be `Web`
@@ -29,7 +28,6 @@ Before enabling OAuth in Immich, a new client application needs to be configured
2. Configure Redirect URIs/Origins
The **Sign-in redirect URIs** should include:
- `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx)
- `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client
- `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client
@@ -37,21 +35,17 @@ Before enabling OAuth in Immich, a new client application needs to be configured
Redirect URIs should contain all the domains you will be using to access Immich. Some examples include:
Mobile
- `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly)
Localhost
- `http://localhost:2283/auth/login`
- `http://localhost:2283/user-settings`
Local IP
- `http://192.168.0.200:2283/auth/login`
- `http://192.168.0.200:2283/user-settings`
Hostname
- `https://immich.example.com/auth/login`
- `https://immich.example.com/user-settings`

View File

@@ -199,13 +199,11 @@ To use your SSH key for commit signing, see the [GitHub guide on SSH commit sign
When the Dev Container starts, it automatically:
1. **Runs post-create script** (`container-server-post-create.sh`):
- Adjusts file permissions for the `node` user
- Installs dependencies: `npm install` in all packages
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
2. **Starts development servers** via VS Code tasks:
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
- `Immich Web Server (Vite)` - Web frontend with hot-reloading on port 3000
- Both servers watch for file changes and recompile automatically
@@ -335,14 +333,12 @@ make install-all # Install all dependencies
The Dev Container is pre-configured for debugging:
1. **API Server Debugging**:
- Set breakpoints in VS Code
- Press `F5` or use "Run and Debug" panel
- Select "Attach to Server" configuration
- Debug port: 9231
2. **Worker Debugging**:
- Use "Attach to Workers" configuration
- Debug port: 9230
@@ -428,7 +424,6 @@ While the Dev Container focuses on server and web development, you can connect m
```
2. **Configure mobile app**:
- Server URL: `http://YOUR_IP:2283/api`
- Ensure firewall allows port 2283

View File

@@ -75,7 +75,6 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata/postgresql/data`). This uses the `appdata` share. Do also create the `postgresql` folder, by running `mkdir /mnt/user/{share_location}/postgresql/data`. If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.

1533
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,9 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "~3.7.0",
"@docusaurus/preset-classic": "~3.7.0",
"@docusaurus/core": "~3.8.0",
"@docusaurus/preset-classic": "~3.8.0",
"@docusaurus/theme-common": "~3.8.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@@ -26,6 +27,7 @@
"clsx": "^2.0.0",
"docusaurus-lunr-search": "^3.3.2",
"docusaurus-preset-openapi": "^0.7.5",
"lunr": "^2.3.9",
"postcss": "^8.4.25",
"prism-react-renderer": "^2.3.1",
"raw-loader": "^4.0.2",
@@ -35,7 +37,7 @@
"url": "^0.11.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.7.0",
"@docusaurus/module-type-aliases": "~3.8.0",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.7.0",
"prettier": "^3.2.4",
@@ -57,6 +59,6 @@
"node": ">=20"
},
"volta": {
"node": "22.16.0"
"node": "22.17.0"
}
}

View File

@@ -58,6 +58,12 @@ const guides: CommunityGuidesProps[] = [
description: 'Access Immich with an end-to-end encrypted connection.',
url: 'https://meshnet.nordvpn.com/how-to/remote-files-media-access/immich-remote-access',
},
{
title: 'Trust Self Signed Certificates with Immich - OAuth Setup',
description:
'Set up Certificate Authority trust with Immich, and your private OAuth2/OpenID service, while using a private CA for HTTPS commication.',
url: 'https://github.com/immich-app/immich/discussions/18614',
},
];
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {

View File

@@ -1 +1 @@
22.16.0
22.17.0

925
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^22.15.32",
"@types/node": "^22.15.33",
"@types/oidc-provider": "^9.0.0",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
@@ -44,7 +44,7 @@
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0",
"sharp": "^0.33.5",
"sharp": "^0.34.0",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^5.3.3",
@@ -53,6 +53,6 @@
"vitest": "^3.0.0"
},
"volta": {
"node": "22.16.0"
"node": "22.17.0"
}
}

View File

@@ -1,146 +0,0 @@
import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto } from 'src/fixtures';
import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeEach, describe, expect, it } from 'vitest';
const { email, password } = signupDto.admin;
describe(`/auth/admin-sign-up`, () => {
beforeEach(async () => {
await utils.resetDatabase();
});
describe('POST /auth/admin-sign-up', () => {
it(`should sign up the admin`, async () => {
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
expect(status).toBe(201);
expect(body).toEqual(signupResponseDto.admin);
});
it('should not allow a second admin to sign up', async () => {
await signUpAdmin({ signUpDto: signupDto.admin });
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
expect(status).toBe(400);
expect(body).toEqual(errorDto.alreadyHasAdmin);
});
});
});
describe('/auth/*', () => {
let admin: LoginResponseDto;
beforeEach(async () => {
await utils.resetDatabase();
await signUpAdmin({ signUpDto: signupDto.admin });
admin = await login({ loginCredentialDto: loginDto.admin });
});
describe(`POST /auth/login`, () => {
it('should reject an incorrect password', async () => {
const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.incorrectLogin);
});
it('should accept a correct password', async () => {
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
expect(status).toBe(201);
expect(body).toEqual(loginResponseDto.admin);
const token = body.accessToken;
expect(token).toBeDefined();
const cookies = headers['set-cookie'];
expect(cookies).toHaveLength(3);
expect(cookies[0].split(';').map((item) => item.trim())).toEqual([
`immich_access_token=${token}`,
'Max-Age=34560000',
'Path=/',
expect.stringContaining('Expires='),
'HttpOnly',
'SameSite=Lax',
]);
expect(cookies[1].split(';').map((item) => item.trim())).toEqual([
'immich_auth_type=password',
'Max-Age=34560000',
'Path=/',
expect.stringContaining('Expires='),
'HttpOnly',
'SameSite=Lax',
]);
expect(cookies[2].split(';').map((item) => item.trim())).toEqual([
'immich_is_authenticated=true',
'Max-Age=34560000',
'Path=/',
expect.stringContaining('Expires='),
'SameSite=Lax',
]);
});
});
describe('POST /auth/validateToken', () => {
it('should reject an invalid token', async () => {
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidToken);
});
it('should accept a valid token', async () => {
const { status, body } = await request(app)
.post(`/auth/validateToken`)
.send({})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ authStatus: true });
});
});
describe('POST /auth/change-password', () => {
it('should require the current password', async () => {
const { status, body } = await request(app)
.post(`/auth/change-password`)
.send({ password: 'wrong-password', newPassword: 'Password1234' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.wrongPassword);
});
it('should change the password', async () => {
const { status } = await request(app)
.post(`/auth/change-password`)
.send({ password, newPassword: 'Password1234' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
await login({
loginCredentialDto: {
email: 'admin@immich.cloud',
password: 'Password1234',
},
});
});
});
describe('POST /auth/logout', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/auth/logout`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should logout the user', async () => {
const { status, body } = await request(app)
.post(`/auth/logout`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
successful: true,
redirectUri: '/auth/login?autoLaunch=0',
});
});
});
});

View File

@@ -117,6 +117,13 @@ describe('/shared-links', () => {
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(`<meta property="og:image" content="http://127.0.0.1:2285`);
});
it('should fall back to my.immich.app og:image meta tag for shared asset if Host header is not present', async () => {
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`).set('Host', '');
expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(`<meta property="og:image" content="https://my.immich.app`);
});

View File

@@ -1,230 +0,0 @@
import {
AssetMediaResponseDto,
AssetVisibility,
LoginResponseDto,
SharedLinkType,
TimeBucketAssetResponseDto,
} from '@immich/sdk';
import { DateTime } from 'luxon';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
// TODO this should probably be a test util function
const today = DateTime.fromObject({
year: 2023,
month: 11,
day: 3,
}) as DateTime<true>;
const yesterday = today.minus({ days: 1 });
describe('/timeline', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
let timeBucketUser: LoginResponseDto;
let user1Assets: AssetMediaResponseDto[];
let user2Assets: AssetMediaResponseDto[];
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[user, timeBucketUser] = await Promise.all([
utils.userSetup(admin.accessToken, createUserDto.create('1')),
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
]);
user1Assets = await Promise.all([
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken, {
isFavorite: true,
fileCreatedAt: yesterday.toISO(),
fileModifiedAt: yesterday.toISO(),
assetData: { filename: 'example.mp4' },
}),
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken),
]);
user2Assets = await Promise.all([
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-12').toISOString() }),
]);
await utils.deleteAssets(timeBucketUser.accessToken, [user2Assets[4].id]);
});
describe('GET /timeline/buckets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/timeline/buckets');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get time buckets by month', async () => {
const { status, body } = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.arrayContaining([
{ count: 3, timeBucket: '1970-02-01' },
{ count: 1, timeBucket: '1970-01-01' },
]),
);
});
it('should not allow access for unrelated shared links', async () => {
const sharedLink = await utils.createSharedLink(user.accessToken, {
type: SharedLinkType.Individual,
assetIds: user1Assets.map(({ id }) => id),
});
const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should return error if time bucket is requested with partners asset and archived', async () => {
const req1 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ withPartners: true, visibility: AssetVisibility.Archive });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest());
const req2 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`)
.query({ withPartners: true, visibility: undefined });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest());
});
it('should return error if time bucket is requested with partners asset and favorite', async () => {
const req1 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ withPartners: true, isFavorite: true });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest());
const req2 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ withPartners: true, isFavorite: false });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest());
});
it('should return error if time bucket is requested with partners asset and trash', async () => {
const req = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`)
.query({ withPartners: true, isTrashed: true });
expect(req.status).toBe(400);
expect(req.body).toEqual(errorDto.badRequest());
});
});
describe('GET /timeline/bucket', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/timeline/bucket').query({
timeBucket: '1900-01-01',
});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should handle 5 digit years', async () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.query({ timeBucket: '012345-01-01' })
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
city: [],
country: [],
duration: [],
id: [],
visibility: [],
isFavorite: [],
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
fileCreatedAt: [],
localOffsetHours: [],
ownerId: [],
projectionType: [],
ratio: [],
status: [],
thumbhash: [],
});
});
// TODO enable date string validation while still accepting 5 digit years
// it('should fail if time bucket is invalid', async () => {
// const { status, body } = await request(app)
// .get('/timeline/bucket')
// .set('Authorization', `Bearer ${user.accessToken}`)
// .query({ timeBucket: 'foo' });
// expect(status).toBe(400);
// expect(body).toEqual(errorDto.badRequest);
// });
it('should return time bucket', async () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ timeBucket: '1970-02-10' });
expect(status).toBe(200);
expect(body).toEqual({
city: [],
country: [],
duration: [],
id: [],
visibility: [],
isFavorite: [],
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
fileCreatedAt: [],
localOffsetHours: [],
ownerId: [],
projectionType: [],
ratio: [],
status: [],
thumbhash: [],
});
});
it('should return time bucket in trash', async () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ timeBucket: '1970-02-01T00:00:00.000Z', isTrashed: true });
expect(status).toBe(200);
const timeBucket: TimeBucketAssetResponseDto = body;
expect(timeBucket.isTrashed).toEqual([true]);
});
});
});

View File

@@ -427,6 +427,7 @@
"app_settings": "App Settings",
"appears_in": "Appears in",
"archive": "Archive",
"archive_action_prompt": "{count} added to Archive",
"archive_or_unarchive_photo": "Archive or unarchive photo",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({count})",
@@ -702,7 +703,7 @@
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"dark": "Dark",
"darkTheme": "Toggle dark theme",
"dark_theme": "Toggle dark theme",
"date_after": "Date after",
"date_and_time": "Date and Time",
"date_before": "Date before",
@@ -983,6 +984,7 @@
"failed_to_load_assets": "Failed to load assets",
"failed_to_load_folder": "Failed to load folder",
"favorite": "Favorite",
"favorite_action_prompt": "{count} added to Favorites",
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
"favorites": "Favorites",
"favorites_page_no_favorites": "No favorite assets found",
@@ -1245,6 +1247,7 @@
"more": "More",
"move": "Move",
"move_off_locked_folder": "Move out of locked folder",
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
"move_to_locked_folder": "Move to locked folder",
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
"moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
@@ -1495,6 +1498,7 @@
"remove_deleted_assets": "Remove Deleted Assets",
"remove_from_album": "Remove from album",
"remove_from_favorites": "Remove from favorites",
"remove_from_lock_folder_action_prompt": "{count} removed from the locked folder",
"remove_from_locked_folder": "Remove from locked folder",
"remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of the locked folder? They will be visible in your library.",
"remove_from_shared_link": "Remove from shared link",

File diff suppressed because one or more lines are too long

View File

@@ -12,3 +12,5 @@ enum TextSearchType {
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
enum SortUserBy { id }
enum ActionSource { timeline, viewer }

View File

@@ -21,6 +21,8 @@ class Album {
final String? thumbnailAssetId;
final bool isActivityEnabled;
final AlbumAssetOrder order;
final int assetCount;
final String ownerName;
const Album({
required this.id,
@@ -32,20 +34,24 @@ class Album {
this.thumbnailAssetId,
required this.isActivityEnabled,
required this.order,
required this.assetCount,
required this.ownerName,
});
@override
String toString() {
return '''Album {
id: $id,
name: $name,
ownerId: $ownerId,
description: $description,
createdAt: $createdAt,
updatedAt: $updatedAt,
isActivityEnabled: $isActivityEnabled,
order: $order,
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
id: $id,
name: $name,
ownerId: $ownerId,
description: $description,
createdAt: $createdAt,
updatedAt: $updatedAt,
isActivityEnabled: $isActivityEnabled,
order: $order,
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
assetCount: $assetCount
ownerName: $ownerName
}''';
}
@@ -61,7 +67,9 @@ class Album {
updatedAt == other.updatedAt &&
thumbnailAssetId == other.thumbnailAssetId &&
isActivityEnabled == other.isActivityEnabled &&
order == other.order;
order == other.order &&
assetCount == other.assetCount &&
ownerName == other.ownerName;
}
@override
@@ -74,6 +82,8 @@ class Album {
updatedAt.hashCode ^
thumbnailAssetId.hashCode ^
isActivityEnabled.hashCode ^
order.hashCode;
order.hashCode ^
assetCount.hashCode ^
ownerName.hashCode;
}
}

View File

@@ -1,4 +1,4 @@
part 'asset.model.dart';
part 'remote_asset.model.dart';
part 'local_asset.model.dart';
enum AssetType {

View File

@@ -8,16 +8,18 @@ enum AssetVisibility {
}
// Model for an asset stored in the server
class Asset extends BaseAsset {
class RemoteAsset extends BaseAsset {
final String id;
final String? localId;
final String? thumbHash;
final AssetVisibility visibility;
final String ownerId;
const Asset({
const RemoteAsset({
required this.id,
this.localId,
required super.name,
required this.ownerId,
required super.checksum,
required super.type,
required super.createdAt,
@@ -37,16 +39,17 @@ class Asset extends BaseAsset {
@override
String toString() {
return '''Asset {
id: $id,
name: $name,
type: $type,
createdAt: $createdAt,
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
localId: ${localId ?? "<NA>"},
isFavorite: $isFavorite,
id: $id,
name: $name,
ownerId: $ownerId,
type: $type,
createdAt: $createdAt,
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
localId: ${localId ?? "<NA>"},
isFavorite: $isFavorite,
thumbHash: ${thumbHash ?? "<NA>"},
visibility: $visibility,
}''';
@@ -54,10 +57,11 @@ class Asset extends BaseAsset {
@override
bool operator ==(Object other) {
if (other is! Asset) return false;
if (other is! RemoteAsset) return false;
if (identical(this, other)) return true;
return super == other &&
id == other.id &&
ownerId == other.ownerId &&
localId == other.localId &&
thumbHash == other.thumbHash &&
visibility == other.visibility;
@@ -67,6 +71,7 @@ class Asset extends BaseAsset {
int get hashCode =>
super.hashCode ^
id.hashCode ^
ownerId.hashCode ^
localId.hashCode ^
thumbHash.hashCode ^
visibility.hashCode;

View File

@@ -5,7 +5,7 @@ import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/drift_store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:logging/logging.dart';
/// Service responsible for handling application logging.
@@ -14,8 +14,8 @@ import 'package:logging/logging.dart';
/// writes them to a persistent [ILogRepository], and manages log levels
/// via [IStoreRepository]
class LogService {
final LogRepository _logRepository;
final IStoreRepository _storeRepository;
final IsarLogRepository _logRepository;
final IsarStoreRepository _storeRepository;
final List<LogMessage> _msgBuffer = [];
@@ -37,8 +37,8 @@ class LogService {
}
static Future<LogService> init({
required LogRepository logRepository,
required IStoreRepository storeRepository,
required IsarLogRepository logRepository,
required IsarStoreRepository storeRepository,
bool shouldBuffer = true,
}) async {
_instance ??= await create(
@@ -50,8 +50,8 @@ class LogService {
}
static Future<LogService> create({
required LogRepository logRepository,
required IStoreRepository storeRepository,
required IsarLogRepository logRepository,
required IsarStoreRepository storeRepository,
bool shouldBuffer = true,
}) async {
final instance = LogService._(logRepository, storeRepository, shouldBuffer);

View File

@@ -0,0 +1,60 @@
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/utils/remote_album.utils.dart';
class RemoteAlbumService {
final DriftRemoteAlbumRepository _repository;
const RemoteAlbumService(this._repository);
Future<List<Album>> getAll() {
return _repository.getAll();
}
List<Album> sortAlbums(
List<Album> albums,
RemoteAlbumSortMode sortMode, {
bool isReverse = false,
}) {
return sortMode.sortFn(albums, isReverse);
}
List<Album> searchAlbums(
List<Album> albums,
String query,
String? userId, [
QuickFilterMode filterMode = QuickFilterMode.all,
]) {
final lowerQuery = query.toLowerCase();
List<Album> filtered = albums;
// Apply text search filter
if (query.isNotEmpty) {
filtered = filtered
.where(
(album) =>
album.name.toLowerCase().contains(lowerQuery) ||
album.description.toLowerCase().contains(lowerQuery),
)
.toList();
}
if (userId != null) {
switch (filterMode) {
case QuickFilterMode.myAlbums:
filtered =
filtered.where((album) => album.ownerId == userId).toList();
break;
case QuickFilterMode.sharedWithMe:
filtered =
filtered.where((album) => album.ownerId != userId).toList();
break;
case QuickFilterMode.all:
break;
}
}
return filtered;
}
}

View File

@@ -1,18 +1,18 @@
import 'dart:async';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/repositories/drift_store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
/// Provides access to a persistent key-value store with an in-memory cache.
/// Listens for repository changes to keep the cache updated.
class StoreService {
final IStoreRepository _storeRepository;
final IsarStoreRepository _storeRepository;
/// In-memory cache. Keys are [StoreKey.id]
final Map<int, Object?> _cache = {};
late final StreamSubscription<StoreDto> _storeUpdateSubscription;
StoreService._({required IStoreRepository storeRepository})
StoreService._({required IsarStoreRepository storeRepository})
: _storeRepository = storeRepository;
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
@@ -26,14 +26,14 @@ class StoreService {
// TODO: Replace the implementation with the one from create after removing the typedef
static Future<StoreService> init({
required IStoreRepository storeRepository,
required IsarStoreRepository storeRepository,
}) async {
_instance ??= await create(storeRepository: storeRepository);
return _instance!;
}
static Future<StoreService> create({
required IStoreRepository storeRepository,
required IsarStoreRepository storeRepository,
}) async {
final instance = StoreService._(storeRepository: storeRepository);
await instance._populateCache();

View File

@@ -64,7 +64,7 @@ class TimelineService {
}) : _assetSource = assetSource,
_bucketSource = bucketSource {
_bucketSubscription =
_bucketSource().listen((_) => unawaited(_reloadBucket()));
_bucketSource().listen((_) => unawaited(reloadBucket()));
}
final AsyncMutex _mutex = AsyncMutex();
@@ -74,7 +74,7 @@ class TimelineService {
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
Future<void> _reloadBucket() => _mutex.run(() async {
Future<void> reloadBucket() => _mutex.run(() async {
_buffer = await _assetSource(_bufferOffset, _buffer.length);
});

View File

@@ -1,13 +0,0 @@
import 'package:isar/isar.dart';
part 'isar_store.entity.g.dart';
/// Internal class for `Store`, do not use elsewhere.
@Collection(inheritance: false)
class StoreValue {
final Id id;
final int? intValue;
final String? strValue;
const StoreValue(this.id, {this.intValue, this.strValue});
}

View File

@@ -1,7 +1,5 @@
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:isar/isar.dart';
import 'package:drift/drift.dart';
part 'log.entity.g.dart';
@@ -47,21 +45,3 @@ class LoggerMessage {
);
}
}
class LoggerMessageEntity extends Table with DriftDefaultsMixin {
const LoggerMessageEntity();
IntColumn get id => integer().autoIncrement()();
TextColumn get message => text()();
TextColumn get details => text().nullable()();
IntColumn get level => intEnum<LogLevel>()();
DateTimeColumn get createdAt => dateTime()();
TextColumn get context1 => text().nullable()();
TextColumn get context2 => text().nullable()();
}

View File

@@ -1,589 +0,0 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart'
as i1;
import 'package:immich_mobile/domain/models/log.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/log.entity.dart' as i3;
typedef $$LoggerMessageEntityTableCreateCompanionBuilder
= i1.LoggerMessageEntityCompanion Function({
required int id,
required String message,
i0.Value<String?> details,
required i2.LogLevel level,
required DateTime createdAt,
i0.Value<String?> context1,
i0.Value<String?> context2,
});
typedef $$LoggerMessageEntityTableUpdateCompanionBuilder
= i1.LoggerMessageEntityCompanion Function({
i0.Value<int> id,
i0.Value<String> message,
i0.Value<String?> details,
i0.Value<i2.LogLevel> level,
i0.Value<DateTime> createdAt,
i0.Value<String?> context1,
i0.Value<String?> context2,
});
class $$LoggerMessageEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LoggerMessageEntityTable> {
$$LoggerMessageEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<int> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get message => $composableBuilder(
column: $table.message, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get details => $composableBuilder(
column: $table.details, builder: (column) => i0.ColumnFilters(column));
i0.ColumnWithTypeConverterFilters<i2.LogLevel, i2.LogLevel, int> get level =>
$composableBuilder(
column: $table.level,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get context1 => $composableBuilder(
column: $table.context1, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get context2 => $composableBuilder(
column: $table.context2, builder: (column) => i0.ColumnFilters(column));
}
class $$LoggerMessageEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LoggerMessageEntityTable> {
$$LoggerMessageEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<int> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get message => $composableBuilder(
column: $table.message, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get details => $composableBuilder(
column: $table.details, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get level => $composableBuilder(
column: $table.level, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get context1 => $composableBuilder(
column: $table.context1, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get context2 => $composableBuilder(
column: $table.context2, builder: (column) => i0.ColumnOrderings(column));
}
class $$LoggerMessageEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LoggerMessageEntityTable> {
$$LoggerMessageEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get message =>
$composableBuilder(column: $table.message, builder: (column) => column);
i0.GeneratedColumn<String> get details =>
$composableBuilder(column: $table.details, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.LogLevel, int> get level =>
$composableBuilder(column: $table.level, builder: (column) => column);
i0.GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
i0.GeneratedColumn<String> get context1 =>
$composableBuilder(column: $table.context1, builder: (column) => column);
i0.GeneratedColumn<String> get context2 =>
$composableBuilder(column: $table.context2, builder: (column) => column);
}
class $$LoggerMessageEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$LoggerMessageEntityTable,
i1.LoggerMessageEntityData,
i1.$$LoggerMessageEntityTableFilterComposer,
i1.$$LoggerMessageEntityTableOrderingComposer,
i1.$$LoggerMessageEntityTableAnnotationComposer,
$$LoggerMessageEntityTableCreateCompanionBuilder,
$$LoggerMessageEntityTableUpdateCompanionBuilder,
(
i1.LoggerMessageEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LoggerMessageEntityTable,
i1.LoggerMessageEntityData>
),
i1.LoggerMessageEntityData,
i0.PrefetchHooks Function()> {
$$LoggerMessageEntityTableTableManager(
i0.GeneratedDatabase db, i1.$LoggerMessageEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () => i1
.$$LoggerMessageEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$LoggerMessageEntityTableOrderingComposer(
$db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$LoggerMessageEntityTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<int> id = const i0.Value.absent(),
i0.Value<String> message = const i0.Value.absent(),
i0.Value<String?> details = const i0.Value.absent(),
i0.Value<i2.LogLevel> level = const i0.Value.absent(),
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<String?> context1 = const i0.Value.absent(),
i0.Value<String?> context2 = const i0.Value.absent(),
}) =>
i1.LoggerMessageEntityCompanion(
id: id,
message: message,
details: details,
level: level,
createdAt: createdAt,
context1: context1,
context2: context2,
),
createCompanionCallback: ({
required int id,
required String message,
i0.Value<String?> details = const i0.Value.absent(),
required i2.LogLevel level,
required DateTime createdAt,
i0.Value<String?> context1 = const i0.Value.absent(),
i0.Value<String?> context2 = const i0.Value.absent(),
}) =>
i1.LoggerMessageEntityCompanion.insert(
id: id,
message: message,
details: details,
level: level,
createdAt: createdAt,
context1: context1,
context2: context2,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$LoggerMessageEntityTableProcessedTableManager
= i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$LoggerMessageEntityTable,
i1.LoggerMessageEntityData,
i1.$$LoggerMessageEntityTableFilterComposer,
i1.$$LoggerMessageEntityTableOrderingComposer,
i1.$$LoggerMessageEntityTableAnnotationComposer,
$$LoggerMessageEntityTableCreateCompanionBuilder,
$$LoggerMessageEntityTableUpdateCompanionBuilder,
(
i1.LoggerMessageEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LoggerMessageEntityTable,
i1.LoggerMessageEntityData>
),
i1.LoggerMessageEntityData,
i0.PrefetchHooks Function()>;
class $LoggerMessageEntityTable extends i3.LoggerMessageEntity
with i0.TableInfo<$LoggerMessageEntityTable, i1.LoggerMessageEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$LoggerMessageEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<int> id = i0.GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: i0.DriftSqlType.int,
requiredDuringInsert: true,
defaultConstraints:
i0.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const i0.VerificationMeta _messageMeta =
const i0.VerificationMeta('message');
@override
late final i0.GeneratedColumn<String> message = i0.GeneratedColumn<String>(
'message', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _detailsMeta =
const i0.VerificationMeta('details');
@override
late final i0.GeneratedColumn<String> details = i0.GeneratedColumn<String>(
'details', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.LogLevel, int> level =
i0.GeneratedColumn<int>('level', aliasedName, false,
type: i0.DriftSqlType.int, requiredDuringInsert: true)
.withConverter<i2.LogLevel>(
i1.$LoggerMessageEntityTable.$converterlevel);
static const i0.VerificationMeta _createdAtMeta =
const i0.VerificationMeta('createdAt');
@override
late final i0.GeneratedColumn<DateTime> createdAt =
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
type: i0.DriftSqlType.dateTime, requiredDuringInsert: true);
static const i0.VerificationMeta _context1Meta =
const i0.VerificationMeta('context1');
@override
late final i0.GeneratedColumn<String> context1 = i0.GeneratedColumn<String>(
'context1', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
static const i0.VerificationMeta _context2Meta =
const i0.VerificationMeta('context2');
@override
late final i0.GeneratedColumn<String> context2 = i0.GeneratedColumn<String>(
'context2', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
@override
List<i0.GeneratedColumn> get $columns =>
[id, message, details, level, createdAt, context1, context2];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'logger_message_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.LoggerMessageEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('message')) {
context.handle(_messageMeta,
message.isAcceptableOrUnknown(data['message']!, _messageMeta));
} else if (isInserting) {
context.missing(_messageMeta);
}
if (data.containsKey('details')) {
context.handle(_detailsMeta,
details.isAcceptableOrUnknown(data['details']!, _detailsMeta));
}
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
} else if (isInserting) {
context.missing(_createdAtMeta);
}
if (data.containsKey('context1')) {
context.handle(_context1Meta,
context1.isAcceptableOrUnknown(data['context1']!, _context1Meta));
}
if (data.containsKey('context2')) {
context.handle(_context2Meta,
context2.isAcceptableOrUnknown(data['context2']!, _context2Meta));
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.LoggerMessageEntityData map(Map<String, dynamic> data,
{String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.LoggerMessageEntityData(
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!,
message: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}message'])!,
details: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}details']),
level: i1.$LoggerMessageEntityTable.$converterlevel.fromSql(
attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}level'])!),
createdAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
context1: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}context1']),
context2: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}context2']),
);
}
@override
$LoggerMessageEntityTable createAlias(String alias) {
return $LoggerMessageEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.LogLevel, int, int> $converterlevel =
const i0.EnumIndexConverter<i2.LogLevel>(i2.LogLevel.values);
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class LoggerMessageEntityData extends i0.DataClass
implements i0.Insertable<i1.LoggerMessageEntityData> {
final int id;
final String message;
final String? details;
final i2.LogLevel level;
final DateTime createdAt;
final String? context1;
final String? context2;
const LoggerMessageEntityData(
{required this.id,
required this.message,
this.details,
required this.level,
required this.createdAt,
this.context1,
this.context2});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<int>(id);
map['message'] = i0.Variable<String>(message);
if (!nullToAbsent || details != null) {
map['details'] = i0.Variable<String>(details);
}
{
map['level'] = i0.Variable<int>(
i1.$LoggerMessageEntityTable.$converterlevel.toSql(level));
}
map['created_at'] = i0.Variable<DateTime>(createdAt);
if (!nullToAbsent || context1 != null) {
map['context1'] = i0.Variable<String>(context1);
}
if (!nullToAbsent || context2 != null) {
map['context2'] = i0.Variable<String>(context2);
}
return map;
}
factory LoggerMessageEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return LoggerMessageEntityData(
id: serializer.fromJson<int>(json['id']),
message: serializer.fromJson<String>(json['message']),
details: serializer.fromJson<String?>(json['details']),
level: i1.$LoggerMessageEntityTable.$converterlevel
.fromJson(serializer.fromJson<int>(json['level'])),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
context1: serializer.fromJson<String?>(json['context1']),
context2: serializer.fromJson<String?>(json['context2']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'message': serializer.toJson<String>(message),
'details': serializer.toJson<String?>(details),
'level': serializer.toJson<int>(
i1.$LoggerMessageEntityTable.$converterlevel.toJson(level)),
'createdAt': serializer.toJson<DateTime>(createdAt),
'context1': serializer.toJson<String?>(context1),
'context2': serializer.toJson<String?>(context2),
};
}
i1.LoggerMessageEntityData copyWith(
{int? id,
String? message,
i0.Value<String?> details = const i0.Value.absent(),
i2.LogLevel? level,
DateTime? createdAt,
i0.Value<String?> context1 = const i0.Value.absent(),
i0.Value<String?> context2 = const i0.Value.absent()}) =>
i1.LoggerMessageEntityData(
id: id ?? this.id,
message: message ?? this.message,
details: details.present ? details.value : this.details,
level: level ?? this.level,
createdAt: createdAt ?? this.createdAt,
context1: context1.present ? context1.value : this.context1,
context2: context2.present ? context2.value : this.context2,
);
LoggerMessageEntityData copyWithCompanion(
i1.LoggerMessageEntityCompanion data) {
return LoggerMessageEntityData(
id: data.id.present ? data.id.value : this.id,
message: data.message.present ? data.message.value : this.message,
details: data.details.present ? data.details.value : this.details,
level: data.level.present ? data.level.value : this.level,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
context1: data.context1.present ? data.context1.value : this.context1,
context2: data.context2.present ? data.context2.value : this.context2,
);
}
@override
String toString() {
return (StringBuffer('LoggerMessageEntityData(')
..write('id: $id, ')
..write('message: $message, ')
..write('details: $details, ')
..write('level: $level, ')
..write('createdAt: $createdAt, ')
..write('context1: $context1, ')
..write('context2: $context2')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(id, message, details, level, createdAt, context1, context2);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.LoggerMessageEntityData &&
other.id == this.id &&
other.message == this.message &&
other.details == this.details &&
other.level == this.level &&
other.createdAt == this.createdAt &&
other.context1 == this.context1 &&
other.context2 == this.context2);
}
class LoggerMessageEntityCompanion
extends i0.UpdateCompanion<i1.LoggerMessageEntityData> {
final i0.Value<int> id;
final i0.Value<String> message;
final i0.Value<String?> details;
final i0.Value<i2.LogLevel> level;
final i0.Value<DateTime> createdAt;
final i0.Value<String?> context1;
final i0.Value<String?> context2;
const LoggerMessageEntityCompanion({
this.id = const i0.Value.absent(),
this.message = const i0.Value.absent(),
this.details = const i0.Value.absent(),
this.level = const i0.Value.absent(),
this.createdAt = const i0.Value.absent(),
this.context1 = const i0.Value.absent(),
this.context2 = const i0.Value.absent(),
});
LoggerMessageEntityCompanion.insert({
required int id,
required String message,
this.details = const i0.Value.absent(),
required i2.LogLevel level,
required DateTime createdAt,
this.context1 = const i0.Value.absent(),
this.context2 = const i0.Value.absent(),
}) : id = i0.Value(id),
message = i0.Value(message),
level = i0.Value(level),
createdAt = i0.Value(createdAt);
static i0.Insertable<i1.LoggerMessageEntityData> custom({
i0.Expression<int>? id,
i0.Expression<String>? message,
i0.Expression<String>? details,
i0.Expression<int>? level,
i0.Expression<DateTime>? createdAt,
i0.Expression<String>? context1,
i0.Expression<String>? context2,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (message != null) 'message': message,
if (details != null) 'details': details,
if (level != null) 'level': level,
if (createdAt != null) 'created_at': createdAt,
if (context1 != null) 'context1': context1,
if (context2 != null) 'context2': context2,
});
}
i1.LoggerMessageEntityCompanion copyWith(
{i0.Value<int>? id,
i0.Value<String>? message,
i0.Value<String?>? details,
i0.Value<i2.LogLevel>? level,
i0.Value<DateTime>? createdAt,
i0.Value<String?>? context1,
i0.Value<String?>? context2}) {
return i1.LoggerMessageEntityCompanion(
id: id ?? this.id,
message: message ?? this.message,
details: details ?? this.details,
level: level ?? this.level,
createdAt: createdAt ?? this.createdAt,
context1: context1 ?? this.context1,
context2: context2 ?? this.context2,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<int>(id.value);
}
if (message.present) {
map['message'] = i0.Variable<String>(message.value);
}
if (details.present) {
map['details'] = i0.Variable<String>(details.value);
}
if (level.present) {
map['level'] = i0.Variable<int>(
i1.$LoggerMessageEntityTable.$converterlevel.toSql(level.value));
}
if (createdAt.present) {
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
}
if (context1.present) {
map['context1'] = i0.Variable<String>(context1.value);
}
if (context2.present) {
map['context2'] = i0.Variable<String>(context2.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('LoggerMessageEntityCompanion(')
..write('id: $id, ')
..write('message: $message, ')
..write('details: $details, ')
..write('level: $level, ')
..write('createdAt: $createdAt, ')
..write('context1: $context1, ')
..write('context2: $context2')
..write(')'))
.toString();
}
}

View File

@@ -37,9 +37,10 @@ class RemoteAssetEntity extends Table
}
extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
Asset toDto() => Asset(
RemoteAsset toDto() => RemoteAsset(
id: id,
name: name,
ownerId: ownerId,
checksum: checksum,
type: type,
createdAt: createdAt,

View File

@@ -1,13 +1,13 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:isar/isar.dart';
class StoreEntity extends Table with DriftDefaultsMixin {
const StoreEntity();
part 'store.entity.g.dart';
IntColumn get id => integer()();
IntColumn get intValue => integer().nullable()();
TextColumn get strValue => text().nullable()();
/// Internal class for `Store`, do not use elsewhere.
@Collection(inheritance: false)
class StoreValue {
final Id id;
final int? intValue;
final String? strValue;
@override
Set<Column> get primaryKey => {id};
const StoreValue(this.id, {this.intValue, this.strValue});
}

View File

@@ -1,364 +0,0 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/store.entity.dart' as i2;
typedef $$StoreEntityTableCreateCompanionBuilder = i1.StoreEntityCompanion
Function({
required int id,
i0.Value<int?> intValue,
i0.Value<String?> strValue,
});
typedef $$StoreEntityTableUpdateCompanionBuilder = i1.StoreEntityCompanion
Function({
i0.Value<int> id,
i0.Value<int?> intValue,
i0.Value<String?> strValue,
});
class $$StoreEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$StoreEntityTable> {
$$StoreEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<int> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get intValue => $composableBuilder(
column: $table.intValue, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get strValue => $composableBuilder(
column: $table.strValue, builder: (column) => i0.ColumnFilters(column));
}
class $$StoreEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$StoreEntityTable> {
$$StoreEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<int> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get intValue => $composableBuilder(
column: $table.intValue, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get strValue => $composableBuilder(
column: $table.strValue, builder: (column) => i0.ColumnOrderings(column));
}
class $$StoreEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$StoreEntityTable> {
$$StoreEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<int> get intValue =>
$composableBuilder(column: $table.intValue, builder: (column) => column);
i0.GeneratedColumn<String> get strValue =>
$composableBuilder(column: $table.strValue, builder: (column) => column);
}
class $$StoreEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$StoreEntityTable,
i1.StoreEntityData,
i1.$$StoreEntityTableFilterComposer,
i1.$$StoreEntityTableOrderingComposer,
i1.$$StoreEntityTableAnnotationComposer,
$$StoreEntityTableCreateCompanionBuilder,
$$StoreEntityTableUpdateCompanionBuilder,
(
i1.StoreEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$StoreEntityTable,
i1.StoreEntityData>
),
i1.StoreEntityData,
i0.PrefetchHooks Function()> {
$$StoreEntityTableTableManager(
i0.GeneratedDatabase db, i1.$StoreEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$StoreEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$StoreEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$StoreEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<int> id = const i0.Value.absent(),
i0.Value<int?> intValue = const i0.Value.absent(),
i0.Value<String?> strValue = const i0.Value.absent(),
}) =>
i1.StoreEntityCompanion(
id: id,
intValue: intValue,
strValue: strValue,
),
createCompanionCallback: ({
required int id,
i0.Value<int?> intValue = const i0.Value.absent(),
i0.Value<String?> strValue = const i0.Value.absent(),
}) =>
i1.StoreEntityCompanion.insert(
id: id,
intValue: intValue,
strValue: strValue,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$StoreEntityTableProcessedTableManager = i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$StoreEntityTable,
i1.StoreEntityData,
i1.$$StoreEntityTableFilterComposer,
i1.$$StoreEntityTableOrderingComposer,
i1.$$StoreEntityTableAnnotationComposer,
$$StoreEntityTableCreateCompanionBuilder,
$$StoreEntityTableUpdateCompanionBuilder,
(
i1.StoreEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$StoreEntityTable,
i1.StoreEntityData>
),
i1.StoreEntityData,
i0.PrefetchHooks Function()>;
class $StoreEntityTable extends i2.StoreEntity
with i0.TableInfo<$StoreEntityTable, i1.StoreEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$StoreEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<int> id = i0.GeneratedColumn<int>(
'id', aliasedName, false,
type: i0.DriftSqlType.int, requiredDuringInsert: true);
static const i0.VerificationMeta _intValueMeta =
const i0.VerificationMeta('intValue');
@override
late final i0.GeneratedColumn<int> intValue = i0.GeneratedColumn<int>(
'int_value', aliasedName, true,
type: i0.DriftSqlType.int, requiredDuringInsert: false);
static const i0.VerificationMeta _strValueMeta =
const i0.VerificationMeta('strValue');
@override
late final i0.GeneratedColumn<String> strValue = i0.GeneratedColumn<String>(
'str_value', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
@override
List<i0.GeneratedColumn> get $columns => [id, intValue, strValue];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'store_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.StoreEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('int_value')) {
context.handle(_intValueMeta,
intValue.isAcceptableOrUnknown(data['int_value']!, _intValueMeta));
}
if (data.containsKey('str_value')) {
context.handle(_strValueMeta,
strValue.isAcceptableOrUnknown(data['str_value']!, _strValueMeta));
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.StoreEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.StoreEntityData(
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!,
intValue: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}int_value']),
strValue: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}str_value']),
);
}
@override
$StoreEntityTable createAlias(String alias) {
return $StoreEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class StoreEntityData extends i0.DataClass
implements i0.Insertable<i1.StoreEntityData> {
final int id;
final int? intValue;
final String? strValue;
const StoreEntityData({required this.id, this.intValue, this.strValue});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<int>(id);
if (!nullToAbsent || intValue != null) {
map['int_value'] = i0.Variable<int>(intValue);
}
if (!nullToAbsent || strValue != null) {
map['str_value'] = i0.Variable<String>(strValue);
}
return map;
}
factory StoreEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return StoreEntityData(
id: serializer.fromJson<int>(json['id']),
intValue: serializer.fromJson<int?>(json['intValue']),
strValue: serializer.fromJson<String?>(json['strValue']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'intValue': serializer.toJson<int?>(intValue),
'strValue': serializer.toJson<String?>(strValue),
};
}
i1.StoreEntityData copyWith(
{int? id,
i0.Value<int?> intValue = const i0.Value.absent(),
i0.Value<String?> strValue = const i0.Value.absent()}) =>
i1.StoreEntityData(
id: id ?? this.id,
intValue: intValue.present ? intValue.value : this.intValue,
strValue: strValue.present ? strValue.value : this.strValue,
);
StoreEntityData copyWithCompanion(i1.StoreEntityCompanion data) {
return StoreEntityData(
id: data.id.present ? data.id.value : this.id,
intValue: data.intValue.present ? data.intValue.value : this.intValue,
strValue: data.strValue.present ? data.strValue.value : this.strValue,
);
}
@override
String toString() {
return (StringBuffer('StoreEntityData(')
..write('id: $id, ')
..write('intValue: $intValue, ')
..write('strValue: $strValue')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, intValue, strValue);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.StoreEntityData &&
other.id == this.id &&
other.intValue == this.intValue &&
other.strValue == this.strValue);
}
class StoreEntityCompanion extends i0.UpdateCompanion<i1.StoreEntityData> {
final i0.Value<int> id;
final i0.Value<int?> intValue;
final i0.Value<String?> strValue;
const StoreEntityCompanion({
this.id = const i0.Value.absent(),
this.intValue = const i0.Value.absent(),
this.strValue = const i0.Value.absent(),
});
StoreEntityCompanion.insert({
required int id,
this.intValue = const i0.Value.absent(),
this.strValue = const i0.Value.absent(),
}) : id = i0.Value(id);
static i0.Insertable<i1.StoreEntityData> custom({
i0.Expression<int>? id,
i0.Expression<int>? intValue,
i0.Expression<String>? strValue,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (intValue != null) 'int_value': intValue,
if (strValue != null) 'str_value': strValue,
});
}
i1.StoreEntityCompanion copyWith(
{i0.Value<int>? id,
i0.Value<int?>? intValue,
i0.Value<String?>? strValue}) {
return i1.StoreEntityCompanion(
id: id ?? this.id,
intValue: intValue ?? this.intValue,
strValue: strValue ?? this.strValue,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<int>(id.value);
}
if (intValue.present) {
map['int_value'] = i0.Variable<int>(intValue.value);
}
if (strValue.present) {
map['str_value'] = i0.Variable<String>(strValue.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('StoreEntityCompanion(')
..write('id: $id, ')
..write('intValue: $intValue, ')
..write('strValue: $strValue')
..write(')'))
.toString();
}
}

View File

@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'isar_store.entity.dart';
part of 'store.entity.dart';
// **************************************************************************
// IsarCollectionGenerator

View File

@@ -7,13 +7,11 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:isar/isar.dart';
@@ -48,8 +46,6 @@ class IsarDatabaseRepository implements IDatabaseRepository {
RemoteAlbumEntity,
RemoteAlbumAssetEntity,
RemoteAlbumUserEntity,
StoreEntity,
LoggerMessageEntity,
],
include: {
'package:immich_mobile/infrastructure/entities/merged_asset.drift',

View File

@@ -23,13 +23,9 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
as i10;
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
as i11;
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i12;
import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart'
as i13;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i14;
import 'package:drift/internal/modular.dart' as i15;
as i12;
import 'package:drift/internal/modular.dart' as i13;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -55,11 +51,8 @@ abstract class $Drift extends i0.GeneratedDatabase {
i10.$RemoteAlbumAssetEntityTable(this);
late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
i11.$RemoteAlbumUserEntityTable(this);
late final i12.$StoreEntityTable storeEntity = i12.$StoreEntityTable(this);
late final i13.$LoggerMessageEntityTable loggerMessageEntity =
i13.$LoggerMessageEntityTable(this);
i14.MergedAssetDrift get mergedAssetDrift => i15.ReadDatabaseContainer(this)
.accessor<i14.MergedAssetDrift>(i14.MergedAssetDrift.new);
i12.MergedAssetDrift get mergedAssetDrift => i13.ReadDatabaseContainer(this)
.accessor<i12.MergedAssetDrift>(i12.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -78,9 +71,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteExifEntity,
remoteAlbumEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
storeEntity,
loggerMessageEntity
remoteAlbumUserEntity
];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>
@@ -217,8 +208,4 @@ class $DriftManager {
_db, _db.remoteAlbumAssetEntity);
i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
i12.$$StoreEntityTableTableManager get storeEntity =>
i12.$$StoreEntityTableTableManager(_db, _db.storeEntity);
i13.$$LoggerMessageEntityTableTableManager get loggerMessageEntity =>
i13.$$LoggerMessageEntityTableTableManager(_db, _db.loggerMessageEntity);
}

View File

@@ -1,145 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/drift_user.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final driftStoreRepositoryProvider = Provider<DriftStoreRepository>(
(ref) => DriftStoreRepository(ref.watch(driftProvider)),
);
class DriftStoreRepository implements IStoreRepository {
final Drift _db;
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
DriftStoreRepository(this._db);
@override
Future<bool> deleteAll() async {
return await _db.transaction(() async {
await _db.delete(_db.storeEntity).go();
return true;
});
}
@override
Stream<StoreDto<Object>> watchAll() {
return (_db.select(_db.storeEntity)
..where((tbl) => tbl.id.isIn(validStoreKeys)))
.watch()
.asyncExpand(
(entities) => Stream.fromFutures(
entities.map((e) async => _toUpdateEvent(e)),
),
);
}
@override
Future<void> delete<T>(StoreKey<T> key) async {
return await _db.transaction(() async {
await (_db.delete(_db.storeEntity)..where((tbl) => tbl.id.equals(key.id)))
.go();
});
}
@override
Future<bool> insert<T>(StoreKey<T> key, T value) async {
return await _db.transaction(() async {
await _db
.into(_db.storeEntity)
.insertOnConflictUpdate(await _fromValue(key, value));
return true;
});
}
@override
Future<T?> tryGet<T>(StoreKey<T> key) async {
final entity = await (_db.select(_db.storeEntity)
..where((tbl) => tbl.id.equals(key.id)))
.getSingleOrNull();
if (entity == null) {
return null;
}
return await _toValue(key, entity);
}
@override
Future<bool> update<T>(StoreKey<T> key, T value) async {
return await _db.transaction(() async {
await _db
.into(_db.storeEntity)
.insertOnConflictUpdate(await _fromValue(key, value));
return true;
});
}
@override
Stream<T?> watch<T>(StoreKey<T> key) async* {
yield* (_db.select(_db.storeEntity)..where((tbl) => tbl.id.equals(key.id)))
.watchSingleOrNull()
.asyncMap((e) async => e == null ? null : await _toValue(key, e));
}
Future<StoreDto<Object>> _toUpdateEvent(StoreEntityData entity) async {
final key = StoreKey.values.firstWhere((e) => e.id == entity.id)
as StoreKey<Object>;
final value = await _toValue(key, entity);
return StoreDto(key, value);
}
Future<T?> _toValue<T>(StoreKey<T> key, StoreEntityData entity) async =>
switch (key.type) {
const (int) => entity.intValue,
const (String) => entity.strValue,
const (bool) => entity.intValue == 1,
const (DateTime) => entity.intValue == null
? null
: DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
const (UserDto) => entity.strValue == null
? null
: await DriftUserRepository(_db).getByUserId(entity.strValue!),
_ => null,
} as T?;
Future<StoreEntityData> _fromValue<T>(StoreKey<T> key, T value) async {
final (int? intValue, String? strValue) = switch (key.type) {
const (int) => (value as int, null),
const (String) => (null, value as String),
const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => (
null,
(await DriftUserRepository(_db).update(value as UserDto)).id,
),
_ => throw UnsupportedError(
"Unsupported primitive type: ${key.type} for key: ${key.name}",
),
};
return StoreEntityData(
id: key.id,
intValue: intValue,
strValue: strValue,
);
}
@override
Future<List<StoreDto<Object>>> getAll() async {
final entities = await (_db.select(_db.storeEntity)
..where((tbl) => tbl.id.isIn(validStoreKeys)))
.get();
return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList());
}
}
abstract class IStoreRepository {
Future<bool> deleteAll();
Stream<StoreDto<Object>> watchAll();
Future<void> delete<T>(StoreKey<T> key);
Future<bool> insert<T>(StoreKey<T> key, T value);
Future<T?> tryGet<T>(StoreKey<T> key);
Future<bool> update<T>(StoreKey<T> key, T value);
Stream<T?> watch<T>(StoreKey<T> key);
Future<List<StoreDto<Object>>> getAll();
}

View File

@@ -1,117 +0,0 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
class DriftUserRepository {
final Drift _db;
const DriftUserRepository(this._db);
Future<void> delete(List<String> ids) async {
await _db.transaction(() async {
await (_db.delete(_db.userEntity)..where((tbl) => tbl.id.isIn(ids))).go();
});
}
Future<void> deleteAll() async {
await _db.transaction(() async {
await _db.delete(_db.userEntity).go();
});
}
Future<List<UserDto>> getAll({SortUserBy? sortBy}) async {
var query = _db.select(_db.userEntity);
if (sortBy != null) {
switch (sortBy) {
case SortUserBy.id:
query = query..orderBy([(u) => OrderingTerm.asc(u.id)]);
}
}
final users = await query.get();
return users.map((u) => _toDto(u)).toList();
}
Future<UserDto?> getByUserId(String id) async {
final user = await (_db.select(_db.userEntity)
..where((tbl) => tbl.id.equals(id)))
.getSingleOrNull();
return user != null ? _toDto(user) : null;
}
Future<List<UserDto?>> getByUserIds(List<String> ids) async {
final users = await (_db.select(_db.userEntity)
..where((tbl) => tbl.id.isIn(ids)))
.get();
// Create a map for quick lookup
final userMap = {for (var user in users) user.id: _toDto(user)};
// Return results in the same order as input ids
return ids.map((id) => userMap[id]).toList();
}
Future<bool> insert(UserDto user) async {
await _db.transaction(() async {
await _db.into(_db.userEntity).insertOnConflictUpdate(_fromDto(user));
});
return true;
}
Future<UserDto> update(UserDto user) async {
await _db.transaction(() async {
await _db.into(_db.userEntity).insertOnConflictUpdate(_fromDto(user));
});
return user;
}
Future<bool> updateAll(List<UserDto> users) async {
await _db.transaction(() async {
await _db.batch((batch) {
for (final user in users) {
batch.insert(_db.userEntity, _fromDto(user),
mode: InsertMode.insertOrReplace);
}
});
});
return true;
}
UserDto _toDto(UserEntityData entity) {
return UserDto(
id: entity.id,
updatedAt: entity.updatedAt,
email: entity.email,
name: entity.name,
isAdmin: entity.isAdmin,
profileImagePath: entity.profileImagePath ?? '',
// Note: These fields are not in the current UserEntity table but are in UserDto
// You may need to add them to the table or provide defaults
isPartnerSharedBy: false,
isPartnerSharedWith: false,
avatarColor: AvatarColor.primary,
memoryEnabled: true,
inTimeline: false,
quotaUsageInBytes: entity.quotaUsageInBytes,
quotaSizeInBytes: entity.quotaSizeInBytes ?? 0,
);
}
UserEntityCompanion _fromDto(UserDto dto) {
return UserEntityCompanion(
id: Value(dto.id),
name: Value(dto.name),
isAdmin: Value(dto.isAdmin),
email: Value(dto.email),
profileImagePath: Value.absentIfNull(
dto.profileImagePath?.isEmpty == true ? null : dto.profileImagePath),
updatedAt: Value(dto.updatedAt),
quotaSizeInBytes: Value.absentIfNull(
dto.quotaSizeInBytes == 0 ? null : dto.quotaSizeInBytes),
quotaUsageInBytes: Value(dto.quotaUsageInBytes),
);
}
}

View File

@@ -1,99 +1,46 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:isar/isar.dart';
final driftLogRepositoryProvider = Provider<LogRepository>(
(ref) => LogRepository(ref.watch(driftProvider)),
);
class LogRepository {
final Drift _db;
const LogRepository(this._db);
class IsarLogRepository extends IsarDatabaseRepository {
final Isar _db;
const IsarLogRepository(super.db) : _db = db;
Future<bool> deleteAll() async {
await _db.transaction(() async {
await _db.delete(_db.loggerMessageEntity).go();
});
await transaction(() async => await _db.loggerMessages.clear());
return true;
}
Future<List<LogMessage>> getAll() async {
final query = _db.select(_db.loggerMessageEntity)
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]);
final results = await query.get();
return results
.map(
(row) => LogMessage(
message: row.message,
level: row.level,
createdAt: row.createdAt,
logger: row.context1,
error: row.details,
stack: row.context2,
),
)
.toList();
final logs =
await _db.loggerMessages.where().sortByCreatedAtDesc().findAll();
return logs.map((l) => l.toDto()).toList();
}
Future<bool> insert(LogMessage log) async {
await _db.transaction(() async {
await _db.into(_db.loggerMessageEntity).insert(
LoggerMessageEntityCompanion.insert(
id: 0, // Will be auto-incremented by the database
message: log.message,
details: Value(log.error),
level: log.level,
createdAt: log.createdAt,
context1: Value(log.logger),
context2: Value(log.stack),
),
);
final logEntity = LoggerMessage.fromDto(log);
await transaction(() async {
await _db.loggerMessages.put(logEntity);
});
return true;
}
Future<bool> insertAll(Iterable<LogMessage> logs) async {
await _db.transaction(() async {
for (final log in logs) {
await _db.into(_db.loggerMessageEntity).insert(
LoggerMessageEntityCompanion.insert(
id: 0, // Will be auto-incremented by the database
message: log.message,
details: Value(log.error),
level: log.level,
createdAt: log.createdAt,
context1: Value(log.logger),
context2: Value(log.stack),
),
);
}
await transaction(() async {
final logEntities =
logs.map((log) => LoggerMessage.fromDto(log)).toList();
await _db.loggerMessages.putAll(logEntities);
});
return true;
}
Future<void> truncate({int limit = 250}) async {
await _db.transaction(() async {
final countQuery = _db.selectOnly(_db.loggerMessageEntity)
..addColumns([_db.loggerMessageEntity.id.count()]);
final countResult = await countQuery.getSingle();
final count = countResult.read(_db.loggerMessageEntity.id.count()) ?? 0;
await transaction(() async {
final count = await _db.loggerMessages.count();
if (count <= limit) return;
final toRemove = count - limit;
final oldestIds = await (_db.select(_db.loggerMessageEntity)
..orderBy([(t) => OrderingTerm.asc(t.createdAt)])
..limit(toRemove))
.get();
final idsToDelete = oldestIds.map((row) => row.id).toList();
await (_db.delete(_db.loggerMessageEntity)
..where((tbl) => tbl.id.isIn(idsToDelete)))
.go();
await _db.loggerMessages.where().limit(toRemove).deleteAll();
});
}
}

View File

@@ -10,26 +10,48 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
const DriftRemoteAlbumRepository(this._db) : super(_db);
Future<List<Album>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {}}) {
final query = _db.remoteAlbumEntity.select();
final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
final query = _db.remoteAlbumEntity.select().join([
leftOuterJoin(
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
useColumns: false,
),
leftOuterJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
),
]);
query
..addColumns([assetCount])
..groupBy([_db.remoteAlbumEntity.id]);
if (sortBy.isNotEmpty) {
final orderings = <OrderClauseGenerator<$RemoteAlbumEntityTable>>[];
final orderings = <OrderingTerm>[];
for (final sort in sortBy) {
orderings.add(
switch (sort) {
SortRemoteAlbumsBy.id => (row) => OrderingTerm.asc(row.id),
SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id),
},
);
}
query.orderBy(orderings);
}
return query.map((row) => row.toDto()).get();
return query
.map(
(row) => row.readTable(_db.remoteAlbumEntity).toDto(
assetCount: row.read(assetCount) ?? 0,
ownerName: row.readTable(_db.userEntity).name,
),
)
.get();
}
}
extension on RemoteAlbumEntityData {
Album toDto() {
Album toDto({int assetCount = 0, required String ownerName}) {
return Album(
id: id,
name: name,
@@ -40,6 +62,8 @@ extension on RemoteAlbumEntityData {
thumbnailAssetId: thumbnailAssetId,
isActivityEnabled: isActivityEnabled,
order: order,
assetCount: assetCount,
ownerName: ownerName,
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final remoteAssetRepositoryProvider = Provider<RemoteAssetRepository>(
(ref) => RemoteAssetRepository(ref.watch(driftProvider)),
);
class RemoteAssetRepository extends DriftDatabaseRepository {
final Drift _db;
const RemoteAssetRepository(this._db) : super(_db);
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
return _db.batch((batch) async {
for (final id in ids) {
batch.update(
_db.remoteAssetEntity,
RemoteAssetEntityCompanion(isFavorite: Value(isFavorite)),
where: (e) => e.id.equals(id),
);
}
});
}
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) {
return _db.batch((batch) async {
for (final id in ids) {
batch.update(
_db.remoteAssetEntity,
RemoteAssetEntityCompanion(visibility: Value(visibility)),
where: (e) => e.id.equals(id),
);
}
});
}
}

View File

@@ -1,19 +1,16 @@
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/isar_store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/drift_store.repository.dart';
import 'package:isar/isar.dart';
class IsarStoreRepository extends IsarDatabaseRepository
implements IStoreRepository {
class IsarStoreRepository extends IsarDatabaseRepository {
final Isar _db;
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
IsarStoreRepository(super.db) : _db = db;
@override
Future<bool> deleteAll() async {
return await transaction(() async {
await _db.storeValues.clear();
@@ -21,7 +18,6 @@ class IsarStoreRepository extends IsarDatabaseRepository
});
}
@override
Stream<StoreDto<Object>> watchAll() {
return _db.storeValues
.filter()
@@ -34,12 +30,10 @@ class IsarStoreRepository extends IsarDatabaseRepository
);
}
@override
Future<void> delete<T>(StoreKey<T> key) async {
return await transaction(() async => await _db.storeValues.delete(key.id));
}
@override
Future<bool> insert<T>(StoreKey<T> key, T value) async {
return await transaction(() async {
await _db.storeValues.put(await _fromValue(key, value));
@@ -47,7 +41,6 @@ class IsarStoreRepository extends IsarDatabaseRepository
});
}
@override
Future<T?> tryGet<T>(StoreKey<T> key) async {
final entity = (await _db.storeValues.get(key.id));
if (entity == null) {
@@ -56,7 +49,6 @@ class IsarStoreRepository extends IsarDatabaseRepository
return await _toValue(key, entity);
}
@override
Future<bool> update<T>(StoreKey<T> key, T value) async {
return await transaction(() async {
await _db.storeValues.put(await _fromValue(key, value));
@@ -64,7 +56,6 @@ class IsarStoreRepository extends IsarDatabaseRepository
});
}
@override
Stream<T?> watch<T>(StoreKey<T> key) async* {
yield* _db.storeValues
.watchObject(key.id, fireImmediately: true)
@@ -109,7 +100,6 @@ class IsarStoreRepository extends IsarDatabaseRepository
return StoreValue(key.id, intValue: intValue, strValue: strValue);
}
@override
Future<List<StoreDto<Object>>> getAll() async {
final entities = await _db.storeValues
.filter()

View File

@@ -70,36 +70,38 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.mergedAssetDrift
.mergedAsset(userIds, limit: Limit(count, offset))
.map(
(row) => row.remoteId != null
? Asset(
id: row.remoteId!,
localId: row.localId,
name: row.name,
checksum: row.checksum,
type: row.type,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
thumbHash: row.thumbHash,
width: row.width,
height: row.height,
isFavorite: row.isFavorite,
durationInSeconds: row.durationInSeconds,
)
: LocalAsset(
id: row.localId!,
remoteId: row.remoteId,
name: row.name,
checksum: row.checksum,
type: row.type,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
width: row.width,
height: row.height,
isFavorite: row.isFavorite,
durationInSeconds: row.durationInSeconds,
),
)
.get();
(row) {
return row.remoteId != null && row.ownerId != null
? RemoteAsset(
id: row.remoteId!,
localId: row.localId,
name: row.name,
ownerId: row.ownerId!,
checksum: row.checksum,
type: row.type,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
thumbHash: row.thumbHash,
width: row.width,
height: row.height,
isFavorite: row.isFavorite,
durationInSeconds: row.durationInSeconds,
)
: LocalAsset(
id: row.localId!,
remoteId: row.remoteId,
name: row.name,
checksum: row.checksum,
type: row.type,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
width: row.width,
height: row.height,
isFavorite: row.isFavorite,
durationInSeconds: row.durationInSeconds,
);
},
).get();
}
Stream<List<Bucket>> watchLocalBucket(

View File

@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@RoutePage()
class TabShellPage extends ConsumerWidget {
@@ -138,12 +138,13 @@ class TabShellPage extends ConsumerWidget {
);
}
final multiselectEnabled = ref.watch(multiselectProvider);
final multiselectEnabled =
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
return AutoTabsRouter(
routes: [
const MainTimelineRoute(),
SearchRoute(),
const AlbumsRoute(),
const DriftAlbumsRoute(),
const LibraryRoute(),
],
duration: const Duration(milliseconds: 600),

View File

@@ -1,11 +1,12 @@
import 'dart:async';
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
// ignore: import_rule_isar
import 'package:isar/isar.dart';
const kDevLoggerTag = 'DEV';
@@ -13,38 +14,28 @@ abstract final class DLog {
const DLog();
static Stream<List<LogMessage>> watchLog() {
final db = Drift();
final db = Isar.getInstance();
if (db == null) {
return const Stream.empty();
}
final query = db.select(db.loggerMessageEntity)
..where((tbl) => tbl.context1.equals(kDevLoggerTag))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]);
return query.watch().map(
(rows) => rows
.map(
(row) => LogMessage(
message: row.message,
level: row.level,
createdAt: row.createdAt,
logger: row.context1,
error: row.details,
stack: row.context2,
),
)
.toList(),
);
return db.loggerMessages
.filter()
.context1EqualTo(kDevLoggerTag)
.sortByCreatedAtDesc()
.watch(fireImmediately: true)
.map((logs) => logs.map((log) => log.toDto()).toList());
}
static void clearLog() {
final db = Drift();
final db = Isar.getInstance();
if (db == null) {
return;
}
unawaited(
db.transaction(() async {
await (db.delete(db.loggerMessageEntity)
..where((tbl) => tbl.context1.equals(kDevLoggerTag)))
.go();
}),
);
db.writeTxnSync(() {
db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync();
});
}
static void log(String message, [Object? error, StackTrace? stackTrace]) {
@@ -58,7 +49,10 @@ abstract final class DLog {
debugPrint('StackTrace: $stackTrace');
}
final db = Drift();
final isar = Isar.getInstance();
if (isar == null) {
return;
}
final record = LogMessage(
message: message,
@@ -69,6 +63,6 @@ abstract final class DLog {
stack: stackTrace?.toString(),
);
unawaited(LogRepository(db).insert(record));
unawaited(IsarLogRepository(isar).insert(record));
}
}

View File

@@ -5,11 +5,8 @@ import 'package:drift/drift.dart' hide Column;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/drift_store.repository.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@@ -17,32 +14,6 @@ import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/routing/router.dart';
final _features = [
_Feature(
name: 'test',
icon: Icons.abc,
onTap: (_, ref) {
final UserDto value = UserDto(
id: "1234",
email: "alex@email.com",
name: "alex",
isAdmin: true,
updatedAt: DateTime.now(),
);
ref
.read(driftStoreRepositoryProvider)
.insert(StoreKey.serverUrl, "https://example.com");
final readback =
ref.read(driftStoreRepositoryProvider).tryGet(StoreKey.serverUrl);
readback.then((value) {
print("Read back: $value");
});
return Future.value();
},
),
_Feature(
name: 'Sync Local',
icon: Icons.photo_album_rounded,

View File

@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -20,7 +18,7 @@ class LocalTimelinePage extends StatelessWidget {
(ref) {
final timelineService =
ref.watch(timelineFactoryProvider).localAlbum(albumId: albumId);
ref.onDispose(() => unawaited(timelineService.dispose()));
ref.onDispose(timelineService.dispose);
return timelineService;
},
),

View File

@@ -1,31 +1,14 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@RoutePage()
class MainTimelinePage extends StatelessWidget {
class MainTimelinePage extends ConsumerWidget {
const MainTimelinePage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final timelineUsers =
ref.watch(timelineUsersProvider).valueOrNull ?? [];
final timelineService =
ref.watch(timelineFactoryProvider).main(timelineUsers);
ref.onDispose(() => unawaited(timelineService.dispose()));
return timelineService;
},
),
],
child: const Timeline(),
);
Widget build(BuildContext context, WidgetRef ref) {
return const Timeline();
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -21,7 +19,7 @@ class RemoteTimelinePage extends StatelessWidget {
final timelineService = ref
.watch(timelineFactoryProvider)
.remoteAlbum(albumId: albumId);
ref.onDispose(() => unawaited(timelineService.dispose()));
ref.onDispose(timelineService.dispose);
return timelineService;
},
),

View File

@@ -0,0 +1,767 @@
import 'dart:async';
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/remote_album.utils.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/search_field.dart';
@RoutePage()
class DriftAlbumsPage extends ConsumerStatefulWidget {
const DriftAlbumsPage({super.key});
@override
ConsumerState<DriftAlbumsPage> createState() => _DriftAlbumsPageState();
}
class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
bool isGrid = false;
final searchController = TextEditingController();
QuickFilterMode filterMode = QuickFilterMode.all;
final searchFocusNode = FocusNode();
@override
void initState() {
super.initState();
// Load albums when component mounts
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(remoteAlbumProvider.notifier).getAll();
});
searchController.addListener(() {
onSearch(searchController.text, filterMode);
});
}
void onSearch(String searchTerm, QuickFilterMode sortMode) {
final userId = ref.watch(currentUserProvider)?.id;
ref
.read(remoteAlbumProvider.notifier)
.searchAlbums(searchTerm, userId, sortMode);
}
Future<void> onRefresh() async {
await ref.read(remoteAlbumProvider.notifier).refresh();
}
void toggleViewMode() {
setState(() {
isGrid = !isGrid;
});
}
void changeFilter(QuickFilterMode sortMode) {
setState(() {
filterMode = sortMode;
});
}
void clearSearch() {
setState(() {
filterMode = QuickFilterMode.all;
searchController.clear();
ref.read(remoteAlbumProvider.notifier).clearSearch();
});
}
@override
void dispose() {
searchController.dispose();
searchFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final albumState = ref.watch(remoteAlbumProvider);
final albums = albumState.filteredAlbums;
final isLoading = albumState.isLoading;
final error = albumState.error;
final userId = ref.watch(currentUserProvider)?.id;
return RefreshIndicator(
onRefresh: onRefresh,
child: CustomScrollView(
slivers: [
const ImmichSliverAppBar(),
_SearchBar(
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearch: onSearch,
filterMode: filterMode,
onClearSearch: clearSearch,
),
_QuickFilterButtonRow(
filterMode: filterMode,
onChangeFilter: changeFilter,
onSearch: onSearch,
searchController: searchController,
),
_QuickSortAndViewMode(
isGrid: isGrid,
onToggleViewMode: toggleViewMode,
),
isGrid
? _AlbumGrid(
albums: albums,
userId: userId,
isLoading: isLoading,
error: error,
)
: _AlbumList(
albums: albums,
userId: userId,
isLoading: isLoading,
error: error,
),
],
),
);
}
}
class _SortButton extends ConsumerStatefulWidget {
const _SortButton();
@override
ConsumerState<_SortButton> createState() => _SortButtonState();
}
class _SortButtonState extends ConsumerState<_SortButton> {
RemoteAlbumSortMode albumSortOption = RemoteAlbumSortMode.lastModified;
bool albumSortIsReverse = true;
void onMenuTapped(RemoteAlbumSortMode sortMode) {
final selected = albumSortOption == sortMode;
// Switch direction
if (selected) {
setState(() {
albumSortIsReverse = !albumSortIsReverse;
});
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
sortMode,
isReverse: albumSortIsReverse,
);
} else {
setState(() {
albumSortOption = sortMode;
});
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
sortMode,
isReverse: albumSortIsReverse,
);
}
}
@override
Widget build(BuildContext context) {
return MenuAnchor(
style: MenuStyle(
elevation: const WidgetStatePropertyAll(1),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
),
padding: const WidgetStatePropertyAll(
EdgeInsets.all(4),
),
),
consumeOutsideTap: true,
menuChildren: RemoteAlbumSortMode.values
.map(
(sortMode) => MenuItemButton(
leadingIcon: albumSortOption == sortMode
? albumSortIsReverse
? Icon(
Icons.keyboard_arrow_down,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: Icon(
Icons.keyboard_arrow_up_rounded,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: const Icon(Icons.abc, color: Colors.transparent),
onPressed: () => onMenuTapped(sortMode),
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.fromLTRB(16, 16, 32, 16),
),
backgroundColor: WidgetStateProperty.all(
albumSortOption == sortMode
? context.colorScheme.primary
: Colors.transparent,
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
),
),
child: Text(
sortMode.key.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface.withAlpha(185),
),
),
),
)
.toList(),
builder: (context, controller, child) {
return GestureDetector(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 5),
child: albumSortIsReverse
? const Icon(
Icons.keyboard_arrow_down,
)
: const Icon(
Icons.keyboard_arrow_up_rounded,
),
),
Text(
albumSortOption.key.t(context: context),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: context.colorScheme.onSurface.withAlpha(225),
),
),
],
),
);
},
);
}
}
class _SearchBar extends StatelessWidget {
const _SearchBar({
required this.searchController,
required this.searchFocusNode,
required this.onSearch,
required this.filterMode,
required this.onClearSearch,
});
final TextEditingController searchController;
final FocusNode searchFocusNode;
final void Function(String, QuickFilterMode) onSearch;
final QuickFilterMode filterMode;
final VoidCallback onClearSearch;
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
sliver: SliverToBoxAdapter(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: context.colorScheme.onSurface.withAlpha(0),
width: 0,
),
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withValues(alpha: 0.075),
context.colorScheme.primary.withValues(alpha: 0.09),
context.colorScheme.primary.withValues(alpha: 0.075),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
transform: const GradientRotation(0.5 * pi),
),
),
child: SearchField(
autofocus: false,
contentPadding: const EdgeInsets.all(16),
hintText: 'search_albums'.tr(),
prefixIcon: const Icon(Icons.search_rounded),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded),
onPressed: onClearSearch,
)
: null,
controller: searchController,
onChanged: (_) => onSearch(searchController.text, filterMode),
focusNode: searchFocusNode,
onTapOutside: (_) => searchFocusNode.unfocus(),
),
),
),
);
}
}
class _QuickFilterButtonRow extends StatelessWidget {
const _QuickFilterButtonRow({
required this.filterMode,
required this.onChangeFilter,
required this.onSearch,
required this.searchController,
});
final QuickFilterMode filterMode;
final void Function(QuickFilterMode) onChangeFilter;
final void Function(String, QuickFilterMode) onSearch;
final TextEditingController searchController;
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Wrap(
spacing: 4,
runSpacing: 4,
children: [
_QuickFilterButton(
label: 'all'.tr(),
isSelected: filterMode == QuickFilterMode.all,
onTap: () {
onChangeFilter(QuickFilterMode.all);
onSearch(searchController.text, QuickFilterMode.all);
},
),
_QuickFilterButton(
label: 'shared_with_me'.tr(),
isSelected: filterMode == QuickFilterMode.sharedWithMe,
onTap: () {
onChangeFilter(QuickFilterMode.sharedWithMe);
onSearch(
searchController.text,
QuickFilterMode.sharedWithMe,
);
},
),
_QuickFilterButton(
label: 'my_albums'.tr(),
isSelected: filterMode == QuickFilterMode.myAlbums,
onTap: () {
onChangeFilter(QuickFilterMode.myAlbums);
onSearch(
searchController.text,
QuickFilterMode.myAlbums,
);
},
),
],
),
),
);
}
}
class _QuickFilterButton extends StatelessWidget {
const _QuickFilterButton({
required this.isSelected,
required this.onTap,
required this.label,
});
final bool isSelected;
final VoidCallback onTap;
final String label;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onTap,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
isSelected ? context.colorScheme.primary : Colors.transparent,
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(25),
width: 1,
),
),
),
),
child: Text(
label,
style: TextStyle(
color: isSelected
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
fontSize: 14,
),
),
);
}
}
class _QuickSortAndViewMode extends StatelessWidget {
const _QuickSortAndViewMode({
required this.isGrid,
required this.onToggleViewMode,
});
final bool isGrid;
final VoidCallback onToggleViewMode;
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const _SortButton(),
IconButton(
icon: Icon(
isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
size: 24,
),
onPressed: onToggleViewMode,
),
],
),
),
);
}
}
class _AlbumList extends StatelessWidget {
const _AlbumList({
required this.isLoading,
required this.error,
required this.albums,
required this.userId,
});
final bool isLoading;
final String? error;
final List<Album> albums;
final String? userId;
@override
Widget build(BuildContext context) {
if (isLoading) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
),
);
}
if (error != null) {
return SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
'Error loading albums: $error',
style: TextStyle(
color: context.colorScheme.error,
),
),
),
),
);
}
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Text('No albums found'),
),
),
);
}
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
sliver: SliverList.builder(
itemBuilder: (_, index) {
final album = albums[index];
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
),
child: LargeLeadingTile(
title: Text(
album.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
'${'items_count'.t(
context: context,
args: {
'count': album.assetCount,
},
)} ${album.ownerId != userId ? 'shared_by_user'.t(
context: context,
args: {
'user': album.ownerName,
},
) : 'owned'.t(context: context)}',
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
),
onTap: () => context.router.push(
RemoteTimelineRoute(albumId: album.id),
),
leadingPadding: const EdgeInsets.only(
right: 16,
),
leading: album.thumbnailAssetId != null
? ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(15),
),
child: SizedBox(
width: 80,
height: 80,
child: Thumbnail(
remoteId: album.thumbnailAssetId,
),
),
)
: const SizedBox(
width: 80,
height: 80,
child: Icon(
Icons.photo_album_rounded,
size: 40,
color: Colors.grey,
),
),
),
);
},
itemCount: albums.length,
),
);
}
}
class _AlbumGrid extends StatelessWidget {
const _AlbumGrid({
required this.albums,
required this.userId,
required this.isLoading,
required this.error,
});
final List<Album> albums;
final String? userId;
final bool isLoading;
final String? error;
@override
Widget build(BuildContext context) {
if (isLoading) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
),
);
}
if (error != null) {
return SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
'Error loading albums: $error',
style: TextStyle(
color: context.colorScheme.error,
),
),
),
),
);
}
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Text('No albums found'),
),
),
);
}
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: .7,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
final album = albums[index];
return _GridAlbumCard(
album: album,
userId: userId,
);
},
childCount: albums.length,
),
),
);
}
}
class _GridAlbumCard extends StatelessWidget {
const _GridAlbumCard({
required this.album,
required this.userId,
});
final Album album;
final String? userId;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => context.router.push(
RemoteTimelineRoute(albumId: album.id),
),
child: Card(
elevation: 0,
color: context.colorScheme.surfaceBright,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(16),
),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(25),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(15),
),
child: SizedBox(
width: double.infinity,
child: album.thumbnailAssetId != null
? Thumbnail(
remoteId: album.thumbnailAssetId,
)
: Container(
color: context.colorScheme.surfaceContainerHighest,
child: const Icon(
Icons.photo_album_rounded,
size: 40,
color: Colors.grey,
),
),
),
),
),
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
album.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
'${'items_count'.t(
context: context,
args: {
'count': album.assetCount,
},
)} ${album.ownerId != userId ? 'shared_by_user'.t(
context: context,
args: {
'user': album.ownerName,
},
) : 'owned'.t(context: context)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.labelMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -1,16 +1,51 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class ArchiveActionButton extends ConsumerWidget {
const ArchiveActionButton({super.key});
final ActionSource source;
const ArchiveActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result = await ref.read(actionProvider.notifier).archive(source);
await ref.read(timelineServiceProvider).reloadBucket();
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'archive_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success
? successMessage
: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.archive_outlined,
label: "archive".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -1,16 +1,51 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class FavoriteActionButton extends ConsumerWidget {
const FavoriteActionButton({super.key});
final ActionSource source;
const FavoriteActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result = await ref.read(actionProvider.notifier).favorite(source);
await ref.read(timelineServiceProvider).reloadBucket();
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'favorite_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success
? successMessage
: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.favorite_border_rounded,
label: "favorite".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -1,10 +1,45 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class MoveToLockFolderActionButton extends ConsumerWidget {
const MoveToLockFolderActionButton({super.key});
final ActionSource source;
const MoveToLockFolderActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result =
await ref.read(actionProvider.notifier).moveToLockFolder(source);
await ref.read(timelineServiceProvider).reloadBucket();
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'move_to_lock_folder_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success
? successMessage
: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -12,6 +47,7 @@ class MoveToLockFolderActionButton extends ConsumerWidget {
maxWidth: 100.0,
iconData: Icons.lock_outline_rounded,
label: "move_to_locked_folder".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -1,10 +1,45 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class RemoveFromLockFolderActionButton extends ConsumerWidget {
const RemoveFromLockFolderActionButton({super.key});
final ActionSource source;
const RemoveFromLockFolderActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result =
await ref.read(actionProvider.notifier).removeFromLockFolder(source);
await ref.read(timelineServiceProvider).reloadBucket();
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'remove_from_lock_folder_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success
? successMessage
: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -12,6 +47,7 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget {
maxWidth: 100.0,
iconData: Icons.lock_open_rounded,
label: "remove_from_locked_folder".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -1,16 +1,50 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class ShareLinkActionButton extends ConsumerWidget {
const ShareLinkActionButton({super.key});
final ActionSource source;
const ShareLinkActionButton({super.key, required this.source});
_onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result =
await ref.read(actionProvider.notifier).shareLink(source, context);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'share_link_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success
? successMessage
: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.link_rounded,
label: "share_link".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
@@ -29,20 +30,22 @@ class HomeBottomAppBar extends ConsumerWidget {
return BaseBottomSheet(
initialChildSize: 0.25,
minChildSize: 0.22,
shouldCloseOnMinExtent: false,
actions: [
const ShareActionButton(),
if (multiselect.isEnabled) const ShareActionButton(),
if (multiselect.hasRemote) ...[
const ShareLinkActionButton(),
const ArchiveActionButton(),
const FavoriteActionButton(),
const ShareLinkActionButton(source: ActionSource.timeline),
const ArchiveActionButton(source: ActionSource.timeline),
const FavoriteActionButton(source: ActionSource.timeline),
const DownloadActionButton(),
isTrashEnable
? const TrashActionButton()
: const DeletePermanentActionButton(),
const EditDateTimeActionButton(),
const EditLocationActionButton(),
const MoveToLockFolderActionButton(),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
const StackActionButton(),
],
if (multiselect.hasLocal) ...[

View File

@@ -10,20 +10,39 @@ import 'package:octo_image/octo_image.dart';
class Thumbnail extends StatelessWidget {
const Thumbnail({
required this.asset,
this.asset,
this.remoteId,
this.size = const Size.square(256),
this.fit = BoxFit.cover,
super.key,
});
}) : assert(
asset != null || remoteId != null,
'Either asset or remoteId must be provided',
);
final BaseAsset asset;
final BaseAsset? asset;
final String? remoteId;
final Size size;
final BoxFit fit;
static ImageProvider imageProvider({
required BaseAsset asset,
BaseAsset? asset,
String? remoteId,
Size size = const Size.square(256),
}) {
assert(
asset != null || remoteId != null,
'Either asset or remoteId must be provided',
);
if (remoteId != null) {
return RemoteThumbProvider(
assetId: remoteId,
height: size.height,
width: size.width,
);
}
if (asset is LocalAsset) {
return LocalThumbProvider(
asset: asset,
@@ -32,7 +51,7 @@ class Thumbnail extends StatelessWidget {
);
}
if (asset is Asset) {
if (asset is RemoteAsset) {
return RemoteThumbProvider(
assetId: asset.id,
height: size.height,
@@ -45,8 +64,10 @@ class Thumbnail extends StatelessWidget {
@override
Widget build(BuildContext context) {
final thumbHash = asset is Asset ? (asset as Asset).thumbHash : null;
final provider = imageProvider(asset: asset, size: size);
final thumbHash =
asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
final provider =
imageProvider(asset: asset, remoteId: remoteId, size: size);
return OctoImage.fromSet(
image: provider,

View File

@@ -30,9 +30,11 @@ class ThumbnailTile extends ConsumerWidget {
? context.primaryColor.darken(amount: 0.6)
: context.primaryColor.lighten(amount: 0.8);
final isSelected = ref
.watch(multiSelectProvider.select((state) => state.selectedAssets))
.contains(asset);
final isSelected = ref.watch(
multiSelectProvider.select(
(multiselect) => multiselect.selectedAssets.contains(asset),
),
);
return Stack(
children: [

View File

@@ -185,7 +185,7 @@ class FixedSegment extends Segment {
/// and prevents duplicate keys even when assets have the same name/timestamp
String _generateUniqueKey(BaseAsset asset, int assetIndex) {
// Try to get the most unique identifier based on asset type
if (asset is Asset) {
if (asset is RemoteAsset) {
// For remote/merged assets, use the remote ID which is globally unique
return 'asset_${asset.id}';
} else if (asset is LocalAsset) {

View File

@@ -9,7 +9,7 @@ import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class TimelineHeader extends ConsumerWidget {
class TimelineHeader extends StatelessWidget {
final Bucket bucket;
final HeaderType header;
final double height;
@@ -36,23 +36,13 @@ class TimelineHeader extends ConsumerWidget {
}
@override
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
if (bucket is! TimeBucket || header == HeaderType.none) {
return const SizedBox.shrink();
}
final date = (bucket as TimeBucket).date;
List<BaseAsset> bucketAssets;
try {
bucketAssets = ref
.watch(timelineServiceProvider)
.getAssets(assetOffset, bucket.assetCount);
} catch (e) {
bucketAssets = <BaseAsset>[];
}
final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets));
final isMonthHeader =
header == HeaderType.month || header == HeaderType.monthAndDay;
final isDayHeader =
@@ -80,16 +70,8 @@ class TimelineHeader extends ConsumerWidget {
const Spacer(),
if (header != HeaderType.monthAndDay)
_BulkSelectIconButton(
isAllSelected: isAllSelected,
onPressed: () {
ref
.read(multiSelectProvider.notifier)
.toggleBucketSelection(
assetOffset,
bucket.assetCount,
);
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
},
bucket: bucket,
assetOffset: assetOffset,
),
],
),
@@ -104,16 +86,8 @@ class TimelineHeader extends ConsumerWidget {
),
const Spacer(),
_BulkSelectIconButton(
isAllSelected: isAllSelected,
onPressed: () {
ref
.read(multiSelectProvider.notifier)
.toggleBucketSelection(
assetOffset,
bucket.assetCount,
);
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
},
bucket: bucket,
assetOffset: assetOffset,
),
],
),
@@ -125,18 +99,35 @@ class TimelineHeader extends ConsumerWidget {
}
class _BulkSelectIconButton extends ConsumerWidget {
final bool isAllSelected;
final VoidCallback onPressed;
final Bucket bucket;
final int assetOffset;
const _BulkSelectIconButton({
required this.isAllSelected,
required this.onPressed,
required this.bucket,
required this.assetOffset,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
List<BaseAsset> bucketAssets;
try {
bucketAssets = ref
.watch(timelineServiceProvider)
.getAssets(assetOffset, bucket.assetCount);
} catch (e) {
bucketAssets = <BaseAsset>[];
}
final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets));
return IconButton(
onPressed: onPressed,
onPressed: () {
ref.read(multiSelectProvider.notifier).toggleBucketSelection(
assetOffset,
bucket.assetCount,
);
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
},
icon: isAllSelected
? Icon(
Icons.check_circle_rounded,

View File

@@ -407,7 +407,7 @@ class _MultiSelectStatusButton extends ConsumerWidget {
final selectCount =
ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
return ElevatedButton.icon(
onPressed: () => ref.read(multiSelectProvider.notifier).clearSelection(),
onPressed: () => ref.read(multiSelectProvider.notifier).reset(),
icon: Icon(
Icons.close_rounded,
color: context.colorScheme.onPrimary,

View File

@@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/action.service.dart';
import 'package:immich_mobile/services/timeline.service.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
final actionProvider = NotifierProvider<ActionNotifier, void>(
ActionNotifier.new,
dependencies: [
multiSelectProvider,
timelineServiceProvider,
],
);
class ActionResult {
final int count;
final bool success;
final String? error;
const ActionResult({required this.count, required this.success, this.error});
@override
String toString() =>
'ActionResult(count: $count, success: $success, error: $error)';
}
class ActionNotifier extends Notifier<void> {
final Logger _logger = Logger('ActionNotifier');
late ActionService _service;
ActionNotifier() : super();
@override
void build() {
_service = ref.watch(actionServiceProvider);
}
List<String> _getIdsForSource<T extends BaseAsset>(ActionSource source) {
final currentUser = ref.read(currentUserProvider);
if (T is RemoteAsset && currentUser == null) {
return [];
}
final Set<BaseAsset> assets = switch (source) {
ActionSource.timeline =>
ref.read(multiSelectProvider.select((s) => s.selectedAssets)),
ActionSource.viewer => {},
};
return switch (T) {
const (RemoteAsset) => assets
.where(
(asset) => asset is RemoteAsset && asset.ownerId == currentUser!.id,
)
.cast<RemoteAsset>()
.map((asset) => asset.id)
.toList(),
const (LocalAsset) =>
assets.whereType<LocalAsset>().map((asset) => asset.id).toList(),
_ => [],
};
}
Future<ActionResult> shareLink(
ActionSource source,
BuildContext context,
) async {
final ids = _getIdsForSource<RemoteAsset>(source);
try {
await _service.shareLink(ids, context);
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to create shared link for assets', error, stack);
return ActionResult(
count: ids.length,
success: false,
error: error.toString(),
);
}
}
Future<ActionResult> favorite(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(source);
try {
await _service.favorite(ids);
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to favorite assets', error, stack);
return ActionResult(
count: ids.length,
success: false,
error: error.toString(),
);
}
}
Future<ActionResult> unFavorite(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(source);
try {
await _service.unFavorite(ids);
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to unfavorite assets', error, stack);
return ActionResult(
count: ids.length,
success: false,
error: error.toString(),
);
}
}
Future<ActionResult> archive(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(source);
try {
await _service.archive(ids);
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to archive assets', error, stack);
return ActionResult(
count: ids.length,
success: false,
error: error.toString(),
);
}
}
Future<ActionResult> unArchive(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(source);
try {
await _service.unArchive(ids);
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to unarchive assets', error, stack);
return ActionResult(
count: ids.length,
success: false,
error: error.toString(),
);
}
}
Future<ActionResult> moveToLockFolder(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(source);
try {
await _service.moveToLockFolder(ids);
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to move assets to lock folder', error, stack);
return ActionResult(
count: ids.length,
success: false,
error: error.toString(),
);
}
}
Future<ActionResult> removeFromLockFolder(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(source);
try {
await _service.removeFromLockFolder(ids);
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to remove assets from lock folder', error, stack);
return ActionResult(
count: ids.length,
success: false,
error: error.toString(),
);
}
}
}

View File

@@ -1,7 +1,9 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
@@ -10,3 +12,14 @@ final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
(ref) => DriftRemoteAlbumRepository(ref.watch(driftProvider)),
);
final remoteAlbumServiceProvider = Provider<RemoteAlbumService>(
(ref) => RemoteAlbumService(ref.watch(remoteAlbumRepository)),
dependencies: [remoteAlbumRepository],
);
final remoteAlbumProvider =
NotifierProvider<RemoteAlbumNotifier, RemoteAlbumState>(
RemoteAlbumNotifier.new,
dependencies: [remoteAlbumServiceProvider],
);

View File

@@ -0,0 +1,121 @@
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/utils/remote_album.utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'album.provider.dart';
class RemoteAlbumState {
final List<Album> albums;
final List<Album> filteredAlbums;
final bool isLoading;
final String? error;
const RemoteAlbumState({
required this.albums,
List<Album>? filteredAlbums,
this.isLoading = false,
this.error,
}) : filteredAlbums = filteredAlbums ?? albums;
RemoteAlbumState copyWith({
List<Album>? albums,
List<Album>? filteredAlbums,
bool? isLoading,
String? error,
}) {
return RemoteAlbumState(
albums: albums ?? this.albums,
filteredAlbums: filteredAlbums ?? this.filteredAlbums,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
@override
String toString() =>
'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length}, isLoading: $isLoading, error: $error)';
@override
bool operator ==(covariant RemoteAlbumState other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.albums, albums) &&
listEquals(other.filteredAlbums, filteredAlbums) &&
other.isLoading == isLoading &&
other.error == error;
}
@override
int get hashCode =>
albums.hashCode ^
filteredAlbums.hashCode ^
isLoading.hashCode ^
error.hashCode;
}
class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
late final RemoteAlbumService _remoteAlbumService;
@override
RemoteAlbumState build() {
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
return const RemoteAlbumState(albums: [], filteredAlbums: []);
}
Future<List<Album>> getAll() async {
state = state.copyWith(isLoading: true, error: null);
try {
final albums = await _remoteAlbumService.getAll();
state = state.copyWith(
albums: albums,
filteredAlbums: albums,
isLoading: false,
);
return albums;
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
rethrow;
}
}
Future<void> refresh() async {
await getAll();
}
void searchAlbums(
String query,
String? userId, [
QuickFilterMode filterMode = QuickFilterMode.all,
]) {
final filtered = _remoteAlbumService.searchAlbums(
state.albums,
query,
userId,
filterMode,
);
state = state.copyWith(
filteredAlbums: filtered,
);
}
void clearSearch() {
state = state.copyWith(
filteredAlbums: state.albums,
);
}
void sortFilteredAlbums(
RemoteAlbumSortMode sortMode, {
bool isReverse = false,
}) {
final sortedAlbums = _remoteAlbumService
.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
state = state.copyWith(filteredAlbums: sortedAlbums);
}
}

View File

@@ -15,9 +15,17 @@ final timelineArgsProvider = Provider.autoDispose<TimelineArgs>(
throw UnimplementedError('Will be overridden through a ProviderScope.'),
);
final timelineServiceProvider = Provider.autoDispose<TimelineService>(
(ref) =>
throw UnimplementedError('Will be overridden through a ProviderScope.'),
final timelineServiceProvider = Provider<TimelineService>(
(ref) {
final timelineUsers = ref.watch(timelineUsersProvider).valueOrNull ?? [];
final timelineService =
ref.watch(timelineFactoryProvider).main(timelineUsers);
ref.onDispose(timelineService.dispose);
return timelineService;
},
// Empty dependencies to inform the framework that this provider
// might be used in a ProviderScope
dependencies: [],
);
final timelineFactoryProvider = Provider<TimelineFactory>(

View File

@@ -1,6 +1,5 @@
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@@ -14,9 +13,7 @@ final multiSelectProvider =
class MultiSelectState {
final Set<BaseAsset> selectedAssets;
const MultiSelectState({
required this.selectedAssets,
});
const MultiSelectState({required this.selectedAssets});
bool get isEnabled => selectedAssets.isNotEmpty;
bool get hasRemote => selectedAssets.any(
@@ -28,9 +25,7 @@ class MultiSelectState {
(asset) => asset.storage == AssetState.local,
);
MultiSelectState copyWith({
Set<BaseAsset>? selectedAssets,
}) {
MultiSelectState copyWith({Set<BaseAsset>? selectedAssets}) {
return MultiSelectState(
selectedAssets: selectedAssets ?? this.selectedAssets,
);
@@ -52,15 +47,11 @@ class MultiSelectState {
}
class MultiSelectNotifier extends Notifier<MultiSelectState> {
late final TimelineService _timelineService;
TimelineService get _timelineService => ref.read(timelineServiceProvider);
@override
MultiSelectState build() {
_timelineService = ref.read(timelineServiceProvider);
return const MultiSelectState(
selectedAssets: {},
);
return const MultiSelectState(selectedAssets: {});
}
void selectAsset(BaseAsset asset) {
@@ -91,10 +82,8 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
}
}
void clearSelection() {
state = state.copyWith(
selectedAssets: {},
);
void reset() {
state = const MultiSelectState(selectedAssets: {});
}
/// Bucket bulk operations
@@ -154,5 +143,5 @@ final bucketSelectionProvider = Provider.family<bool, List<BaseAsset>>(
// Check if all assets in the bucket are selected
return bucketAssets.every((asset) => selectedAssets.contains(asset));
},
dependencies: [multiSelectProvider],
dependencies: [multiSelectProvider, timelineServiceProvider],
);

View File

@@ -56,6 +56,15 @@ class AssetApiRepository extends ApiRepository {
);
}
Future<void> updateFavorite(
List<String> ids,
bool isFavorite,
) async {
return _api.updateAssets(
AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite),
);
}
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
AssetVisibilityEnum.hidden => AssetVisibility.hidden,

View File

@@ -69,6 +69,7 @@ import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/routing/auth_guard.dart';
@@ -172,7 +173,7 @@ class AppRouter extends RootStackRouter {
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: AlbumsRoute.page,
page: DriftAlbumsRoute.page,
guards: [_authGuard, _duplicateGuard],
),
],

View File

@@ -14,7 +14,7 @@ part of 'router.dart';
/// [ActivitiesPage]
class ActivitiesRoute extends PageRouteInfo<void> {
const ActivitiesRoute({List<PageRouteInfo>? children})
: super(ActivitiesRoute.name, initialChildren: children);
: super(ActivitiesRoute.name, initialChildren: children);
static const String name = 'ActivitiesRoute';
@@ -35,13 +35,13 @@ class AlbumAdditionalSharedUserSelectionRoute
required Album album,
List<PageRouteInfo>? children,
}) : super(
AlbumAdditionalSharedUserSelectionRoute.name,
args: AlbumAdditionalSharedUserSelectionRouteArgs(
key: key,
album: album,
),
initialChildren: children,
);
AlbumAdditionalSharedUserSelectionRoute.name,
args: AlbumAdditionalSharedUserSelectionRouteArgs(
key: key,
album: album,
),
initialChildren: children,
);
static const String name = 'AlbumAdditionalSharedUserSelectionRoute';
@@ -83,14 +83,14 @@ class AlbumAssetSelectionRoute
bool canDeselect = false,
List<PageRouteInfo>? children,
}) : super(
AlbumAssetSelectionRoute.name,
args: AlbumAssetSelectionRouteArgs(
key: key,
existingAssets: existingAssets,
canDeselect: canDeselect,
),
initialChildren: children,
);
AlbumAssetSelectionRoute.name,
args: AlbumAssetSelectionRouteArgs(
key: key,
existingAssets: existingAssets,
canDeselect: canDeselect,
),
initialChildren: children,
);
static const String name = 'AlbumAssetSelectionRoute';
@@ -130,7 +130,7 @@ class AlbumAssetSelectionRouteArgs {
/// [AlbumOptionsPage]
class AlbumOptionsRoute extends PageRouteInfo<void> {
const AlbumOptionsRoute({List<PageRouteInfo>? children})
: super(AlbumOptionsRoute.name, initialChildren: children);
: super(AlbumOptionsRoute.name, initialChildren: children);
static const String name = 'AlbumOptionsRoute';
@@ -150,10 +150,10 @@ class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
required Album album,
List<PageRouteInfo>? children,
}) : super(
AlbumPreviewRoute.name,
args: AlbumPreviewRouteArgs(key: key, album: album),
initialChildren: children,
);
AlbumPreviewRoute.name,
args: AlbumPreviewRouteArgs(key: key, album: album),
initialChildren: children,
);
static const String name = 'AlbumPreviewRoute';
@@ -188,10 +188,10 @@ class AlbumSharedUserSelectionRoute
required Set<Asset> assets,
List<PageRouteInfo>? children,
}) : super(
AlbumSharedUserSelectionRoute.name,
args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets),
initialChildren: children,
);
AlbumSharedUserSelectionRoute.name,
args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets),
initialChildren: children,
);
static const String name = 'AlbumSharedUserSelectionRoute';
@@ -225,10 +225,10 @@ class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> {
required int albumId,
List<PageRouteInfo>? children,
}) : super(
AlbumViewerRoute.name,
args: AlbumViewerRouteArgs(key: key, albumId: albumId),
initialChildren: children,
);
AlbumViewerRoute.name,
args: AlbumViewerRouteArgs(key: key, albumId: albumId),
initialChildren: children,
);
static const String name = 'AlbumViewerRoute';
@@ -258,7 +258,7 @@ class AlbumViewerRouteArgs {
/// [AlbumsPage]
class AlbumsRoute extends PageRouteInfo<void> {
const AlbumsRoute({List<PageRouteInfo>? children})
: super(AlbumsRoute.name, initialChildren: children);
: super(AlbumsRoute.name, initialChildren: children);
static const String name = 'AlbumsRoute';
@@ -274,7 +274,7 @@ class AlbumsRoute extends PageRouteInfo<void> {
/// [AllMotionPhotosPage]
class AllMotionPhotosRoute extends PageRouteInfo<void> {
const AllMotionPhotosRoute({List<PageRouteInfo>? children})
: super(AllMotionPhotosRoute.name, initialChildren: children);
: super(AllMotionPhotosRoute.name, initialChildren: children);
static const String name = 'AllMotionPhotosRoute';
@@ -290,7 +290,7 @@ class AllMotionPhotosRoute extends PageRouteInfo<void> {
/// [AllPeoplePage]
class AllPeopleRoute extends PageRouteInfo<void> {
const AllPeopleRoute({List<PageRouteInfo>? children})
: super(AllPeopleRoute.name, initialChildren: children);
: super(AllPeopleRoute.name, initialChildren: children);
static const String name = 'AllPeopleRoute';
@@ -306,7 +306,7 @@ class AllPeopleRoute extends PageRouteInfo<void> {
/// [AllPlacesPage]
class AllPlacesRoute extends PageRouteInfo<void> {
const AllPlacesRoute({List<PageRouteInfo>? children})
: super(AllPlacesRoute.name, initialChildren: children);
: super(AllPlacesRoute.name, initialChildren: children);
static const String name = 'AllPlacesRoute';
@@ -322,7 +322,7 @@ class AllPlacesRoute extends PageRouteInfo<void> {
/// [AllVideosPage]
class AllVideosRoute extends PageRouteInfo<void> {
const AllVideosRoute({List<PageRouteInfo>? children})
: super(AllVideosRoute.name, initialChildren: children);
: super(AllVideosRoute.name, initialChildren: children);
static const String name = 'AllVideosRoute';
@@ -342,10 +342,10 @@ class AppLogDetailRoute extends PageRouteInfo<AppLogDetailRouteArgs> {
required LogMessage logMessage,
List<PageRouteInfo>? children,
}) : super(
AppLogDetailRoute.name,
args: AppLogDetailRouteArgs(key: key, logMessage: logMessage),
initialChildren: children,
);
AppLogDetailRoute.name,
args: AppLogDetailRouteArgs(key: key, logMessage: logMessage),
initialChildren: children,
);
static const String name = 'AppLogDetailRoute';
@@ -375,7 +375,7 @@ class AppLogDetailRouteArgs {
/// [AppLogPage]
class AppLogRoute extends PageRouteInfo<void> {
const AppLogRoute({List<PageRouteInfo>? children})
: super(AppLogRoute.name, initialChildren: children);
: super(AppLogRoute.name, initialChildren: children);
static const String name = 'AppLogRoute';
@@ -391,7 +391,7 @@ class AppLogRoute extends PageRouteInfo<void> {
/// [ArchivePage]
class ArchiveRoute extends PageRouteInfo<void> {
const ArchiveRoute({List<PageRouteInfo>? children})
: super(ArchiveRoute.name, initialChildren: children);
: super(ArchiveRoute.name, initialChildren: children);
static const String name = 'ArchiveRoute';
@@ -407,7 +407,7 @@ class ArchiveRoute extends PageRouteInfo<void> {
/// [BackupAlbumSelectionPage]
class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
const BackupAlbumSelectionRoute({List<PageRouteInfo>? children})
: super(BackupAlbumSelectionRoute.name, initialChildren: children);
: super(BackupAlbumSelectionRoute.name, initialChildren: children);
static const String name = 'BackupAlbumSelectionRoute';
@@ -423,7 +423,7 @@ class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
/// [BackupControllerPage]
class BackupControllerRoute extends PageRouteInfo<void> {
const BackupControllerRoute({List<PageRouteInfo>? children})
: super(BackupControllerRoute.name, initialChildren: children);
: super(BackupControllerRoute.name, initialChildren: children);
static const String name = 'BackupControllerRoute';
@@ -439,7 +439,7 @@ class BackupControllerRoute extends PageRouteInfo<void> {
/// [BackupOptionsPage]
class BackupOptionsRoute extends PageRouteInfo<void> {
const BackupOptionsRoute({List<PageRouteInfo>? children})
: super(BackupOptionsRoute.name, initialChildren: children);
: super(BackupOptionsRoute.name, initialChildren: children);
static const String name = 'BackupOptionsRoute';
@@ -455,7 +455,7 @@ class BackupOptionsRoute extends PageRouteInfo<void> {
/// [ChangePasswordPage]
class ChangePasswordRoute extends PageRouteInfo<void> {
const ChangePasswordRoute({List<PageRouteInfo>? children})
: super(ChangePasswordRoute.name, initialChildren: children);
: super(ChangePasswordRoute.name, initialChildren: children);
static const String name = 'ChangePasswordRoute';
@@ -475,10 +475,10 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
List<Asset>? assets,
List<PageRouteInfo>? children,
}) : super(
CreateAlbumRoute.name,
args: CreateAlbumRouteArgs(key: key, assets: assets),
initialChildren: children,
);
CreateAlbumRoute.name,
args: CreateAlbumRouteArgs(key: key, assets: assets),
initialChildren: children,
);
static const String name = 'CreateAlbumRoute';
@@ -515,10 +515,10 @@ class CropImageRoute extends PageRouteInfo<CropImageRouteArgs> {
required Asset asset,
List<PageRouteInfo>? children,
}) : super(
CropImageRoute.name,
args: CropImageRouteArgs(key: key, image: image, asset: asset),
initialChildren: children,
);
CropImageRoute.name,
args: CropImageRouteArgs(key: key, image: image, asset: asset),
initialChildren: children,
);
static const String name = 'CropImageRoute';
@@ -550,6 +550,22 @@ class CropImageRouteArgs {
}
}
/// generated route for
/// [DriftAlbumsPage]
class DriftAlbumsRoute extends PageRouteInfo<void> {
const DriftAlbumsRoute({List<PageRouteInfo>? children})
: super(DriftAlbumsRoute.name, initialChildren: children);
static const String name = 'DriftAlbumsRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftAlbumsPage();
},
);
}
/// generated route for
/// [EditImagePage]
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
@@ -560,15 +576,15 @@ class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
required bool isEdited,
List<PageRouteInfo>? children,
}) : super(
EditImageRoute.name,
args: EditImageRouteArgs(
key: key,
asset: asset,
image: image,
isEdited: isEdited,
),
initialChildren: children,
);
EditImageRoute.name,
args: EditImageRouteArgs(
key: key,
asset: asset,
image: image,
isEdited: isEdited,
),
initialChildren: children,
);
static const String name = 'EditImageRoute';
@@ -612,7 +628,7 @@ class EditImageRouteArgs {
/// [FailedBackupStatusPage]
class FailedBackupStatusRoute extends PageRouteInfo<void> {
const FailedBackupStatusRoute({List<PageRouteInfo>? children})
: super(FailedBackupStatusRoute.name, initialChildren: children);
: super(FailedBackupStatusRoute.name, initialChildren: children);
static const String name = 'FailedBackupStatusRoute';
@@ -628,7 +644,7 @@ class FailedBackupStatusRoute extends PageRouteInfo<void> {
/// [FavoritesPage]
class FavoritesRoute extends PageRouteInfo<void> {
const FavoritesRoute({List<PageRouteInfo>? children})
: super(FavoritesRoute.name, initialChildren: children);
: super(FavoritesRoute.name, initialChildren: children);
static const String name = 'FavoritesRoute';
@@ -644,7 +660,7 @@ class FavoritesRoute extends PageRouteInfo<void> {
/// [FeatInDevPage]
class FeatInDevRoute extends PageRouteInfo<void> {
const FeatInDevRoute({List<PageRouteInfo>? children})
: super(FeatInDevRoute.name, initialChildren: children);
: super(FeatInDevRoute.name, initialChildren: children);
static const String name = 'FeatInDevRoute';
@@ -665,10 +681,10 @@ class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> {
required Asset asset,
List<PageRouteInfo>? children,
}) : super(
FilterImageRoute.name,
args: FilterImageRouteArgs(key: key, image: image, asset: asset),
initialChildren: children,
);
FilterImageRoute.name,
args: FilterImageRouteArgs(key: key, image: image, asset: asset),
initialChildren: children,
);
static const String name = 'FilterImageRoute';
@@ -712,10 +728,10 @@ class FolderRoute extends PageRouteInfo<FolderRouteArgs> {
RecursiveFolder? folder,
List<PageRouteInfo>? children,
}) : super(
FolderRoute.name,
args: FolderRouteArgs(key: key, folder: folder),
initialChildren: children,
);
FolderRoute.name,
args: FolderRouteArgs(key: key, folder: folder),
initialChildren: children,
);
static const String name = 'FolderRoute';
@@ -754,16 +770,16 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
bool showStack = false,
List<PageRouteInfo>? children,
}) : super(
GalleryViewerRoute.name,
args: GalleryViewerRouteArgs(
key: key,
renderList: renderList,
initialIndex: initialIndex,
heroOffset: heroOffset,
showStack: showStack,
),
initialChildren: children,
);
GalleryViewerRoute.name,
args: GalleryViewerRouteArgs(
key: key,
renderList: renderList,
initialIndex: initialIndex,
heroOffset: heroOffset,
showStack: showStack,
),
initialChildren: children,
);
static const String name = 'GalleryViewerRoute';
@@ -811,7 +827,7 @@ class GalleryViewerRouteArgs {
/// [HeaderSettingsPage]
class HeaderSettingsRoute extends PageRouteInfo<void> {
const HeaderSettingsRoute({List<PageRouteInfo>? children})
: super(HeaderSettingsRoute.name, initialChildren: children);
: super(HeaderSettingsRoute.name, initialChildren: children);
static const String name = 'HeaderSettingsRoute';
@@ -827,7 +843,7 @@ class HeaderSettingsRoute extends PageRouteInfo<void> {
/// [LibraryPage]
class LibraryRoute extends PageRouteInfo<void> {
const LibraryRoute({List<PageRouteInfo>? children})
: super(LibraryRoute.name, initialChildren: children);
: super(LibraryRoute.name, initialChildren: children);
static const String name = 'LibraryRoute';
@@ -843,7 +859,7 @@ class LibraryRoute extends PageRouteInfo<void> {
/// [LocalAlbumsPage]
class LocalAlbumsRoute extends PageRouteInfo<void> {
const LocalAlbumsRoute({List<PageRouteInfo>? children})
: super(LocalAlbumsRoute.name, initialChildren: children);
: super(LocalAlbumsRoute.name, initialChildren: children);
static const String name = 'LocalAlbumsRoute';
@@ -859,7 +875,7 @@ class LocalAlbumsRoute extends PageRouteInfo<void> {
/// [LocalMediaSummaryPage]
class LocalMediaSummaryRoute extends PageRouteInfo<void> {
const LocalMediaSummaryRoute({List<PageRouteInfo>? children})
: super(LocalMediaSummaryRoute.name, initialChildren: children);
: super(LocalMediaSummaryRoute.name, initialChildren: children);
static const String name = 'LocalMediaSummaryRoute';
@@ -879,10 +895,10 @@ class LocalTimelineRoute extends PageRouteInfo<LocalTimelineRouteArgs> {
required String albumId,
List<PageRouteInfo>? children,
}) : super(
LocalTimelineRoute.name,
args: LocalTimelineRouteArgs(key: key, albumId: albumId),
initialChildren: children,
);
LocalTimelineRoute.name,
args: LocalTimelineRouteArgs(key: key, albumId: albumId),
initialChildren: children,
);
static const String name = 'LocalTimelineRoute';
@@ -912,7 +928,7 @@ class LocalTimelineRouteArgs {
/// [LockedPage]
class LockedRoute extends PageRouteInfo<void> {
const LockedRoute({List<PageRouteInfo>? children})
: super(LockedRoute.name, initialChildren: children);
: super(LockedRoute.name, initialChildren: children);
static const String name = 'LockedRoute';
@@ -928,7 +944,7 @@ class LockedRoute extends PageRouteInfo<void> {
/// [LoginPage]
class LoginRoute extends PageRouteInfo<void> {
const LoginRoute({List<PageRouteInfo>? children})
: super(LoginRoute.name, initialChildren: children);
: super(LoginRoute.name, initialChildren: children);
static const String name = 'LoginRoute';
@@ -944,7 +960,7 @@ class LoginRoute extends PageRouteInfo<void> {
/// [MainTimelinePage]
class MainTimelineRoute extends PageRouteInfo<void> {
const MainTimelineRoute({List<PageRouteInfo>? children})
: super(MainTimelineRoute.name, initialChildren: children);
: super(MainTimelineRoute.name, initialChildren: children);
static const String name = 'MainTimelineRoute';
@@ -964,13 +980,13 @@ class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
LatLng initialLatLng = const LatLng(0, 0),
List<PageRouteInfo>? children,
}) : super(
MapLocationPickerRoute.name,
args: MapLocationPickerRouteArgs(
key: key,
initialLatLng: initialLatLng,
),
initialChildren: children,
);
MapLocationPickerRoute.name,
args: MapLocationPickerRouteArgs(
key: key,
initialLatLng: initialLatLng,
),
initialChildren: children,
);
static const String name = 'MapLocationPickerRoute';
@@ -1008,11 +1024,11 @@ class MapLocationPickerRouteArgs {
/// [MapPage]
class MapRoute extends PageRouteInfo<MapRouteArgs> {
MapRoute({Key? key, LatLng? initialLocation, List<PageRouteInfo>? children})
: super(
MapRoute.name,
args: MapRouteArgs(key: key, initialLocation: initialLocation),
initialChildren: children,
);
: super(
MapRoute.name,
args: MapRouteArgs(key: key, initialLocation: initialLocation),
initialChildren: children,
);
static const String name = 'MapRoute';
@@ -1049,14 +1065,14 @@ class MemoryRoute extends PageRouteInfo<MemoryRouteArgs> {
Key? key,
List<PageRouteInfo>? children,
}) : super(
MemoryRoute.name,
args: MemoryRouteArgs(
memories: memories,
memoryIndex: memoryIndex,
key: key,
),
initialChildren: children,
);
MemoryRoute.name,
args: MemoryRouteArgs(
memories: memories,
memoryIndex: memoryIndex,
key: key,
),
initialChildren: children,
);
static const String name = 'MemoryRoute';
@@ -1103,16 +1119,16 @@ class NativeVideoViewerRoute extends PageRouteInfo<NativeVideoViewerRouteArgs> {
int playbackDelayFactor = 1,
List<PageRouteInfo>? children,
}) : super(
NativeVideoViewerRoute.name,
args: NativeVideoViewerRouteArgs(
key: key,
asset: asset,
image: image,
showControls: showControls,
playbackDelayFactor: playbackDelayFactor,
),
initialChildren: children,
);
NativeVideoViewerRoute.name,
args: NativeVideoViewerRouteArgs(
key: key,
asset: asset,
image: image,
showControls: showControls,
playbackDelayFactor: playbackDelayFactor,
),
initialChildren: children,
);
static const String name = 'NativeVideoViewerRoute';
@@ -1164,10 +1180,10 @@ class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> {
required UserDto partner,
List<PageRouteInfo>? children,
}) : super(
PartnerDetailRoute.name,
args: PartnerDetailRouteArgs(key: key, partner: partner),
initialChildren: children,
);
PartnerDetailRoute.name,
args: PartnerDetailRouteArgs(key: key, partner: partner),
initialChildren: children,
);
static const String name = 'PartnerDetailRoute';
@@ -1197,7 +1213,7 @@ class PartnerDetailRouteArgs {
/// [PartnerPage]
class PartnerRoute extends PageRouteInfo<void> {
const PartnerRoute({List<PageRouteInfo>? children})
: super(PartnerRoute.name, initialChildren: children);
: super(PartnerRoute.name, initialChildren: children);
static const String name = 'PartnerRoute';
@@ -1213,7 +1229,7 @@ class PartnerRoute extends PageRouteInfo<void> {
/// [PeopleCollectionPage]
class PeopleCollectionRoute extends PageRouteInfo<void> {
const PeopleCollectionRoute({List<PageRouteInfo>? children})
: super(PeopleCollectionRoute.name, initialChildren: children);
: super(PeopleCollectionRoute.name, initialChildren: children);
static const String name = 'PeopleCollectionRoute';
@@ -1229,7 +1245,7 @@ class PeopleCollectionRoute extends PageRouteInfo<void> {
/// [PermissionOnboardingPage]
class PermissionOnboardingRoute extends PageRouteInfo<void> {
const PermissionOnboardingRoute({List<PageRouteInfo>? children})
: super(PermissionOnboardingRoute.name, initialChildren: children);
: super(PermissionOnboardingRoute.name, initialChildren: children);
static const String name = 'PermissionOnboardingRoute';
@@ -1250,14 +1266,14 @@ class PersonResultRoute extends PageRouteInfo<PersonResultRouteArgs> {
required String personName,
List<PageRouteInfo>? children,
}) : super(
PersonResultRoute.name,
args: PersonResultRouteArgs(
key: key,
personId: personId,
personName: personName,
),
initialChildren: children,
);
PersonResultRoute.name,
args: PersonResultRouteArgs(
key: key,
personId: personId,
personName: personName,
),
initialChildren: children,
);
static const String name = 'PersonResultRoute';
@@ -1297,7 +1313,7 @@ class PersonResultRouteArgs {
/// [PhotosPage]
class PhotosRoute extends PageRouteInfo<void> {
const PhotosRoute({List<PageRouteInfo>? children})
: super(PhotosRoute.name, initialChildren: children);
: super(PhotosRoute.name, initialChildren: children);
static const String name = 'PhotosRoute';
@@ -1317,10 +1333,10 @@ class PinAuthRoute extends PageRouteInfo<PinAuthRouteArgs> {
bool createPinCode = false,
List<PageRouteInfo>? children,
}) : super(
PinAuthRoute.name,
args: PinAuthRouteArgs(key: key, createPinCode: createPinCode),
initialChildren: children,
);
PinAuthRoute.name,
args: PinAuthRouteArgs(key: key, createPinCode: createPinCode),
initialChildren: children,
);
static const String name = 'PinAuthRoute';
@@ -1356,13 +1372,13 @@ class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
LatLng? currentLocation,
List<PageRouteInfo>? children,
}) : super(
PlacesCollectionRoute.name,
args: PlacesCollectionRouteArgs(
key: key,
currentLocation: currentLocation,
),
initialChildren: children,
);
PlacesCollectionRoute.name,
args: PlacesCollectionRouteArgs(
key: key,
currentLocation: currentLocation,
),
initialChildren: children,
);
static const String name = 'PlacesCollectionRoute';
@@ -1397,7 +1413,7 @@ class PlacesCollectionRouteArgs {
/// [RecentlyTakenPage]
class RecentlyTakenRoute extends PageRouteInfo<void> {
const RecentlyTakenRoute({List<PageRouteInfo>? children})
: super(RecentlyTakenRoute.name, initialChildren: children);
: super(RecentlyTakenRoute.name, initialChildren: children);
static const String name = 'RecentlyTakenRoute';
@@ -1413,7 +1429,7 @@ class RecentlyTakenRoute extends PageRouteInfo<void> {
/// [RemoteMediaSummaryPage]
class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
const RemoteMediaSummaryRoute({List<PageRouteInfo>? children})
: super(RemoteMediaSummaryRoute.name, initialChildren: children);
: super(RemoteMediaSummaryRoute.name, initialChildren: children);
static const String name = 'RemoteMediaSummaryRoute';
@@ -1433,10 +1449,10 @@ class RemoteTimelineRoute extends PageRouteInfo<RemoteTimelineRouteArgs> {
required String albumId,
List<PageRouteInfo>? children,
}) : super(
RemoteTimelineRoute.name,
args: RemoteTimelineRouteArgs(key: key, albumId: albumId),
initialChildren: children,
);
RemoteTimelineRoute.name,
args: RemoteTimelineRouteArgs(key: key, albumId: albumId),
initialChildren: children,
);
static const String name = 'RemoteTimelineRoute';
@@ -1470,10 +1486,10 @@ class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
SearchFilter? prefilter,
List<PageRouteInfo>? children,
}) : super(
SearchRoute.name,
args: SearchRouteArgs(key: key, prefilter: prefilter),
initialChildren: children,
);
SearchRoute.name,
args: SearchRouteArgs(key: key, prefilter: prefilter),
initialChildren: children,
);
static const String name = 'SearchRoute';
@@ -1505,7 +1521,7 @@ class SearchRouteArgs {
/// [SettingsPage]
class SettingsRoute extends PageRouteInfo<void> {
const SettingsRoute({List<PageRouteInfo>? children})
: super(SettingsRoute.name, initialChildren: children);
: super(SettingsRoute.name, initialChildren: children);
static const String name = 'SettingsRoute';
@@ -1525,10 +1541,10 @@ class SettingsSubRoute extends PageRouteInfo<SettingsSubRouteArgs> {
Key? key,
List<PageRouteInfo>? children,
}) : super(
SettingsSubRoute.name,
args: SettingsSubRouteArgs(section: section, key: key),
initialChildren: children,
);
SettingsSubRoute.name,
args: SettingsSubRouteArgs(section: section, key: key),
initialChildren: children,
);
static const String name = 'SettingsSubRoute';
@@ -1562,10 +1578,10 @@ class ShareIntentRoute extends PageRouteInfo<ShareIntentRouteArgs> {
required List<ShareIntentAttachment> attachments,
List<PageRouteInfo>? children,
}) : super(
ShareIntentRoute.name,
args: ShareIntentRouteArgs(key: key, attachments: attachments),
initialChildren: children,
);
ShareIntentRoute.name,
args: ShareIntentRouteArgs(key: key, attachments: attachments),
initialChildren: children,
);
static const String name = 'ShareIntentRoute';
@@ -1601,15 +1617,15 @@ class SharedLinkEditRoute extends PageRouteInfo<SharedLinkEditRouteArgs> {
String? albumId,
List<PageRouteInfo>? children,
}) : super(
SharedLinkEditRoute.name,
args: SharedLinkEditRouteArgs(
key: key,
existingLink: existingLink,
assetsList: assetsList,
albumId: albumId,
),
initialChildren: children,
);
SharedLinkEditRoute.name,
args: SharedLinkEditRouteArgs(
key: key,
existingLink: existingLink,
assetsList: assetsList,
albumId: albumId,
),
initialChildren: children,
);
static const String name = 'SharedLinkEditRoute';
@@ -1655,7 +1671,7 @@ class SharedLinkEditRouteArgs {
/// [SharedLinkPage]
class SharedLinkRoute extends PageRouteInfo<void> {
const SharedLinkRoute({List<PageRouteInfo>? children})
: super(SharedLinkRoute.name, initialChildren: children);
: super(SharedLinkRoute.name, initialChildren: children);
static const String name = 'SharedLinkRoute';
@@ -1671,7 +1687,7 @@ class SharedLinkRoute extends PageRouteInfo<void> {
/// [SplashScreenPage]
class SplashScreenRoute extends PageRouteInfo<void> {
const SplashScreenRoute({List<PageRouteInfo>? children})
: super(SplashScreenRoute.name, initialChildren: children);
: super(SplashScreenRoute.name, initialChildren: children);
static const String name = 'SplashScreenRoute';
@@ -1687,7 +1703,7 @@ class SplashScreenRoute extends PageRouteInfo<void> {
/// [TabControllerPage]
class TabControllerRoute extends PageRouteInfo<void> {
const TabControllerRoute({List<PageRouteInfo>? children})
: super(TabControllerRoute.name, initialChildren: children);
: super(TabControllerRoute.name, initialChildren: children);
static const String name = 'TabControllerRoute';
@@ -1703,7 +1719,7 @@ class TabControllerRoute extends PageRouteInfo<void> {
/// [TabShellPage]
class TabShellRoute extends PageRouteInfo<void> {
const TabShellRoute({List<PageRouteInfo>? children})
: super(TabShellRoute.name, initialChildren: children);
: super(TabShellRoute.name, initialChildren: children);
static const String name = 'TabShellRoute';
@@ -1719,7 +1735,7 @@ class TabShellRoute extends PageRouteInfo<void> {
/// [TrashPage]
class TrashRoute extends PageRouteInfo<void> {
const TrashRoute({List<PageRouteInfo>? children})
: super(TrashRoute.name, initialChildren: children);
: super(TrashRoute.name, initialChildren: children);
static const String name = 'TrashRoute';

View File

@@ -0,0 +1,84 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
final actionServiceProvider = Provider<ActionService>(
(ref) => ActionService(
ref.watch(assetApiRepositoryProvider),
ref.watch(remoteAssetRepositoryProvider),
),
);
class ActionService {
final AssetApiRepository _assetApiRepository;
final RemoteAssetRepository _remoteAssetRepository;
const ActionService(this._assetApiRepository, this._remoteAssetRepository);
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
context.pushRoute(
SharedLinkEditRoute(
assetsList: remoteIds,
),
);
}
Future<void> favorite(List<String> remoteIds) async {
await _assetApiRepository.updateFavorite(remoteIds, true);
await _remoteAssetRepository.updateFavorite(remoteIds, true);
}
Future<void> unFavorite(List<String> remoteIds) async {
await _assetApiRepository.updateFavorite(remoteIds, false);
await _remoteAssetRepository.updateFavorite(remoteIds, false);
}
Future<void> archive(List<String> remoteIds) async {
await _assetApiRepository.updateVisibility(
remoteIds,
AssetVisibilityEnum.archive,
);
await _remoteAssetRepository.updateVisibility(
remoteIds,
AssetVisibility.archive,
);
}
Future<void> unArchive(List<String> remoteIds) async {
await _assetApiRepository.updateVisibility(
remoteIds,
AssetVisibilityEnum.timeline,
);
await _remoteAssetRepository.updateVisibility(
remoteIds,
AssetVisibility.timeline,
);
}
Future<void> moveToLockFolder(List<String> remoteIds) async {
await _assetApiRepository.updateVisibility(
remoteIds,
AssetVisibilityEnum.locked,
);
await _remoteAssetRepository.updateVisibility(
remoteIds,
AssetVisibility.locked,
);
}
Future<void> removeFromLockFolder(List<String> remoteIds) async {
await _assetApiRepository.updateVisibility(
remoteIds,
AssetVisibilityEnum.timeline,
);
await _remoteAssetRepository.updateVisibility(
remoteIds,
AssetVisibility.timeline,
);
}
}

View File

@@ -13,12 +13,10 @@ import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/entities/isar_store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/drift_store.repository.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
@@ -50,32 +48,15 @@ abstract final class Bootstrap {
);
}
static Future<Drift> initDrift() async {
return Drift();
}
static Future<void> initDomain(
Isar db, {
bool shouldBufferLogs = true,
}) async {
final driftDb = Drift();
await StoreService.init(storeRepository: IsarStoreRepository(db));
await LogService.init(
logRepository: LogRepository(driftDb),
logRepository: IsarLogRepository(db),
storeRepository: IsarStoreRepository(db),
shouldBuffer: shouldBufferLogs,
);
}
static Future<void> initDomainWithDrift(
Drift db, {
bool shouldBufferLogs = true,
}) async {
await StoreService.init(storeRepository: DriftStoreRepository(db));
await LogService.init(
logRepository: LogRepository(db),
storeRepository: DriftStoreRepository(db),
shouldBuffer: shouldBufferLogs,
);
}
}

View File

@@ -14,8 +14,8 @@ import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/isar_store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/utils/diff.dart';

View File

@@ -0,0 +1,71 @@
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
typedef AlbumSortFn = List<Album> Function(List<Album> albums, bool isReverse);
class _RemoteAlbumSortHandlers {
const _RemoteAlbumSortHandlers._();
static const AlbumSortFn created = _sortByCreated;
static List<Album> _sortByCreated(List<Album> albums, bool isReverse) {
final sorted = albums.sortedBy((album) => album.createdAt);
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn title = _sortByTitle;
static List<Album> _sortByTitle(List<Album> albums, bool isReverse) {
final sorted = albums.sortedBy((album) => album.name);
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn lastModified = _sortByLastModified;
static List<Album> _sortByLastModified(List<Album> albums, bool isReverse) {
final sorted = albums.sortedBy((album) => album.updatedAt);
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn assetCount = _sortByAssetCount;
static List<Album> _sortByAssetCount(List<Album> albums, bool isReverse) {
final sorted =
albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount));
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn mostRecent = _sortByMostRecent;
static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
final sorted = albums.sorted((a, b) {
// For most recent, we sort by updatedAt in descending order
return b.updatedAt.compareTo(a.updatedAt);
});
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn mostOldest = _sortByMostOldest;
static List<Album> _sortByMostOldest(List<Album> albums, bool isReverse) {
final sorted = albums.sorted((a, b) {
// For oldest, we sort by createdAt in ascending order
return a.createdAt.compareTo(b.createdAt);
});
return (isReverse ? sorted.reversed : sorted).toList();
}
}
enum RemoteAlbumSortMode {
title("library_page_sort_title", _RemoteAlbumSortHandlers.title),
assetCount(
"library_page_sort_asset_count",
_RemoteAlbumSortHandlers.assetCount,
),
lastModified(
"library_page_sort_last_modified",
_RemoteAlbumSortHandlers.lastModified,
),
created("library_page_sort_created", _RemoteAlbumSortHandlers.created),
mostRecent("sort_recent", _RemoteAlbumSortHandlers.mostRecent),
mostOldest("sort_oldest", _RemoteAlbumSortHandlers.mostOldest);
final String key;
final AlbumSortFn sortFn;
const RemoteAlbumSortMode(this.key, this.sortFn);
}

View File

@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/models/server_info/server_info.model.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -62,8 +63,14 @@ class ImmichSliverAppBar extends ConsumerWidget {
),
),
IconButton(
icon: const Icon(Icons.science_rounded),
onPressed: () => context.pushRoute(const FeatInDevRoute()),
icon: const Icon(Icons.swipe_left_alt_rounded),
onPressed: () => context.pop(),
),
IconButton(
onPressed: () => ref.read(backgroundSyncProvider).syncRemote(),
icon: const Icon(
Icons.sync,
),
),
if (isCasting)
Padding(

View File

@@ -482,6 +482,8 @@ Class | Method | HTTP request | Description
- [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md)
- [SyncPartnerV1](doc//SyncPartnerV1.md)
- [SyncRequestType](doc//SyncRequestType.md)
- [SyncStackDeleteV1](doc//SyncStackDeleteV1.md)
- [SyncStackV1](doc//SyncStackV1.md)
- [SyncStreamDto](doc//SyncStreamDto.md)
- [SyncUserDeleteV1](doc//SyncUserDeleteV1.md)
- [SyncUserV1](doc//SyncUserV1.md)

View File

@@ -265,6 +265,8 @@ part 'model/sync_memory_v1.dart';
part 'model/sync_partner_delete_v1.dart';
part 'model/sync_partner_v1.dart';
part 'model/sync_request_type.dart';
part 'model/sync_stack_delete_v1.dart';
part 'model/sync_stack_v1.dart';
part 'model/sync_stream_dto.dart';
part 'model/sync_user_delete_v1.dart';
part 'model/sync_user_v1.dart';

View File

@@ -586,6 +586,10 @@ class ApiClient {
return SyncPartnerV1.fromJson(value);
case 'SyncRequestType':
return SyncRequestTypeTypeTransformer().decode(value);
case 'SyncStackDeleteV1':
return SyncStackDeleteV1.fromJson(value);
case 'SyncStackV1':
return SyncStackV1.fromJson(value);
case 'SyncStreamDto':
return SyncStreamDto.fromJson(value);
case 'SyncUserDeleteV1':

View File

@@ -35,6 +35,9 @@ class SyncEntityType {
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1');
static const partnerStackBackfillV1 = SyncEntityType._(r'PartnerStackBackfillV1');
static const partnerStackDeleteV1 = SyncEntityType._(r'PartnerStackDeleteV1');
static const partnerStackV1 = SyncEntityType._(r'PartnerStackV1');
static const albumV1 = SyncEntityType._(r'AlbumV1');
static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1');
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
@@ -51,6 +54,8 @@ class SyncEntityType {
static const memoryDeleteV1 = SyncEntityType._(r'MemoryDeleteV1');
static const memoryToAssetV1 = SyncEntityType._(r'MemoryToAssetV1');
static const memoryToAssetDeleteV1 = SyncEntityType._(r'MemoryToAssetDeleteV1');
static const stackV1 = SyncEntityType._(r'StackV1');
static const stackDeleteV1 = SyncEntityType._(r'StackDeleteV1');
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
/// List of all possible values in this [enum][SyncEntityType].
@@ -67,6 +72,9 @@ class SyncEntityType {
partnerAssetDeleteV1,
partnerAssetExifV1,
partnerAssetExifBackfillV1,
partnerStackBackfillV1,
partnerStackDeleteV1,
partnerStackV1,
albumV1,
albumDeleteV1,
albumUserV1,
@@ -83,6 +91,8 @@ class SyncEntityType {
memoryDeleteV1,
memoryToAssetV1,
memoryToAssetDeleteV1,
stackV1,
stackDeleteV1,
syncAckV1,
];
@@ -134,6 +144,9 @@ class SyncEntityTypeTypeTransformer {
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1;
case r'PartnerStackBackfillV1': return SyncEntityType.partnerStackBackfillV1;
case r'PartnerStackDeleteV1': return SyncEntityType.partnerStackDeleteV1;
case r'PartnerStackV1': return SyncEntityType.partnerStackV1;
case r'AlbumV1': return SyncEntityType.albumV1;
case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1;
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
@@ -150,6 +163,8 @@ class SyncEntityTypeTypeTransformer {
case r'MemoryDeleteV1': return SyncEntityType.memoryDeleteV1;
case r'MemoryToAssetV1': return SyncEntityType.memoryToAssetV1;
case r'MemoryToAssetDeleteV1': return SyncEntityType.memoryToAssetDeleteV1;
case r'StackV1': return SyncEntityType.stackV1;
case r'StackDeleteV1': return SyncEntityType.stackDeleteV1;
case r'SyncAckV1': return SyncEntityType.syncAckV1;
default:
if (!allowNull) {

View File

@@ -23,35 +23,39 @@ class SyncRequestType {
String toJson() => value;
static const usersV1 = SyncRequestType._(r'UsersV1');
static const partnersV1 = SyncRequestType._(r'PartnersV1');
static const assetsV1 = SyncRequestType._(r'AssetsV1');
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
static const albumsV1 = SyncRequestType._(r'AlbumsV1');
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
static const assetsV1 = SyncRequestType._(r'AssetsV1');
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
static const memoriesV1 = SyncRequestType._(r'MemoriesV1');
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
static const partnersV1 = SyncRequestType._(r'PartnersV1');
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1');
static const stacksV1 = SyncRequestType._(r'StacksV1');
static const usersV1 = SyncRequestType._(r'UsersV1');
/// List of all possible values in this [enum][SyncRequestType].
static const values = <SyncRequestType>[
usersV1,
partnersV1,
assetsV1,
assetExifsV1,
partnerAssetsV1,
partnerAssetExifsV1,
albumsV1,
albumUsersV1,
albumToAssetsV1,
albumAssetsV1,
albumAssetExifsV1,
assetsV1,
assetExifsV1,
memoriesV1,
memoryToAssetsV1,
partnersV1,
partnerAssetsV1,
partnerAssetExifsV1,
partnerStacksV1,
stacksV1,
usersV1,
];
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
@@ -90,19 +94,21 @@ class SyncRequestTypeTypeTransformer {
SyncRequestType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'UsersV1': return SyncRequestType.usersV1;
case r'PartnersV1': return SyncRequestType.partnersV1;
case r'AssetsV1': return SyncRequestType.assetsV1;
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
case r'AlbumsV1': return SyncRequestType.albumsV1;
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
case r'AssetsV1': return SyncRequestType.assetsV1;
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
case r'MemoriesV1': return SyncRequestType.memoriesV1;
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
case r'PartnersV1': return SyncRequestType.partnersV1;
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1;
case r'StacksV1': return SyncRequestType.stacksV1;
case r'UsersV1': return SyncRequestType.usersV1;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');

View File

@@ -0,0 +1,99 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncStackDeleteV1 {
/// Returns a new [SyncStackDeleteV1] instance.
SyncStackDeleteV1({
required this.stackId,
});
String stackId;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncStackDeleteV1 &&
other.stackId == stackId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(stackId.hashCode);
@override
String toString() => 'SyncStackDeleteV1[stackId=$stackId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'stackId'] = this.stackId;
return json;
}
/// Returns a new [SyncStackDeleteV1] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncStackDeleteV1? fromJson(dynamic value) {
upgradeDto(value, "SyncStackDeleteV1");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncStackDeleteV1(
stackId: mapValueOfType<String>(json, r'stackId')!,
);
}
return null;
}
static List<SyncStackDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncStackDeleteV1>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncStackDeleteV1.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncStackDeleteV1> mapFromJson(dynamic json) {
final map = <String, SyncStackDeleteV1>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncStackDeleteV1.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncStackDeleteV1-objects as value to a dart map
static Map<String, List<SyncStackDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncStackDeleteV1>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncStackDeleteV1.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'stackId',
};
}

View File

@@ -0,0 +1,131 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncStackV1 {
/// Returns a new [SyncStackV1] instance.
SyncStackV1({
required this.createdAt,
required this.id,
required this.ownerId,
required this.primaryAssetId,
required this.updatedAt,
});
DateTime createdAt;
String id;
String ownerId;
String primaryAssetId;
DateTime updatedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncStackV1 &&
other.createdAt == createdAt &&
other.id == id &&
other.ownerId == ownerId &&
other.primaryAssetId == primaryAssetId &&
other.updatedAt == updatedAt;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(createdAt.hashCode) +
(id.hashCode) +
(ownerId.hashCode) +
(primaryAssetId.hashCode) +
(updatedAt.hashCode);
@override
String toString() => 'SyncStackV1[createdAt=$createdAt, id=$id, ownerId=$ownerId, primaryAssetId=$primaryAssetId, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
json[r'id'] = this.id;
json[r'ownerId'] = this.ownerId;
json[r'primaryAssetId'] = this.primaryAssetId;
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
return json;
}
/// Returns a new [SyncStackV1] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncStackV1? fromJson(dynamic value) {
upgradeDto(value, "SyncStackV1");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncStackV1(
createdAt: mapDateTime(json, r'createdAt', r'')!,
id: mapValueOfType<String>(json, r'id')!,
ownerId: mapValueOfType<String>(json, r'ownerId')!,
primaryAssetId: mapValueOfType<String>(json, r'primaryAssetId')!,
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
);
}
return null;
}
static List<SyncStackV1> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncStackV1>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncStackV1.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncStackV1> mapFromJson(dynamic json) {
final map = <String, SyncStackV1>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncStackV1.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncStackV1-objects as value to a dart map
static Map<String, List<SyncStackV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncStackV1>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncStackV1.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'createdAt',
'id',
'ownerId',
'primaryAssetId',
'updatedAt',
};
}

View File

@@ -1,13 +1,12 @@
import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/drift_store.repository.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:isar/isar.dart';
import '../../fixtures/user.stub.dart';
import '../../test_utils.dart';
const _kTestAccessToken = "#TestToken";
final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45);
@@ -15,28 +14,16 @@ const _kTestVersion = 10;
const _kTestColorfulInterface = false;
final _kTestUser = UserStub.admin;
Future<void> _addIntStoreValue(Drift db, StoreKey key, int? value) async {
await db.into(db.storeEntity).insert(
StoreEntityCompanion.insert(
id: key.id,
intValue: Value(value),
strValue: const Value(null),
),
);
Future<void> _addIntStoreValue(Isar db, StoreKey key, int? value) async {
await db.storeValues.put(StoreValue(key.id, intValue: value, strValue: null));
}
Future<void> _addStrStoreValue(Drift db, StoreKey key, String? value) async {
await db.into(db.storeEntity).insert(
StoreEntityCompanion.insert(
id: key.id,
intValue: const Value(null),
strValue: Value(value),
),
);
Future<void> _addStrStoreValue(Isar db, StoreKey key, String? value) async {
await db.storeValues.put(StoreValue(key.id, intValue: null, strValue: value));
}
Future<void> _populateStore(Drift db) async {
await db.transaction(() async {
Future<void> _populateStore(Isar db) async {
await db.writeTxn(() async {
await _addIntStoreValue(
db,
StoreKey.colorfulInterface,
@@ -53,12 +40,12 @@ Future<void> _populateStore(Drift db) async {
}
void main() {
late Drift db;
late DriftStoreRepository sut;
late Isar db;
late IsarStoreRepository sut;
setUp(() async {
db = Drift(NativeDatabase.memory());
sut = DriftStoreRepository(db);
db = await TestUtils.initIsar();
sut = IsarStoreRepository(db);
});
group('Store Repository converters:', () {
@@ -118,16 +105,10 @@ void main() {
});
test('deleteAll()', () async {
final countQuery = db.selectOnly(db.storeEntity)
..addColumns([db.storeEntity.id.count()]);
final countResult = await countQuery.getSingle();
final count = countResult.read(db.storeEntity.id.count()) ?? 0;
final count = await db.storeValues.count();
expect(count, isNot(isZero));
await sut.deleteAll();
final newCountResult = await countQuery.getSingle();
final newCount = newCountResult.read(db.storeEntity.id.count()) ?? 0;
expect(newCount, isZero);
expectLater(await db.storeValues.count(), isZero);
});
});

View File

@@ -12,7 +12,7 @@ import 'package:mocktail/mocktail.dart';
class MockStoreRepository extends Mock implements IsarStoreRepository {}
class MockLogRepository extends Mock implements LogRepository {}
class MockLogRepository extends Mock implements IsarLogRepository {}
class MockIsarUserRepository extends Mock implements IsarUserRepository {}

View File

@@ -1,4 +1,3 @@
import 'package:drift/native.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/constants/enums.dart';
@@ -9,7 +8,6 @@ import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
@@ -82,12 +80,12 @@ void main() {
setUpAll(() async {
WidgetsFlutterBinding.ensureInitialized();
final db = await TestUtils.initIsar();
final driftDb = Drift(NativeDatabase.memory());
db.writeTxnSync(() => db.clearSync());
await StoreService.init(storeRepository: IsarStoreRepository(db));
await Store.put(StoreKey.currentUser, owner);
await LogService.init(
logRepository: LogRepository(driftDb),
logRepository: IsarLogRepository(db),
storeRepository: IsarStoreRepository(db),
);
});

View File

@@ -15,6 +15,7 @@ import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:isar/isar.dart';
import 'package:mocktail/mocktail.dart';
@@ -40,6 +41,7 @@ abstract final class TestUtils {
final db = await Isar.open(
[
StoreValueSchema,
ExifInfoSchema,
AssetSchema,
AlbumSchema,

View File

@@ -13804,6 +13804,9 @@
"PartnerAssetDeleteV1",
"PartnerAssetExifV1",
"PartnerAssetExifBackfillV1",
"PartnerStackBackfillV1",
"PartnerStackDeleteV1",
"PartnerStackV1",
"AlbumV1",
"AlbumDeleteV1",
"AlbumUserV1",
@@ -13820,6 +13823,8 @@
"MemoryDeleteV1",
"MemoryToAssetV1",
"MemoryToAssetDeleteV1",
"StackV1",
"StackDeleteV1",
"SyncAckV1"
],
"type": "string"
@@ -13971,22 +13976,64 @@
},
"SyncRequestType": {
"enum": [
"UsersV1",
"PartnersV1",
"AssetsV1",
"AssetExifsV1",
"PartnerAssetsV1",
"PartnerAssetExifsV1",
"AlbumsV1",
"AlbumUsersV1",
"AlbumToAssetsV1",
"AlbumAssetsV1",
"AlbumAssetExifsV1",
"AssetsV1",
"AssetExifsV1",
"MemoriesV1",
"MemoryToAssetsV1"
"MemoryToAssetsV1",
"PartnersV1",
"PartnerAssetsV1",
"PartnerAssetExifsV1",
"PartnerStacksV1",
"StacksV1",
"UsersV1"
],
"type": "string"
},
"SyncStackDeleteV1": {
"properties": {
"stackId": {
"type": "string"
}
},
"required": [
"stackId"
],
"type": "object"
},
"SyncStackV1": {
"properties": {
"createdAt": {
"format": "date-time",
"type": "string"
},
"id": {
"type": "string"
},
"ownerId": {
"type": "string"
},
"primaryAssetId": {
"type": "string"
},
"updatedAt": {
"format": "date-time",
"type": "string"
}
},
"required": [
"createdAt",
"id",
"ownerId",
"primaryAssetId",
"updatedAt"
],
"type": "object"
},
"SyncStreamDto": {
"properties": {
"types": {

View File

@@ -1 +1 @@
22.16.0
22.17.0

View File

@@ -12,7 +12,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.15.32",
"@types/node": "^22.15.33",
"typescript": "^5.3.3"
}
},
@@ -23,9 +23,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.15.32",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz",
"integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
"version": "22.15.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz",
"integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==",
"dev": true,
"license": "MIT",
"dependencies": {

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