Compare commits

...

41 Commits

Author SHA1 Message Date
shenlong-tanwen
023d2195f9 feat: inline storage columns in remote and local tables 2025-07-11 01:37:02 +05:30
mertalev
c482bdfae7 thumbhash render box 2025-07-10 16:41:53 +03:00
mertalev
0dadfc52dd thumbhash improvements 2025-07-08 16:20:11 +03:00
Jason Rasmussen
df4a27e8a7 feat: sql-tools overrides (#19796) 2025-07-08 08:17:40 -04:00
renovate[bot]
1f9813a28e chore(deps): update github/codeql-action action to v3.29.2 (#19806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 12:26:07 +01:00
renovate[bot]
bbfff45058 chore(deps): update redis:6.2-alpine docker digest to 03fd052 (#19804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 12:18:58 +02:00
Alex
87dd09d103 feat: selection mode timeline (#19734)
* feat: new page

* force multi-selection state

* fix: provider scoping

* Return selected assets

* lint

* lint

* simplify provider scope and drop drilling

* selection styling
2025-07-08 10:11:37 +05:30
Alex
dd94ad17aa fix: scrubber scroll error when page is not long enough (#19809) 2025-07-07 23:30:47 -05:00
renovate[bot]
a87c2e82cd fix(deps): update typescript-projects (#19666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-07-07 20:29:58 +00:00
bo0tzz
a11ab4c3f7 chore: tidy up DCM analysis workflow (#19797) 2025-07-07 15:11:44 -05:00
Daimolean
ebf2f9fd7b feat(mobile): drift library page (#19789)
* feat(mobile): drift library page

* merge main & fix sliver padding

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-07 15:11:16 -05:00
Daimolean
683af67344 feat(mobile): drift video page (#19784)
* feat(mobile): drift video page

* filter motional parts

* remove status indicator join

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-07 18:51:49 +00:00
Daimolean
d149d6fa3f feat(mobile): drift favorite page (#19783)
* feat(mobile): drift favorite page

* remove status indicator join
2025-07-07 12:57:08 -05:00
Alex
8c5269c002 chore: lock DCM version in GHA (#19795) 2025-07-07 17:56:54 +00:00
Daimolean
cf91d9bdfc feat(mobile): drift locked folder page (#19746)
* feat(mobile): drift locked folder page

* feat: include local indicator

* remove join in bucket

* remove status indicator join

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-07 04:29:33 +00:00
Daimolean
5579554532 feat(mobile): drift trash page (#19745)
* feat(mobile): drift trash page

* feat: include local indicator

* remove join in bucket

* remove indicator join

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-07 04:05:40 +00:00
Damiano Ferrari
7e35e6985e chore: Use a contrasted color for text avatar (#19756) 2025-07-06 22:59:48 -05:00
Daimolean
56756baea2 feat(mobile): drift archive page (#19740)
* feat(mobile): drift archive page

* fix: lint

* feat: include local indicator

* remove join in bucket

* remove showing local indicator in bucket

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: alex <alex@localhost-live.localdomain>
2025-07-07 03:51:58 +00:00
matthieu-db
d5923241b5 fix: add quiet zone to QR code (#19771)
Add quiet zone to QR code

This is needed for the QR code to be readable by many QR readers. It is also a requirement for it to be a valid QR code.
2025-07-06 22:06:36 -05:00
Daimolean
cc471806fe feat(mobile): stack sync (#19735)
* feat(mobile): stack sync

* fix: lint

* Update mobile/lib/infrastructure/repositories/sync_api.repository.dart

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

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-06 22:01:09 -05:00
Daniel Dietzler
4ce9bce414 feat: oauth role claim (#19758) 2025-07-06 18:45:32 -04:00
OffsetMonkey538
2f5d75ce21 docs: fix typo of webp listed under jpeg (#19743) 2025-07-05 15:52:22 +00:00
Daimolean
fb384fe90b fix(web): viewing asset lock (#19499)
* fix(web): viewing asset lock

* fix: lint

* make mutex stateless

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-05 02:52:39 +00:00
shenlong
73733370a2 feat: adds bottom sheet map and actions (#19726)
* reduce timeline rebuilds

* feat: adds bottom sheet map and actions (#19692)

* adds bottom sheet map and actions

* PR feedbacks

* only reload the asset viewer if asset is changed

* styling tweak

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>

* rename singleton and remove event prefix

* adds bottom sheet map and actions

* PR feedbacks

* refactor: use provider for viewer state

* feat: adds top and bottom app bar

* add safe area to bottom app bar

* change app and bottom bar color

* viewer - always have black background

* use the full width for the bottom sheet on landscape as well

* constraint the bottom sheet to not expand all the way

* add padding for location details in landscape

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-04 14:08:06 -05:00
Alex
4a2cf28882 feat: memories in new timeline (#19720)
* feat: memories sliver

* memories lane

* display and show memory

* fix: get correct memories

* naming

* pr feedback

* use equalsValue for visibility

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-04 13:49:15 -05:00
shenlong
181efb9010 refactor: reduce timeline rebuilds (#19704)
* reduce timeline rebuilds

* feat: adds bottom sheet map and actions (#19692)

* adds bottom sheet map and actions

* PR feedbacks

* only reload the asset viewer if asset is changed

* styling tweak

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>

* rename singleton and remove event prefix

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-04 10:30:34 -05:00
renovate[bot]
b00d44a00c fix(deps): update machine-learning (#19647) 2025-07-03 20:28:34 +00:00
Jason Rasmussen
6044663e26 refactor: sql-tools (#19717) 2025-07-03 10:59:17 -04:00
aviv926
484529e61e feat(server): add immich and postgres version to the database backup name (#19603) 2025-07-03 10:35:24 +01:00
Alex
445f9174ea feat: memories sync (#19644)
* feat: memories sync

* Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* show sync information

* tests and pr feedback

* pr feedback

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-02 19:18:37 +00:00
shenlong
7855974a29 feat(mobile): sqlite asset viewer (#19552)
* add full image provider and refactor thumb providers

* photo_view updates

* wip: asset-viewer

* fix controller dispose on page change

* wip: bottom sheet

* fix interactions

* more bottomsheet changes

* generate schema

* PR feedback

* refactor asset viewer

* never rotate and fix background on page change

* use photoview as the loading builder

* precache after delay

* claude: optimizing rebuild of image provider

* claude: optimizing image decoding and caching

* use proper cache for new full size image providers

* chore: load local HEIC fullsize for iOS

* make controller callbacks nullable

* remove imageprovider cache

* do not handle drag gestures when zoomed

* use loadOriginal setting for HEIC / larger images

* preload assets outside timer

* never use same controllers in photo-view gallery

* fix: cannot scroll down once swipe with bottom sheet

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-02 18:24:37 +00:00
Daimolean
ec603a008c feat(mobile): unarchive and unfavorite action (#19678) 2025-07-02 12:27:30 -05:00
shenlong
14276f41d8 fix: handle null bucket name during android sync (#19685)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-02 12:26:42 -05:00
Daimolean
a644cabab6 feat(mobile): trash and delete action (#19681)
* feat(mobile): trash and delete action

* fix lint
2025-07-02 12:26:07 -05:00
Daimolean
b8e67d0ef9 fix(mobile): filter deleted assets (#19683) 2025-07-02 12:25:14 -05:00
Min Idzelis
ca78bc91b6 feat: fully qualified path in error msg (#19674)
* feat: fully qualified path in error msg

* import style
2025-07-02 09:31:20 -04:00
shenlong
f2f3db3a79 refactor: action provider (#19677)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-01 23:08:52 -05:00
Jason Rasmussen
c435bdb5d3 refactor: sql-tools readers (#19672) 2025-07-01 22:57:17 -04:00
Min Idzelis
15da0d5a71 fix: email button (#19675) 2025-07-01 22:48:41 -04:00
Min Idzelis
090d87f82e chore: dev environment improvements and dependency updates (#19676) 2025-07-01 22:47:59 -04:00
Alex
25efba8fe6 chore: remove share link success prompt (#19671) 2025-07-01 16:55:17 +00:00
342 changed files with 12298 additions and 4087 deletions

View File

@@ -73,10 +73,8 @@ install_dependencies() {
log "Installing dependencies"
(
cd "${IMMICH_WORKSPACE}" || exit 1
run_cmd make ci-server
run_cmd make ci-sdk
run_cmd make build-sdk
run_cmd make ci-web
export CI=1 FROZEN=1 OFFLINE=1
run_cmd make setup-dev
)
log ""
}

View File

@@ -22,7 +22,7 @@ services:
immich-machine-learning:
env_file: !reset []
database:
env_file: !reset []
environment: !override
@@ -31,7 +31,7 @@ services:
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
POSTGRES_INITDB_ARGS: '--data-checksums'
POSTGRES_HOST_AUTH_METHOD: md5
volumes:
volumes:
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
redis:

4
.github/.prettierignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

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

@@ -9,9 +9,9 @@
}
},
"node_modules/prettier": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {

View File

@@ -50,7 +50,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -76,6 +76,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
with:
category: '/language:${{matrix.language}}'

View File

@@ -42,6 +42,9 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./mobile
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -56,27 +59,23 @@ jobs:
- name: Install dependencies
run: dart pub get
working-directory: ./mobile
- name: Install DCM
run: |
sudo apt-get update
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
sudo apt-get update
sudo apt-get install dcm
# TODO: Move to upstream after https://github.com/CQLabs/setup-dcm/pull/235 merges
uses: bo0tzz/setup-dcm@b4952ab813659c03513b57bd78bfe3f634171f8a
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
version: auto
working-directory: ./mobile
- name: Generate translation file
run: make translation
working-directory: ./mobile
- name: Run Build Runner
run: make build
working-directory: ./mobile
- name: Generate platform API
run: make pigeon
working-directory: ./mobile
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -98,19 +97,16 @@ jobs:
- name: Run dart analyze
run: dart analyze --fatal-infos
working-directory: ./mobile
- name: Run dart format
run: dart format lib/ --set-exit-if-changed
working-directory: ./mobile
- name: Run dart custom_lint
run: dart run custom_lint
working-directory: ./mobile
# TODO: Use https://github.com/CQLabs/dcm-action
- name: Run DCM
run: dcm analyze lib --fatal-style --fatal-warnings
working-directory: ./mobile
zizmor:
name: zizmor
@@ -134,7 +130,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
with:
sarif_file: results.sarif
category: zizmor

162
cli/package-lock.json generated
View File

@@ -16,7 +16,7 @@
"micromatch": "^4.0.8"
},
"bin": {
"immich": "dist/index.js"
"immich": "bin/immich"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -607,9 +607,9 @@
}
},
"node_modules/@eslint/config-array": {
"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==",
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -622,9 +622,9 @@
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
"integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz",
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -682,9 +682,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
"integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1365,17 +1365,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
"integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@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",
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/type-utils": "8.35.1",
"@typescript-eslint/utils": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -1389,7 +1389,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.35.0",
"@typescript-eslint/parser": "^8.35.1",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@@ -1405,16 +1405,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz",
"integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"debug": "^4.3.4"
},
"engines": {
@@ -1430,14 +1430,14 @@
}
},
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz",
"integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.35.0",
"@typescript-eslint/types": "^8.35.0",
"@typescript-eslint/tsconfig-utils": "^8.35.1",
"@typescript-eslint/types": "^8.35.1",
"debug": "^4.3.4"
},
"engines": {
@@ -1452,14 +1452,14 @@
}
},
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz",
"integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0"
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1470,9 +1470,9 @@
}
},
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz",
"integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1487,14 +1487,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz",
"integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.35.0",
"@typescript-eslint/utils": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/utils": "8.35.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -1511,9 +1511,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz",
"integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1525,16 +1525,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz",
"integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@typescript-eslint/project-service": "8.35.1",
"@typescript-eslint/tsconfig-utils": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1580,16 +1580,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz",
"integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.0"
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.35.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1604,13 +1604,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz",
"integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/types": "8.35.1",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -2305,19 +2305,19 @@
}
},
"node_modules/eslint": {
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.20.1",
"@eslint/config-helpers": "^0.2.1",
"@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.3.0",
"@eslint/core": "^0.14.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.29.0",
"@eslint/js": "9.30.1",
"@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -2822,9 +2822,9 @@
}
},
"node_modules/globals": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3505,9 +3505,9 @@
}
},
"node_modules/prettier": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -4125,15 +4125,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz",
"integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz",
"integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.35.0",
"@typescript-eslint/parser": "8.35.0",
"@typescript-eslint/utils": "8.35.0"
"@typescript-eslint/eslint-plugin": "8.35.1",
"@typescript-eslint/parser": "8.35.1",
"@typescript-eslint/utils": "8.35.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -62,6 +62,7 @@ Once you have a new OAuth client application configured, Immich can be configure
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |

View File

@@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
| `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `PNG` | `.png` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |

View File

@@ -16459,9 +16459,9 @@
}
},
"node_modules/prettier": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {

View File

@@ -36,7 +36,7 @@ services:
- 2285:2285
redis:
image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
image: redis:6.2-alpine@sha256:03fd052257735b41cd19f3d8ae9782926bf9b704fb6a9dc5e29f9ccfbe8827f0
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea

342
e2e/package-lock.json generated
View File

@@ -56,7 +56,7 @@
"micromatch": "^4.0.8"
},
"bin": {
"immich": "dist/index.js"
"immich": "bin/immich"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -658,9 +658,9 @@
}
},
"node_modules/@eslint/config-array": {
"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==",
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -673,9 +673,9 @@
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz",
"integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==",
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz",
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -733,9 +733,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
"integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1509,13 +1509,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
"integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.2.tgz",
"integrity": "sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.53.1"
"playwright": "1.53.2"
},
"bin": {
"playwright": "cli.js"
@@ -2028,66 +2028,6 @@
"pg-types": "^2.2.0"
}
},
"node_modules/@types/pg/node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@types/pg/node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@types/pg/node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@types/pg/node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@types/pg/node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@types/pngjs": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
@@ -2160,17 +2100,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
"integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@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",
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/type-utils": "8.35.1",
"@typescript-eslint/utils": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -2184,7 +2124,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.35.0",
"@typescript-eslint/parser": "^8.35.1",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@@ -2200,16 +2140,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz",
"integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"debug": "^4.3.4"
},
"engines": {
@@ -2225,14 +2165,14 @@
}
},
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz",
"integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.35.0",
"@typescript-eslint/types": "^8.35.0",
"@typescript-eslint/tsconfig-utils": "^8.35.1",
"@typescript-eslint/types": "^8.35.1",
"debug": "^4.3.4"
},
"engines": {
@@ -2247,14 +2187,14 @@
}
},
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz",
"integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0"
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2265,9 +2205,9 @@
}
},
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz",
"integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2282,14 +2222,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz",
"integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.35.0",
"@typescript-eslint/utils": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/utils": "8.35.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -2306,9 +2246,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz",
"integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2320,16 +2260,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"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==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz",
"integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@typescript-eslint/project-service": "8.35.1",
"@typescript-eslint/tsconfig-utils": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -2375,16 +2315,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz",
"integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.0"
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.35.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2399,13 +2339,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz",
"integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/types": "8.35.1",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -3498,19 +3438,19 @@
}
},
"node_modules/eslint": {
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.20.1",
"@eslint/config-helpers": "^0.2.1",
"@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.3.0",
"@eslint/core": "^0.14.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.29.0",
"@eslint/js": "9.30.1",
"@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -4279,9 +4219,9 @@
}
},
"node_modules/globals": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5430,15 +5370,15 @@
}
},
"node_modules/pg": {
"version": "8.16.2",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz",
"integrity": "sha512-OtLWF0mKLmpxelOt9BqVq83QV6bTfsS0XLegIeAKqKjurRnRKie1Dc1iL89MugmSLhftxw6NNCyZhm1yQFLMEQ==",
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.2",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
@@ -5446,7 +5386,7 @@
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.6"
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
@@ -5499,7 +5439,7 @@
"dev": true,
"license": "MIT"
},
"node_modules/pg/node_modules/pg-types": {
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
@@ -5516,49 +5456,6 @@
"node": ">=4"
}
},
"node_modules/pg/node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/pg/node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pg/node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pg/node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
@@ -5590,13 +5487,13 @@
}
},
"node_modules/playwright": {
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
"integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.53.1"
"playwright-core": "1.53.2"
},
"bin": {
"playwright": "cli.js"
@@ -5609,9 +5506,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
"integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -5689,6 +5586,49 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5700,9 +5640,9 @@
}
},
"node_modules/prettier": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -6837,15 +6777,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz",
"integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==",
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz",
"integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.35.0",
"@typescript-eslint/parser": "8.35.0",
"@typescript-eslint/utils": "8.35.0"
"@typescript-eslint/eslint-plugin": "8.35.1",
"@typescript-eslint/parser": "8.35.1",
"@typescript-eslint/utils": "8.35.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -227,6 +227,21 @@ describe(`/oauth`, () => {
expect(user.storageLabel).toBe('user-username');
});
it('should set the admin status from a role claim', async () => {
const callbackParams = await loginWithOAuth(OAuthUser.WITH_ROLE);
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(201);
expect(body).toMatchObject({
accessToken: expect.any(String),
userId: expect.any(String),
userEmail: 'oauth-with-role@immich.app',
isAdmin: true,
});
const user = await getMyUser({ headers: asBearerAuth(body.accessToken) });
expect(user.isAdmin).toBe(true);
});
it('should work with RS256 signed tokens', async () => {
await setupOAuth(admin.accessToken, {
enabled: true,

View File

@@ -12,6 +12,7 @@ export enum OAuthUser {
NO_NAME = 'no-name',
WITH_QUOTA = 'with-quota',
WITH_USERNAME = 'with-username',
WITH_ROLE = 'with-role',
}
const claims = [
@@ -34,6 +35,12 @@ const claims = [
preferred_username: 'user-quota',
immich_quota: 25,
},
{
sub: OAuthUser.WITH_ROLE,
email: 'oauth-with-role@immich.app',
email_verified: true,
immich_role: 'admin',
},
];
const withDefaultClaims = (sub: string) => ({
@@ -64,7 +71,15 @@ const setup = async () => {
claims: {
openid: ['sub'],
email: ['email', 'email_verified'],
profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'],
profile: [
'name',
'given_name',
'family_name',
'preferred_username',
'immich_quota',
'immich_username',
'immich_role',
],
},
features: {
jwtUserinfo: {

View File

@@ -60,6 +60,7 @@ import { io, type Socket } from 'socket.io-client';
import { loginDto, signupDto } from 'src/fixtures';
import { makeRandomImage } from 'src/generators';
import request from 'supertest';
export type { Emitter } from '@socket.io/component-emitter';
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
@@ -84,10 +85,10 @@ export const immichAdmin = (args: string[]) =>
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const executeCommand = (command: string, args: string[]) => {
const executeCommand = (command: string, args: string[], options?: { cwd?: string }) => {
let _resolve: (value: CommandResponse) => void;
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const child = spawn(command, args, { stdio: 'pipe' });
const child = spawn(command, args, { stdio: 'pipe', cwd: options?.cwd });
let stdout = '';
let stderr = '';

View File

@@ -196,6 +196,8 @@
"oauth_mobile_redirect_uri": "Mobile redirect URI",
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
"oauth_role_claim": "Role Claim",
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
"oauth_settings": "OAuth",
"oauth_settings_description": "Manage OAuth login settings",
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
@@ -719,6 +721,7 @@
"default_locale": "Default Locale",
"default_locale_description": "Format dates and numbers based on your browser locale",
"delete": "Delete",
"delete_action_prompt": "{count} deleted permanently",
"delete_album": "Delete album",
"delete_api_key_prompt": "Are you sure you want to delete this API key?",
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
@@ -1842,6 +1845,7 @@
"total": "Total",
"total_usage": "Total usage",
"trash": "Trash",
"trash_action_prompt": "{count} moved to trash",
"trash_all": "Trash All",
"trash_count": "Trash {count, number}",
"trash_delete_asset": "Trash/Delete Asset",
@@ -1859,9 +1863,11 @@
"unable_to_change_pin_code": "Unable to change PIN code",
"unable_to_setup_pin_code": "Unable to setup PIN code",
"unarchive": "Unarchive",
"unarchive_action_prompt": "{count} removed from Archive",
"unarchived_count": "{count, plural, other {Unarchived #}}",
"undo": "Undo",
"unfavorite": "Unfavorite",
"unfavorite_action_prompt": "{count} removed from Favorites",
"unhide_person": "Unhide person",
"unknown": "Unknown",
"unknown_country": "Unknown Country",

View File

@@ -517,16 +517,16 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.115.13"
version = "0.115.14"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" },
{ url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" },
]
[[package]]
@@ -900,7 +900,7 @@ wheels = [
[[package]]
name = "huggingface-hub"
version = "0.33.0"
version = "0.33.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -912,9 +912,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/91/8a/1362d565fefabaa4185cf3ae842a98dbc5b35146f5694f7080f043a6952f/huggingface_hub-0.33.0.tar.gz", hash = "sha256:aa31f70d29439d00ff7a33837c03f1f9dd83971ce4e29ad664d63ffb17d3bb97", size = 426179, upload-time = "2025-06-11T17:08:07.913Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/42/8a95c5632080ae312c0498744b2b852195e10b05a20b1be11c5141092f4c/huggingface_hub-0.33.2.tar.gz", hash = "sha256:84221defaec8fa09c090390cd68c78b88e3c4c2b7befba68d3dc5aacbc3c2c5f", size = 426637, upload-time = "2025-07-02T06:26:05.156Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/fb/53587a89fbc00799e4179796f51b3ad713c5de6bb680b2becb6d37c94649/huggingface_hub-0.33.0-py3-none-any.whl", hash = "sha256:e8668875b40c68f9929150d99727d39e5ebb8a05a98e4191b908dc7ded9074b3", size = 514799, upload-time = "2025-06-11T17:08:05.757Z" },
{ url = "https://files.pythonhosted.org/packages/44/f4/5f3f22e762ad1965f01122b42dae5bf0e009286e2dba601ce1d0dba72424/huggingface_hub-0.33.2-py3-none-any.whl", hash = "sha256:3749498bfa91e8cde2ddc2c1db92c79981f40e66434c20133b39e5928ac9bcc5", size = 515373, upload-time = "2025-07-02T06:26:03.072Z" },
]
[[package]]
@@ -1044,7 +1044,7 @@ requires-dist = [
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" },
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" },
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" },
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" },
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" },
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" },
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
{ name = "orjson", specifier = ">=3.9.5" },
@@ -1568,7 +1568,7 @@ wheels = [
[[package]]
name = "onnxruntime-gpu"
version = "1.19.2"
source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" }
source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" }
dependencies = [
{ name = "coloredlogs" },
{ name = "flatbuffers" },
@@ -1936,16 +1936,16 @@ wheels = [
[[package]]
name = "pydantic-settings"
version = "2.9.1"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
]
[[package]]
@@ -2304,27 +2304,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.12.0"
version = "0.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" },
{ url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" },
{ url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" },
{ url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" },
{ url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" },
{ url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" },
{ url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" },
{ url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" },
{ url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" },
{ url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" },
{ url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" },
{ url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" },
{ url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" },
{ url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" },
{ url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" },
{ url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
{ url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
{ url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
{ url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
{ url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
{ url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
{ url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
{ url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
{ url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
{ url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
{ url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
{ url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
{ url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
{ url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
{ url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
{ url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
]
[[package]]
@@ -2504,27 +2504,27 @@ wheels = [
[[package]]
name = "tokenizers"
version = "0.21.1"
version = "0.21.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
]
sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload-time = "2025-03-13T10:51:18.189Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload-time = "2025-03-13T10:51:09.459Z" },
{ url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload-time = "2025-03-13T10:51:07.692Z" },
{ url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload-time = "2025-03-13T10:50:56.679Z" },
{ url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload-time = "2025-03-13T10:50:59.525Z" },
{ url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload-time = "2025-03-13T10:51:04.678Z" },
{ url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload-time = "2025-03-13T10:51:01.261Z" },
{ url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload-time = "2025-03-13T10:51:03.243Z" },
{ url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload-time = "2025-03-13T10:51:06.235Z" },
{ url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload-time = "2025-03-13T10:51:10.927Z" },
{ url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload-time = "2025-03-13T10:51:12.688Z" },
{ url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload-time = "2025-03-13T10:51:14.723Z" },
{ url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload-time = "2025-03-13T10:51:16.526Z" },
{ url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload-time = "2025-03-13T10:51:20.643Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload-time = "2025-03-13T10:51:19.243Z" },
{ url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" },
{ url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" },
{ url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" },
{ url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" },
{ url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" },
{ url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" },
{ url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" },
{ url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" },
{ url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" },
{ url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" },
{ url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" },
{ url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" },
{ url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" },
{ url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" },
]
[[package]]
@@ -2628,16 +2628,16 @@ wheels = [
[[package]]
name = "uvicorn"
version = "0.34.3"
version = "0.35.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
]
[package.optional-dependencies]

View File

@@ -5,6 +5,7 @@ import android.content.Context
import android.database.Cursor
import android.provider.MediaStore
import android.util.Log
import androidx.core.database.getStringOrNull
import java.io.File
import java.io.FileInputStream
import java.security.MessageDigest
@@ -152,7 +153,8 @@ open class NativeSyncApiImplBase(context: Context) {
continue
}
val name = cursor.getString(bucketNameColumn)
// MediaStore might return null for bucket name (commonly for the Root Directory), so default to "Internal Storage"
val name = cursor.getStringOrNull(bucketNameColumn) ?: "Internal Storage"
val updatedAt = cursor.getLong(dateModified)
albums.add(PlatformAlbum(id, name, updatedAt, false, 0))
albumsCount[id] = 1

File diff suppressed because one or more lines are too long

View File

@@ -18,7 +18,7 @@ const String kSecuredPinCode = "secured_pin_code";
// Timeline constants
const int kTimelineNoneSegmentSize = 120;
const int kTimelineAssetLoadBatchSize = 256;
const int kTimelineAssetLoadBatchSize = 1024;
const int kTimelineAssetLoadOppositeSize = 64;
// Widget keys

View File

@@ -1,5 +1,5 @@
part 'remote_asset.model.dart';
part 'local_asset.model.dart';
part 'remote_asset.model.dart';
enum AssetType {
// do not change this order!
@@ -40,7 +40,24 @@ sealed class BaseAsset {
bool get isImage => type == AssetType.image;
bool get isVideo => type == AssetType.video;
double? get aspectRatio {
if (width != null && height != null && height! > 0) {
return width! / height!;
}
return null;
}
bool get hasRemote =>
storage == AssetState.remote || storage == AssetState.merged;
bool get hasLocal =>
storage == AssetState.local || storage == AssetState.merged;
bool get isLocalOnly => storage == AssetState.local;
bool get isRemoteOnly => storage == AssetState.remote;
// Overridden in subclasses
AssetState get storage;
String get heroTag;
@override
String toString() {

View File

@@ -22,6 +22,9 @@ class LocalAsset extends BaseAsset {
AssetState get storage =>
remoteId == null ? AssetState.local : AssetState.merged;
@override
String get heroTag => '${id}_${remoteId ?? checksum}';
@override
String toString() {
return '''LocalAsset {

View File

@@ -36,6 +36,9 @@ class RemoteAsset extends BaseAsset {
AssetState get storage =>
localId == null ? AssetState.remote : AssetState.merged;
@override
String get heroTag => '${localId ?? checksum}_$id';
@override
String toString() {
return '''Asset {
@@ -75,4 +78,38 @@ class RemoteAsset extends BaseAsset {
localId.hashCode ^
thumbHash.hashCode ^
visibility.hashCode;
RemoteAsset copyWith({
String? id,
String? localId,
String? name,
String? ownerId,
String? checksum,
AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
int? width,
int? height,
int? durationInSeconds,
bool? isFavorite,
String? thumbHash,
AssetVisibility? visibility,
}) {
return RemoteAsset(
id: id ?? this.id,
localId: localId ?? this.localId,
name: name ?? this.name,
ownerId: ownerId ?? this.ownerId,
checksum: checksum ?? this.checksum,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
isFavorite: isFavorite ?? this.isFavorite,
thumbHash: thumbHash ?? this.thumbHash,
visibility: visibility ?? this.visibility,
);
}
}

View File

@@ -3,6 +3,8 @@ class ExifInfo {
final int? fileSize;
final String? description;
final bool isFlipped;
final double? width;
final double? height;
final String? orientation;
final String? timeZone;
final DateTime? dateTimeOriginal;
@@ -45,6 +47,8 @@ class ExifInfo {
this.fileSize,
this.description,
this.orientation,
this.width,
this.height,
this.timeZone,
this.dateTimeOriginal,
this.isFlipped = false,
@@ -68,6 +72,9 @@ class ExifInfo {
return other.fileSize == fileSize &&
other.description == description &&
other.isFlipped == isFlipped &&
other.width == width &&
other.height == height &&
other.orientation == orientation &&
other.timeZone == timeZone &&
other.dateTimeOriginal == dateTimeOriginal &&
@@ -91,6 +98,9 @@ class ExifInfo {
return fileSize.hashCode ^
description.hashCode ^
orientation.hashCode ^
isFlipped.hashCode ^
width.hashCode ^
height.hashCode ^
timeZone.hashCode ^
dateTimeOriginal.hashCode ^
latitude.hashCode ^
@@ -114,6 +124,9 @@ class ExifInfo {
fileSize: ${fileSize ?? 'NA'},
description: ${description ?? 'NA'},
orientation: ${orientation ?? 'NA'},
width: ${width ?? 'NA'},
height: ${height ?? 'NA'},
isFlipped: $isFlipped,
timeZone: ${timeZone ?? 'NA'},
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
latitude: ${latitude ?? 'NA'},

View File

@@ -0,0 +1,166 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
enum MemoryTypeEnum {
// do not change this order!
onThisDay,
}
class MemoryData {
final int year;
const MemoryData({
required this.year,
});
MemoryData copyWith({
int? year,
}) {
return MemoryData(
year: year ?? this.year,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'year': year,
};
}
factory MemoryData.fromMap(Map<String, dynamic> map) {
return MemoryData(
year: map['year'] as int,
);
}
String toJson() => json.encode(toMap());
factory MemoryData.fromJson(String source) =>
MemoryData.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'MemoryData(year: $year)';
@override
bool operator ==(covariant MemoryData other) {
if (identical(this, other)) return true;
return other.year == year;
}
@override
int get hashCode => year.hashCode;
}
// Model for a memory stored in the server
class DriftMemory {
final String id;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? deletedAt;
final String ownerId;
// enum
final MemoryTypeEnum type;
final MemoryData data;
final bool isSaved;
final DateTime memoryAt;
final DateTime? seenAt;
final DateTime? showAt;
final DateTime? hideAt;
final List<RemoteAsset> assets;
const DriftMemory({
required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.ownerId,
required this.type,
required this.data,
required this.isSaved,
required this.memoryAt,
this.seenAt,
this.showAt,
this.hideAt,
required this.assets,
});
DriftMemory copyWith({
String? id,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? deletedAt,
String? ownerId,
MemoryTypeEnum? type,
MemoryData? data,
bool? isSaved,
DateTime? memoryAt,
DateTime? seenAt,
DateTime? showAt,
DateTime? hideAt,
List<RemoteAsset>? assets,
}) {
return DriftMemory(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
deletedAt: deletedAt ?? this.deletedAt,
ownerId: ownerId ?? this.ownerId,
type: type ?? this.type,
data: data ?? this.data,
isSaved: isSaved ?? this.isSaved,
memoryAt: memoryAt ?? this.memoryAt,
seenAt: seenAt ?? this.seenAt,
showAt: showAt ?? this.showAt,
hideAt: hideAt ?? this.hideAt,
assets: assets ?? this.assets,
);
}
@override
String toString() {
return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt, assets: $assets)';
}
@override
bool operator ==(covariant DriftMemory other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;
return other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
other.deletedAt == deletedAt &&
other.ownerId == ownerId &&
other.type == type &&
other.data == data &&
other.isSaved == isSaved &&
other.memoryAt == memoryAt &&
other.seenAt == seenAt &&
other.showAt == showAt &&
other.hideAt == hideAt &&
listEquals(other.assets, assets);
}
@override
int get hashCode {
return id.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode ^
deletedAt.hashCode ^
ownerId.hashCode ^
type.hashCode ^
data.hashCode ^
isSaved.hashCode ^
memoryAt.hashCode ^
seenAt.hashCode ^
showAt.hashCode ^
hideAt.hashCode ^
assets.hashCode;
}
}

View File

@@ -3,7 +3,11 @@ import 'package:immich_mobile/domain/models/store.model.dart';
enum Setting<T> {
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
showStorageIndicator<bool>(StoreKey.storageIndicator, true);
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
loadOriginal<bool>(StoreKey.loadOriginal, false),
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
;
const Setting(this.storeKey, this.defaultValue);

View File

@@ -0,0 +1,84 @@
import 'dart:convert';
// Model for a stack stored in the server
class Stack {
final String id;
final DateTime createdAt;
final DateTime updatedAt;
final String ownerId;
final String primaryAssetId;
const Stack({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.ownerId,
required this.primaryAssetId,
});
Stack copyWith({
String? id,
DateTime? createdAt,
DateTime? updatedAt,
String? ownerId,
String? primaryAssetId,
}) {
return Stack(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
ownerId: ownerId ?? this.ownerId,
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'id': id,
'createdAt': createdAt.millisecondsSinceEpoch,
'updatedAt': updatedAt.millisecondsSinceEpoch,
'ownerId': ownerId,
'primaryAssetId': primaryAssetId,
};
}
factory Stack.fromMap(Map<String, dynamic> map) {
return Stack(
id: map['id'] as String,
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int),
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int),
ownerId: map['ownerId'] as String,
primaryAssetId: map['primaryAssetId'] as String,
);
}
String toJson() => json.encode(toMap());
factory Stack.fromJson(String source) =>
Stack.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() {
return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)';
}
@override
bool operator ==(covariant Stack other) {
if (identical(this, other)) return true;
return other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
other.ownerId == ownerId &&
other.primaryAssetId == primaryAssetId;
}
@override
int get hashCode {
return id.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode ^
ownerId.hashCode ^
primaryAssetId.hashCode;
}
}

View File

@@ -0,0 +1,30 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
class AssetService {
final RemoteAssetRepository _remoteAssetRepository;
final DriftLocalAssetRepository _localAssetRepository;
const AssetService({
required RemoteAssetRepository remoteAssetRepository,
required DriftLocalAssetRepository localAssetRepository,
}) : _remoteAssetRepository = remoteAssetRepository,
_localAssetRepository = localAssetRepository;
Stream<BaseAsset?> watchAsset(BaseAsset asset) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
return asset is LocalAsset
? _localAssetRepository.watchAsset(id)
: _remoteAssetRepository.watchAsset(id);
}
Future<ExifInfo?> getExif(BaseAsset asset) async {
if (asset is LocalAsset || asset is! RemoteAsset) {
return null;
}
return _remoteAssetRepository.getExif(asset.id);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart';
import 'package:logging/logging.dart';
class DriftMemoryService {
final log = Logger("DriftMemoryService");
final DriftMemoryRepository _repository;
DriftMemoryService(this._repository);
Future<List<DriftMemory>> getMemoryLane(String ownerId) {
return _repository.getAll(ownerId);
}
}

View File

@@ -1,6 +1,11 @@
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
// Singleton instance of SettingsService, to use in places
// where reactivity is not required
// ignore: non_constant_identifier_names
final AppSetting = SettingsService(storeService: StoreService.I);
class SettingsService {
final StoreService _storeService;

View File

@@ -146,6 +146,33 @@ class SyncStreamService {
// to acknowledge that the client has processed all the backfill events
case SyncEntityType.syncAckV1:
return;
case SyncEntityType.memoryV1:
return _syncStreamRepository.updateMemoriesV1(data.cast());
case SyncEntityType.memoryDeleteV1:
return _syncStreamRepository.deleteMemoriesV1(data.cast());
case SyncEntityType.memoryToAssetV1:
return _syncStreamRepository.updateMemoryAssetsV1(data.cast());
case SyncEntityType.memoryToAssetDeleteV1:
return _syncStreamRepository.deleteMemoryAssetsV1(data.cast());
case SyncEntityType.stackV1:
return _syncStreamRepository.updateStacksV1(data.cast());
case SyncEntityType.stackDeleteV1:
return _syncStreamRepository.deleteStacksV1(data.cast());
case SyncEntityType.partnerStackV1:
return _syncStreamRepository.updateStacksV1(
data.cast(),
debugLabel: 'partner',
);
case SyncEntityType.partnerStackBackfillV1:
return _syncStreamRepository.updateStacksV1(
data.cast(),
debugLabel: 'partner backfill',
);
case SyncEntityType.partnerStackDeleteV1:
return _syncStreamRepository.deleteStacksV1(
data.cast(),
debugLabel: 'partner',
);
default:
_logger.warning("Unknown sync data type: $type");
}

View File

@@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
@@ -41,30 +42,85 @@ class TimelineFactory {
TimelineService localAlbum({required String albumId}) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getLocalBucketAssets(albumId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchLocalBucket(albumId, groupBy: groupBy),
.getLocalAlbumBucketAssets(albumId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchLocalAlbumBucket(
albumId,
groupBy: groupBy,
),
);
TimelineService remoteAlbum({required String albumId}) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getRemoteBucketAssets(albumId, offset: offset, count: count),
.getRemoteAlbumBucketAssets(albumId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchRemoteAlbumBucket(
albumId,
groupBy: groupBy,
),
);
TimelineService remoteAssets(List<String> timelineUsers) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getRemoteBucketAssets(timelineUsers, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchRemoteBucket(
timelineUsers,
groupBy: GroupAssetsBy.month,
),
);
TimelineService favorite(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getFavoriteBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy),
_timelineRepository.watchFavoriteBucket(userId, groupBy: groupBy),
);
TimelineService trash(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getTrashBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchTrashBucket(userId, groupBy: groupBy),
);
TimelineService archive(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getArchiveBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchArchiveBucket(userId, groupBy: groupBy),
);
TimelineService lockedFolder(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getLockedFolderBucketAssets(userId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchLockedFolderBucket(
userId,
groupBy: groupBy,
),
);
TimelineService video(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getVideoBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchVideoBucket(userId, groupBy: groupBy),
);
}
class TimelineService {
final TimelineAssetSource _assetSource;
final TimelineBucketSource _bucketSource;
int _totalAssets = 0;
int get totalAssets => _totalAssets;
TimelineService({
required TimelineAssetSource assetSource,
required TimelineBucketSource bucketSource,
}) : _assetSource = assetSource,
_bucketSource = bucketSource {
_bucketSubscription =
_bucketSource().listen((_) => unawaited(reloadBucket()));
_bucketSubscription = _bucketSource().listen((buckets) {
_totalAssets =
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
unawaited(_reloadBucket());
});
}
final AsyncMutex _mutex = AsyncMutex();
@@ -74,8 +130,9 @@ 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);
EventStream.shared.emit(const TimelineReloadEvent());
});
Future<List<BaseAsset>> loadAssets(int index, int count) =>
@@ -117,6 +174,7 @@ class TimelineService {
index >= _bufferOffset && index + count <= _bufferOffset + _buffer.length;
List<BaseAsset> getAssets(int index, int count) {
assert(index + count <= totalAssets);
if (!hasRange(index, count)) {
throw RangeError('TimelineService::getAssets Index out of range');
}
@@ -124,6 +182,17 @@ class TimelineService {
return _buffer.slice(start, start + count);
}
// Pre-cache assets around the given index for asset viewer
Future<void> preCacheAssets(int index) =>
_mutex.run(() => _loadAssets(index, 5));
BaseAsset getAsset(int index) {
if (!hasRange(index, 1)) {
throw RangeError('TimelineService::getAsset Index out of range');
}
return _buffer.elementAt(index - _bufferOffset);
}
Future<void> dispose() async {
await _bucketSubscription?.cancel();
_bucketSubscription = null;

View File

@@ -0,0 +1,52 @@
import 'dart:async';
sealed class Event {
const Event();
}
class TimelineReloadEvent extends Event {
const TimelineReloadEvent();
}
class ViewerOpenBottomSheetEvent extends Event {
const ViewerOpenBottomSheetEvent();
}
class EventStream {
EventStream._();
static final EventStream shared = EventStream._();
final StreamController<Event> _controller =
StreamController<Event>.broadcast();
void emit(Event event) {
_controller.add(event);
}
Stream<T> where<T extends Event>() {
if (T == Event) {
return _controller.stream as Stream<T>;
}
return _controller.stream.where((event) => event is T).cast<T>();
}
StreamSubscription<T> listen<T extends Event>(
void Function(T event)? onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) {
return where<T>().listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
/// Closes the stream controller
void dispose() {
_controller.close();
}
}

View File

@@ -0,0 +1,58 @@
import 'remote_asset.entity.dart';
import 'local_asset.entity.dart';
-- TRIGGERS ON local_asset_entity
-- Find and update the remote_id in local_asset_entity and local_id in remote_asset_entity when checksum is set
CREATE TRIGGER IF NOT EXISTS tr_local_asset_update_checksum_set_ids
AFTER UPDATE OF checksum ON local_asset_entity
FOR EACH ROW
WHEN NEW.checksum IS NOT NULL
BEGIN
UPDATE local_asset_entity
SET remote_id = (SELECT id FROM remote_asset_entity WHERE checksum = NEW.checksum LIMIT 1)
WHERE id = NEW.id;
UPDATE remote_asset_entity
SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = NEW.checksum ORDER BY id ASC LIMIT 1)
WHERE checksum = NEW.checksum;
END;
-- When a local asset is updated, relink remote assets that had a checksum match
CREATE TRIGGER IF NOT EXISTS tr_local_asset_update_old_checksum_set_remote_asset_local_id
AFTER UPDATE OF checksum ON local_asset_entity
FOR EACH ROW
WHEN OLD.checksum IS NOT NULL
BEGIN
UPDATE remote_asset_entity
SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = OLD.checksum ORDER BY id ASC LIMIT 1)
WHERE checksum = OLD.checksum;
END;
-- remote_asset_entity.checksum is a 1..* relationship with local_asset_entity.checksum.
-- When a local asset is deleted, update remote assets that had a checksum match
-- to ensure their local_id is set to the first matching local asset or NULL
CREATE TRIGGER IF NOT EXISTS tr_local_asset_delete_update_remote_asset_local_id
AFTER DELETE ON local_asset_entity
FOR EACH ROW
WHEN OLD.checksum IS NOT NULL
BEGIN
UPDATE remote_asset_entity
SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = OLD.checksum ORDER BY id ASC LIMIT 1)
WHERE checksum = OLD.checksum;
END;
-- TRIGGERS ON remote_asset_entity
-- Find and update local_id in remote_asset_entity when a new remote asset is inserted
CREATE TRIGGER IF NOT EXISTS tr_remote_asset_insert_set_local_id
AFTER INSERT ON remote_asset_entity
FOR EACH ROW
BEGIN
UPDATE remote_asset_entity
SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = NEW.checksum ORDER BY id ASC LIMIT 1)
WHERE id = NEW.id;
UPDATE local_asset_entity SET remote_id = NEW.id WHERE checksum = NEW.checksum;
END;

View File

@@ -0,0 +1,16 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
i0.Trigger get trLocalAssetUpdateChecksumSetIds => i0.Trigger(
'CREATE TRIGGER IF NOT EXISTS tr_local_asset_update_checksum_set_ids AFTER UPDATE OF checksum ON local_asset_entity WHEN NEW.checksum IS NOT NULL BEGIN UPDATE local_asset_entity SET remote_id = (SELECT id FROM remote_asset_entity WHERE checksum = NEW.checksum LIMIT 1) WHERE id = NEW.id;UPDATE remote_asset_entity SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = NEW.checksum ORDER BY id ASC LIMIT 1) WHERE checksum = NEW.checksum;END',
'tr_local_asset_update_checksum_set_ids');
i0.Trigger get trLocalAssetUpdateOldChecksumSetRemoteAssetLocalId => i0.Trigger(
'CREATE TRIGGER IF NOT EXISTS tr_local_asset_update_old_checksum_set_remote_asset_local_id AFTER UPDATE OF checksum ON local_asset_entity WHEN OLD.checksum IS NOT NULL BEGIN UPDATE remote_asset_entity SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = OLD.checksum ORDER BY id ASC LIMIT 1) WHERE checksum = OLD.checksum;END',
'tr_local_asset_update_old_checksum_set_remote_asset_local_id');
i0.Trigger get trLocalAssetDeleteUpdateRemoteAssetLocalId => i0.Trigger(
'CREATE TRIGGER IF NOT EXISTS tr_local_asset_delete_update_remote_asset_local_id AFTER DELETE ON local_asset_entity WHEN OLD.checksum IS NOT NULL BEGIN UPDATE remote_asset_entity SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = OLD.checksum ORDER BY id ASC LIMIT 1) WHERE checksum = OLD.checksum;END',
'tr_local_asset_delete_update_remote_asset_local_id');
i0.Trigger get trRemoteAssetInsertSetLocalId => i0.Trigger(
'CREATE TRIGGER IF NOT EXISTS tr_remote_asset_insert_set_local_id AFTER INSERT ON remote_asset_entity BEGIN UPDATE remote_asset_entity SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = NEW.checksum ORDER BY id ASC LIMIT 1) WHERE id = NEW.id;UPDATE local_asset_entity SET remote_id = NEW.id WHERE checksum = NEW.checksum;END',
'tr_remote_asset_insert_set_local_id');

View File

@@ -1,5 +1,6 @@
import 'package:drift/drift.dart' hide Query;
import 'package:immich_mobile/domain/models/exif.model.dart' as domain;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
@@ -132,6 +133,8 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin {
TextColumn get model => text().nullable()();
TextColumn get lens => text().nullable()();
TextColumn get orientation => text().nullable()();
TextColumn get timeZone => text().nullable()();
@@ -143,3 +146,27 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin {
@override
Set<Column> get primaryKey => {assetId};
}
extension RemoteExifEntityDataDomainEx on RemoteExifEntityData {
domain.ExifInfo toDto() => domain.ExifInfo(
fileSize: fileSize,
dateTimeOriginal: dateTimeOriginal,
timeZone: timeZone,
make: make,
model: model,
iso: iso,
city: city,
state: state,
country: country,
description: description,
orientation: orientation,
latitude: latitude,
longitude: longitude,
f: fNumber?.toDouble(),
mm: focalLength?.toDouble(),
lens: lens,
width: width?.toDouble(),
height: height?.toDouble(),
isFlipped: ExifDtoConverter.isOrientationFlipped(orientation),
);
}

View File

@@ -27,6 +27,7 @@ typedef $$RemoteExifEntityTableCreateCompanionBuilder
i0.Value<int?> iso,
i0.Value<String?> make,
i0.Value<String?> model,
i0.Value<String?> lens,
i0.Value<String?> orientation,
i0.Value<String?> timeZone,
i0.Value<int?> rating,
@@ -51,6 +52,7 @@ typedef $$RemoteExifEntityTableUpdateCompanionBuilder
i0.Value<int?> iso,
i0.Value<String?> make,
i0.Value<String?> model,
i0.Value<String?> lens,
i0.Value<String?> orientation,
i0.Value<String?> timeZone,
i0.Value<int?> rating,
@@ -150,6 +152,9 @@ class $$RemoteExifEntityTableFilterComposer
i0.ColumnFilters<String> get model => $composableBuilder(
column: $table.model, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get lens => $composableBuilder(
column: $table.lens, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get orientation => $composableBuilder(
column: $table.orientation,
builder: (column) => i0.ColumnFilters(column));
@@ -249,6 +254,9 @@ class $$RemoteExifEntityTableOrderingComposer
i0.ColumnOrderings<String> get model => $composableBuilder(
column: $table.model, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get lens => $composableBuilder(
column: $table.lens, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get orientation => $composableBuilder(
column: $table.orientation,
builder: (column) => i0.ColumnOrderings(column));
@@ -345,6 +353,9 @@ class $$RemoteExifEntityTableAnnotationComposer
i0.GeneratedColumn<String> get model =>
$composableBuilder(column: $table.model, builder: (column) => column);
i0.GeneratedColumn<String> get lens =>
$composableBuilder(column: $table.lens, builder: (column) => column);
i0.GeneratedColumn<String> get orientation => $composableBuilder(
column: $table.orientation, builder: (column) => column);
@@ -424,6 +435,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> iso = const i0.Value.absent(),
i0.Value<String?> make = const i0.Value.absent(),
i0.Value<String?> model = const i0.Value.absent(),
i0.Value<String?> lens = const i0.Value.absent(),
i0.Value<String?> orientation = const i0.Value.absent(),
i0.Value<String?> timeZone = const i0.Value.absent(),
i0.Value<int?> rating = const i0.Value.absent(),
@@ -447,6 +459,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
iso: iso,
make: make,
model: model,
lens: lens,
orientation: orientation,
timeZone: timeZone,
rating: rating,
@@ -470,6 +483,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> iso = const i0.Value.absent(),
i0.Value<String?> make = const i0.Value.absent(),
i0.Value<String?> model = const i0.Value.absent(),
i0.Value<String?> lens = const i0.Value.absent(),
i0.Value<String?> orientation = const i0.Value.absent(),
i0.Value<String?> timeZone = const i0.Value.absent(),
i0.Value<int?> rating = const i0.Value.absent(),
@@ -493,6 +507,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
iso: iso,
make: make,
model: model,
lens: lens,
orientation: orientation,
timeZone: timeZone,
rating: rating,
@@ -666,6 +681,12 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
late final i0.GeneratedColumn<String> model = i0.GeneratedColumn<String>(
'model', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
static const i0.VerificationMeta _lensMeta =
const i0.VerificationMeta('lens');
@override
late final i0.GeneratedColumn<String> lens = i0.GeneratedColumn<String>(
'lens', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
static const i0.VerificationMeta _orientationMeta =
const i0.VerificationMeta('orientation');
@override
@@ -709,6 +730,7 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
iso,
make,
model,
lens,
orientation,
timeZone,
rating,
@@ -803,6 +825,10 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
context.handle(
_modelMeta, model.isAcceptableOrUnknown(data['model']!, _modelMeta));
}
if (data.containsKey('lens')) {
context.handle(
_lensMeta, lens.isAcceptableOrUnknown(data['lens']!, _lensMeta));
}
if (data.containsKey('orientation')) {
context.handle(
_orientationMeta,
@@ -868,6 +894,8 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
.read(i0.DriftSqlType.string, data['${effectivePrefix}make']),
model: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}model']),
lens: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}lens']),
orientation: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}orientation']),
timeZone: attachedDatabase.typeMapping
@@ -909,6 +937,7 @@ class RemoteExifEntityData extends i0.DataClass
final int? iso;
final String? make;
final String? model;
final String? lens;
final String? orientation;
final String? timeZone;
final int? rating;
@@ -931,6 +960,7 @@ class RemoteExifEntityData extends i0.DataClass
this.iso,
this.make,
this.model,
this.lens,
this.orientation,
this.timeZone,
this.rating,
@@ -987,6 +1017,9 @@ class RemoteExifEntityData extends i0.DataClass
if (!nullToAbsent || model != null) {
map['model'] = i0.Variable<String>(model);
}
if (!nullToAbsent || lens != null) {
map['lens'] = i0.Variable<String>(lens);
}
if (!nullToAbsent || orientation != null) {
map['orientation'] = i0.Variable<String>(orientation);
}
@@ -1024,6 +1057,7 @@ class RemoteExifEntityData extends i0.DataClass
iso: serializer.fromJson<int?>(json['iso']),
make: serializer.fromJson<String?>(json['make']),
model: serializer.fromJson<String?>(json['model']),
lens: serializer.fromJson<String?>(json['lens']),
orientation: serializer.fromJson<String?>(json['orientation']),
timeZone: serializer.fromJson<String?>(json['timeZone']),
rating: serializer.fromJson<int?>(json['rating']),
@@ -1051,6 +1085,7 @@ class RemoteExifEntityData extends i0.DataClass
'iso': serializer.toJson<int?>(iso),
'make': serializer.toJson<String?>(make),
'model': serializer.toJson<String?>(model),
'lens': serializer.toJson<String?>(lens),
'orientation': serializer.toJson<String?>(orientation),
'timeZone': serializer.toJson<String?>(timeZone),
'rating': serializer.toJson<int?>(rating),
@@ -1076,6 +1111,7 @@ class RemoteExifEntityData extends i0.DataClass
i0.Value<int?> iso = const i0.Value.absent(),
i0.Value<String?> make = const i0.Value.absent(),
i0.Value<String?> model = const i0.Value.absent(),
i0.Value<String?> lens = const i0.Value.absent(),
i0.Value<String?> orientation = const i0.Value.absent(),
i0.Value<String?> timeZone = const i0.Value.absent(),
i0.Value<int?> rating = const i0.Value.absent(),
@@ -1101,6 +1137,7 @@ class RemoteExifEntityData extends i0.DataClass
iso: iso.present ? iso.value : this.iso,
make: make.present ? make.value : this.make,
model: model.present ? model.value : this.model,
lens: lens.present ? lens.value : this.lens,
orientation: orientation.present ? orientation.value : this.orientation,
timeZone: timeZone.present ? timeZone.value : this.timeZone,
rating: rating.present ? rating.value : this.rating,
@@ -1132,6 +1169,7 @@ class RemoteExifEntityData extends i0.DataClass
iso: data.iso.present ? data.iso.value : this.iso,
make: data.make.present ? data.make.value : this.make,
model: data.model.present ? data.model.value : this.model,
lens: data.lens.present ? data.lens.value : this.lens,
orientation:
data.orientation.present ? data.orientation.value : this.orientation,
timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone,
@@ -1162,6 +1200,7 @@ class RemoteExifEntityData extends i0.DataClass
..write('iso: $iso, ')
..write('make: $make, ')
..write('model: $model, ')
..write('lens: $lens, ')
..write('orientation: $orientation, ')
..write('timeZone: $timeZone, ')
..write('rating: $rating, ')
@@ -1189,6 +1228,7 @@ class RemoteExifEntityData extends i0.DataClass
iso,
make,
model,
lens,
orientation,
timeZone,
rating,
@@ -1215,6 +1255,7 @@ class RemoteExifEntityData extends i0.DataClass
other.iso == this.iso &&
other.make == this.make &&
other.model == this.model &&
other.lens == this.lens &&
other.orientation == this.orientation &&
other.timeZone == this.timeZone &&
other.rating == this.rating &&
@@ -1240,6 +1281,7 @@ class RemoteExifEntityCompanion
final i0.Value<int?> iso;
final i0.Value<String?> make;
final i0.Value<String?> model;
final i0.Value<String?> lens;
final i0.Value<String?> orientation;
final i0.Value<String?> timeZone;
final i0.Value<int?> rating;
@@ -1262,6 +1304,7 @@ class RemoteExifEntityCompanion
this.iso = const i0.Value.absent(),
this.make = const i0.Value.absent(),
this.model = const i0.Value.absent(),
this.lens = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
this.timeZone = const i0.Value.absent(),
this.rating = const i0.Value.absent(),
@@ -1285,6 +1328,7 @@ class RemoteExifEntityCompanion
this.iso = const i0.Value.absent(),
this.make = const i0.Value.absent(),
this.model = const i0.Value.absent(),
this.lens = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
this.timeZone = const i0.Value.absent(),
this.rating = const i0.Value.absent(),
@@ -1308,6 +1352,7 @@ class RemoteExifEntityCompanion
i0.Expression<int>? iso,
i0.Expression<String>? make,
i0.Expression<String>? model,
i0.Expression<String>? lens,
i0.Expression<String>? orientation,
i0.Expression<String>? timeZone,
i0.Expression<int>? rating,
@@ -1331,6 +1376,7 @@ class RemoteExifEntityCompanion
if (iso != null) 'iso': iso,
if (make != null) 'make': make,
if (model != null) 'model': model,
if (lens != null) 'lens': lens,
if (orientation != null) 'orientation': orientation,
if (timeZone != null) 'time_zone': timeZone,
if (rating != null) 'rating': rating,
@@ -1356,6 +1402,7 @@ class RemoteExifEntityCompanion
i0.Value<int?>? iso,
i0.Value<String?>? make,
i0.Value<String?>? model,
i0.Value<String?>? lens,
i0.Value<String?>? orientation,
i0.Value<String?>? timeZone,
i0.Value<int?>? rating,
@@ -1378,6 +1425,7 @@ class RemoteExifEntityCompanion
iso: iso ?? this.iso,
make: make ?? this.make,
model: model ?? this.model,
lens: lens ?? this.lens,
orientation: orientation ?? this.orientation,
timeZone: timeZone ?? this.timeZone,
rating: rating ?? this.rating,
@@ -1439,6 +1487,9 @@ class RemoteExifEntityCompanion
if (model.present) {
map['model'] = i0.Variable<String>(model.value);
}
if (lens.present) {
map['lens'] = i0.Variable<String>(lens.value);
}
if (orientation.present) {
map['orientation'] = i0.Variable<String>(orientation.value);
}
@@ -1474,6 +1525,7 @@ class RemoteExifEntityCompanion
..write('iso: $iso, ')
..write('make: $make, ')
..write('model: $model, ')
..write('lens: $lens, ')
..write('orientation: $orientation, ')
..write('timeZone: $timeZone, ')
..write('rating: $rating, ')

View File

@@ -1,6 +1,7 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@@ -9,6 +10,11 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const LocalAssetEntity();
TextColumn get id => text()();
TextColumn get remoteId => text()
.nullable()
.references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull)();
TextColumn get checksum => text().nullable()();
// Only used during backup to mirror the favorite status of the asset in the server
@@ -28,5 +34,8 @@ extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
updatedAt: updatedAt,
durationInSeconds: durationInSeconds,
isFavorite: isFavorite,
height: height,
width: width,
remoteId: remoteId,
);
}

View File

@@ -7,6 +7,9 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$LocalAssetEntityTableCreateCompanionBuilder
= i1.LocalAssetEntityCompanion Function({
@@ -18,6 +21,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
required String id,
i0.Value<String?> remoteId,
i0.Value<String?> checksum,
i0.Value<bool> isFavorite,
});
@@ -31,10 +35,43 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
i0.Value<String> id,
i0.Value<String?> remoteId,
i0.Value<String?> checksum,
i0.Value<bool> isFavorite,
});
final class $$LocalAssetEntityTableReferences extends i0.BaseReferences<
i0.GeneratedDatabase, i1.$LocalAssetEntityTable, i1.LocalAssetEntityData> {
$$LocalAssetEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i5.$RemoteAssetEntityTable _remoteIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$LocalAssetEntityTable>('local_asset_entity')
.remoteId,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
.id));
i5.$$RemoteAssetEntityTableProcessedTableManager? get remoteId {
final $_column = $_itemColumn<String>('remote_id');
if ($_column == null) return null;
final manager = i5
.$$RemoteAssetEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_remoteIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$LocalAssetEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetEntityTable> {
$$LocalAssetEntityTableFilterComposer({
@@ -76,6 +113,28 @@ class $$LocalAssetEntityTableFilterComposer
i0.ColumnFilters<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column));
i5.$$RemoteAssetEntityTableFilterComposer get remoteId {
final i5.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.remoteId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$RemoteAssetEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$LocalAssetEntityTableOrderingComposer
@@ -120,6 +179,30 @@ class $$LocalAssetEntityTableOrderingComposer
i0.ColumnOrderings<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite,
builder: (column) => i0.ColumnOrderings(column));
i5.$$RemoteAssetEntityTableOrderingComposer get remoteId {
final i5.$$RemoteAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.remoteId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$RemoteAssetEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$LocalAssetEntityTableAnnotationComposer
@@ -160,6 +243,30 @@ class $$LocalAssetEntityTableAnnotationComposer
i0.GeneratedColumn<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite, builder: (column) => column);
i5.$$RemoteAssetEntityTableAnnotationComposer get remoteId {
final i5.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.remoteId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
@@ -171,13 +278,9 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
i1.$$LocalAssetEntityTableAnnotationComposer,
$$LocalAssetEntityTableCreateCompanionBuilder,
$$LocalAssetEntityTableUpdateCompanionBuilder,
(
i1.LocalAssetEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAssetEntityTable,
i1.LocalAssetEntityData>
),
(i1.LocalAssetEntityData, i1.$$LocalAssetEntityTableReferences),
i1.LocalAssetEntityData,
i0.PrefetchHooks Function()> {
i0.PrefetchHooks Function({bool remoteId})> {
$$LocalAssetEntityTableTableManager(
i0.GeneratedDatabase db, i1.$LocalAssetEntityTable table)
: super(i0.TableManagerState(
@@ -199,6 +302,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String?> remoteId = const i0.Value.absent(),
i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
}) =>
@@ -211,6 +315,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
height: height,
durationInSeconds: durationInSeconds,
id: id,
remoteId: remoteId,
checksum: checksum,
isFavorite: isFavorite,
),
@@ -223,6 +328,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
required String id,
i0.Value<String?> remoteId = const i0.Value.absent(),
i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
}) =>
@@ -235,13 +341,52 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
height: height,
durationInSeconds: durationInSeconds,
id: id,
remoteId: remoteId,
checksum: checksum,
isFavorite: isFavorite,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.map((e) => (
e.readTable(table),
i1.$$LocalAssetEntityTableReferences(db, table, e)
))
.toList(),
prefetchHooksCallback: null,
prefetchHooksCallback: ({remoteId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins: <
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic>>(state) {
if (remoteId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.remoteId,
referencedTable:
i1.$$LocalAssetEntityTableReferences._remoteIdTable(db),
referencedColumn: i1.$$LocalAssetEntityTableReferences
._remoteIdTable(db)
.id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
));
}
@@ -254,13 +399,9 @@ typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager<
i1.$$LocalAssetEntityTableAnnotationComposer,
$$LocalAssetEntityTableCreateCompanionBuilder,
$$LocalAssetEntityTableUpdateCompanionBuilder,
(
i1.LocalAssetEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAssetEntityTable,
i1.LocalAssetEntityData>
),
(i1.LocalAssetEntityData, i1.$$LocalAssetEntityTableReferences),
i1.LocalAssetEntityData,
i0.PrefetchHooks Function()>;
i0.PrefetchHooks Function({bool remoteId})>;
i0.Index get idxLocalAssetChecksum => i0.Index('idx_local_asset_checksum',
'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)');
@@ -321,6 +462,15 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _remoteIdMeta =
const i0.VerificationMeta('remoteId');
@override
late final i0.GeneratedColumn<String> remoteId = i0.GeneratedColumn<String>(
'remote_id', aliasedName, true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE SET NULL'));
static const i0.VerificationMeta _checksumMeta =
const i0.VerificationMeta('checksum');
@override
@@ -347,6 +497,7 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
height,
durationInSeconds,
id,
remoteId,
checksum,
isFavorite
];
@@ -394,6 +545,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('remote_id')) {
context.handle(_remoteIdMeta,
remoteId.isAcceptableOrUnknown(data['remote_id']!, _remoteIdMeta));
}
if (data.containsKey('checksum')) {
context.handle(_checksumMeta,
checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta));
@@ -431,6 +586,8 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']),
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
remoteId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}remote_id']),
checksum: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']),
isFavorite: attachedDatabase.typeMapping
@@ -461,6 +618,7 @@ class LocalAssetEntityData extends i0.DataClass
final int? height;
final int? durationInSeconds;
final String id;
final String? remoteId;
final String? checksum;
final bool isFavorite;
const LocalAssetEntityData(
@@ -472,6 +630,7 @@ class LocalAssetEntityData extends i0.DataClass
this.height,
this.durationInSeconds,
required this.id,
this.remoteId,
this.checksum,
required this.isFavorite});
@override
@@ -494,6 +653,9 @@ class LocalAssetEntityData extends i0.DataClass
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds);
}
map['id'] = i0.Variable<String>(id);
if (!nullToAbsent || remoteId != null) {
map['remote_id'] = i0.Variable<String>(remoteId);
}
if (!nullToAbsent || checksum != null) {
map['checksum'] = i0.Variable<String>(checksum);
}
@@ -514,6 +676,7 @@ class LocalAssetEntityData extends i0.DataClass
height: serializer.fromJson<int?>(json['height']),
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
id: serializer.fromJson<String>(json['id']),
remoteId: serializer.fromJson<String?>(json['remoteId']),
checksum: serializer.fromJson<String?>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
);
@@ -531,6 +694,7 @@ class LocalAssetEntityData extends i0.DataClass
'height': serializer.toJson<int?>(height),
'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
'id': serializer.toJson<String>(id),
'remoteId': serializer.toJson<String?>(remoteId),
'checksum': serializer.toJson<String?>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite),
};
@@ -545,6 +709,7 @@ class LocalAssetEntityData extends i0.DataClass
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
String? id,
i0.Value<String?> remoteId = const i0.Value.absent(),
i0.Value<String?> checksum = const i0.Value.absent(),
bool? isFavorite}) =>
i1.LocalAssetEntityData(
@@ -558,6 +723,7 @@ class LocalAssetEntityData extends i0.DataClass
? durationInSeconds.value
: this.durationInSeconds,
id: id ?? this.id,
remoteId: remoteId.present ? remoteId.value : this.remoteId,
checksum: checksum.present ? checksum.value : this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
);
@@ -573,6 +739,7 @@ class LocalAssetEntityData extends i0.DataClass
? data.durationInSeconds.value
: this.durationInSeconds,
id: data.id.present ? data.id.value : this.id,
remoteId: data.remoteId.present ? data.remoteId.value : this.remoteId,
checksum: data.checksum.present ? data.checksum.value : this.checksum,
isFavorite:
data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
@@ -590,6 +757,7 @@ class LocalAssetEntityData extends i0.DataClass
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ')
..write('remoteId: $remoteId, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite')
..write(')'))
@@ -598,7 +766,7 @@ class LocalAssetEntityData extends i0.DataClass
@override
int get hashCode => Object.hash(name, type, createdAt, updatedAt, width,
height, durationInSeconds, id, checksum, isFavorite);
height, durationInSeconds, id, remoteId, checksum, isFavorite);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -611,6 +779,7 @@ class LocalAssetEntityData extends i0.DataClass
other.height == this.height &&
other.durationInSeconds == this.durationInSeconds &&
other.id == this.id &&
other.remoteId == this.remoteId &&
other.checksum == this.checksum &&
other.isFavorite == this.isFavorite);
}
@@ -625,6 +794,7 @@ class LocalAssetEntityCompanion
final i0.Value<int?> height;
final i0.Value<int?> durationInSeconds;
final i0.Value<String> id;
final i0.Value<String?> remoteId;
final i0.Value<String?> checksum;
final i0.Value<bool> isFavorite;
const LocalAssetEntityCompanion({
@@ -636,6 +806,7 @@ class LocalAssetEntityCompanion
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
this.id = const i0.Value.absent(),
this.remoteId = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
});
@@ -648,6 +819,7 @@ class LocalAssetEntityCompanion
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
required String id,
this.remoteId = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
}) : name = i0.Value(name),
@@ -662,6 +834,7 @@ class LocalAssetEntityCompanion
i0.Expression<int>? height,
i0.Expression<int>? durationInSeconds,
i0.Expression<String>? id,
i0.Expression<String>? remoteId,
i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite,
}) {
@@ -674,6 +847,7 @@ class LocalAssetEntityCompanion
if (height != null) 'height': height,
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
if (id != null) 'id': id,
if (remoteId != null) 'remote_id': remoteId,
if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite,
});
@@ -688,6 +862,7 @@ class LocalAssetEntityCompanion
i0.Value<int?>? height,
i0.Value<int?>? durationInSeconds,
i0.Value<String>? id,
i0.Value<String?>? remoteId,
i0.Value<String?>? checksum,
i0.Value<bool>? isFavorite}) {
return i1.LocalAssetEntityCompanion(
@@ -699,6 +874,7 @@ class LocalAssetEntityCompanion
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
id: id ?? this.id,
remoteId: remoteId ?? this.remoteId,
checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
);
@@ -732,6 +908,9 @@ class LocalAssetEntityCompanion
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (remoteId.present) {
map['remote_id'] = i0.Variable<String>(remoteId.value);
}
if (checksum.present) {
map['checksum'] = i0.Variable<String>(checksum.value);
}
@@ -752,6 +931,7 @@ class LocalAssetEntityCompanion
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ')
..write('remoteId: $remoteId, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite')
..write(')'))

View File

@@ -0,0 +1,36 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class MemoryEntity extends Table with DriftDefaultsMixin {
const MemoryEntity();
TextColumn get id => text()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get deletedAt => dateTime().nullable()();
TextColumn get ownerId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
IntColumn get type => intEnum<MemoryTypeEnum>()();
TextColumn get data => text()();
BoolColumn get isSaved => boolean().withDefault(const Constant(false))();
DateTimeColumn get memoryAt => dateTime()();
DateTimeColumn get seenAt => dateTime().nullable()();
DateTimeColumn get showAt => dateTime().nullable()();
DateTimeColumn get hideAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {id};
}

View File

@@ -0,0 +1,970 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i1;
import 'package:immich_mobile/domain/models/memory.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart' as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$MemoryEntityTableCreateCompanionBuilder = i1.MemoryEntityCompanion
Function({
required String id,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
i0.Value<DateTime?> deletedAt,
required String ownerId,
required i2.MemoryTypeEnum type,
required String data,
i0.Value<bool> isSaved,
required DateTime memoryAt,
i0.Value<DateTime?> seenAt,
i0.Value<DateTime?> showAt,
i0.Value<DateTime?> hideAt,
});
typedef $$MemoryEntityTableUpdateCompanionBuilder = i1.MemoryEntityCompanion
Function({
i0.Value<String> id,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
i0.Value<DateTime?> deletedAt,
i0.Value<String> ownerId,
i0.Value<i2.MemoryTypeEnum> type,
i0.Value<String> data,
i0.Value<bool> isSaved,
i0.Value<DateTime> memoryAt,
i0.Value<DateTime?> seenAt,
i0.Value<DateTime?> showAt,
i0.Value<DateTime?> hideAt,
});
final class $$MemoryEntityTableReferences extends i0.BaseReferences<
i0.GeneratedDatabase, i1.$MemoryEntityTable, i1.MemoryEntityData> {
$$MemoryEntityTableReferences(super.$_db, super.$_table, super.$_typedResult);
static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$MemoryEntityTable>('memory_entity')
.ownerId,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get ownerId {
final $_column = $_itemColumn<String>('owner_id')!;
final manager = i5
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$MemoryEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryEntityTable> {
$$MemoryEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder(
column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnWithTypeConverterFilters<i2.MemoryTypeEnum, i2.MemoryTypeEnum, int>
get type => $composableBuilder(
column: $table.type,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i0.ColumnFilters<String> get data => $composableBuilder(
column: $table.data, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<bool> get isSaved => $composableBuilder(
column: $table.isSaved, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get memoryAt => $composableBuilder(
column: $table.memoryAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get seenAt => $composableBuilder(
column: $table.seenAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get showAt => $composableBuilder(
column: $table.showAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get hideAt => $composableBuilder(
column: $table.hideAt, builder: (column) => i0.ColumnFilters(column));
i5.$$UserEntityTableFilterComposer get ownerId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$MemoryEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryEntityTable> {
$$MemoryEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get deletedAt => $composableBuilder(
column: $table.deletedAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get type => $composableBuilder(
column: $table.type, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get data => $composableBuilder(
column: $table.data, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<bool> get isSaved => $composableBuilder(
column: $table.isSaved, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get memoryAt => $composableBuilder(
column: $table.memoryAt, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get seenAt => $composableBuilder(
column: $table.seenAt, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get showAt => $composableBuilder(
column: $table.showAt, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get hideAt => $composableBuilder(
column: $table.hideAt, builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get ownerId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$MemoryEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryEntityTable> {
$$MemoryEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get deletedAt =>
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.MemoryTypeEnum, int> get type =>
$composableBuilder(column: $table.type, builder: (column) => column);
i0.GeneratedColumn<String> get data =>
$composableBuilder(column: $table.data, builder: (column) => column);
i0.GeneratedColumn<bool> get isSaved =>
$composableBuilder(column: $table.isSaved, builder: (column) => column);
i0.GeneratedColumn<DateTime> get memoryAt =>
$composableBuilder(column: $table.memoryAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get seenAt =>
$composableBuilder(column: $table.seenAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get showAt =>
$composableBuilder(column: $table.showAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get hideAt =>
$composableBuilder(column: $table.hideAt, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get ownerId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$MemoryEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$MemoryEntityTable,
i1.MemoryEntityData,
i1.$$MemoryEntityTableFilterComposer,
i1.$$MemoryEntityTableOrderingComposer,
i1.$$MemoryEntityTableAnnotationComposer,
$$MemoryEntityTableCreateCompanionBuilder,
$$MemoryEntityTableUpdateCompanionBuilder,
(i1.MemoryEntityData, i1.$$MemoryEntityTableReferences),
i1.MemoryEntityData,
i0.PrefetchHooks Function({bool ownerId})> {
$$MemoryEntityTableTableManager(
i0.GeneratedDatabase db, i1.$MemoryEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$MemoryEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$MemoryEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$MemoryEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String> ownerId = const i0.Value.absent(),
i0.Value<i2.MemoryTypeEnum> type = const i0.Value.absent(),
i0.Value<String> data = const i0.Value.absent(),
i0.Value<bool> isSaved = const i0.Value.absent(),
i0.Value<DateTime> memoryAt = const i0.Value.absent(),
i0.Value<DateTime?> seenAt = const i0.Value.absent(),
i0.Value<DateTime?> showAt = const i0.Value.absent(),
i0.Value<DateTime?> hideAt = const i0.Value.absent(),
}) =>
i1.MemoryEntityCompanion(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
deletedAt: deletedAt,
ownerId: ownerId,
type: type,
data: data,
isSaved: isSaved,
memoryAt: memoryAt,
seenAt: seenAt,
showAt: showAt,
hideAt: hideAt,
),
createCompanionCallback: ({
required String id,
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
required String ownerId,
required i2.MemoryTypeEnum type,
required String data,
i0.Value<bool> isSaved = const i0.Value.absent(),
required DateTime memoryAt,
i0.Value<DateTime?> seenAt = const i0.Value.absent(),
i0.Value<DateTime?> showAt = const i0.Value.absent(),
i0.Value<DateTime?> hideAt = const i0.Value.absent(),
}) =>
i1.MemoryEntityCompanion.insert(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
deletedAt: deletedAt,
ownerId: ownerId,
type: type,
data: data,
isSaved: isSaved,
memoryAt: memoryAt,
seenAt: seenAt,
showAt: showAt,
hideAt: hideAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (
e.readTable(table),
i1.$$MemoryEntityTableReferences(db, table, e)
))
.toList(),
prefetchHooksCallback: ({ownerId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins: <
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic>>(state) {
if (ownerId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.ownerId,
referencedTable:
i1.$$MemoryEntityTableReferences._ownerIdTable(db),
referencedColumn:
i1.$$MemoryEntityTableReferences._ownerIdTable(db).id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
));
}
typedef $$MemoryEntityTableProcessedTableManager = i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$MemoryEntityTable,
i1.MemoryEntityData,
i1.$$MemoryEntityTableFilterComposer,
i1.$$MemoryEntityTableOrderingComposer,
i1.$$MemoryEntityTableAnnotationComposer,
$$MemoryEntityTableCreateCompanionBuilder,
$$MemoryEntityTableUpdateCompanionBuilder,
(i1.MemoryEntityData, i1.$$MemoryEntityTableReferences),
i1.MemoryEntityData,
i0.PrefetchHooks Function({bool ownerId})>;
class $MemoryEntityTable extends i3.MemoryEntity
with i0.TableInfo<$MemoryEntityTable, i1.MemoryEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$MemoryEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _createdAtMeta =
const i0.VerificationMeta('createdAt');
@override
late final i0.GeneratedColumn<DateTime> createdAt =
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime);
static const i0.VerificationMeta _updatedAtMeta =
const i0.VerificationMeta('updatedAt');
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime);
static const i0.VerificationMeta _deletedAtMeta =
const i0.VerificationMeta('deletedAt');
@override
late final i0.GeneratedColumn<DateTime> deletedAt =
i0.GeneratedColumn<DateTime>('deleted_at', aliasedName, true,
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
static const i0.VerificationMeta _ownerIdMeta =
const i0.VerificationMeta('ownerId');
@override
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
'owner_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
@override
late final i0.GeneratedColumnWithTypeConverter<i2.MemoryTypeEnum, int> type =
i0.GeneratedColumn<int>('type', aliasedName, false,
type: i0.DriftSqlType.int, requiredDuringInsert: true)
.withConverter<i2.MemoryTypeEnum>(
i1.$MemoryEntityTable.$convertertype);
static const i0.VerificationMeta _dataMeta =
const i0.VerificationMeta('data');
@override
late final i0.GeneratedColumn<String> data = i0.GeneratedColumn<String>(
'data', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _isSavedMeta =
const i0.VerificationMeta('isSaved');
@override
late final i0.GeneratedColumn<bool> isSaved = i0.GeneratedColumn<bool>(
'is_saved', aliasedName, false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints:
i0.GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'),
defaultValue: const i4.Constant(false));
static const i0.VerificationMeta _memoryAtMeta =
const i0.VerificationMeta('memoryAt');
@override
late final i0.GeneratedColumn<DateTime> memoryAt =
i0.GeneratedColumn<DateTime>('memory_at', aliasedName, false,
type: i0.DriftSqlType.dateTime, requiredDuringInsert: true);
static const i0.VerificationMeta _seenAtMeta =
const i0.VerificationMeta('seenAt');
@override
late final i0.GeneratedColumn<DateTime> seenAt = i0.GeneratedColumn<DateTime>(
'seen_at', aliasedName, true,
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
static const i0.VerificationMeta _showAtMeta =
const i0.VerificationMeta('showAt');
@override
late final i0.GeneratedColumn<DateTime> showAt = i0.GeneratedColumn<DateTime>(
'show_at', aliasedName, true,
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
static const i0.VerificationMeta _hideAtMeta =
const i0.VerificationMeta('hideAt');
@override
late final i0.GeneratedColumn<DateTime> hideAt = i0.GeneratedColumn<DateTime>(
'hide_at', aliasedName, true,
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
@override
List<i0.GeneratedColumn> get $columns => [
id,
createdAt,
updatedAt,
deletedAt,
ownerId,
type,
data,
isSaved,
memoryAt,
seenAt,
showAt,
hideAt
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'memory_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.MemoryEntityData> 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('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
if (data.containsKey('updated_at')) {
context.handle(_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
}
if (data.containsKey('deleted_at')) {
context.handle(_deletedAtMeta,
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta));
}
if (data.containsKey('owner_id')) {
context.handle(_ownerIdMeta,
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta));
} else if (isInserting) {
context.missing(_ownerIdMeta);
}
if (data.containsKey('data')) {
context.handle(
_dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta));
} else if (isInserting) {
context.missing(_dataMeta);
}
if (data.containsKey('is_saved')) {
context.handle(_isSavedMeta,
isSaved.isAcceptableOrUnknown(data['is_saved']!, _isSavedMeta));
}
if (data.containsKey('memory_at')) {
context.handle(_memoryAtMeta,
memoryAt.isAcceptableOrUnknown(data['memory_at']!, _memoryAtMeta));
} else if (isInserting) {
context.missing(_memoryAtMeta);
}
if (data.containsKey('seen_at')) {
context.handle(_seenAtMeta,
seenAt.isAcceptableOrUnknown(data['seen_at']!, _seenAtMeta));
}
if (data.containsKey('show_at')) {
context.handle(_showAtMeta,
showAt.isAcceptableOrUnknown(data['show_at']!, _showAtMeta));
}
if (data.containsKey('hide_at')) {
context.handle(_hideAtMeta,
hideAt.isAcceptableOrUnknown(data['hide_at']!, _hideAtMeta));
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.MemoryEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.MemoryEntityData(
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
createdAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
deletedAt: attachedDatabase.typeMapping
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']),
ownerId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!,
type: i1.$MemoryEntityTable.$convertertype.fromSql(attachedDatabase
.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!),
data: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}data'])!,
isSaved: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_saved'])!,
memoryAt: attachedDatabase.typeMapping
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!,
seenAt: attachedDatabase.typeMapping
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}seen_at']),
showAt: attachedDatabase.typeMapping
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}show_at']),
hideAt: attachedDatabase.typeMapping
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}hide_at']),
);
}
@override
$MemoryEntityTable createAlias(String alias) {
return $MemoryEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.MemoryTypeEnum, int, int> $convertertype =
const i0.EnumIndexConverter<i2.MemoryTypeEnum>(i2.MemoryTypeEnum.values);
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class MemoryEntityData extends i0.DataClass
implements i0.Insertable<i1.MemoryEntityData> {
final String id;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? deletedAt;
final String ownerId;
final i2.MemoryTypeEnum type;
final String data;
final bool isSaved;
final DateTime memoryAt;
final DateTime? seenAt;
final DateTime? showAt;
final DateTime? hideAt;
const MemoryEntityData(
{required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.ownerId,
required this.type,
required this.data,
required this.isSaved,
required this.memoryAt,
this.seenAt,
this.showAt,
this.hideAt});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['created_at'] = i0.Variable<DateTime>(createdAt);
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
if (!nullToAbsent || deletedAt != null) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
}
map['owner_id'] = i0.Variable<String>(ownerId);
{
map['type'] =
i0.Variable<int>(i1.$MemoryEntityTable.$convertertype.toSql(type));
}
map['data'] = i0.Variable<String>(data);
map['is_saved'] = i0.Variable<bool>(isSaved);
map['memory_at'] = i0.Variable<DateTime>(memoryAt);
if (!nullToAbsent || seenAt != null) {
map['seen_at'] = i0.Variable<DateTime>(seenAt);
}
if (!nullToAbsent || showAt != null) {
map['show_at'] = i0.Variable<DateTime>(showAt);
}
if (!nullToAbsent || hideAt != null) {
map['hide_at'] = i0.Variable<DateTime>(hideAt);
}
return map;
}
factory MemoryEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return MemoryEntityData(
id: serializer.fromJson<String>(json['id']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
ownerId: serializer.fromJson<String>(json['ownerId']),
type: i1.$MemoryEntityTable.$convertertype
.fromJson(serializer.fromJson<int>(json['type'])),
data: serializer.fromJson<String>(json['data']),
isSaved: serializer.fromJson<bool>(json['isSaved']),
memoryAt: serializer.fromJson<DateTime>(json['memoryAt']),
seenAt: serializer.fromJson<DateTime?>(json['seenAt']),
showAt: serializer.fromJson<DateTime?>(json['showAt']),
hideAt: serializer.fromJson<DateTime?>(json['hideAt']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'createdAt': serializer.toJson<DateTime>(createdAt),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
'ownerId': serializer.toJson<String>(ownerId),
'type': serializer
.toJson<int>(i1.$MemoryEntityTable.$convertertype.toJson(type)),
'data': serializer.toJson<String>(data),
'isSaved': serializer.toJson<bool>(isSaved),
'memoryAt': serializer.toJson<DateTime>(memoryAt),
'seenAt': serializer.toJson<DateTime?>(seenAt),
'showAt': serializer.toJson<DateTime?>(showAt),
'hideAt': serializer.toJson<DateTime?>(hideAt),
};
}
i1.MemoryEntityData copyWith(
{String? id,
DateTime? createdAt,
DateTime? updatedAt,
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
String? ownerId,
i2.MemoryTypeEnum? type,
String? data,
bool? isSaved,
DateTime? memoryAt,
i0.Value<DateTime?> seenAt = const i0.Value.absent(),
i0.Value<DateTime?> showAt = const i0.Value.absent(),
i0.Value<DateTime?> hideAt = const i0.Value.absent()}) =>
i1.MemoryEntityData(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
ownerId: ownerId ?? this.ownerId,
type: type ?? this.type,
data: data ?? this.data,
isSaved: isSaved ?? this.isSaved,
memoryAt: memoryAt ?? this.memoryAt,
seenAt: seenAt.present ? seenAt.value : this.seenAt,
showAt: showAt.present ? showAt.value : this.showAt,
hideAt: hideAt.present ? hideAt.value : this.hideAt,
);
MemoryEntityData copyWithCompanion(i1.MemoryEntityCompanion data) {
return MemoryEntityData(
id: data.id.present ? data.id.value : this.id,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
type: data.type.present ? data.type.value : this.type,
data: data.data.present ? data.data.value : this.data,
isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved,
memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt,
seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt,
showAt: data.showAt.present ? data.showAt.value : this.showAt,
hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt,
);
}
@override
String toString() {
return (StringBuffer('MemoryEntityData(')
..write('id: $id, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('deletedAt: $deletedAt, ')
..write('ownerId: $ownerId, ')
..write('type: $type, ')
..write('data: $data, ')
..write('isSaved: $isSaved, ')
..write('memoryAt: $memoryAt, ')
..write('seenAt: $seenAt, ')
..write('showAt: $showAt, ')
..write('hideAt: $hideAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, createdAt, updatedAt, deletedAt, ownerId,
type, data, isSaved, memoryAt, seenAt, showAt, hideAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.MemoryEntityData &&
other.id == this.id &&
other.createdAt == this.createdAt &&
other.updatedAt == this.updatedAt &&
other.deletedAt == this.deletedAt &&
other.ownerId == this.ownerId &&
other.type == this.type &&
other.data == this.data &&
other.isSaved == this.isSaved &&
other.memoryAt == this.memoryAt &&
other.seenAt == this.seenAt &&
other.showAt == this.showAt &&
other.hideAt == this.hideAt);
}
class MemoryEntityCompanion extends i0.UpdateCompanion<i1.MemoryEntityData> {
final i0.Value<String> id;
final i0.Value<DateTime> createdAt;
final i0.Value<DateTime> updatedAt;
final i0.Value<DateTime?> deletedAt;
final i0.Value<String> ownerId;
final i0.Value<i2.MemoryTypeEnum> type;
final i0.Value<String> data;
final i0.Value<bool> isSaved;
final i0.Value<DateTime> memoryAt;
final i0.Value<DateTime?> seenAt;
final i0.Value<DateTime?> showAt;
final i0.Value<DateTime?> hideAt;
const MemoryEntityCompanion({
this.id = const i0.Value.absent(),
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
this.ownerId = const i0.Value.absent(),
this.type = const i0.Value.absent(),
this.data = const i0.Value.absent(),
this.isSaved = const i0.Value.absent(),
this.memoryAt = const i0.Value.absent(),
this.seenAt = const i0.Value.absent(),
this.showAt = const i0.Value.absent(),
this.hideAt = const i0.Value.absent(),
});
MemoryEntityCompanion.insert({
required String id,
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
required String ownerId,
required i2.MemoryTypeEnum type,
required String data,
this.isSaved = const i0.Value.absent(),
required DateTime memoryAt,
this.seenAt = const i0.Value.absent(),
this.showAt = const i0.Value.absent(),
this.hideAt = const i0.Value.absent(),
}) : id = i0.Value(id),
ownerId = i0.Value(ownerId),
type = i0.Value(type),
data = i0.Value(data),
memoryAt = i0.Value(memoryAt);
static i0.Insertable<i1.MemoryEntityData> custom({
i0.Expression<String>? id,
i0.Expression<DateTime>? createdAt,
i0.Expression<DateTime>? updatedAt,
i0.Expression<DateTime>? deletedAt,
i0.Expression<String>? ownerId,
i0.Expression<int>? type,
i0.Expression<String>? data,
i0.Expression<bool>? isSaved,
i0.Expression<DateTime>? memoryAt,
i0.Expression<DateTime>? seenAt,
i0.Expression<DateTime>? showAt,
i0.Expression<DateTime>? hideAt,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (createdAt != null) 'created_at': createdAt,
if (updatedAt != null) 'updated_at': updatedAt,
if (deletedAt != null) 'deleted_at': deletedAt,
if (ownerId != null) 'owner_id': ownerId,
if (type != null) 'type': type,
if (data != null) 'data': data,
if (isSaved != null) 'is_saved': isSaved,
if (memoryAt != null) 'memory_at': memoryAt,
if (seenAt != null) 'seen_at': seenAt,
if (showAt != null) 'show_at': showAt,
if (hideAt != null) 'hide_at': hideAt,
});
}
i1.MemoryEntityCompanion copyWith(
{i0.Value<String>? id,
i0.Value<DateTime>? createdAt,
i0.Value<DateTime>? updatedAt,
i0.Value<DateTime?>? deletedAt,
i0.Value<String>? ownerId,
i0.Value<i2.MemoryTypeEnum>? type,
i0.Value<String>? data,
i0.Value<bool>? isSaved,
i0.Value<DateTime>? memoryAt,
i0.Value<DateTime?>? seenAt,
i0.Value<DateTime?>? showAt,
i0.Value<DateTime?>? hideAt}) {
return i1.MemoryEntityCompanion(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
deletedAt: deletedAt ?? this.deletedAt,
ownerId: ownerId ?? this.ownerId,
type: type ?? this.type,
data: data ?? this.data,
isSaved: isSaved ?? this.isSaved,
memoryAt: memoryAt ?? this.memoryAt,
seenAt: seenAt ?? this.seenAt,
showAt: showAt ?? this.showAt,
hideAt: hideAt ?? this.hideAt,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (createdAt.present) {
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
if (deletedAt.present) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
}
if (ownerId.present) {
map['owner_id'] = i0.Variable<String>(ownerId.value);
}
if (type.present) {
map['type'] = i0.Variable<int>(
i1.$MemoryEntityTable.$convertertype.toSql(type.value));
}
if (data.present) {
map['data'] = i0.Variable<String>(data.value);
}
if (isSaved.present) {
map['is_saved'] = i0.Variable<bool>(isSaved.value);
}
if (memoryAt.present) {
map['memory_at'] = i0.Variable<DateTime>(memoryAt.value);
}
if (seenAt.present) {
map['seen_at'] = i0.Variable<DateTime>(seenAt.value);
}
if (showAt.present) {
map['show_at'] = i0.Variable<DateTime>(showAt.value);
}
if (hideAt.present) {
map['hide_at'] = i0.Variable<DateTime>(hideAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('MemoryEntityCompanion(')
..write('id: $id, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('deletedAt: $deletedAt, ')
..write('ownerId: $ownerId, ')
..write('type: $type, ')
..write('data: $data, ')
..write('isSaved: $isSaved, ')
..write('memoryAt: $memoryAt, ')
..write('seenAt: $seenAt, ')
..write('showAt: $showAt, ')
..write('hideAt: $hideAt')
..write(')'))
.toString();
}
}

View File

@@ -0,0 +1,17 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class MemoryAssetEntity extends Table with DriftDefaultsMixin {
const MemoryAssetEntity();
TextColumn get assetId =>
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get memoryId =>
text().references(MemoryEntity, #id, onDelete: KeyAction.cascade)();
@override
Set<Column> get primaryKey => {assetId, memoryId};
}

View File

@@ -0,0 +1,550 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'
as i2;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i3;
import 'package:drift/internal/modular.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i5;
typedef $$MemoryAssetEntityTableCreateCompanionBuilder
= i1.MemoryAssetEntityCompanion Function({
required String assetId,
required String memoryId,
});
typedef $$MemoryAssetEntityTableUpdateCompanionBuilder
= i1.MemoryAssetEntityCompanion Function({
i0.Value<String> assetId,
i0.Value<String> memoryId,
});
final class $$MemoryAssetEntityTableReferences extends i0.BaseReferences<
i0.GeneratedDatabase,
i1.$MemoryAssetEntityTable,
i1.MemoryAssetEntityData> {
$$MemoryAssetEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i4.ReadDatabaseContainer(db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$MemoryAssetEntityTable>('memory_asset_entity')
.assetId,
i4.ReadDatabaseContainer(db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
.id));
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!;
final manager = i3
.$$RemoteAssetEntityTableTableManager(
$_db,
i4.ReadDatabaseContainer($_db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
static i5.$MemoryEntityTable _memoryIdTable(i0.GeneratedDatabase db) =>
i4.ReadDatabaseContainer(db)
.resultSet<i5.$MemoryEntityTable>('memory_entity')
.createAlias(i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$MemoryAssetEntityTable>('memory_asset_entity')
.memoryId,
i4.ReadDatabaseContainer(db)
.resultSet<i5.$MemoryEntityTable>('memory_entity')
.id));
i5.$$MemoryEntityTableProcessedTableManager get memoryId {
final $_column = $_itemColumn<String>('memory_id')!;
final manager = i5
.$$MemoryEntityTableTableManager(
$_db,
i4.ReadDatabaseContainer($_db)
.resultSet<i5.$MemoryEntityTable>('memory_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_memoryIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$MemoryAssetEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryAssetEntityTable> {
$$MemoryAssetEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i3.$$RemoteAssetEntityTableFilterComposer get assetId {
final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i3.$$RemoteAssetEntityTableFilterComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i5.$$MemoryEntityTableFilterComposer get memoryId {
final i5.$$MemoryEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.memoryId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$MemoryEntityTableFilterComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$MemoryAssetEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryAssetEntityTable> {
$$MemoryAssetEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i3.$$RemoteAssetEntityTableOrderingComposer get assetId {
final i3.$$RemoteAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i3.$$RemoteAssetEntityTableOrderingComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i3.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i5.$$MemoryEntityTableOrderingComposer get memoryId {
final i5.$$MemoryEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.memoryId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$MemoryEntityTableOrderingComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$MemoryAssetEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryAssetEntityTable> {
$$MemoryAssetEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i3.$$RemoteAssetEntityTableAnnotationComposer get assetId {
final i3.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i3.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i3.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i5.$$MemoryEntityTableAnnotationComposer get memoryId {
final i5.$$MemoryEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.memoryId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$MemoryEntityTableAnnotationComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$MemoryAssetEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$MemoryAssetEntityTable,
i1.MemoryAssetEntityData,
i1.$$MemoryAssetEntityTableFilterComposer,
i1.$$MemoryAssetEntityTableOrderingComposer,
i1.$$MemoryAssetEntityTableAnnotationComposer,
$$MemoryAssetEntityTableCreateCompanionBuilder,
$$MemoryAssetEntityTableUpdateCompanionBuilder,
(i1.MemoryAssetEntityData, i1.$$MemoryAssetEntityTableReferences),
i1.MemoryAssetEntityData,
i0.PrefetchHooks Function({bool assetId, bool memoryId})> {
$$MemoryAssetEntityTableTableManager(
i0.GeneratedDatabase db, i1.$MemoryAssetEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$MemoryAssetEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () => i1
.$$MemoryAssetEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$MemoryAssetEntityTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<String> assetId = const i0.Value.absent(),
i0.Value<String> memoryId = const i0.Value.absent(),
}) =>
i1.MemoryAssetEntityCompanion(
assetId: assetId,
memoryId: memoryId,
),
createCompanionCallback: ({
required String assetId,
required String memoryId,
}) =>
i1.MemoryAssetEntityCompanion.insert(
assetId: assetId,
memoryId: memoryId,
),
withReferenceMapper: (p0) => p0
.map((e) => (
e.readTable(table),
i1.$$MemoryAssetEntityTableReferences(db, table, e)
))
.toList(),
prefetchHooksCallback: ({assetId = false, memoryId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins: <
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic>>(state) {
if (assetId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.assetId,
referencedTable:
i1.$$MemoryAssetEntityTableReferences._assetIdTable(db),
referencedColumn: i1.$$MemoryAssetEntityTableReferences
._assetIdTable(db)
.id,
) as T;
}
if (memoryId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.memoryId,
referencedTable: i1.$$MemoryAssetEntityTableReferences
._memoryIdTable(db),
referencedColumn: i1.$$MemoryAssetEntityTableReferences
._memoryIdTable(db)
.id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
));
}
typedef $$MemoryAssetEntityTableProcessedTableManager
= i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$MemoryAssetEntityTable,
i1.MemoryAssetEntityData,
i1.$$MemoryAssetEntityTableFilterComposer,
i1.$$MemoryAssetEntityTableOrderingComposer,
i1.$$MemoryAssetEntityTableAnnotationComposer,
$$MemoryAssetEntityTableCreateCompanionBuilder,
$$MemoryAssetEntityTableUpdateCompanionBuilder,
(i1.MemoryAssetEntityData, i1.$$MemoryAssetEntityTableReferences),
i1.MemoryAssetEntityData,
i0.PrefetchHooks Function({bool assetId, bool memoryId})>;
class $MemoryAssetEntityTable extends i2.MemoryAssetEntity
with i0.TableInfo<$MemoryAssetEntityTable, i1.MemoryAssetEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$MemoryAssetEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _assetIdMeta =
const i0.VerificationMeta('assetId');
@override
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
'asset_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE'));
static const i0.VerificationMeta _memoryIdMeta =
const i0.VerificationMeta('memoryId');
@override
late final i0.GeneratedColumn<String> memoryId = i0.GeneratedColumn<String>(
'memory_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES memory_entity (id) ON DELETE CASCADE'));
@override
List<i0.GeneratedColumn> get $columns => [assetId, memoryId];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'memory_asset_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.MemoryAssetEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('asset_id')) {
context.handle(_assetIdMeta,
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta));
} else if (isInserting) {
context.missing(_assetIdMeta);
}
if (data.containsKey('memory_id')) {
context.handle(_memoryIdMeta,
memoryId.isAcceptableOrUnknown(data['memory_id']!, _memoryIdMeta));
} else if (isInserting) {
context.missing(_memoryIdMeta);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {assetId, memoryId};
@override
i1.MemoryAssetEntityData map(Map<String, dynamic> data,
{String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.MemoryAssetEntityData(
assetId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!,
memoryId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}memory_id'])!,
);
}
@override
$MemoryAssetEntityTable createAlias(String alias) {
return $MemoryAssetEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class MemoryAssetEntityData extends i0.DataClass
implements i0.Insertable<i1.MemoryAssetEntityData> {
final String assetId;
final String memoryId;
const MemoryAssetEntityData({required this.assetId, required this.memoryId});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['asset_id'] = i0.Variable<String>(assetId);
map['memory_id'] = i0.Variable<String>(memoryId);
return map;
}
factory MemoryAssetEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return MemoryAssetEntityData(
assetId: serializer.fromJson<String>(json['assetId']),
memoryId: serializer.fromJson<String>(json['memoryId']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'assetId': serializer.toJson<String>(assetId),
'memoryId': serializer.toJson<String>(memoryId),
};
}
i1.MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) =>
i1.MemoryAssetEntityData(
assetId: assetId ?? this.assetId,
memoryId: memoryId ?? this.memoryId,
);
MemoryAssetEntityData copyWithCompanion(i1.MemoryAssetEntityCompanion data) {
return MemoryAssetEntityData(
assetId: data.assetId.present ? data.assetId.value : this.assetId,
memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId,
);
}
@override
String toString() {
return (StringBuffer('MemoryAssetEntityData(')
..write('assetId: $assetId, ')
..write('memoryId: $memoryId')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(assetId, memoryId);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.MemoryAssetEntityData &&
other.assetId == this.assetId &&
other.memoryId == this.memoryId);
}
class MemoryAssetEntityCompanion
extends i0.UpdateCompanion<i1.MemoryAssetEntityData> {
final i0.Value<String> assetId;
final i0.Value<String> memoryId;
const MemoryAssetEntityCompanion({
this.assetId = const i0.Value.absent(),
this.memoryId = const i0.Value.absent(),
});
MemoryAssetEntityCompanion.insert({
required String assetId,
required String memoryId,
}) : assetId = i0.Value(assetId),
memoryId = i0.Value(memoryId);
static i0.Insertable<i1.MemoryAssetEntityData> custom({
i0.Expression<String>? assetId,
i0.Expression<String>? memoryId,
}) {
return i0.RawValuesInsertable({
if (assetId != null) 'asset_id': assetId,
if (memoryId != null) 'memory_id': memoryId,
});
}
i1.MemoryAssetEntityCompanion copyWith(
{i0.Value<String>? assetId, i0.Value<String>? memoryId}) {
return i1.MemoryAssetEntityCompanion(
assetId: assetId ?? this.assetId,
memoryId: memoryId ?? this.memoryId,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (assetId.present) {
map['asset_id'] = i0.Variable<String>(assetId.value);
}
if (memoryId.present) {
map['memory_id'] = i0.Variable<String>(memoryId.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('MemoryAssetEntityCompanion(')
..write('assetId: $assetId, ')
..write('memoryId: $memoryId')
..write(')'))
.toString();
}
}

View File

@@ -1,11 +1,11 @@
import 'remote_asset.entity.dart';
import 'local_asset.entity.dart';
mergedAsset: SELECT * FROM
mergedAsset: SELECT * FROM
(
SELECT
rae.id as remote_id,
lae.id as local_id,
rae.local_id as local_id,
rae.name,
rae."type",
rae.created_at,
@@ -19,13 +19,11 @@ mergedAsset: SELECT * FROM
rae.owner_id
FROM
remote_asset_entity rae
LEFT JOIN
local_asset_entity lae ON rae.checksum = lae.checksum
WHERE
rae.visibility = 0 AND rae.owner_id in ?
rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
UNION ALL
SELECT
NULL as remote_id,
lae.remote_id as remote_id,
lae.id as local_id,
lae.name,
lae."type",
@@ -40,20 +38,18 @@ mergedAsset: SELECT * FROM
NULL as owner_id
FROM
local_asset_entity lae
LEFT JOIN
remote_asset_entity rae ON rae.checksum = lae.checksum
WHERE
rae.id IS NULL
lae.remote_id IS NULL
)
ORDER BY created_at DESC
LIMIT $limit;
mergedBucket(:group_by AS INTEGER):
SELECT
mergedBucket(:group_by AS INTEGER):
SELECT
COUNT(*) as asset_count,
CASE
WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', created_at) -- day
WHEN :group_by = 1 THEN STRFTIME('%Y-%m', created_at) -- month
WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', created_at, 'localtime') -- day
WHEN :group_by = 1 THEN STRFTIME('%Y-%m', created_at, 'localtime') -- month
END AS bucket_date
FROM
(
@@ -62,20 +58,16 @@ FROM
rae.created_at
FROM
remote_asset_entity rae
LEFT JOIN
local_asset_entity lae ON rae.checksum = lae.checksum
WHERE
rae.visibility = 0 AND rae.owner_id in ?
rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
UNION ALL
SELECT
lae.name,
lae.created_at
FROM
local_asset_entity lae
LEFT JOIN
remote_asset_entity rae ON rae.checksum = lae.checksum
WHERE
rae.id IS NULL
lae.remote_id IS NULL
)
GROUP BY bucket_date
ORDER BY bucket_date DESC;

View File

@@ -18,7 +18,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
$arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect(
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
'SELECT * FROM (SELECT rae.id AS remote_id, rae.local_id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id FROM remote_asset_entity AS rae WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT lae.remote_id AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id FROM local_asset_entity AS lae WHERE lae.remote_id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [
for (var $ in var1) i0.Variable<String>($),
...generatedlimit.introducedVariables
@@ -51,7 +51,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
$arrayStartIndex += var2.length;
return customSelect(
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at) WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at) END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae WHERE lae.remote_id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
variables: [
i0.Variable<int>(groupBy),
for (var $ in var2) i0.Variable<String>($)

View File

@@ -17,6 +17,8 @@ class RemoteAssetEntity extends Table
TextColumn get id => text()();
TextColumn get localId => text().nullable()();
TextColumn get checksum => text()();
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
@@ -51,6 +53,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
width: width,
thumbHash: thumbHash,
visibility: visibility,
localId: null,
localId: localId,
);
}

View File

@@ -21,6 +21,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
required String id,
i0.Value<String?> localId,
required String checksum,
i0.Value<bool> isFavorite,
required String ownerId,
@@ -39,6 +40,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
i0.Value<String> id,
i0.Value<String?> localId,
i0.Value<String> checksum,
i0.Value<bool> isFavorite,
i0.Value<String> ownerId,
@@ -118,6 +120,9 @@ class $$RemoteAssetEntityTableFilterComposer
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get localId => $composableBuilder(
column: $table.localId, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get checksum => $composableBuilder(
column: $table.checksum, builder: (column) => i0.ColumnFilters(column));
@@ -198,6 +203,9 @@ class $$RemoteAssetEntityTableOrderingComposer
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get localId => $composableBuilder(
column: $table.localId, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get checksum => $composableBuilder(
column: $table.checksum, builder: (column) => i0.ColumnOrderings(column));
@@ -277,6 +285,9 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get localId =>
$composableBuilder(column: $table.localId, builder: (column) => column);
i0.GeneratedColumn<String> get checksum =>
$composableBuilder(column: $table.checksum, builder: (column) => column);
@@ -352,6 +363,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String?> localId = const i0.Value.absent(),
i0.Value<String> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<String> ownerId = const i0.Value.absent(),
@@ -369,6 +381,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
height: height,
durationInSeconds: durationInSeconds,
id: id,
localId: localId,
checksum: checksum,
isFavorite: isFavorite,
ownerId: ownerId,
@@ -386,6 +399,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
required String id,
i0.Value<String?> localId = const i0.Value.absent(),
required String checksum,
i0.Value<bool> isFavorite = const i0.Value.absent(),
required String ownerId,
@@ -403,6 +417,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
height: height,
durationInSeconds: durationInSeconds,
id: id,
localId: localId,
checksum: checksum,
isFavorite: isFavorite,
ownerId: ownerId,
@@ -530,6 +545,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _localIdMeta =
const i0.VerificationMeta('localId');
@override
late final i0.GeneratedColumn<String> localId = i0.GeneratedColumn<String>(
'local_id', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
static const i0.VerificationMeta _checksumMeta =
const i0.VerificationMeta('checksum');
@override
@@ -589,6 +610,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
height,
durationInSeconds,
id,
localId,
checksum,
isFavorite,
ownerId,
@@ -641,6 +663,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('local_id')) {
context.handle(_localIdMeta,
localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta));
}
if (data.containsKey('checksum')) {
context.handle(_checksumMeta,
checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta));
@@ -700,6 +726,8 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']),
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
localId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}local_id']),
checksum: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}checksum'])!,
isFavorite: attachedDatabase.typeMapping
@@ -744,6 +772,7 @@ class RemoteAssetEntityData extends i0.DataClass
final int? height;
final int? durationInSeconds;
final String id;
final String? localId;
final String checksum;
final bool isFavorite;
final String ownerId;
@@ -760,6 +789,7 @@ class RemoteAssetEntityData extends i0.DataClass
this.height,
this.durationInSeconds,
required this.id,
this.localId,
required this.checksum,
required this.isFavorite,
required this.ownerId,
@@ -787,6 +817,9 @@ class RemoteAssetEntityData extends i0.DataClass
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds);
}
map['id'] = i0.Variable<String>(id);
if (!nullToAbsent || localId != null) {
map['local_id'] = i0.Variable<String>(localId);
}
map['checksum'] = i0.Variable<String>(checksum);
map['is_favorite'] = i0.Variable<bool>(isFavorite);
map['owner_id'] = i0.Variable<String>(ownerId);
@@ -819,6 +852,7 @@ class RemoteAssetEntityData extends i0.DataClass
height: serializer.fromJson<int?>(json['height']),
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
id: serializer.fromJson<String>(json['id']),
localId: serializer.fromJson<String?>(json['localId']),
checksum: serializer.fromJson<String>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
ownerId: serializer.fromJson<String>(json['ownerId']),
@@ -842,6 +876,7 @@ class RemoteAssetEntityData extends i0.DataClass
'height': serializer.toJson<int?>(height),
'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
'id': serializer.toJson<String>(id),
'localId': serializer.toJson<String?>(localId),
'checksum': serializer.toJson<String>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite),
'ownerId': serializer.toJson<String>(ownerId),
@@ -862,6 +897,7 @@ class RemoteAssetEntityData extends i0.DataClass
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
String? id,
i0.Value<String?> localId = const i0.Value.absent(),
String? checksum,
bool? isFavorite,
String? ownerId,
@@ -880,6 +916,7 @@ class RemoteAssetEntityData extends i0.DataClass
? durationInSeconds.value
: this.durationInSeconds,
id: id ?? this.id,
localId: localId.present ? localId.value : this.localId,
checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
ownerId: ownerId ?? this.ownerId,
@@ -901,6 +938,7 @@ class RemoteAssetEntityData extends i0.DataClass
? data.durationInSeconds.value
: this.durationInSeconds,
id: data.id.present ? data.id.value : this.id,
localId: data.localId.present ? data.localId.value : this.localId,
checksum: data.checksum.present ? data.checksum.value : this.checksum,
isFavorite:
data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
@@ -926,6 +964,7 @@ class RemoteAssetEntityData extends i0.DataClass
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ')
..write('localId: $localId, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
..write('ownerId: $ownerId, ')
@@ -947,6 +986,7 @@ class RemoteAssetEntityData extends i0.DataClass
height,
durationInSeconds,
id,
localId,
checksum,
isFavorite,
ownerId,
@@ -966,6 +1006,7 @@ class RemoteAssetEntityData extends i0.DataClass
other.height == this.height &&
other.durationInSeconds == this.durationInSeconds &&
other.id == this.id &&
other.localId == this.localId &&
other.checksum == this.checksum &&
other.isFavorite == this.isFavorite &&
other.ownerId == this.ownerId &&
@@ -985,6 +1026,7 @@ class RemoteAssetEntityCompanion
final i0.Value<int?> height;
final i0.Value<int?> durationInSeconds;
final i0.Value<String> id;
final i0.Value<String?> localId;
final i0.Value<String> checksum;
final i0.Value<bool> isFavorite;
final i0.Value<String> ownerId;
@@ -1001,6 +1043,7 @@ class RemoteAssetEntityCompanion
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
this.id = const i0.Value.absent(),
this.localId = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
this.ownerId = const i0.Value.absent(),
@@ -1018,6 +1061,7 @@ class RemoteAssetEntityCompanion
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
required String id,
this.localId = const i0.Value.absent(),
required String checksum,
this.isFavorite = const i0.Value.absent(),
required String ownerId,
@@ -1040,6 +1084,7 @@ class RemoteAssetEntityCompanion
i0.Expression<int>? height,
i0.Expression<int>? durationInSeconds,
i0.Expression<String>? id,
i0.Expression<String>? localId,
i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite,
i0.Expression<String>? ownerId,
@@ -1057,6 +1102,7 @@ class RemoteAssetEntityCompanion
if (height != null) 'height': height,
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
if (id != null) 'id': id,
if (localId != null) 'local_id': localId,
if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite,
if (ownerId != null) 'owner_id': ownerId,
@@ -1076,6 +1122,7 @@ class RemoteAssetEntityCompanion
i0.Value<int?>? height,
i0.Value<int?>? durationInSeconds,
i0.Value<String>? id,
i0.Value<String?>? localId,
i0.Value<String>? checksum,
i0.Value<bool>? isFavorite,
i0.Value<String>? ownerId,
@@ -1092,6 +1139,7 @@ class RemoteAssetEntityCompanion
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
id: id ?? this.id,
localId: localId ?? this.localId,
checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
ownerId: ownerId ?? this.ownerId,
@@ -1130,6 +1178,9 @@ class RemoteAssetEntityCompanion
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (localId.present) {
map['local_id'] = i0.Variable<String>(localId.value);
}
if (checksum.present) {
map['checksum'] = i0.Variable<String>(checksum.value);
}
@@ -1167,6 +1218,7 @@ class RemoteAssetEntityCompanion
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ')
..write('localId: $localId, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
..write('ownerId: $ownerId, ')

View File

@@ -0,0 +1,22 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class StackEntity extends Table with DriftDefaultsMixin {
const StackEntity();
TextColumn get id => text()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get ownerId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get primaryAssetId => text().references(RemoteAssetEntity, #id)();
@override
Set<Column> get primaryKey => {id};
}

View File

@@ -0,0 +1,706 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart' as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i4;
import 'package:drift/internal/modular.dart' as i5;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i6;
typedef $$StackEntityTableCreateCompanionBuilder = i1.StackEntityCompanion
Function({
required String id,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
required String ownerId,
required String primaryAssetId,
});
typedef $$StackEntityTableUpdateCompanionBuilder = i1.StackEntityCompanion
Function({
i0.Value<String> id,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
i0.Value<String> ownerId,
i0.Value<String> primaryAssetId,
});
final class $$StackEntityTableReferences extends i0.BaseReferences<
i0.GeneratedDatabase, i1.$StackEntityTable, i1.StackEntityData> {
$$StackEntityTableReferences(super.$_db, super.$_table, super.$_typedResult);
static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$StackEntityTable>('stack_entity')
.ownerId,
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.id));
i4.$$UserEntityTableProcessedTableManager get ownerId {
final $_column = $_itemColumn<String>('owner_id')!;
final manager = i4
.$$UserEntityTableTableManager(
$_db,
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
static i6.$RemoteAssetEntityTable _primaryAssetIdTable(
i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$StackEntityTable>('stack_entity')
.primaryAssetId,
i5.ReadDatabaseContainer(db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
.id));
i6.$$RemoteAssetEntityTableProcessedTableManager get primaryAssetId {
final $_column = $_itemColumn<String>('primary_asset_id')!;
final manager = i6
.$$RemoteAssetEntityTableTableManager(
$_db,
i5.ReadDatabaseContainer($_db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_primaryAssetIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$StackEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
$$StackEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
i4.$$UserEntityTableFilterComposer get ownerId {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableFilterComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i6.$$RemoteAssetEntityTableFilterComposer get primaryAssetId {
final i6.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.primaryAssetId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i6.$$RemoteAssetEntityTableFilterComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$StackEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
$$StackEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column));
i4.$$UserEntityTableOrderingComposer get ownerId {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i6.$$RemoteAssetEntityTableOrderingComposer get primaryAssetId {
final i6.$$RemoteAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.primaryAssetId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i6.$$RemoteAssetEntityTableOrderingComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$StackEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
$$StackEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
i4.$$UserEntityTableAnnotationComposer get ownerId {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i6.$$RemoteAssetEntityTableAnnotationComposer get primaryAssetId {
final i6.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.primaryAssetId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i6.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$StackEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$StackEntityTable,
i1.StackEntityData,
i1.$$StackEntityTableFilterComposer,
i1.$$StackEntityTableOrderingComposer,
i1.$$StackEntityTableAnnotationComposer,
$$StackEntityTableCreateCompanionBuilder,
$$StackEntityTableUpdateCompanionBuilder,
(i1.StackEntityData, i1.$$StackEntityTableReferences),
i1.StackEntityData,
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})> {
$$StackEntityTableTableManager(
i0.GeneratedDatabase db, i1.$StackEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$StackEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$StackEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$StackEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<String> ownerId = const i0.Value.absent(),
i0.Value<String> primaryAssetId = const i0.Value.absent(),
}) =>
i1.StackEntityCompanion(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
ownerId: ownerId,
primaryAssetId: primaryAssetId,
),
createCompanionCallback: ({
required String id,
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
required String ownerId,
required String primaryAssetId,
}) =>
i1.StackEntityCompanion.insert(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
ownerId: ownerId,
primaryAssetId: primaryAssetId,
),
withReferenceMapper: (p0) => p0
.map((e) => (
e.readTable(table),
i1.$$StackEntityTableReferences(db, table, e)
))
.toList(),
prefetchHooksCallback: ({ownerId = false, primaryAssetId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins: <
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic>>(state) {
if (ownerId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.ownerId,
referencedTable:
i1.$$StackEntityTableReferences._ownerIdTable(db),
referencedColumn:
i1.$$StackEntityTableReferences._ownerIdTable(db).id,
) as T;
}
if (primaryAssetId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.primaryAssetId,
referencedTable: i1.$$StackEntityTableReferences
._primaryAssetIdTable(db),
referencedColumn: i1.$$StackEntityTableReferences
._primaryAssetIdTable(db)
.id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
));
}
typedef $$StackEntityTableProcessedTableManager = i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$StackEntityTable,
i1.StackEntityData,
i1.$$StackEntityTableFilterComposer,
i1.$$StackEntityTableOrderingComposer,
i1.$$StackEntityTableAnnotationComposer,
$$StackEntityTableCreateCompanionBuilder,
$$StackEntityTableUpdateCompanionBuilder,
(i1.StackEntityData, i1.$$StackEntityTableReferences),
i1.StackEntityData,
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})>;
class $StackEntityTable extends i2.StackEntity
with i0.TableInfo<$StackEntityTable, i1.StackEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$StackEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _createdAtMeta =
const i0.VerificationMeta('createdAt');
@override
late final i0.GeneratedColumn<DateTime> createdAt =
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime);
static const i0.VerificationMeta _updatedAtMeta =
const i0.VerificationMeta('updatedAt');
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime);
static const i0.VerificationMeta _ownerIdMeta =
const i0.VerificationMeta('ownerId');
@override
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
'owner_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
static const i0.VerificationMeta _primaryAssetIdMeta =
const i0.VerificationMeta('primaryAssetId');
@override
late final i0.GeneratedColumn<String> primaryAssetId =
i0.GeneratedColumn<String>(
'primary_asset_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id)'));
@override
List<i0.GeneratedColumn> get $columns =>
[id, createdAt, updatedAt, ownerId, primaryAssetId];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'stack_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.StackEntityData> 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('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
if (data.containsKey('updated_at')) {
context.handle(_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
}
if (data.containsKey('owner_id')) {
context.handle(_ownerIdMeta,
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta));
} else if (isInserting) {
context.missing(_ownerIdMeta);
}
if (data.containsKey('primary_asset_id')) {
context.handle(
_primaryAssetIdMeta,
primaryAssetId.isAcceptableOrUnknown(
data['primary_asset_id']!, _primaryAssetIdMeta));
} else if (isInserting) {
context.missing(_primaryAssetIdMeta);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.StackEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.StackEntityData(
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
createdAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
ownerId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!,
primaryAssetId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!,
);
}
@override
$StackEntityTable createAlias(String alias) {
return $StackEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class StackEntityData extends i0.DataClass
implements i0.Insertable<i1.StackEntityData> {
final String id;
final DateTime createdAt;
final DateTime updatedAt;
final String ownerId;
final String primaryAssetId;
const StackEntityData(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.ownerId,
required this.primaryAssetId});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['created_at'] = i0.Variable<DateTime>(createdAt);
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
map['owner_id'] = i0.Variable<String>(ownerId);
map['primary_asset_id'] = i0.Variable<String>(primaryAssetId);
return map;
}
factory StackEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return StackEntityData(
id: serializer.fromJson<String>(json['id']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
ownerId: serializer.fromJson<String>(json['ownerId']),
primaryAssetId: serializer.fromJson<String>(json['primaryAssetId']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'createdAt': serializer.toJson<DateTime>(createdAt),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'ownerId': serializer.toJson<String>(ownerId),
'primaryAssetId': serializer.toJson<String>(primaryAssetId),
};
}
i1.StackEntityData copyWith(
{String? id,
DateTime? createdAt,
DateTime? updatedAt,
String? ownerId,
String? primaryAssetId}) =>
i1.StackEntityData(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
ownerId: ownerId ?? this.ownerId,
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
);
StackEntityData copyWithCompanion(i1.StackEntityCompanion data) {
return StackEntityData(
id: data.id.present ? data.id.value : this.id,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
primaryAssetId: data.primaryAssetId.present
? data.primaryAssetId.value
: this.primaryAssetId,
);
}
@override
String toString() {
return (StringBuffer('StackEntityData(')
..write('id: $id, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('ownerId: $ownerId, ')
..write('primaryAssetId: $primaryAssetId')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.StackEntityData &&
other.id == this.id &&
other.createdAt == this.createdAt &&
other.updatedAt == this.updatedAt &&
other.ownerId == this.ownerId &&
other.primaryAssetId == this.primaryAssetId);
}
class StackEntityCompanion extends i0.UpdateCompanion<i1.StackEntityData> {
final i0.Value<String> id;
final i0.Value<DateTime> createdAt;
final i0.Value<DateTime> updatedAt;
final i0.Value<String> ownerId;
final i0.Value<String> primaryAssetId;
const StackEntityCompanion({
this.id = const i0.Value.absent(),
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.ownerId = const i0.Value.absent(),
this.primaryAssetId = const i0.Value.absent(),
});
StackEntityCompanion.insert({
required String id,
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
required String ownerId,
required String primaryAssetId,
}) : id = i0.Value(id),
ownerId = i0.Value(ownerId),
primaryAssetId = i0.Value(primaryAssetId);
static i0.Insertable<i1.StackEntityData> custom({
i0.Expression<String>? id,
i0.Expression<DateTime>? createdAt,
i0.Expression<DateTime>? updatedAt,
i0.Expression<String>? ownerId,
i0.Expression<String>? primaryAssetId,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (createdAt != null) 'created_at': createdAt,
if (updatedAt != null) 'updated_at': updatedAt,
if (ownerId != null) 'owner_id': ownerId,
if (primaryAssetId != null) 'primary_asset_id': primaryAssetId,
});
}
i1.StackEntityCompanion copyWith(
{i0.Value<String>? id,
i0.Value<DateTime>? createdAt,
i0.Value<DateTime>? updatedAt,
i0.Value<String>? ownerId,
i0.Value<String>? primaryAssetId}) {
return i1.StackEntityCompanion(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
ownerId: ownerId ?? this.ownerId,
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (createdAt.present) {
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
if (ownerId.present) {
map['owner_id'] = i0.Variable<String>(ownerId.value);
}
if (primaryAssetId.present) {
map['primary_asset_id'] = i0.Variable<String>(primaryAssetId.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('StackEntityCompanion(')
..write('id: $id, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('ownerId: $ownerId, ')
..write('primaryAssetId: $primaryAssetId')
..write(')'))
.toString();
}
}

View File

@@ -7,11 +7,14 @@ 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/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/stack.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';
@@ -46,9 +49,13 @@ class IsarDatabaseRepository implements IDatabaseRepository {
RemoteAlbumEntity,
RemoteAlbumAssetEntity,
RemoteAlbumUserEntity,
MemoryEntity,
MemoryAssetEntity,
StackEntity,
],
include: {
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
'package:immich_mobile/infrastructure/entities/asset_triggers.drift',
},
)
class Drift extends $Drift implements IDatabaseRepository {

View File

@@ -7,25 +7,33 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
as i2;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i3;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/asset_triggers.drift.dart'
as i4;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i5;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i6;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i7;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i8;
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i9;
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
as i10;
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
as i11;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
as i12;
import 'package:drift/internal/modular.dart' as i13;
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i13;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i14;
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i15;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i16;
import 'package:drift/internal/modular.dart' as i17;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -35,24 +43,28 @@ abstract class $Drift extends i0.GeneratedDatabase {
i2.$RemoteAssetEntityTable(this);
late final i3.$LocalAssetEntityTable localAssetEntity =
i3.$LocalAssetEntityTable(this);
late final i4.$UserMetadataEntityTable userMetadataEntity =
i4.$UserMetadataEntityTable(this);
late final i5.$PartnerEntityTable partnerEntity =
i5.$PartnerEntityTable(this);
late final i6.$LocalAlbumEntityTable localAlbumEntity =
i6.$LocalAlbumEntityTable(this);
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
i7.$LocalAlbumAssetEntityTable(this);
late final i8.$RemoteExifEntityTable remoteExifEntity =
i8.$RemoteExifEntityTable(this);
late final i9.$RemoteAlbumEntityTable remoteAlbumEntity =
i9.$RemoteAlbumEntityTable(this);
late final i10.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity =
i10.$RemoteAlbumAssetEntityTable(this);
late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
i11.$RemoteAlbumUserEntityTable(this);
i12.MergedAssetDrift get mergedAssetDrift => i13.ReadDatabaseContainer(this)
.accessor<i12.MergedAssetDrift>(i12.MergedAssetDrift.new);
late final i5.$UserMetadataEntityTable userMetadataEntity =
i5.$UserMetadataEntityTable(this);
late final i6.$PartnerEntityTable partnerEntity =
i6.$PartnerEntityTable(this);
late final i7.$LocalAlbumEntityTable localAlbumEntity =
i7.$LocalAlbumEntityTable(this);
late final i8.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
i8.$LocalAlbumAssetEntityTable(this);
late final i9.$RemoteExifEntityTable remoteExifEntity =
i9.$RemoteExifEntityTable(this);
late final i10.$RemoteAlbumEntityTable remoteAlbumEntity =
i10.$RemoteAlbumEntityTable(this);
late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity =
i11.$RemoteAlbumAssetEntityTable(this);
late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
i12.$RemoteAlbumUserEntityTable(this);
late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this);
late final i14.$MemoryAssetEntityTable memoryAssetEntity =
i14.$MemoryAssetEntityTable(this);
late final i15.$StackEntityTable stackEntity = i15.$StackEntityTable(this);
i16.MergedAssetDrift get mergedAssetDrift => i17.ReadDatabaseContainer(this)
.accessor<i16.MergedAssetDrift>(i16.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -61,6 +73,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
userEntity,
remoteAssetEntity,
localAssetEntity,
i4.trLocalAssetUpdateChecksumSetIds,
i4.trLocalAssetUpdateOldChecksumSetRemoteAssetLocalId,
i4.trLocalAssetDeleteUpdateRemoteAssetLocalId,
i4.trRemoteAssetInsertSetLocalId,
i3.idxLocalAssetChecksum,
i2.uQRemoteAssetOwnerChecksum,
i2.idxRemoteAssetChecksum,
@@ -71,7 +87,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteExifEntity,
remoteAlbumEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
stackEntity
];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>
@@ -84,6 +103,43 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('local_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('local_asset_entity',
limitUpdateKind: i0.UpdateKind.update),
result: [
i0.TableUpdate('local_asset_entity', kind: i0.UpdateKind.update),
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('local_asset_entity',
limitUpdateKind: i0.UpdateKind.update),
result: [
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('local_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
limitUpdateKind: i0.UpdateKind.insert),
result: [
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.update),
i0.TableUpdate('local_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
@@ -175,6 +231,34 @@ abstract class $Drift extends i0.GeneratedDatabase {
kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('memory_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('memory_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete),
],
),
],
);
@override
@@ -191,21 +275,27 @@ class $DriftManager {
i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
i3.$$LocalAssetEntityTableTableManager get localAssetEntity =>
i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
i4.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i4.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i5.$$PartnerEntityTableTableManager get partnerEntity =>
i5.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
i5.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i5.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i6.$$PartnerEntityTableTableManager get partnerEntity =>
i6.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i7.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
i7.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i8.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i8
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
i8.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
i9.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
i9.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
i10.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
i10.$$RemoteAlbumAssetEntityTableTableManager(
i9.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
i11.$$RemoteAlbumAssetEntityTableTableManager(
_db, _db.remoteAlbumAssetEntity);
i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11
i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
i13.$$MemoryEntityTableTableManager get memoryEntity =>
i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i15.$$StackEntityTableTableManager get stackEntity =>
i15.$$StackEntityTableTableManager(_db, _db.stackEntity);
}

View File

@@ -1,8 +1,6 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
@@ -43,36 +41,3 @@ class IsarExifRepository extends IsarDatabaseRepository {
});
}
}
class DriftRemoteExifRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftRemoteExifRepository(this._db) : super(_db);
Future<ExifInfo?> get(String assetId) {
final query = _db.remoteExifEntity.select()
..where((exif) => exif.assetId.equals(assetId));
return query.map((asset) => asset.toDto()).getSingleOrNull();
}
}
extension on RemoteExifEntityData {
ExifInfo toDto() {
return ExifInfo(
fileSize: fileSize,
description: description,
orientation: orientation,
timeZone: timeZone,
dateTimeOriginal: dateTimeOriginal,
latitude: latitude,
longitude: longitude,
city: city,
state: state,
country: country,
make: make,
model: model,
f: fNumber,
iso: iso,
);
}
}

View File

@@ -282,6 +282,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
durationInSeconds: Value(asset.durationInSeconds),
id: asset.id,
checksum: const Value(null),
remoteId: const Value(null),
);
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
_db.localAssetEntity,

View File

@@ -1,5 +1,6 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
@@ -7,6 +8,13 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftLocalAssetRepository(this._db) : super(_db);
Stream<LocalAsset?> watchAsset(String id) {
final query = _db.localAssetEntity.select()
..where((row) => row.id.equals(id));
return query.map((row) => row.toDto()).watchSingleOrNull();
}
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
if (hashes.isEmpty) {
return Future.value();

View File

@@ -0,0 +1,81 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftMemoryRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftMemoryRepository(this._db) : super(_db);
Future<List<DriftMemory>> getAll(String ownerId) async {
final now = DateTime.now();
final localUtc = DateTime.utc(now.year, now.month, now.day, 0, 0, 0);
final query = _db.select(_db.memoryEntity).join([
leftOuterJoin(
_db.memoryAssetEntity,
_db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id),
),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.memoryAssetEntity.assetId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline),
),
])
..where(_db.memoryEntity.ownerId.equals(ownerId))
..where(_db.memoryEntity.deletedAt.isNull())
..where(
_db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc),
)
..where(
_db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc),
)
..orderBy([
OrderingTerm.desc(_db.memoryEntity.memoryAt),
OrderingTerm.asc(_db.remoteAssetEntity.createdAt),
]);
final rows = await query.get();
final Map<String, DriftMemory> memoriesMap = {};
for (final row in rows) {
final memory = row.readTable(_db.memoryEntity);
final asset = row.readTable(_db.remoteAssetEntity);
final existingMemory = memoriesMap[memory.id];
if (existingMemory != null) {
existingMemory.assets.add(asset.toDto());
} else {
final assets = [asset.toDto()];
memoriesMap[memory.id] = memory.toDto().copyWith(assets: assets);
}
}
return memoriesMap.values.toList();
}
}
extension on MemoryEntityData {
DriftMemory toDto() {
return DriftMemory(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
deletedAt: deletedAt,
ownerId: ownerId,
type: type,
data: MemoryData.fromJson(data),
isSaved: isSaved,
memoryAt: memoryAt,
seenAt: seenAt,
showAt: showAt,
hideAt: hideAt,
assets: [],
);
}
}

View File

@@ -1,13 +1,47 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
hide ExifInfo;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class DriftRemoteAssetRepository extends DriftDatabaseRepository {
class RemoteAssetRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftRemoteAssetRepository(this._db) : super(_db);
const RemoteAssetRepository(this._db) : super(_db);
/// For testing purposes
Future<List<RemoteAsset>> getSome(String userId) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(10);
return query.map((row) => row.toDto()).get();
}
Stream<RemoteAsset?> watchAsset(String id) {
final query = _db.remoteAssetEntity.select()
..where((row) => row.id.equals(id));
return query.map((row) => row.toDto()).watchSingleOrNull();
}
Future<ExifInfo?> getExif(String id) {
return _db.managers.remoteExifEntity
.filter((row) => row.assetId.id.equals(id))
.map((row) => row.toDto())
.getSingleOrNull();
}
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
return _db.batch((batch) async {
@@ -33,6 +67,22 @@ class DriftRemoteAssetRepository extends DriftDatabaseRepository {
});
}
Future<void> trash(List<String> ids) {
return _db.batch((batch) async {
for (final id in ids) {
batch.update(
_db.remoteAssetEntity,
RemoteAssetEntityCompanion(deletedAt: Value(DateTime.now())),
where: (e) => e.id.equals(id),
);
}
});
}
Future<void> delete(List<String> ids) {
return _db.remoteAssetEntity.deleteWhere((row) => row.id.isIn(ids));
}
Future<void> updateLocation(List<String> ids, LatLng location) {
return _db.batch((batch) async {
for (final id in ids) {

View File

@@ -0,0 +1,30 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/stack.model.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftStackRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftStackRepository(this._db) : super(_db);
Future<List<Stack>> getAll(String userId) {
final query = _db.stackEntity.select()
..where((e) => e.ownerId.equals(userId));
return query.map((stack) {
return stack.toDto();
}).get();
}
}
extension on StackEntityData {
Stack toDto() {
return Stack(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
ownerId: ownerId,
primaryAssetId: primaryAssetId,
);
}
}

View File

@@ -5,20 +5,21 @@ import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';
class StorageRepository {
final _log = Logger('StorageRepository');
const StorageRepository();
Future<File?> getFileForAsset(LocalAsset asset) async {
final log = Logger('StorageRepository');
File? file;
try {
final entity = await AssetEntity.fromId(asset.id);
file = await entity?.originFile;
if (file == null) {
_log.warning(
log.warning(
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
);
}
} catch (error, stackTrace) {
_log.warning(
log.warning(
"Error getting file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
error,
stackTrace,

View File

@@ -52,6 +52,10 @@ class SyncApiRepository {
SyncRequestType.albumAssetsV1,
SyncRequestType.albumAssetExifsV1,
SyncRequestType.albumToAssetsV1,
SyncRequestType.memoriesV1,
SyncRequestType.memoryToAssetsV1,
SyncRequestType.stacksV1,
SyncRequestType.partnerStacksV1,
],
).toJson(),
);
@@ -157,6 +161,15 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
SyncEntityType.memoryV1: SyncMemoryV1.fromJson,
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson,
SyncEntityType.stackV1: SyncStackV1.fromJson,
SyncEntityType.stackDeleteV1: SyncStackDeleteV1.fromJson,
SyncEntityType.partnerStackV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
};
class _SyncAckV1 {

View File

@@ -1,12 +1,18 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:logging/logging.dart';
@@ -64,8 +70,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: SyncPartnerDeleteV1', e, s);
} catch (error, stack) {
_logger.severe('Error: SyncPartnerDeleteV1', error, stack);
rethrow;
}
}
@@ -87,8 +93,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: SyncPartnerV1', e, s);
} catch (error, stack) {
_logger.severe('Error: SyncPartnerV1', error, stack);
rethrow;
}
}
@@ -98,10 +104,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
String debugLabel = 'user',
}) async {
try {
await _db.remoteAssetEntity
.deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId)));
} catch (e, s) {
_logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s);
await _db.remoteAssetEntity.deleteWhere(
(row) => row.id.isIn(data.map((e) => e.assetId)),
);
} catch (error, stack) {
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack);
rethrow;
}
}
@@ -136,8 +143,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAssetsV1 - $debugLabel', e, s);
} catch (error, stack) {
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stack);
rethrow;
}
}
@@ -161,8 +168,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
fNumber: Value(exif.fNumber),
fileSize: Value(exif.fileSizeInByte),
focalLength: Value(exif.focalLength),
latitude: Value(exif.latitude),
longitude: Value(exif.longitude),
latitude: Value(exif.latitude?.toDouble()),
longitude: Value(exif.longitude?.toDouble()),
iso: Value(exif.iso),
make: Value(exif.make),
model: Value(exif.model),
@@ -170,6 +177,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
timeZone: Value(exif.timeZone),
rating: Value(exif.rating),
projectionType: Value(exif.projectionType),
lens: Value(exif.lensModel),
);
batch.insert(
@@ -179,18 +187,23 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s);
} catch (error, stack) {
_logger.severe(
'Error: updateAssetsExifV1 - $debugLabel',
error,
stack,
);
rethrow;
}
}
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
try {
await _db.remoteAlbumEntity
.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId)));
} catch (e, s) {
_logger.severe('Error: deleteAlbumsV1', e, s);
await _db.remoteAlbumEntity.deleteWhere(
(row) => row.id.isIn(data.map((e) => e.albumId)),
);
} catch (error, stack) {
_logger.severe('Error: deleteAlbumsV1', error, stack);
rethrow;
}
}
@@ -217,8 +230,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAlbumsV1', e, s);
} catch (error, stack) {
_logger.severe('Error: updateAlbumsV1', error, stack);
rethrow;
}
}
@@ -236,8 +249,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: deleteAlbumUsersV1', e, s);
} catch (error, stack) {
_logger.severe('Error: deleteAlbumUsersV1', error, stack);
rethrow;
}
}
@@ -263,8 +276,12 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s);
} catch (error, stack) {
_logger.severe(
'Error: updateAlbumUsersV1 - $debugLabel',
error,
stack,
);
rethrow;
}
}
@@ -284,8 +301,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: deleteAlbumToAssetsV1', e, s);
} catch (error, stack) {
_logger.severe('Error: deleteAlbumToAssetsV1', error, stack);
rethrow;
}
}
@@ -309,8 +326,137 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s);
} catch (error, stack) {
_logger.severe(
'Error: updateAlbumToAssetsV1 - $debugLabel',
error,
stack,
);
rethrow;
}
}
Future<void> updateMemoriesV1(Iterable<SyncMemoryV1> data) async {
try {
await _db.batch((batch) {
for (final memory in data) {
final companion = MemoryEntityCompanion(
createdAt: Value(memory.createdAt),
deletedAt: Value(memory.deletedAt),
ownerId: Value(memory.ownerId),
type: Value(memory.type.toMemoryType()),
data: Value(jsonEncode(memory.data)),
isSaved: Value(memory.isSaved),
memoryAt: Value(memory.memoryAt),
seenAt: Value.absentIfNull(memory.seenAt),
showAt: Value.absentIfNull(memory.showAt),
hideAt: Value.absentIfNull(memory.hideAt),
);
batch.insert(
_db.memoryEntity,
companion.copyWith(id: Value(memory.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: updateMemoriesV1', error, stack);
rethrow;
}
}
Future<void> deleteMemoriesV1(Iterable<SyncMemoryDeleteV1> data) async {
try {
await _db.memoryEntity.deleteWhere(
(row) => row.id.isIn(data.map((e) => e.memoryId)),
);
} catch (error, stack) {
_logger.severe('Error: deleteMemoriesV1', error, stack);
rethrow;
}
}
Future<void> updateMemoryAssetsV1(Iterable<SyncMemoryAssetV1> data) async {
try {
await _db.batch((batch) {
for (final asset in data) {
final companion = MemoryAssetEntityCompanion(
memoryId: Value(asset.memoryId),
assetId: Value(asset.assetId),
);
batch.insert(
_db.memoryAssetEntity,
companion,
onConflict: DoNothing(),
);
}
});
} catch (error, stack) {
_logger.severe('Error: updateMemoryAssetsV1', error, stack);
rethrow;
}
}
Future<void> deleteMemoryAssetsV1(
Iterable<SyncMemoryAssetDeleteV1> data,
) async {
try {
await _db.batch((batch) {
for (final asset in data) {
batch.delete(
_db.memoryAssetEntity,
MemoryAssetEntityCompanion(
memoryId: Value(asset.memoryId),
assetId: Value(asset.assetId),
),
);
}
});
} catch (error, stack) {
_logger.severe('Error: deleteMemoryAssetsV1', error, stack);
rethrow;
}
}
Future<void> updateStacksV1(
Iterable<SyncStackV1> data, {
String debugLabel = 'user',
}) async {
try {
await _db.batch((batch) {
for (final stack in data) {
final companion = StackEntityCompanion(
createdAt: Value(stack.createdAt),
updatedAt: Value(stack.updatedAt),
ownerId: Value(stack.ownerId),
primaryAssetId: Value(stack.primaryAssetId),
);
batch.insert(
_db.stackEntity,
companion.copyWith(id: Value(stack.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: updateStacksV1 - $debugLabel', error, stack);
rethrow;
}
}
Future<void> deleteStacksV1(
Iterable<SyncStackDeleteV1> data, {
String debugLabel = 'user',
}) async {
try {
await _db.stackEntity.deleteWhere(
(row) => row.id.isIn(data.map((e) => e.stackId)),
);
} catch (error, stack) {
_logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack);
rethrow;
}
}
@@ -334,6 +480,13 @@ extension on AssetOrder {
};
}
extension on MemoryType {
MemoryTypeEnum toMemoryType() => switch (this) {
MemoryType.onThisDay => MemoryTypeEnum.onThisDay,
_ => throw Exception('Unknown MemoryType value: $this'),
};
}
extension on api.AlbumUserRole {
AlbumUserRole toAlbumUserRole() => switch (this) {
api.AlbumUserRole.editor => AlbumUserRole.editor,

View File

@@ -104,7 +104,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
).get();
}
Stream<List<Bucket>> watchLocalBucket(
Stream<List<Bucket>> watchLocalAlbumBucket(
String albumId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
@@ -137,7 +137,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch();
}
Future<List<BaseAsset>> getLocalBucketAssets(
Future<List<BaseAsset>> getLocalAlbumBucketAssets(
String albumId, {
required int offset,
required int count,
@@ -158,7 +158,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.get();
}
Stream<List<Bucket>> watchRemoteBucket(
Stream<List<Bucket>> watchRemoteAlbumBucket(
String albumId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
@@ -192,7 +192,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch();
}
Future<List<BaseAsset>> getRemoteBucketAssets(
Future<List<BaseAsset>> getRemoteAlbumBucketAssets(
String albumId, {
required int offset,
required int count,
@@ -213,6 +213,317 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
.get();
}
Stream<List<Bucket>> watchFavoriteBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.isFavorite.equals(true) & row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.isFavorite.equals(true),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getFavoriteBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) => row.isFavorite.equals(true) & row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchTrashBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNotNull() & row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNotNull(),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getTrashBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchArchiveBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.visibility.equalsValue(AssetVisibility.archive) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.archive),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getArchiveBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.archive),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchLockedFolderBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.locked),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getLockedFolderBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchVideoBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.type.equalsValue(AssetType.video) &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getVideoBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchRemoteBucket(
List<String> userIds, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.isIn(userIds),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.ownerId.isIn(userIds),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getRemoteBucketAssets(
List<String> userIds, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.isIn(userIds),
)
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
}
extension on Expression<DateTime> {

View File

@@ -238,7 +238,7 @@ class GalleryViewerPage extends HookConsumerWidget {
PhotoViewGalleryPageOptions buildImage(Asset asset) {
return PhotoViewGalleryPageOptions(
onDragStart: (_, details, __) {
onDragStart: (_, details, __, ___) {
localPosition.value = details.localPosition;
},
onDragUpdate: (_, details, __) {
@@ -267,7 +267,7 @@ class GalleryViewerPage extends HookConsumerWidget {
PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) {
return PhotoViewGalleryPageOptions.customChild(
onDragStart: (_, details, __) =>
onDragStart: (_, details, __, ___) =>
localPosition.value = details.localPosition,
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
heroAttributes: _getHeroAttributes(asset),
@@ -370,7 +370,7 @@ class GalleryViewerPage extends HookConsumerWidget {
),
itemCount: totalAssets.value,
scrollDirection: Axis.horizontal,
onPageChanged: (value) {
onPageChanged: (value, _) {
final next = currentIndex.value < value ? value + 1 : value - 1;
ref.read(hapticFeedbackProvider.notifier).selectionClick();

View File

@@ -42,22 +42,6 @@ class TabShellPage extends ConsumerWidget {
);
}
void onNavigationSelected(TabsRouter router, int index) {
// On Photos page menu tapped
if (router.activeIndex == 0 && index == 0) {
scrollToTopNotifierProvider.scrollToTop();
}
// On Search page tapped
if (router.activeIndex == 1 && index == 1) {
ref.read(searchInputFocusProvider).requestFocus();
}
ref.read(hapticFeedbackProvider.notifier).selectionClick();
router.setActiveIndex(index);
ref.read(tabProvider.notifier).state = TabEnum.values[index];
}
final navigationDestinations = [
NavigationDestination(
label: 'photos'.tr(),
@@ -110,15 +94,6 @@ class TabShellPage extends ConsumerWidget {
),
];
Widget bottomNavigationBar(TabsRouter tabsRouter) {
return NavigationBar(
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: (index) =>
onNavigationSelected(tabsRouter, index),
destinations: navigationDestinations,
);
}
Widget navigationRail(TabsRouter tabsRouter) {
return NavigationRail(
destinations: navigationDestinations
@@ -131,21 +106,19 @@ class TabShellPage extends ConsumerWidget {
)
.toList(),
onDestinationSelected: (index) =>
onNavigationSelected(tabsRouter, index),
_onNavigationSelected(tabsRouter, index, ref),
selectedIndex: tabsRouter.activeIndex,
labelType: NavigationRailLabelType.all,
groupAlignment: 0.0,
);
}
final multiselectEnabled =
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
return AutoTabsRouter(
routes: [
const MainTimelineRoute(),
SearchRoute(),
const DriftAlbumsRoute(),
const LibraryRoute(),
const DriftLibraryRoute(),
],
duration: const Duration(milliseconds: 600),
transitionBuilder: (context, child, animation) => FadeTransition(
@@ -173,12 +146,57 @@ class TabShellPage extends ConsumerWidget {
],
)
: heroedChild,
bottomNavigationBar: multiselectEnabled || isScreenLandscape
? null
: bottomNavigationBar(tabsRouter),
bottomNavigationBar: _BottomNavigationBar(
tabsRouter: tabsRouter,
destinations: navigationDestinations,
),
),
);
},
);
}
}
void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
// On Photos page menu tapped
if (router.activeIndex == 0 && index == 0) {
scrollToTopNotifierProvider.scrollToTop();
}
// On Search page tapped
if (router.activeIndex == 1 && index == 1) {
ref.read(searchInputFocusProvider).requestFocus();
}
ref.read(hapticFeedbackProvider.notifier).selectionClick();
router.setActiveIndex(index);
ref.read(tabProvider.notifier).state = TabEnum.values[index];
}
class _BottomNavigationBar extends ConsumerWidget {
const _BottomNavigationBar({
required this.tabsRouter,
required this.destinations,
});
final List<Widget> destinations;
final TabsRouter tabsRouter;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isScreenLandscape = context.orientation == Orientation.landscape;
final isMultiselectEnabled =
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
if (isScreenLandscape || isMultiselectEnabled) {
return const SizedBox.shrink();
}
return NavigationBar(
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: (index) =>
_onNavigationSelected(tabsRouter, index, ref),
destinations: destinations,
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftArchivePage extends StatelessWidget {
const DriftArchivePage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access archive');
}
final timelineService =
ref.watch(timelineFactoryProvider).archive(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftFavoritePage extends StatelessWidget {
const DriftFavoritePage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access favorite');
}
final timelineService =
ref.watch(timelineFactoryProvider).favorite(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftLockedFolderPage extends StatelessWidget {
const DriftLockedFolderPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access locked folder');
}
final timelineService =
ref.watch(timelineFactoryProvider).lockedFolder(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftTrashPage extends StatelessWidget {
const DriftTrashPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access trash');
}
final timelineService =
ref.watch(timelineFactoryProvider).trash(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftVideoPage extends StatelessWidget {
const DriftVideoPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to video');
}
final timelineService =
ref.watch(timelineFactoryProvider).video(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -5,15 +5,43 @@ 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/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.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/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
final _features = [
_Feature(
name: 'Selection Mode Timeline',
icon: Icons.developer_mode_rounded,
onTap: (ctx, ref) async {
final user = ref.watch(currentUserProvider);
if (user == null) {
return Future.value();
}
final assets =
await ref.read(remoteAssetRepositoryProvider).getSome(user.id);
final selectedAssets = await ctx.pushRoute<Set<BaseAsset>>(
DriftAssetSelectionTimelineRoute(
lockedSelectionAssets: assets.toSet(),
),
);
DLog.log(
"Selected ${selectedAssets?.length ?? 0} assets",
);
return Future.value();
},
),
_Feature(
name: 'Sync Local',
icon: Icons.photo_album_rounded,
@@ -66,6 +94,9 @@ final _features = [
await db.remoteAlbumEntity.deleteAll();
await db.remoteAlbumUserEntity.deleteAll();
await db.remoteAlbumAssetEntity.deleteAll();
await db.memoryEntity.deleteAll();
await db.memoryAssetEntity.deleteAll();
await db.stackEntity.deleteAll();
},
),
_Feature(
@@ -96,6 +127,11 @@ final _features = [
icon: Icons.timeline_rounded,
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
),
_Feature(
name: 'Video',
icon: Icons.video_collection_outlined,
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
),
];
@RoutePage()

View File

@@ -1,7 +1,9 @@
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/memory/memory_lane.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
@RoutePage()
class MainTimelinePage extends ConsumerWidget {
@@ -9,6 +11,22 @@ class MainTimelinePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return const Timeline();
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
return memoryLaneProvider.when(
data: (memories) {
return memories.isEmpty
? const Timeline()
: Timeline(
topSliverWidget: SliverToBoxAdapter(
key: Key('memory-lane-${memories.first.assets.first.id}'),
child: DriftMemoryLane(memories: memories),
),
topSliverWidgetHeight: 200,
);
},
loading: () => const Timeline(),
error: (error, stackTrace) => const Timeline(),
);
}
}

View File

@@ -154,6 +154,18 @@ final _remoteStats = [
name: 'Remote Albums',
load: (db) => db.managers.remoteAlbumEntity.count(),
),
_Stat(
name: 'Memories',
load: (db) => db.managers.memoryEntity.count(),
),
_Stat(
name: 'Memories Assets',
load: (db) => db.managers.memoryAssetEntity.count(),
),
_Stat(
name: 'Stacks',
load: (db) => db.managers.stackEntity.count(),
),
];
@RoutePage()

View File

@@ -0,0 +1,44 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@RoutePage()
class DriftAssetSelectionTimelinePage extends ConsumerWidget {
final Set<BaseAsset> lockedSelectionAssets;
const DriftAssetSelectionTimelinePage({
super.key,
this.lockedSelectionAssets = const {},
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ProviderScope(
overrides: [
multiSelectProvider.overrideWith(
() => MultiSelectNotifier(
MultiSelectState(
selectedAssets: {},
lockedSelectionAssets: lockedSelectionAssets,
forceEnable: true,
),
),
),
timelineServiceProvider.overrideWith(
(ref) {
final timelineUsers =
ref.watch(timelineUsersProvider).valueOrNull ?? [];
final timelineService =
ref.watch(timelineFactoryProvider).remoteAssets(timelineUsers);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -0,0 +1,501 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/partner.provider.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/user_avatar.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@RoutePage()
class DriftLibraryPage extends ConsumerWidget {
const DriftLibraryPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return const Scaffold(
body: CustomScrollView(
slivers: [
ImmichSliverAppBar(),
_ActionButtonGrid(),
_CollectionCards(),
_QuickAccessButtonList(),
],
),
);
}
}
class _ActionButtonGrid extends ConsumerWidget {
const _ActionButtonGrid();
@override
Widget build(BuildContext context, WidgetRef ref) {
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
return SliverPadding(
padding: const EdgeInsets.only(left: 16, top: 16, right: 16, bottom: 12),
sliver: SliverToBoxAdapter(
child: Column(
children: [
Row(
children: [
_ActionButton(
icon: Icons.favorite_outline_rounded,
onTap: () => context.pushRoute(const DriftFavoriteRoute()),
label: 'favorites'.t(context: context),
),
const SizedBox(width: 8),
_ActionButton(
icon: Icons.archive_outlined,
onTap: () => context.pushRoute(const DriftArchiveRoute()),
label: 'archived'.t(context: context),
),
],
),
const SizedBox(height: 8),
Row(
children: [
_ActionButton(
icon: Icons.link_outlined,
onTap: () => context.pushRoute(const SharedLinkRoute()),
label: 'shared_links'.t(context: context),
),
isTrashEnable
? const SizedBox(width: 8)
: const SizedBox.shrink(),
isTrashEnable
? _ActionButton(
icon: Icons.delete_outline_rounded,
onTap: () => context.pushRoute(const DriftTrashRoute()),
label: 'trash'.t(context: context),
)
: const SizedBox.shrink(),
],
),
],
),
),
);
}
}
class _ActionButton extends StatelessWidget {
const _ActionButton({
required this.icon,
required this.onTap,
required this.label,
});
final IconData icon;
final VoidCallback onTap;
final String label;
@override
Widget build(BuildContext context) {
return Expanded(
child: FilledButton.icon(
onPressed: onTap,
label: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
label,
style: TextStyle(
color: context.colorScheme.onSurface,
fontSize: 15,
),
),
),
style: FilledButton.styleFrom(
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
backgroundColor: context.colorScheme.surfaceContainerLow,
alignment: Alignment.centerLeft,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(25)),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(10),
width: 1,
),
),
),
icon: Icon(
icon,
color: context.primaryColor,
),
),
);
}
}
class _CollectionCards extends StatelessWidget {
const _CollectionCards();
@override
Widget build(BuildContext context) {
return const SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
_PeopleCollectionCard(),
_PlacesCollectionCard(),
_LocalAlbumsCollectionCard(),
],
),
),
);
}
}
class _PeopleCollectionCard extends ConsumerWidget {
const _PeopleCollectionCard();
@override
Widget build(BuildContext context, WidgetRef ref) {
final people = ref.watch(getAllPeopleProvider);
return LayoutBuilder(
builder: (context, constraints) {
final isTablet = constraints.maxWidth > 600;
final widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: size,
width: size,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(30),
context.colorScheme.primary.withAlpha(25),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: people.widgetWhen(
onLoading: () => const Center(
child: CircularProgressIndicator(),
),
onData: (people) {
return GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
physics: const NeverScrollableScrollPhysics(),
children: people.take(4).map((person) {
return CircleAvatar(
backgroundImage: NetworkImage(
getFaceThumbnailUrl(person.id),
headers: ApiService.getRequestHeaders(),
),
);
}).toList(),
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'people'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
},
);
}
}
class _PlacesCollectionCard extends StatelessWidget {
const _PlacesCollectionCard();
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isTablet = constraints.maxWidth > 600;
final widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(
PlacesCollectionRoute(
currentLocation: null,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: size,
width: size,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
color:
context.colorScheme.secondaryContainer.withAlpha(100),
),
child: IgnorePointer(
child: MapThumbnail(
zoom: 8,
centre: const LatLng(
21.44950,
-157.91959,
),
showAttribution: false,
themeMode: context.isDarkTheme
? ThemeMode.dark
: ThemeMode.light,
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'places'.t(),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
},
);
}
}
class _LocalAlbumsCollectionCard extends ConsumerWidget {
const _LocalAlbumsCollectionCard();
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Migrate to the drift after local album page
final albums = ref.watch(localAlbumsProvider);
return LayoutBuilder(
builder: (context, constraints) {
final isTablet = constraints.maxWidth > 600;
final widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(
const LocalAlbumsRoute(),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: size,
width: size,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(30),
context.colorScheme.primary.withAlpha(25),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
physics: const NeverScrollableScrollPhysics(),
children: albums.take(4).map((album) {
return AlbumThumbnailCard(
album: album,
showTitle: false,
);
}).toList(),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'on_this_device'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
},
);
}
}
class _QuickAccessButtonList extends ConsumerWidget {
const _QuickAccessButtonList();
@override
Widget build(BuildContext context, WidgetRef ref) {
final partners = ref.watch(partnerSharedWithProvider);
return SliverPadding(
padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32),
sliver: SliverToBoxAdapter(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: context.colorScheme.onSurface.withAlpha(10),
width: 1,
),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(10),
context.colorScheme.primary.withAlpha(15),
context.colorScheme.primary.withAlpha(20),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(0),
physics: const NeverScrollableScrollPhysics(),
children: [
ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20),
topRight: const Radius.circular(20),
bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0),
bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
),
),
leading: const Icon(
Icons.folder_outlined,
size: 26,
),
title: Text(
'folders'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
onTap: () => context.pushRoute(FolderRoute()),
),
ListTile(
leading: const Icon(
Icons.lock_outline_rounded,
size: 26,
),
title: Text(
'locked_folder'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
// TODO: PIN code is needed
onTap: () => context.pushRoute(const DriftLockedFolderRoute()),
),
ListTile(
leading: const Icon(
Icons.group_outlined,
size: 26,
),
title: Text(
'partners'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
onTap: () => context.pushRoute(const PartnerRoute()),
),
_PartnerList(partners: partners),
],
),
),
),
);
}
}
class _PartnerList extends StatelessWidget {
const _PartnerList({required this.partners});
final List<UserDto> partners;
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(0),
physics: const NeverScrollableScrollPhysics(),
itemCount: partners.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final partner = partners[index];
final isLastItem = index == partners.length - 1;
return ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(isLastItem ? 20 : 0),
bottomRight: Radius.circular(isLastItem ? 20 : 0),
),
),
contentPadding: const EdgeInsets.only(
left: 12.0,
right: 18.0,
),
leading: userAvatar(context, partner, radius: 16),
title: const Text(
"partner_list_user_photos",
style: TextStyle(
fontWeight: FontWeight.w500,
),
).t(context: context, args: {'user': partner.name}),
onTap: () => context.pushRoute(PartnerDetailRoute(partner: partner)),
);
},
);
}
}

View File

@@ -0,0 +1,394 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart';
import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/widgets/memories/memory_epilogue.dart';
import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart';
/// Expects [currentAssetNotifier] to be set before navigating to this page
@RoutePage()
class DriftMemoryPage extends HookConsumerWidget {
final List<DriftMemory> memories;
final int memoryIndex;
const DriftMemoryPage({
required this.memories,
required this.memoryIndex,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentMemory = useState(memories[memoryIndex]);
final currentAssetPage = useState(0);
final currentMemoryIndex = useState(memoryIndex);
final assetProgress = useState(
"${currentAssetPage.value + 1}|${currentMemory.value.assets.length}",
);
const bgColor = Colors.black;
final currentAsset = useState<RemoteAsset?>(null);
/// The list of all of the asset page controllers
final memoryAssetPageControllers =
List.generate(memories.length, (i) => usePageController());
/// The main vertically scrolling page controller with each list of memories
final memoryPageController = usePageController(initialPage: memoryIndex);
useEffect(() {
// Memories is an immersive activity
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
return () {
// Clean up to normal edge to edge when we are done
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
};
});
toNextMemory() {
memoryPageController.nextPage(
duration: const Duration(milliseconds: 500),
curve: Curves.easeIn,
);
}
void toPreviousMemory() {
if (currentMemoryIndex.value > 0) {
// Move to the previous memory page
memoryPageController.previousPage(
duration: const Duration(milliseconds: 500),
curve: Curves.easeIn,
);
// Wait for the next frame to ensure the page is built
SchedulerBinding.instance.addPostFrameCallback((_) {
final previousIndex = currentMemoryIndex.value - 1;
final previousMemoryController =
memoryAssetPageControllers[previousIndex];
// Ensure the controller is attached
if (previousMemoryController.hasClients) {
previousMemoryController
.jumpToPage(memories[previousIndex].assets.length - 1);
} else {
// Wait for the next frame until it is attached
SchedulerBinding.instance.addPostFrameCallback((_) {
if (previousMemoryController.hasClients) {
previousMemoryController
.jumpToPage(memories[previousIndex].assets.length - 1);
}
});
}
});
}
}
toNextAsset(int currentAssetIndex) {
if (currentAssetIndex + 1 < currentMemory.value.assets.length) {
// Go to the next asset
PageController controller =
memoryAssetPageControllers[currentMemoryIndex.value];
controller.nextPage(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 500),
);
} else {
// Go to the next memory since we are at the end of our assets
toNextMemory();
}
}
toPreviousAsset(int currentAssetIndex) {
if (currentAssetIndex > 0) {
// Go to the previous asset
PageController controller =
memoryAssetPageControllers[currentMemoryIndex.value];
controller.previousPage(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 500),
);
} else {
// Go to the previous memory since we are at the end of our assets
toPreviousMemory();
}
}
updateProgressText() {
assetProgress.value =
"${currentAssetPage.value + 1}|${currentMemory.value.assets.length}";
}
/// Downloads and caches the image for the asset at this [currentMemory]'s index
precacheAsset(int index) async {
// Guard index out of range
if (index < 0) {
return;
}
// Context might be removed due to popping out of Memory Lane during Scroll handling
if (!context.mounted) {
return;
}
late RemoteAsset asset;
if (index < currentMemory.value.assets.length) {
// Uses the next asset in this current memory
asset = currentMemory.value.assets[index];
} else {
// Precache the first asset in the next memory if available
final currentMemoryIndex = memories.indexOf(currentMemory.value);
// Guard no memory found
if (currentMemoryIndex == -1) {
return;
}
final nextMemoryIndex = currentMemoryIndex + 1;
// Guard no next memory
if (nextMemoryIndex >= memories.length) {
return;
}
// Get the first asset from the next memory
asset = memories[nextMemoryIndex].assets.first;
}
// Precache the asset
final size = MediaQuery.sizeOf(context);
await precacheImage(
getFullImageProvider(
asset,
size: Size(size.width, size.height),
),
context,
size: size,
);
}
// Precache the next page right away if we are on the first page
if (currentAssetPage.value == 0) {
Future.delayed(const Duration(milliseconds: 200))
.then((_) => precacheAsset(1));
}
Future<void> onAssetChanged(int otherIndex) async {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
currentAssetPage.value = otherIndex;
updateProgressText();
// Wait for page change animation to finish
await Future.delayed(const Duration(milliseconds: 400));
// And then precache the next asset
await precacheAsset(otherIndex + 1);
final asset = currentMemory.value.assets[otherIndex];
currentAsset.value = asset;
ref.read(currentAssetNotifier.notifier).setAsset(asset);
// if (asset.isVideo || asset.isMotionPhoto) {
if (asset.isVideo) {
ref.read(videoPlaybackValueProvider.notifier).reset();
}
}
/* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called
* when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final
* page during the end of scroll is different than the current page
*/
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
// Calculate OverScroll manually using the number of pixels away from maxScrollExtent
// maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1
// or sum of vertical pixels of all memories for depth = 0
if (notification is ScrollUpdateNotification) {
final isEpiloguePage =
(memoryPageController.page?.floor() ?? 0) >= memories.length;
final offset = notification.metrics.pixels;
if (isEpiloguePage &&
(offset > notification.metrics.maxScrollExtent + 150)) {
context.maybePop();
return true;
}
}
return false;
},
child: Scaffold(
backgroundColor: bgColor,
body: SafeArea(
child: PageView.builder(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
scrollDirection: Axis.vertical,
controller: memoryPageController,
onPageChanged: (pageNumber) {
ref.read(hapticFeedbackProvider.notifier).mediumImpact();
if (pageNumber < memories.length) {
currentMemoryIndex.value = pageNumber;
currentMemory.value = memories[pageNumber];
}
currentAssetPage.value = 0;
updateProgressText();
},
itemCount: memories.length + 1,
itemBuilder: (context, mIndex) {
// Build last page
if (mIndex == memories.length) {
return MemoryEpilogue(
onStartOver: () => memoryPageController.animateToPage(
0,
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
),
);
}
final yearsAgo = DateTime.now().year - memories[mIndex].data.year;
final title = 'years_ago'.t(
context: context,
args: {
'years': yearsAgo.toString(),
},
);
// Build horizontal page
final assetController = memoryAssetPageControllers[mIndex];
return Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 24.0,
right: 24.0,
top: 8.0,
bottom: 2.0,
),
child: AnimatedBuilder(
animation: assetController,
builder: (context, child) {
double value = 0.0;
if (assetController.hasClients) {
// We can only access [page] if this has clients
value = assetController.page ?? 0;
}
return MemoryProgressIndicator(
ticks: memories[mIndex].assets.length,
value: (value + 1) / memories[mIndex].assets.length,
);
},
),
),
Expanded(
child: Stack(
children: [
PageView.builder(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
controller: assetController,
onPageChanged: onAssetChanged,
scrollDirection: Axis.horizontal,
itemCount: memories[mIndex].assets.length,
itemBuilder: (context, index) {
final asset = memories[mIndex].assets[index];
return Stack(
children: [
Container(
color: Colors.black,
child: DriftMemoryCard(
asset: asset,
title: title,
showTitle: index == 0,
),
),
Positioned.fill(
child: Row(
children: [
// Left side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toPreviousAsset(index);
},
),
),
// Right side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toNextAsset(index);
},
),
),
],
),
),
],
);
},
),
Positioned(
top: 8,
left: 8,
child: MaterialButton(
minWidth: 0,
onPressed: () {
// auto_route doesn't invoke pop scope, so
// turn off full screen mode here
// https://github.com/Milad-Akarie/auto_route_library/issues/1799
context.maybePop();
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.edgeToEdge,
);
},
shape: const CircleBorder(),
color: Colors.white.withValues(alpha: 0.2),
elevation: 0,
child: const Icon(
Icons.close_rounded,
color: Colors.white,
),
),
),
if (currentAsset.value != null &&
currentAsset.value!.isVideo)
Positioned(
bottom: 24,
right: 32,
child: Icon(
Icons.videocam_outlined,
color: Colors.grey[200],
),
),
],
),
),
DriftMemoryBottomInfo(
memory: memories[mIndex],
title: title,
),
],
);
},
),
),
),
);
}
}

View File

@@ -5,7 +5,6 @@ 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';
@@ -20,7 +19,6 @@ class ArchiveActionButton extends ConsumerWidget {
}
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(

View File

@@ -9,17 +9,33 @@ class BaseActionButton extends StatelessWidget {
this.onPressed,
this.onLongPressed,
this.maxWidth = 90.0,
this.minWidth,
this.menuItem = false,
});
final String label;
final IconData iconData;
final double maxWidth;
final double? minWidth;
final bool menuItem;
final void Function()? onPressed;
final void Function()? onLongPressed;
@override
Widget build(BuildContext context) {
final minWidth = context.isMobile ? context.width / 4.5 : 75.0;
final miniWidth =
minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
final iconTheme = IconTheme.of(context);
final iconSize = iconTheme.size ?? 24.0;
final iconColor = iconTheme.color ?? context.themeData.iconTheme.color;
final textColor = context.themeData.textTheme.labelLarge?.color;
if (menuItem) {
return IconButton(
onPressed: onPressed,
icon: Icon(iconData, size: iconSize, color: iconColor),
);
}
return ConstrainedBox(
constraints: BoxConstraints(
@@ -30,19 +46,22 @@ class BaseActionButton extends StatelessWidget {
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
textColor: textColor,
onPressed: onPressed,
onLongPress: onLongPressed,
minWidth: minWidth,
minWidth: miniWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(iconData, size: 24),
Icon(iconData, size: iconSize, color: iconColor),
const SizedBox(height: 8),
Text(
label,
style:
const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400),
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w400,
),
maxLines: 3,
textAlign: TextAlign.center,
),

View File

@@ -1,10 +1,42 @@
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 DeletePermanentActionButton extends ConsumerWidget {
const DeletePermanentActionButton({super.key});
final ActionSource source;
const DeletePermanentActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result = await ref.read(actionProvider.notifier).delete(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'delete_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 +44,7 @@ class DeletePermanentActionButton extends ConsumerWidget {
maxWidth: 110.0,
iconData: Icons.delete_forever,
label: "delete_dialog_title".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -5,14 +5,18 @@ 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 {
final ActionSource source;
final bool menuItem;
const FavoriteActionButton({super.key, required this.source});
const FavoriteActionButton({
super.key,
required this.source,
this.menuItem = false,
});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
@@ -20,7 +24,11 @@ class FavoriteActionButton extends ConsumerWidget {
}
final result = await ref.read(actionProvider.notifier).favorite(source);
await ref.read(timelineServiceProvider).reloadBucket();
if (source == ActionSource.viewer) {
return;
}
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'favorite_action_prompt'.t(
@@ -45,6 +53,7 @@ class FavoriteActionButton extends ConsumerWidget {
return BaseActionButton(
iconData: Icons.favorite_border_rounded,
label: "favorite".t(context: context),
menuItem: menuItem,
onPressed: () => _onTap(context, ref),
);
}

View File

@@ -5,7 +5,6 @@ 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';
@@ -21,7 +20,6 @@ class MoveToLockFolderActionButton extends ConsumerWidget {
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(

View File

@@ -5,7 +5,6 @@ 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';
@@ -21,7 +20,6 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget {
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(

View File

@@ -1,12 +1,9 @@
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 {
final ActionSource source;
@@ -18,25 +15,7 @@ class ShareLinkActionButton extends ConsumerWidget {
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,
);
}
await ref.read(actionProvider.notifier).shareLink(source, context);
}
@override

View File

@@ -1,17 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
class TrashActionButton extends ConsumerWidget {
const TrashActionButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
maxWidth: 85.0,
iconData: Icons.delete_outline_rounded,
label: "control_bottom_app_bar_trash_from_immich".t(context: context),
);
}
}

View File

@@ -0,0 +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 TrashActionButton extends ConsumerWidget {
final ActionSource source;
const TrashActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result = await ref.read(actionProvider.notifier).trash(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'trash_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(
maxWidth: 85.0,
iconData: Icons.delete_outline_rounded,
label: "control_bottom_app_bar_trash_from_immich".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -1,16 +1,49 @@
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 UnarchiveActionButton extends ConsumerWidget {
const UnarchiveActionButton({super.key});
final ActionSource source;
const UnarchiveActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result = await ref.read(actionProvider.notifier).unArchive(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'unarchive_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.unarchive_outlined,
label: "unarchive".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -1,16 +1,60 @@
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 UnFavoriteActionButton extends ConsumerWidget {
const UnFavoriteActionButton({super.key});
final ActionSource source;
final bool menuItem;
const UnFavoriteActionButton({
super.key,
required this.source,
this.menuItem = false,
});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result = await ref.read(actionProvider.notifier).unFavorite(source);
if (source == ActionSource.viewer) {
return;
}
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'unfavorite_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_rounded,
label: "unfavorite".t(context: context),
onPressed: () => _onTap(context, ref),
menuItem: menuItem,
);
}
}

View File

@@ -0,0 +1,529 @@
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/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/scroll_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
import 'package:platform/platform.dart';
@RoutePage()
class AssetViewerPage extends StatelessWidget {
final int initialIndex;
final TimelineService timelineService;
const AssetViewerPage({
super.key,
required this.initialIndex,
required this.timelineService,
});
@override
Widget build(BuildContext context) {
// This is necessary to ensure that the timeline service is available
// since the Timeline and AssetViewer are on different routes / Widget subtrees.
return ProviderScope(
overrides: [timelineServiceProvider.overrideWithValue(timelineService)],
child: AssetViewer(initialIndex: initialIndex),
);
}
}
class AssetViewer extends ConsumerStatefulWidget {
final int initialIndex;
final Platform? platform;
const AssetViewer({
super.key,
required this.initialIndex,
this.platform,
});
@override
ConsumerState createState() => _AssetViewerState();
}
const double _kBottomSheetMinimumExtent = 0.4;
const double _kBottomSheetSnapExtent = 0.7;
class _AssetViewerState extends ConsumerState<AssetViewer> {
late PageController pageController;
late DraggableScrollableController bottomSheetController;
PersistentBottomSheetController? sheetCloseController;
// PhotoViewGallery takes care of disposing it's controllers
PhotoViewControllerBase? viewController;
StreamSubscription? reloadSubscription;
late Platform platform;
late PhotoViewControllerValue initialPhotoViewState;
bool? hasDraggedDown;
bool isSnapping = false;
bool blockGestures = false;
bool dragInProgress = false;
bool shouldPopOnDrag = false;
double? initialScale;
double previousExtent = _kBottomSheetMinimumExtent;
Offset dragDownPosition = Offset.zero;
int totalAssets = 0;
BuildContext? scaffoldContext;
// Delayed operations that should be cancelled on disposal
final List<Timer> _delayedOperations = [];
@override
void initState() {
super.initState();
pageController = PageController(initialPage: widget.initialIndex);
platform = widget.platform ?? const LocalPlatform();
totalAssets = ref.read(timelineServiceProvider).totalAssets;
bottomSheetController = DraggableScrollableController();
WidgetsBinding.instance.addPostFrameCallback((_) {
_onAssetChanged(widget.initialIndex);
});
reloadSubscription = EventStream.shared.listen(_onEvent);
}
@override
void dispose() {
pageController.dispose();
bottomSheetController.dispose();
_cancelTimers();
reloadSubscription?.cancel();
super.dispose();
}
bool get showingBottomSheet =>
ref.read(assetViewerProvider.select((s) => s.showingBottomSheet));
Color get backgroundColor {
final opacity =
ref.read(assetViewerProvider.select((s) => s.backgroundOpacity));
return Colors.black.withAlpha(opacity);
}
void _cancelTimers() {
for (final timer in _delayedOperations) {
timer.cancel();
}
_delayedOperations.clear();
}
// This is used to calculate the scale of the asset when the bottom sheet is showing.
// It is a small increment to ensure that the asset is slightly zoomed in when the
// bottom sheet is showing, which emulates the zoom effect.
double get _getScaleForBottomSheet =>
(viewController?.prevValue.scale ?? viewController?.value.scale ?? 1.0) +
0.01;
double _getVerticalOffsetForBottomSheet(double extent) =>
(context.height * extent) - (context.height * _kBottomSheetMinimumExtent);
Future<void> _precacheImage(int index) async {
if (!mounted || index < 0 || index >= totalAssets) {
return;
}
final asset = ref.read(timelineServiceProvider).getAsset(index);
final screenSize = Size(context.width, context.height);
// Precache both thumbnail and full image for smooth transitions
unawaited(
Future.wait([
precacheImage(
getThumbnailImageProvider(asset: asset, size: screenSize),
context,
onError: (_, __) {},
),
precacheImage(
getFullImageProvider(asset, size: screenSize),
context,
onError: (_, __) {},
),
]),
);
}
void _onAssetChanged(int index) {
final asset = ref.read(timelineServiceProvider).getAsset(index);
ref.read(currentAssetNotifier.notifier).setAsset(asset);
unawaited(ref.read(timelineServiceProvider).preCacheAssets(index));
_cancelTimers();
// This will trigger the pre-caching of adjacent assets ensuring
// that they are ready when the user navigates to them.
final timer = Timer(Durations.medium4, () {
// Check if widget is still mounted before proceeding
if (!mounted) return;
for (final offset in [-1, 1]) {
unawaited(_precacheImage(index + offset));
}
});
_delayedOperations.add(timer);
}
void _onPageBuild(PhotoViewControllerBase controller) {
viewController ??= controller;
if (showingBottomSheet) {
final verticalOffset = (context.height * bottomSheetController.size) -
(context.height * _kBottomSheetMinimumExtent);
controller.position = Offset(0, -verticalOffset);
}
}
void _onPageChanged(int index, PhotoViewControllerBase? controller) {
_onAssetChanged(index);
viewController = controller;
// If the bottom sheet is showing, we need to adjust scale the asset to
// emulate the zoom effect
if (showingBottomSheet) {
initialScale = controller?.scale;
controller?.scale = _getScaleForBottomSheet;
}
}
void _onDragStart(
_,
DragStartDetails details,
PhotoViewControllerBase controller,
PhotoViewScaleStateController scaleStateController,
) {
viewController = controller;
dragDownPosition = details.localPosition;
initialPhotoViewState = controller.value;
final isZoomed =
scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
scaleStateController.scaleState == PhotoViewScaleState.covering;
if (!showingBottomSheet && isZoomed) {
blockGestures = true;
}
}
void _onDragEnd(BuildContext ctx, _, __) {
dragInProgress = false;
if (shouldPopOnDrag) {
// Dismiss immediately without state updates to avoid rebuilds
ctx.maybePop();
return;
}
// Do not reset the state if the bottom sheet is showing
if (showingBottomSheet) {
_snapBottomSheet();
return;
}
// If the gestures are blocked, do not reset the state
if (blockGestures) {
blockGestures = false;
return;
}
shouldPopOnDrag = false;
hasDraggedDown = null;
viewController?.animateMultiple(
position: initialPhotoViewState.position,
scale: initialPhotoViewState.scale,
rotation: initialPhotoViewState.rotation,
);
ref.read(assetViewerProvider.notifier).setOpacity(255);
}
void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, _) {
if (blockGestures) {
return;
}
dragInProgress = true;
final delta = details.localPosition - dragDownPosition;
hasDraggedDown ??= delta.dy > 0;
if (!hasDraggedDown! || showingBottomSheet) {
_handleDragUp(ctx, delta);
return;
}
_handleDragDown(ctx, delta);
}
void _handleDragUp(BuildContext ctx, Offset delta) {
const double openThreshold = 50;
final position = initialPhotoViewState.position + Offset(0, delta.dy);
final distanceToOrigin = position.distance;
viewController?.updateMultiple(position: position);
// Moves the bottom sheet when the asset is being dragged up
if (showingBottomSheet && bottomSheetController.isAttached) {
final centre = (ctx.height * _kBottomSheetMinimumExtent);
bottomSheetController.jumpTo((centre + distanceToOrigin) / ctx.height);
}
if (distanceToOrigin > openThreshold && !showingBottomSheet) {
_openBottomSheet(ctx);
}
}
void _handleDragDown(BuildContext ctx, Offset delta) {
const double dragRatio = 0.2;
const double popThreshold = 75;
final distance = delta.distance;
shouldPopOnDrag = delta.dy > 0 && distance > popThreshold;
final maxScaleDistance = ctx.height * 0.5;
final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio);
double? updatedScale;
if (initialPhotoViewState.scale != null) {
updatedScale = initialPhotoViewState.scale! * (1.0 - scaleReduction);
}
final backgroundOpacity =
(255 * (1.0 - (scaleReduction / dragRatio))).round();
viewController?.updateMultiple(
position: initialPhotoViewState.position + delta,
scale: updatedScale,
);
ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity);
}
void _onTapDown(_, __, ___) {
if (!showingBottomSheet) {
ref.read(assetViewerProvider.notifier).toggleControls();
}
}
bool _onNotification(Notification delta) {
if (delta is DraggableScrollableNotification) {
_handleDraggableNotification(delta);
}
// Handle sheet snap manually so that the it snaps only at _kBottomSheetSnapExtent but not after
// the isSnapping guard is to prevent the notification from recursively handling the
// notification, eventually resulting in a heap overflow
if (!isSnapping && delta is ScrollEndNotification) {
_snapBottomSheet();
}
return false;
}
void _handleDraggableNotification(DraggableScrollableNotification delta) {
final currentExtent = delta.extent;
final isDraggingDown = currentExtent < previousExtent;
previousExtent = currentExtent;
// Closes the bottom sheet if the user is dragging down
if (isDraggingDown && delta.extent < 0.5) {
if (dragInProgress) {
blockGestures = true;
}
sheetCloseController?.close();
}
// If the asset is being dragged down, we do not want to update the asset position again
if (dragInProgress) {
return;
}
final verticalOffset = _getVerticalOffsetForBottomSheet(delta.extent);
// Moves the asset when the bottom sheet is being dragged
if (verticalOffset > 0) {
viewController?.position = Offset(0, -verticalOffset);
}
}
void _onEvent(Event event) {
if (event is TimelineReloadEvent) {
_onTimelineReload(event);
return;
}
if (event is ViewerOpenBottomSheetEvent) {
final extent = _kBottomSheetMinimumExtent + 0.3;
_openBottomSheet(scaffoldContext!, extent: extent);
final offset = _getVerticalOffsetForBottomSheet(extent);
viewController?.position = Offset(0, -offset);
return;
}
}
void _onTimelineReload(_) {
setState(() {
totalAssets = ref.read(timelineServiceProvider).totalAssets;
if (totalAssets == 0) {
context.maybePop();
return;
}
final index = pageController.page?.round() ?? 0;
final newAsset = ref.read(timelineServiceProvider).getAsset(index);
final currentAsset = ref.read(currentAssetNotifier);
// Do not reload / close the bottom sheet if the asset has not changed
if (newAsset.heroTag == currentAsset?.heroTag) {
return;
}
_onAssetChanged(pageController.page!.round());
sheetCloseController?.close();
});
}
void _openBottomSheet(
BuildContext ctx, {
double extent = _kBottomSheetMinimumExtent,
}) {
ref.read(assetViewerProvider.notifier).setBottomSheet(true);
initialScale = viewController?.scale;
viewController?.updateMultiple(scale: _getScaleForBottomSheet);
previousExtent = _kBottomSheetMinimumExtent;
sheetCloseController = showBottomSheet(
context: ctx,
sheetAnimationStyle: AnimationStyle(
duration: Durations.short4,
reverseDuration: Durations.short2,
),
constraints: const BoxConstraints(maxWidth: double.infinity),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
),
backgroundColor: ctx.colorScheme.surfaceContainerLowest,
builder: (_) {
return NotificationListener<Notification>(
onNotification: _onNotification,
child: AssetDetailBottomSheet(
controller: bottomSheetController,
initialChildSize: extent,
),
);
},
);
sheetCloseController?.closed.then((_) => _handleSheetClose());
}
void _handleSheetClose() {
viewController?.animateMultiple(position: Offset.zero);
viewController?.updateMultiple(scale: initialScale);
ref.read(assetViewerProvider.notifier).setBottomSheet(false);
sheetCloseController = null;
shouldPopOnDrag = false;
hasDraggedDown = null;
}
void _snapBottomSheet() {
if (bottomSheetController.size > _kBottomSheetSnapExtent ||
bottomSheetController.size < 0.4) {
return;
}
isSnapping = true;
bottomSheetController.animateTo(
_kBottomSheetSnapExtent,
duration: Durations.short3,
curve: Curves.easeOut,
);
}
Widget _placeholderBuilder(
BuildContext ctx,
ImageChunkEvent? progress,
int index,
) {
final asset = ref.read(timelineServiceProvider).getAsset(index);
return Container(
width: double.infinity,
height: double.infinity,
color: backgroundColor,
child: Thumbnail(
asset: asset,
fit: BoxFit.contain,
size: Size(
ctx.width,
ctx.height,
),
),
);
}
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
scaffoldContext ??= ctx;
final asset = ref.read(timelineServiceProvider).getAsset(index);
final size = Size(ctx.width, ctx.height);
return PhotoViewGalleryPageOptions(
key: ValueKey(asset.heroTag),
imageProvider: getFullImageProvider(asset, size: size),
heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag),
filterQuality: FilterQuality.high,
tightMode: true,
initialScale: PhotoViewComputedScale.contained * 0.999,
minScale: PhotoViewComputedScale.contained * 0.999,
disableScaleGestures: showingBottomSheet,
onDragStart: _onDragStart,
onDragUpdate: _onDragUpdate,
onDragEnd: _onDragEnd,
onTapDown: _onTapDown,
errorBuilder: (_, __, ___) => Container(
width: ctx.width,
height: ctx.height,
color: backgroundColor,
child: Thumbnail(
asset: asset,
fit: BoxFit.contain,
size: size,
),
),
);
}
void _onPop<T>(bool didPop, T? result) {
ref.read(currentAssetNotifier.notifier).dispose();
}
@override
Widget build(BuildContext context) {
// Rebuild the widget when the asset viewer state changes
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
// Issue: https://github.com/flutter/flutter/issues/109037
// TODO: Add a custom scrum builder once the fix lands on stable
return PopScope(
onPopInvokedWithResult: _onPop,
child: Scaffold(
backgroundColor: backgroundColor,
appBar: const ViewerTopAppBar(),
extendBody: true,
extendBodyBehindAppBar: true,
body: PhotoViewGallery.builder(
gaplessPlayback: true,
loadingBuilder: _placeholderBuilder,
pageController: pageController,
scrollPhysics: platform.isIOS
? const FastScrollPhysics() // Use bouncing physics for iOS
: const FastClampingScrollPhysics() // Use heavy physics for Android
,
itemCount: totalAssets,
onPageChanged: _onPageChanged,
onPageBuild: _onPageBuild,
builder: _assetBuilder,
backgroundDecoration: BoxDecoration(color: backgroundColor),
enablePanAlways: true,
),
bottomNavigationBar: const ViewerBottomBar(),
),
);
}
}

View File

@@ -0,0 +1,76 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
class AssetViewerState {
final int backgroundOpacity;
final bool showingBottomSheet;
final bool showingControls;
const AssetViewerState({
this.backgroundOpacity = 255,
this.showingBottomSheet = false,
this.showingControls = true,
});
AssetViewerState copyWith({
int? backgroundOpacity,
bool? showingBottomSheet,
bool? showingControls,
}) {
return AssetViewerState(
backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity,
showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet,
showingControls: showingControls ?? this.showingControls,
);
}
@override
String toString() {
return 'AssetViewerState(opacity: $backgroundOpacity, bottomSheet: $showingBottomSheet, controls: $showingControls)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is AssetViewerState &&
other.backgroundOpacity == backgroundOpacity &&
other.showingBottomSheet == showingBottomSheet &&
other.showingControls == showingControls;
}
@override
int get hashCode =>
backgroundOpacity.hashCode ^
showingBottomSheet.hashCode ^
showingControls.hashCode;
}
class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
@override
AssetViewerState build() {
return const AssetViewerState();
}
void setOpacity(int opacity) {
state = state.copyWith(
backgroundOpacity: opacity,
showingControls: opacity == 255 ? true : state.showingControls,
);
}
void setBottomSheet(bool showing) {
state = state.copyWith(
showingBottomSheet: showing,
showingControls: showing ? true : state.showingControls,
);
}
void toggleControls() {
state = state.copyWith(showingControls: !state.showingControls);
}
}
final assetViewerProvider =
AutoDisposeNotifierProvider<AssetViewerStateNotifier, AssetViewerState>(
AssetViewerStateNotifier.new,
);

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
class ViewerBottomBar extends ConsumerWidget {
const ViewerBottomBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) {
return const SizedBox.shrink();
}
final user = ref.watch(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
final isSheetOpen = ref.watch(
assetViewerProvider.select((s) => s.showingBottomSheet),
);
int opacity = ref.watch(
assetViewerProvider.select((state) => state.backgroundOpacity),
);
final showControls =
ref.watch(assetViewerProvider.select((s) => s.showingControls));
if (!showControls) {
opacity = 0;
}
final actions = <Widget>[
const ShareActionButton(),
const _EditActionButton(),
if (asset.hasRemote && isOwner)
const ArchiveActionButton(source: ActionSource.viewer),
];
return IgnorePointer(
ignoring: opacity < 255,
child: AnimatedOpacity(
opacity: opacity / 255,
duration: Durations.short2,
child: AnimatedSwitcher(
duration: Durations.short4,
child: isSheetOpen
? const SizedBox.shrink()
: SafeArea(
child: Theme(
data: context.themeData.copyWith(
iconTheme:
const IconThemeData(size: 22, color: Colors.white),
textTheme: context.themeData.textTheme.copyWith(
labelLarge:
context.themeData.textTheme.labelLarge?.copyWith(
color: Colors.white,
),
),
),
child: Container(
height: 80,
color: Colors.black.withAlpha(125),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: actions,
),
),
),
),
),
),
);
}
}
class _EditActionButton extends ConsumerWidget {
const _EditActionButton();
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.tune_outlined,
label: 'edit'.t(context: context),
);
}
}

View File

@@ -0,0 +1,253 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.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';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
const _kSeparator = '';
class AssetDetailBottomSheet extends ConsumerWidget {
final DraggableScrollableController? controller;
final double initialChildSize;
const AssetDetailBottomSheet({
this.controller,
this.initialChildSize = 0.35,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) {
return const SizedBox.shrink();
}
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final actions = <Widget>[
const ShareActionButton(),
if (asset.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.viewer),
const ArchiveActionButton(source: ActionSource.viewer),
if (!asset.hasLocal) const DownloadActionButton(),
isTrashEnable
? const TrashActionButton(source: ActionSource.viewer)
: const DeletePermanentActionButton(source: ActionSource.viewer),
const MoveToLockFolderActionButton(
source: ActionSource.viewer,
),
],
if (asset.storage == AssetState.local) ...[
const DeleteLocalActionButton(),
const UploadActionButton(),
],
];
return BaseBottomSheet(
actions: actions,
slivers: const [_AssetDetailBottomSheet()],
controller: controller,
initialChildSize: initialChildSize,
minChildSize: 0.1,
maxChildSize: 0.88,
expand: false,
shouldCloseOnMinExtent: false,
resizeOnScroll: false,
);
}
}
class _AssetDetailBottomSheet extends ConsumerWidget {
const _AssetDetailBottomSheet();
String _getDateTime(BuildContext ctx, BaseAsset asset) {
final dateTime = asset.createdAt.toLocal();
final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime);
final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime);
return '$date$_kSeparator$time';
}
String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) {
final height = asset.height ?? exifInfo?.height;
final width = asset.width ?? exifInfo?.width;
final resolution =
(width != null && height != null) ? "$width x $height" : null;
final fileSize =
exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null;
return switch ((fileSize, resolution)) {
(null, null) => '',
(String fileSize, null) => fileSize,
(null, String resolution) => resolution,
(String fileSize, String resolution) =>
'$fileSize$_kSeparator$resolution',
};
}
String? _getCameraInfoTitle(ExifInfo? exifInfo) {
if (exifInfo == null) {
return null;
}
return switch ((exifInfo.make, exifInfo.model)) {
(null, null) => null,
(String make, null) => make,
(null, String model) => model,
(String make, String model) => '$make $model',
};
}
String? _getCameraInfoSubtitle(ExifInfo? exifInfo) {
if (exifInfo == null) {
return null;
}
final fNumber =
exifInfo.fNumber.isNotEmpty ? 'ƒ/${exifInfo.fNumber}' : null;
final exposureTime =
exifInfo.exposureTime.isNotEmpty ? exifInfo.exposureTime : null;
final focalLength =
exifInfo.focalLength.isNotEmpty ? '${exifInfo.focalLength} mm' : null;
final iso = exifInfo.iso != null ? 'ISO ${exifInfo.iso}' : null;
return [fNumber, exposureTime, focalLength, iso]
.where((spec) => spec != null && spec.isNotEmpty)
.join(_kSeparator);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
final cameraTitle = _getCameraInfoTitle(exifInfo);
return SliverList.list(
children: [
// Asset Date and Time
_SheetTile(
title: _getDateTime(context, asset),
titleStyle: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 16,
),
),
const SheetLocationDetails(),
// Details header
_SheetTile(
title: 'exif_bottom_sheet_details'.t(context: context),
titleStyle: context.textTheme.labelLarge,
),
// File info
_SheetTile(
title: asset.name,
titleStyle: context.textTheme.labelLarge
?.copyWith(fontWeight: FontWeight.w600),
leading: Icon(
asset.isImage ? Icons.image_outlined : Icons.videocam_outlined,
size: 30,
color: context.textTheme.labelLarge?.color,
),
subtitle: _getFileInfo(asset, exifInfo),
subtitleStyle: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelLarge?.color?.withAlpha(200),
),
),
// Camera info
if (cameraTitle != null)
_SheetTile(
title: cameraTitle,
titleStyle: context.textTheme.labelLarge
?.copyWith(fontWeight: FontWeight.w600),
leading: Icon(
Icons.camera_outlined,
size: 30,
color: context.textTheme.labelLarge?.color,
),
subtitle: _getCameraInfoSubtitle(exifInfo),
subtitleStyle: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelLarge?.color?.withAlpha(200),
),
),
],
);
}
}
class _SheetTile extends StatelessWidget {
final String title;
final Widget? leading;
final String? subtitle;
final TextStyle? titleStyle;
final TextStyle? subtitleStyle;
const _SheetTile({
required this.title,
this.titleStyle,
this.leading,
this.subtitle,
this.subtitleStyle,
});
@override
Widget build(BuildContext context) {
final Widget titleWidget;
if (leading == null) {
titleWidget = LimitedBox(
maxWidth: double.infinity,
child: Text(title, style: titleStyle),
);
} else {
titleWidget = Container(
width: double.infinity,
padding: const EdgeInsets.only(left: 15),
child: Text(title, style: titleStyle),
);
}
final Widget? subtitleWidget;
if (leading == null && subtitle != null) {
subtitleWidget = Text(subtitle!, style: subtitleStyle);
} else if (leading != null && subtitle != null) {
subtitleWidget = Padding(
padding: const EdgeInsets.only(left: 15),
child: Text(subtitle!, style: subtitleStyle),
);
} else {
subtitleWidget = null;
}
return ListTile(
dense: true,
visualDensity: VisualDensity.compact,
title: titleWidget,
titleAlignment: ListTileTitleAlignment.center,
leading: leading,
contentPadding: leading == null ? null : const EdgeInsets.only(left: 25),
subtitle: subtitleWidget,
);
}
}

View File

@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class SheetLocationDetails extends ConsumerStatefulWidget {
const SheetLocationDetails({super.key});
@override
ConsumerState createState() => _SheetLocationDetailsState();
}
class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
BaseAsset? asset;
ExifInfo? exifInfo;
MapLibreMapController? _mapController;
String? _getLocationName(ExifInfo? exifInfo) {
if (exifInfo == null) {
return null;
}
final cityName = exifInfo.city;
final stateName = exifInfo.state;
if (cityName != null && stateName != null) {
return "$cityName, $stateName";
}
return null;
}
void _onMapCreated(MapLibreMapController controller) {
_mapController = controller;
}
void _onExifChanged(
AsyncValue<ExifInfo?>? previous,
AsyncValue<ExifInfo?> current,
) {
asset = ref.read(currentAssetNotifier);
setState(() {
exifInfo = current.valueOrNull;
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
if (exifInfo != null && hasCoordinates) {
_mapController?.moveCamera(
CameraUpdate.newLatLng(
LatLng(exifInfo!.latitude!, exifInfo!.longitude!),
),
);
}
});
}
@override
void initState() {
super.initState();
ref.listenManual(
currentAssetExifProvider,
_onExifChanged,
fireImmediately: true,
);
}
@override
Widget build(BuildContext context) {
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
// Guard no lat/lng
if (!hasCoordinates ||
(asset is LocalAsset && !(asset as LocalAsset).hasRemote)) {
return const SizedBox.shrink();
}
final remoteId = asset is LocalAsset
? (asset as LocalAsset).remoteId
: (asset as RemoteAsset).id;
final locationName = _getLocationName(exifInfo);
final coordinates =
"${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}";
return Padding(
padding: EdgeInsets.symmetric(
vertical: 16.0,
horizontal: context.isMobile ? 16.0 : 56.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
"exif_bottom_sheet_location".t(context: context),
style: context.textTheme.labelLarge,
),
),
ExifMap(
exifInfo: exifInfo!,
markerId: remoteId,
onMapCreated: _onMapCreated,
),
const SizedBox(height: 15),
if (locationName != null)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
locationName,
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
Text(
coordinates,
style: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelLarge?.color?.withAlpha(150),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,114 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
const ViewerTopAppBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) {
return const SizedBox.shrink();
}
final user = ref.watch(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
final isShowingSheet = ref
.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
int opacity = ref.watch(
assetViewerProvider.select((state) => state.backgroundOpacity),
);
final showControls =
ref.watch(assetViewerProvider.select((s) => s.showingControls));
if (!showControls) {
opacity = 0;
}
final actions = <Widget>[
if (asset.hasRemote && isOwner && !asset.isFavorite)
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
if (asset.hasRemote && isOwner && asset.isFavorite)
const UnFavoriteActionButton(
source: ActionSource.viewer,
menuItem: true,
),
const _KebabMenu(),
];
return IgnorePointer(
ignoring: opacity < 255,
child: AnimatedOpacity(
opacity: opacity / 255,
duration: Durations.short2,
child: AppBar(
backgroundColor:
isShowingSheet ? Colors.transparent : Colors.black.withAlpha(125),
leading: const _AppBarBackButton(),
iconTheme: const IconThemeData(size: 22, color: Colors.white),
actionsIconTheme: const IconThemeData(size: 22, color: Colors.white),
shape: const Border(),
actions: isShowingSheet ? null : actions,
),
),
);
}
@override
Size get preferredSize => const Size.fromHeight(60.0);
}
class _KebabMenu extends ConsumerWidget {
const _KebabMenu();
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
onPressed: () {
EventStream.shared.emit(const ViewerOpenBottomSheetEvent());
},
icon: const Icon(Icons.more_vert_rounded),
);
}
}
class _AppBarBackButton extends ConsumerWidget {
const _AppBarBackButton();
@override
Widget build(BuildContext context, WidgetRef ref) {
final isShowingSheet = ref
.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
final backgroundColor =
isShowingSheet && !context.isDarkTheme ? Colors.white : Colors.black;
final foregroundColor =
isShowingSheet && !context.isDarkTheme ? Colors.black : Colors.white;
return Padding(
padding: const EdgeInsets.only(left: 12.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
shape: const CircleBorder(),
iconSize: 22,
iconColor: foregroundColor,
padding: EdgeInsets.zero,
elevation: isShowingSheet ? 4 : 0,
),
onPressed: context.maybePop,
child: const Icon(Icons.arrow_back_rounded),
),
);
}
}

View File

@@ -1,6 +1,7 @@
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/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
class BaseBottomSheet extends ConsumerStatefulWidget {
@@ -74,10 +75,7 @@ class _BaseDraggableScrollableSheetState
clipBehavior: Clip.antiAlias,
elevation: 6.0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(6),
topRight: Radius.circular(6),
),
borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
),
margin: const EdgeInsets.symmetric(horizontal: 0),
child: CustomScrollView(
@@ -86,17 +84,22 @@ class _BaseDraggableScrollableSheetState
SliverToBoxAdapter(
child: Column(
children: [
const SizedBox(height: 16),
const SizedBox(height: 10),
const _DragHandle(),
const SizedBox(height: 16),
SizedBox(
height: 120,
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: widget.actions,
const SizedBox(height: 14),
if (widget.actions.isNotEmpty)
SizedBox(
height: 115,
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: widget.actions,
),
),
),
if (widget.actions.isNotEmpty) ...[
const Divider(indent: 16, endIndent: 16),
const SizedBox(height: 16),
],
],
),
),
@@ -118,7 +121,7 @@ class _DragHandle extends StatelessWidget {
height: 6,
width: 32,
decoration: BoxDecoration(
color: context.themeData.dividerColor,
color: context.themeData.dividerColor.lighten(amount: 0.6),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
);

View File

@@ -12,7 +12,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_buton.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -39,8 +39,10 @@ class HomeBottomAppBar extends ConsumerWidget {
const FavoriteActionButton(source: ActionSource.timeline),
const DownloadActionButton(),
isTrashEnable
? const TrashActionButton()
: const DeletePermanentActionButton(),
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(
source: ActionSource.timeline,
),
const EditDateTimeActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
import 'package:octo_image/octo_image.dart';
class FullImage extends StatelessWidget {
const FullImage(
this.asset, {
required this.size,
this.fit = BoxFit.cover,
this.placeholder = const ThumbnailPlaceholder(),
super.key,
});
final BaseAsset asset;
final Size size;
final Widget? placeholder;
final BoxFit fit;
@override
Widget build(BuildContext context) {
final provider = getFullImageProvider(asset, size: size);
return OctoImage(
fadeInDuration: const Duration(milliseconds: 0),
fadeOutDuration: const Duration(milliseconds: 100),
placeholderBuilder: placeholder != null ? (_) => placeholder! : null,
image: provider,
width: size.width,
height: size.height,
fit: fit,
errorBuilder: (context, error, stackTrace) {
provider.evict();
return const Icon(Icons.image_not_supported_outlined, size: 32);
},
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
ImageProvider getFullImageProvider(
BaseAsset asset, {
Size size = const Size(1080, 1920),
}) {
// Create new provider and cache it
final ImageProvider provider;
if (_shouldUseLocalAsset(asset)) {
provider = LocalFullImageProvider(asset: asset as LocalAsset, size: size);
} else {
final String assetId;
if (asset is LocalAsset && asset.hasRemote) {
assetId = asset.remoteId!;
} else if (asset is RemoteAsset) {
assetId = asset.id;
} else {
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
}
provider = RemoteFullImageProvider(assetId: assetId);
}
return provider;
}
ImageProvider getThumbnailImageProvider({
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);
}
if (_shouldUseLocalAsset(asset!)) {
return LocalThumbProvider(asset: asset as LocalAsset, size: size);
}
final String assetId;
if (asset is LocalAsset && asset.hasRemote) {
assetId = asset.remoteId!;
} else if (asset is RemoteAsset) {
assetId = asset.id;
} else {
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
}
return RemoteThumbProvider(assetId: assetId);
}
bool _shouldUseLocalAsset(BaseAsset asset) =>
asset is LocalAsset &&
(!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));

View File

@@ -0,0 +1,241 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
import 'package:logging/logging.dart';
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
final AssetMediaRepository _assetMediaRepository =
const AssetMediaRepository();
final CacheManager? cacheManager;
final LocalAsset asset;
final Size size;
const LocalThumbProvider({
required this.asset,
this.size = const Size.square(kTimelineFixedTileExtent),
this.cacheManager,
});
@override
Future<LocalThumbProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
LocalThumbProvider key,
ImageDecoderCallback decode,
) {
final cache = cacheManager ?? ThumbnailImageCacheManager();
return MultiFrameImageStreamCompleter(
codec: _codec(key, cache, decode),
scale: 1.0,
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<LocalAsset>('Asset', key.asset),
],
);
}
Future<Codec> _codec(
LocalThumbProvider key,
CacheManager cache,
ImageDecoderCallback decode,
) async {
final cacheKey =
'${key.asset.id}-${key.asset.updatedAt}-${key.size.width}x${key.size.height}';
final fileFromCache = await cache.getFileFromCache(cacheKey);
if (fileFromCache != null) {
try {
final buffer =
await ImmutableBuffer.fromFilePath(fileFromCache.file.path);
return decode(buffer);
} catch (_) {}
}
final thumbnailBytes =
await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size);
if (thumbnailBytes == null) {
PaintingBinding.instance.imageCache.evict(key);
throw StateError(
"Loading thumb for local photo ${key.asset.name} failed",
);
}
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
unawaited(cache.putFile(cacheKey, thumbnailBytes));
return decode(buffer);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is LocalThumbProvider) {
return asset.id == other.asset.id &&
asset.updatedAt == other.asset.updatedAt;
}
return false;
}
@override
int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode;
}
class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
final AssetMediaRepository _assetMediaRepository =
const AssetMediaRepository();
final StorageRepository _storageRepository = const StorageRepository();
final LocalAsset asset;
final Size size;
const LocalFullImageProvider({
required this.asset,
required this.size,
});
@override
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) {
return MultiImageStreamCompleter(
codec: _codec(key, decode),
scale: 1.0,
informationCollector: () sync* {
yield ErrorDescription(asset.name);
},
);
}
// Streams in each stage of the image as we ask for it
Stream<Codec> _codec(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) async* {
try {
switch (key.asset.type) {
case AssetType.image:
yield* _decodeProgressive(key, decode);
break;
case AssetType.video:
final codec = await _getThumbnailCodec(key, decode);
if (codec == null) {
throw StateError("Failed to load preview for ${key.asset.name}");
}
yield codec;
break;
case AssetType.other:
case AssetType.audio:
throw StateError('Unsupported asset type ${key.asset.type}');
}
} catch (error, stack) {
Logger('ImmichLocalImageProvider')
.severe('Error loading local image ${key.asset.name}', error, stack);
throw const ImageLoadingException(
'Could not load image from local storage',
);
}
}
Future<Codec?> _getThumbnailCodec(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) async {
final thumbBytes =
await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size);
if (thumbBytes == null) {
return null;
}
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
return decode(buffer);
}
Stream<Codec> _decodeProgressive(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) async* {
final file = await _storageRepository.getFileForAsset(key.asset);
if (file == null) {
throw StateError("Opening file for asset ${key.asset.name} failed");
}
final fileSize = await file.length();
final devicePixelRatio =
PlatformDispatcher.instance.views.first.devicePixelRatio;
final isLargeFile = fileSize > 20 * 1024 * 1024; // 20MB
final isHEIC = file.path.toLowerCase().contains(RegExp(r'\.(heic|heif)$'));
final isProgressive = isLargeFile || (isHEIC && !Platform.isIOS);
if (isProgressive) {
try {
final progressiveMultiplier = devicePixelRatio >= 3.0 ? 1.3 : 1.2;
final size = Size(
(key.size.width * progressiveMultiplier).clamp(256, 1024),
(key.size.height * progressiveMultiplier).clamp(256, 1024),
);
final mediumThumb =
await _assetMediaRepository.getThumbnail(key.asset.id, size: size);
if (mediumThumb != null) {
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
yield await decode(mediumBuffer);
}
} catch (_) {}
}
// Load original only when the file is smaller or if the user wants to load original images
// Or load a slightly larger image for progressive loading
if (isProgressive && !(AppSetting.get(Setting.loadOriginal))) {
final progressiveMultiplier = devicePixelRatio >= 3.0 ? 2.0 : 1.6;
final size = Size(
(key.size.width * progressiveMultiplier).clamp(512, 2048),
(key.size.height * progressiveMultiplier).clamp(512, 2048),
);
final highThumb =
await _assetMediaRepository.getThumbnail(key.asset.id, size: size);
if (highThumb != null) {
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
yield await decode(highBuffer);
}
return;
}
final buffer = await ImmutableBuffer.fromFilePath(file.path);
yield await decode(buffer);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is LocalFullImageProvider) {
return asset.id == other.asset.id &&
asset.updatedAt == other.asset.updatedAt &&
size == other.size;
}
return false;
}
@override
int get hashCode =>
asset.id.hashCode ^ asset.updatedAt.hashCode ^ size.hashCode;
}

View File

@@ -1,95 +0,0 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
final AssetMediaRepository _assetMediaRepository =
const AssetMediaRepository();
final CacheManager? cacheManager;
final LocalAsset asset;
final double height;
final double width;
LocalThumbProvider({
required this.asset,
this.height = kTimelineFixedTileExtent,
this.width = kTimelineFixedTileExtent,
this.cacheManager,
});
@override
Future<LocalThumbProvider> obtainKey(
ImageConfiguration configuration,
) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
LocalThumbProvider key,
ImageDecoderCallback decode,
) {
final cache = cacheManager ?? ThumbnailImageCacheManager();
return MultiFrameImageStreamCompleter(
codec: _codec(key, cache, decode),
scale: 1.0,
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<LocalAsset>('Asset', key.asset),
],
);
}
Future<Codec> _codec(
LocalThumbProvider key,
CacheManager cache,
ImageDecoderCallback decode,
) async {
final cacheKey = '${key.asset.id}-${key.asset.updatedAt}-${width}x$height';
final fileFromCache = await cache.getFileFromCache(cacheKey);
if (fileFromCache != null) {
try {
final buffer =
await ImmutableBuffer.fromFilePath(fileFromCache.file.path);
return await decode(buffer);
} catch (_) {}
}
final thumbnailBytes = await _assetMediaRepository.getThumbnail(
key.asset.id,
size: Size(key.width, key.height),
);
if (thumbnailBytes == null) {
PaintingBinding.instance.imageCache.evict(key);
throw StateError(
"Loading thumb for local photo ${key.asset.name} failed",
);
}
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
unawaited(cache.putFile(cacheKey, thumbnailBytes));
return decode(buffer);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is LocalThumbProvider) {
return asset.id == other.asset.id &&
asset.updatedAt == other.asset.updatedAt;
}
return false;
}
@override
int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode;
}

View File

@@ -0,0 +1,142 @@
import 'dart:async';
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
final String assetId;
final CacheManager? cacheManager;
const RemoteThumbProvider({
required this.assetId,
this.cacheManager,
});
@override
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
RemoteThumbProvider key,
ImageDecoderCallback decode,
) {
final cache = cacheManager ?? RemoteImageCacheManager();
final chunkController = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _codec(key, cache, decode, chunkController),
scale: 1.0,
chunkEvents: chunkController.stream,
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<String>('Asset Id', key.assetId),
],
);
}
Future<Codec> _codec(
RemoteThumbProvider key,
CacheManager cache,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkController,
) async {
final preview = getThumbnailUrlForRemoteId(
key.assetId,
);
return ImageLoader.loadImageFromCache(
preview,
cache: cache,
decode: decode,
chunkEvents: chunkController,
).whenComplete(chunkController.close);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is RemoteThumbProvider) {
return assetId == other.assetId;
}
return false;
}
@override
int get hashCode => assetId.hashCode;
}
class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
final String assetId;
final CacheManager? cacheManager;
const RemoteFullImageProvider({
required this.assetId,
this.cacheManager,
});
@override
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
RemoteFullImageProvider key,
ImageDecoderCallback decode,
) {
final cache = cacheManager ?? RemoteImageCacheManager();
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(
codec: _codec(key, cache, decode, chunkEvents),
scale: 1.0,
chunkEvents: chunkEvents.stream,
);
}
Stream<Codec> _codec(
RemoteFullImageProvider key,
CacheManager cache,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkController,
) async* {
yield await ImageLoader.loadImageFromCache(
getPreviewUrlForRemoteId(key.assetId),
cache: cache,
decode: decode,
chunkEvents: chunkController,
);
if (AppSetting.get(Setting.loadOriginal)) {
yield await ImageLoader.loadImageFromCache(
getOriginalUrlForRemoteId(key.assetId),
cache: cache,
decode: decode,
chunkEvents: chunkController,
);
}
await chunkController.close();
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is RemoteFullImageProvider) {
return assetId == other.assetId;
}
return false;
}
@override
int get hashCode => assetId.hashCode;
}

View File

@@ -1,80 +0,0 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
final String assetId;
final double height;
final double width;
final CacheManager? cacheManager;
const RemoteThumbProvider({
required this.assetId,
this.height = kTimelineFixedTileExtent,
this.width = kTimelineFixedTileExtent,
this.cacheManager,
});
@override
Future<RemoteThumbProvider> obtainKey(
ImageConfiguration configuration,
) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
RemoteThumbProvider key,
ImageDecoderCallback decode,
) {
final cache = cacheManager ?? ThumbnailImageCacheManager();
final chunkController = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _codec(key, cache, decode, chunkController),
scale: 1.0,
chunkEvents: chunkController.stream,
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<String>('Asset Id', key.assetId),
],
);
}
Future<Codec> _codec(
RemoteThumbProvider key,
CacheManager cache,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkController,
) async {
final preview = getThumbnailUrlForRemoteId(
key.assetId,
);
return ImageLoader.loadImageFromCache(
preview,
cache: cache,
decode: decode,
chunkEvents: chunkController,
).whenComplete(chunkController.close);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is RemoteThumbProvider) {
return assetId == other.assetId;
}
return false;
}
@override
int get hashCode => assetId.hashCode;
}

View File

@@ -1,50 +0,0 @@
import 'dart:convert' hide Codec;
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:thumbhash/thumbhash.dart';
class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
final String thumbHash;
const ThumbHashProvider({
required this.thumbHash,
});
@override
Future<ThumbHashProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
ThumbHashProvider key,
ImageDecoderCallback decode,
) {
return MultiFrameImageStreamCompleter(
codec: _loadCodec(key, decode),
scale: 1.0,
);
}
Future<Codec> _loadCodec(
ThumbHashProvider key,
ImageDecoderCallback decode,
) async {
final image = thumbHashToRGBA(base64Decode(key.thumbHash));
return decode(await ImmutableBuffer.fromUint8List(rgbaToBmp(image)));
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is ThumbHashProvider) {
return thumbHash == other.thumbHash;
}
return false;
}
@override
int get hashCode => thumbHash.hashCode;
}

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