Compare commits

..

63 Commits

Author SHA1 Message Date
Min Idzelis
65f7b3a86d feat: webdav 2025-06-07 04:18:50 +00:00
shenlong
ce6631f7e0 feat(mobile): hash assets in isolate (#18924) 2025-06-06 11:23:05 +05:30
Dag Stuan
b46e066cc2 feat(web): add a user setting for default album sort order. (#18950)
* Add a user setting for default album sort order.

Add a user setting under "Features" to control the initial sort order
when creating an album. Default to the existing behavior of
"newest first".

* chore: patch openapi

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-05 23:31:34 -05:00
Min Idzelis
55f4e93456 fix: regression: sort day by fileCreatedAt again (#18732)
* fix: regression: sort day by fileCreatedAt again

* lint

* e2e test

* inline function

* e2e

* Address comments. Drop dayGroup and timezone in favor of localOffsetMinutes

* lint and some api-doc

* lint, more api-doc

* format

* Move minutes to fractional hours

* make sql

* merge/conflict

* merge fallout, review comments

* spelling

* drop offset from returned date

* move description into decorator where possible, regen api
2025-06-05 20:56:32 -05:00
shenlong
81423420c8 chore(mobile): patch isOnboarded (#18949)
fix: patch isOnboarded

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-05 17:23:45 +00:00
renovate[bot]
a9bd651692 chore(deps): update docker.io/valkey/valkey:8-bookworm docker digest to a19bebe (#18879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 10:49:30 +01:00
renovate[bot]
afda7b9525 chore(deps): update ghcr.io/immich-app/postgres:14-vectorchord0.3.0 docker digest to 9c704fb (#18883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 10:48:14 +01:00
Brandon Wees
86f64fd0bf fix(server): default current users to an onboarded state migration (#18929)
* on database migration, assume every user is onboarded

* dont overwrite key if conflict in migration
2025-06-04 21:33:23 -05:00
Min Idzelis
19013af58f fix: wait for db to start before server for e2e test (#18936)
* fix: wait for db to start before server for e2e test

* empty - trigger checks
2025-06-04 21:32:29 -05:00
Daniel Dietzler
e746d27f5e chore: more cursed knowledge (#18932) 2025-06-04 21:31:53 -05:00
Min Idzelis
90c8fdba96 fix: thumbnail fade in (#18935) 2025-06-04 21:29:58 -05:00
Min Idzelis
e2ffc9d5a1 refactor: asset-store (#18938)
* refactor: asset-store

* Potential fix for code scanning alert no. 152: Prototype-polluting function

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-06-05 02:27:54 +00:00
Alex
f64a3003af chore: album's header styling (#18930) 2025-06-04 21:09:53 -05:00
Robin Brisa
a26d703335 feat(web): display number of likes in asset viewer (#18911)
* feat: display number of likes

* fix: properly decrement like count on unlike

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>

* chore: pr feedback

* chore: updated related test

* chore: formatter run

* chore: force numberOfLikes to null in album context to pass lint

* chore: open-api updated

* fix: use undefined, not null

* styling tweaks

* chore: updated sql

---------

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-06-04 12:41:50 -05:00
JobiJoba
5d0ad853f4 feat(mobile): add album description functionality (#18886)
* feat(mobile): add album description functionality

- Introduced a new optional `description` field in the `Album` entity.
- Updated `AlbumViewerPageState` to manage `editDescriptionText`.
- Created `AlbumDescription` and `AlbumViewerEditableDescription` widgets for displaying and editing album descriptions.
- Enhanced `CreateAlbumPage` to include a description input field.
- Implemented backend support for updating album descriptions in `AlbumApiRepository` and `AlbumService`.
- Updated sync logic to handle album descriptions during data synchronization.
- Adjusted UI components to accommodate the new description feature.

* fix dart analysis error

* remove comment that shouldn't be there

* Album header styling

* fix: disable edit after album creation

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-04 17:41:28 +00:00
xCJPECKOVERx
19ff39c2b9 feat(web): undo delete (#18729)
* feat(web): Undo asset delete

* - lints and checks
- Update English translation

* Update delete-assets.svelte

Make onUndoDelete optional in Props interface

* - Ensure undo button not available on permanent delete, or trash disabled.
- Enforce lint requirement for no-negated-condition

* Fix formatting

* fix: lint

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-06-04 15:46:07 +00:00
JobiJoba
8733d1e554 feat(mobile): add bulk download functionality (#18878)
* feat(mobile): add bulk download functionality and update UI messages

- Added `downloadAll` method to `IDownloadRepository` and its implementation in `DownloadRepository` to handle multiple asset downloads.
- Implemented `downloadAllAsset` in `DownloadStateNotifier` to trigger bulk downloads.
- Updated `DownloadService` to create download tasks for all selected assets.
- Enhanced UI with new download success and failure messages in `en.json`.
- Added download button to `ControlBottomAppBar` and integrated download functionality in `MultiselectGrid`.

* translations use i18n method t()

* Update mobile/lib/services/download.service.dart

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>

* fix(mobile): update download logic in DownloadService

- Changed the download method to utilize downloadAll for handling multiple tasks.
- Simplified remoteId check by removing unnecessary condition.

* sort i18n keys

* remove the download signature from interface and logic as we use the downloadAll now

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
2025-06-04 09:49:43 -05:00
JobiJoba
1fb8861e35 fix(mobile): prevent upload intent replacement in splash screen and reset upload button when minimize app (#18914)
fix(mobile): prevent upload intent replacement in splash screen

- Added a check in the SplashScreenPage to ensure that the route is only replaced when it's not a share intent
- Added lifecycle event to reset the isUpload.value when minimize the app
2025-06-04 08:30:23 -05:00
shenlong
70b9a4c8f1 chore: add missing api properties on sync enums (#18916)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-04 08:21:34 -05:00
xCJPECKOVERx
2da94439c7 fix(web): add tag button not using translation (#18910) 2025-06-04 09:52:07 +02:00
Jin Xuan
3d3e5dc547 chore(server): cleanup unused query parameters in time bucket (#18893)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-03 19:29:11 +00:00
Daimolean
daf1bee7ac fix(web): persisted store (#18385)
* fix(web): persisted store

* fix: translation

* fix: test

* fix: test

* revert i18n changes

* fix blank locale
2025-06-03 19:27:23 +00:00
xCJPECKOVERx
6b4d5e3beb fix(web): asset-viewer error when selecting a stacked asset (#18881)
* Clear out the previewStackedAsset when selecting.

* undo package-lock update
2025-06-03 14:24:20 -05:00
Alex
6b9233c71a fix(deps): revert update typescript-projects (#18908) 2025-06-03 21:13:56 +02:00
shenlong
b4a798c39f feat(mobile): remote asset & exif sync (#18756)
* feat(mobile): remote asset & exif sync

* add visibility and update constraints

* chore: generate drifts

* update ids to be strings

* clear remote entities on logout

* reset sqlite button

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-03 11:01:50 -05:00
waclaw66
edae9c2d3d fix(mobile): t function localization (#18768)
* fix(mobile): items translation

* Intl.defaultLocale null coalescence
2025-06-03 09:52:29 -05:00
JobiJoba
246d593c9d fix(mobile): reset current asset if we try to go on a activity list page (#18895) 2025-06-03 14:36:14 +00:00
Thien Dang
e4322ae0a2 feat(mobile): Add new language to mobile (#18891)
add pt_BR, bg, ta, te locates

Co-authored-by: dvbthien <dvbthien@gmail.com>
2025-06-03 14:33:13 +00:00
Thien Dang
e506c7fb19 feat(mobile): Improve language setting UI (#18854)
* improve language ui

* fix lint

* add search language, add safe area, fix button in dark

* hide apply button when search not found

---------

Co-authored-by: dvbthien <dvbthien@gmail.com>
2025-06-03 09:30:39 -05:00
renovate[bot]
393e8d50b2 fix(deps): update typescript-projects (#18889)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 11:39:55 +00:00
Brandon Wees
74438f5bd8 feat(web): improved user onboarding (#18782)
* wip

* added user metadata key

* wip

* restructure onboarding system and add initial locale

* update language card and fix translation updating

* remove prints

* new card formattings

* fix cursed unmount effect

* add OAuth route onboarding

* remove required admin auth for onboarding

* delete the hotwire button

* update open-api files

* delete import

* fix failing oauth onboarding fields

* fix e2e test

* fix web e2e test

* add onboarding to user registration e2e test

* remove todo

this was a holdover during dev and didn't get deleted

* fix server small tests

* use onDestroy to save settings rather than a bind:this

* change to false for isOnboarded

* fix other auth small test

* provide type annotation in user factory metadata field

* remove onboardingCompelted from UserDto

* move translations to onboarding steps array and mark as derived so they update

* break language selector out into its own component as per @danieldietzler suggestion

* remove hello header on card

* fix flixkering on server privacy card

* label/id fixes

* openapi

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-06-02 21:09:13 +00:00
Daniel Dietzler
e7d7886f44 chore: move slideshow settings modal to modals folder (#18869) 2025-06-02 14:22:22 -04:00
Daniel Dietzler
97e86e409a refactor: asset tag modal (#18867) 2025-06-02 12:41:28 -04:00
Leonardo
72401aa6b1 fix: translation in the tag people window (#18777) 2025-06-02 16:08:31 +00:00
bo0tzz
fb94fd3132 chore: cleanup unused actions (#18865) 2025-06-02 16:13:50 +01:00
Brandon Wees
a02e1f5e7c chore(web): migrate CircleIconButton to @immich/ui IconButton (#18486)
* remove import and referenced file

* first pass at replacing all CircleIconButtons

* fix linting issues

* fix combobox formatting issues

* fix button context menu coloring

* remove circle icon button from search history box

* use theme switcher from UI lib

* dark mode force the asset viewer icons

* fix forced dark mode icons

* dark mode memory viewer icons

* fix: back button in memory viewer

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-02 14:47:23 +00:00
Dag Stuan
d544053c67 feat(web): improve slideshow quality of life (#18778)
* Add a new setting to toggle autoplay when showing the slideshow.
* Fix an issue where the slideshow would restart automatically when
navigating after it was paused.
* Add a keyboard shortcut 's' to start the slideshow from the asset
viewer.
* Add a keyboard shortcut ' ' to toggle the slideshow play/paused.
* Change the timeout for hiding the slideshow controls from 10 to 2.5
seconds.
* Add English translation for the 'autoplay_slideshow' setting.

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-02 14:45:39 +00:00
shenlong
df927dd3ce fix(mobile): photo_manager ignore filters (#18742)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-02 09:43:59 -05:00
JobiJoba
d48702f943 fix(mobile): Showing videos of partner in search page quick links (#18855)
Add userId to the contact of the timeline interface method watchAllVideosTimeline and modify the query in the repository
2025-06-02 09:35:18 -05:00
Mert
fa22e865a4 fix(server): tighten asset visibility (#18699)
* tighten visibility

* update sql

* elevated access util function

* fix potential sync issue

* include in user stats

* include hidden assets in size usage

* filter visibility in search duplicates query

* stack visibility
2025-06-02 09:33:08 -05:00
Arno
b5c3a675b2 feat: upload assets to locked folder (#18806)
* feat: upload assets to locked folder

* chore: refactor params
2025-06-01 21:45:39 -05:00
Dag Stuan
5589616921 fix(web): Improve zoom behavior in photo-viewer. (#18803)
* Fix an issue where clicking the zoom-button after having zoomed in
would not zoom completely out, but leave the image in the zoomed-in
state. The new behavior properly zoomes the image completely out after
clicking the zoom-button.
* Revert to the default setting for `wheelZoomRatio` as the previous
setting of 0.2 was borderline unusable on a trackpad. This could
probably be moved to a user setting if needed.
* Add a keyboard shortcut 'z' to toggle image zoom.
2025-06-01 21:06:48 -05:00
Thien Dang
a53d033622 fix(mobile): notification, dialog that don't translate properly (#18827)
* Fix notification, dialog that don't translate properly

* use localeProvider to re-build

---------

Co-authored-by: dvbthien <dvbthien@gmail.com>
2025-06-01 21:03:22 -05:00
JobiJoba
36506250c4 fix(mobile): Set the currentAsset to the asset clicked when opening an asset from folders (#18825)
Set the currentAsset to the asset clicked when opening an asset from the folder view ; fix issue #17691
2025-06-01 21:03:03 -05:00
Bence Ferdinandy
31af44dd2a feat: add --json-output option to upload command (#18845)
* fix(docs): update the cli upload usage

The cli upload usage is missing some options compared to what is the current
output of `immich upload --help`. Update the docs accordingly.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>

* feat(cli): add --json-output option to upload command

Add an option that allows retrieving per-file information about the
upload process. The output includes the newFiles, duplicates and
newAssets lists, but could accommodate more information later if needed.

One use case this allows for is using --dry-run to get a list of all the
files that would be uploaded, and checking them manually before an
upload. This can be particularly useful when a curated subset of images
have already been uploaded to immich and we want to double check for
some stragglers without uploading everything to immich.

The upload command has a few lines of logging, so to get an actually
parsable json one needs to strip those lines:

  immich upload --dry-run * | tail -n +4 | jq .newFiles[]

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>

---------

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
2025-06-02 01:58:58 +00:00
Daimolean
c89ac5b5e5 fix(server): cannot share album to owner (#18802)
* fix(server): create shared album

* add test

* trigger ci

* resolve conversation
2025-06-01 20:58:07 -05:00
aviv926
daf1a48b54 fix: update en.json (#18835)
Update en.json
2025-06-01 20:54:10 -05:00
shenlong
091a101f39 fix(mobile): group settings not respected without restart (#18823)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-01 20:53:45 -05:00
Daniel Dietzler
d118b46c3f chore: remove postcss (#18831) 2025-06-01 20:52:17 -05:00
bo0tzz
ad3f58bcda chore: document backup ordering (#18807)
* chore: document backup ordering

* chore: fix formatting

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-31 15:10:14 -05:00
renovate[bot]
0711a9006f chore(deps): update dependency @types/express to v5 (#18818)
* chore(deps): update dependency @types/express to v5

* fix: properly handle promise

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-05-31 15:31:36 -04:00
Arno
9c18fef9b2 chore: Refactor external library modals (#18655) 2025-05-31 15:30:08 +02:00
bo0tzz
d00c872dc1 fix: cursed knowledge date index (#18801) 2025-05-31 14:37:43 +02:00
Nicholas
3a5fed99e1 fix(server): rename android-links api endpoint to apk-links (#18790)
* remove auth from endpoint and change android to apk

* add auth back to `apk-links`
2025-05-31 00:27:55 -04:00
Frank de Lange
e2defbc49a feat: start oauth with autoLaunch=1 (#18763)
* Add automatic OpenID Connect login by using parameter `autoLaunch=1`

By launching Immich with `/auth/login?autoLaunch=1` an OpenID Connect login attempt is directly initated on installations where OAuth Auto Launch is not enabled. The intended use for this parameter is to enable Immich to be launched from e.g. Nextcloud using the _External sites_ app and the _oids_ OpenID Connect provider app so as to enable the user to directly interact with Immich without the need to press the `Login with ...` button.

* Add documentation for autolaunch by navigating to `/auth/login?autoLaunch=1`

* Look ma, no braces!

_This could be a single line_

And now it is, as is its predecessor.

* Change formatting to satisfy _prettier_

* if (condition) return true -> return condition

* More _prettier_ reformatting

* Look ma, braces!
2025-05-30 22:12:53 +00:00
Yaros
f4e4e6628e fix(mobile): center loading spinner in people page (#18781)
fix: center loading spinner in people page
2025-05-30 16:45:29 -05:00
Daniel Dietzler
9d04853b34 fix: oauth (#18725) 2025-05-30 22:04:52 +02:00
Yaros
97503d11c5 fix(web): datetime in storage template example (#18784)
fix: datetime in storage template example
2025-05-30 14:18:22 -04:00
Brandon Wees
cbf68b006e chore: add google cast feature switch to user admin pane (#18783)
add gogole cast feature switch to user admin pane
2025-05-30 14:17:32 -04:00
Yaros
4b9a7b2ce0 fix(mobile): android status bar overlays icon in map (#18780) 2025-05-30 16:04:20 +00:00
shenlong
b854a3dd47 feat(server): add originalFileName to SyncAssetV1 (#18767)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-05-30 09:56:35 -05:00
Nicholas
aebd68e24e fix: change URL to Url in the Obtainium apk links api endpoint (#18764)
change `URL` to `Url`
2025-05-30 00:50:09 -04:00
Thien Dang
0f42babb6b fix: Update locked folder text and improve translations (#18622)
* Update locked folder text and remove unused translations

* uppercase Locked folder in Menu

* convert some translates to icu and improve

* add iOS debug info translations for background processes

* fix lint

---------

Co-authored-by: dvbthien <dvbthien@gmail.com>
2025-05-29 15:06:08 -05:00
367 changed files with 12673 additions and 4985 deletions

View File

@@ -1,118 +0,0 @@
name: 'Single arch image build'
description: 'Build single-arch image on platform appropriate runner'
inputs:
image:
description: 'Name of the image to build'
required: true
ghcr-token:
description: 'GitHub Container Registry token'
required: true
platform:
description: 'Platform to build for'
required: true
artifact-key-base:
description: 'Base key for artifact name'
required: true
context:
description: 'Path to build context'
required: true
dockerfile:
description: 'Path to Dockerfile'
required: true
build-args:
description: 'Docker build arguments'
required: false
runs:
using: 'composite'
steps:
- name: Prepare
id: prepare
shell: bash
env:
PLATFORM: ${{ inputs.platform }}
run: |
echo "platform-pair=${PLATFORM//\//-}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ inputs.ghcr-token }}
- name: Generate cache key suffix
id: cache-key-suffix
shell: bash
env:
REF: ${{ github.ref_name }}
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "cache-key-suffix=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT
else
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
echo "suffix=${SUFFIX}" >> $GITHUB_OUTPUT
fi
- name: Generate cache target
id: cache-target
shell: bash
env:
BUILD_ARGS: ${{ inputs.build-args }}
IMAGE: ${{ inputs.image }}
SUFFIX: ${{ steps.cache-key-suffix.outputs.suffix }}
PLATFORM_PAIR: ${{ steps.prepare.outputs.platform-pair }}
run: |
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
CACHE_KEY="${PLATFORM_PAIR}-${HASH}"
echo "cache-key-base=${CACHE_KEY}" >> $GITHUB_OUTPUT
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,ref=${IMAGE}-build-cache:${CACHE_KEY}-${SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
- name: Build and push image
id: build
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: ${{ inputs.platform }}
labels: ${{ steps.meta.outputs.labels }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-${{ steps.cache-key-suffix.outputs.suffix }}
type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-main
outputs: type=image,"name=${{ inputs.image }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.meta.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
${{ inputs.build-args }}
- name: Export digest
shell: bash
run: | # zizmor: ignore[template-injection]
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ inputs.artifact-key-base }}-${{ steps.cache-target.outputs.cache-key-base }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

View File

@@ -1,185 +0,0 @@
name: 'Multi-runner container image build'
on:
workflow_call:
inputs:
image:
description: 'Name of the image'
type: string
required: true
context:
description: 'Path to build context'
type: string
required: true
dockerfile:
description: 'Path to Dockerfile'
type: string
required: true
tag-suffix:
description: 'Suffix to append to the image tag'
type: string
default: ''
dockerhub-push:
description: 'Push to Docker Hub'
type: boolean
default: false
build-args:
description: 'Docker build arguments'
type: string
required: false
platforms:
description: 'Platforms to build for'
type: string
runner-mapping:
description: 'Mapping from platforms to runners'
type: string
secrets:
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_TOKEN:
required: false
env:
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ inputs.image }}
DOCKERHUB_IMAGE: altran1502/${{ inputs.image }}
jobs:
matrix:
name: 'Generate matrix'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
key: ${{ steps.artifact-key.outputs.base }}
steps:
- name: Generate build matrix
id: matrix
shell: bash
env:
PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64' }}
RUNNER_MAPPING: ${{ inputs.runner-mapping || '{"linux/amd64":"ubuntu-latest","linux/arm64":"ubuntu-24.04-arm"}' }}
run: |
matrix=$(jq -R -c \
--argjson runner_mapping "${RUNNER_MAPPING}" \
'split(",") | map({platform: ., runner: $runner_mapping[.]})' \
<<< "${PLATFORMS}")
echo "${matrix}"
echo "matrix=${matrix}" >> $GITHUB_OUTPUT
- name: Determine artifact key
id: artifact-key
shell: bash
env:
IMAGE: ${{ inputs.image }}
SUFFIX: ${{ inputs.tag-suffix }}
run: |
if [[ -n "${SUFFIX}" ]]; then
base="${IMAGE}${SUFFIX}-digests"
else
base="${IMAGE}-digests"
fi
echo "${base}"
echo "base=${base}" >> $GITHUB_OUTPUT
build:
needs: matrix
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.matrix.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: ./.github/actions/image-build
with:
context: ${{ inputs.context }}
dockerfile: ${{ inputs.dockerfile }}
image: ${{ env.GHCR_IMAGE }}
ghcr-token: ${{ secrets.GITHUB_TOKEN }}
platform: ${{ matrix.platform }}
artifact-key-base: ${{ needs.matrix.outputs.key }}
build-args: ${{ inputs.build-args }}
merge:
needs: [matrix, build]
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.head.repo.fork }}
permissions:
contents: read
actions: read
packages: write
steps:
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: ${{ runner.temp }}/digests
pattern: ${{ needs.matrix.outputs.key }}-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ inputs.dockerhub-push }}
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
flavor: |
# Disable latest tag
latest=false
suffix=${{ inputs.tag-suffix }}
images: |
name=${{ env.GHCR_IMAGE }}
name=${{ env.DOCKERHUB_IMAGE }},enable=${{ inputs.dockerhub-push }}
tags: |
# Tag with branch name
type=ref,event=branch
# Tag with pr-number
type=ref,event=pr
# Tag with long commit sha hash
type=sha,format=long,prefix=commit-
# Tag with git tag on release
type=ref,event=tag
type=raw,value=release,enable=${{ github.event_name == 'release' }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
# Process annotations
declare -a ANNOTATIONS=()
if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then
while IFS= read -r annotation; do
# Extract key and value by removing the manifest: prefix
if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then
key="${BASH_REMATCH[1]}"
value="${BASH_REMATCH[2]}"
# Use array to properly handle arguments with spaces
ANNOTATIONS+=(--annotation "index:$key=$value")
fi
done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
fi
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
SOURCE_ARGS=$(printf "${GHCR_IMAGE}@sha256:%s " *)
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS

View File

@@ -43,6 +43,7 @@ export interface UploadOptionsDto {
concurrency: number;
progress?: boolean;
watch?: boolean;
jsonOutput?: boolean;
}
class UploadFile extends File {
@@ -65,6 +66,9 @@ class UploadFile extends File {
const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
const { newFiles, duplicates } = await checkForDuplicates(files, options);
const newAssets = await uploadFiles(newFiles, options);
if (options.jsonOutput) {
console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4));
}
await updateAlbums([...newAssets, ...duplicates], options);
await deleteFiles(newFiles, options);
};

View File

@@ -68,6 +68,11 @@ program
.env('IMMICH_UPLOAD_CONCURRENCY')
.default(4),
)
.addOption(
new Option('-j, --json-output', 'Output detailed information in json format')
.env('IMMICH_JSON_OUTPUT')
.default(false),
)
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
.addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true))
.addOption(

View File

@@ -116,7 +116,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:ff21bc0f8194dc9c105b769aeabf9585fea6a8ed649c0781caeac5cb3c247884
image: docker.io/valkey/valkey:8-bookworm@sha256:a19bebed6a91bd5e6e2106fef015f9602a3392deeb7c9ed47548378dcee3dfc2
healthcheck:
test: redis-cli ping || exit 1

View File

@@ -56,7 +56,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:ff21bc0f8194dc9c105b769aeabf9585fea6a8ed649c0781caeac5cb3c247884
image: docker.io/valkey/valkey:8-bookworm@sha256:a19bebed6a91bd5e6e2106fef015f9602a3392deeb7c9ed47548378dcee3dfc2
healthcheck:
test: redis-cli ping || exit 1
restart: always

View File

@@ -49,7 +49,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:ff21bc0f8194dc9c105b769aeabf9585fea6a8ed649c0781caeac5cb3c247884
image: docker.io/valkey/valkey:8-bookworm@sha256:a19bebed6a91bd5e6e2106fef015f9602a3392deeb7c9ed47548378dcee3dfc2
healthcheck:
test: redis-cli ping || exit 1
restart: always

View File

@@ -219,3 +219,10 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files.
You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface.
:::
## Backup ordering
A backup of Immich should contain both the database and the asset files. When backing these up it's possible for them to get out of sync, potentially resulting in broken assets after you restore.
The best way of dealing with this is to stop the immich-server container while you take a backup. If nothing is changing then the backup will always be in sync.
If stopping the container is not an option, then the recommended order is to back up the database first, and the filesystem second. This way, the worst case scenario is that there are files on the filesystem that the database doesn't know about. If necessary, these can be (re)uploaded manually after a restore. If the backup is done the other way around, with the filesystem first and the database second, it's possible for the restored database to reference files that aren't in the filesystem backup, thus resulting in broken assets.

View File

@@ -93,6 +93,7 @@ The `.well-known/openid-configuration` part of the url is optional and will be a
## Auto Launch
When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`.
Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?authLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich.
## Mobile Redirect URI

View File

@@ -90,19 +90,22 @@ Usage: immich upload [paths...] [options]
Upload assets
Arguments:
paths One or more paths to assets to be uploaded
paths One or more paths to assets to be uploaded
Options:
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
-i, --ignore [paths...] Paths to ignore (default: [], env: IMMICH_IGNORE_PATHS)
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
--help display help for command
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
-i, --ignore <pattern> Pattern to ignore (env: IMMICH_IGNORE_PATHS)
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
-j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT)
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
--no-progress Hide progress bars (env: IMMICH_PROGRESS_BAR)
--watch Watch for changes and upload automatically (default: false, env: IMMICH_WATCH_CHANGES)
--help display help for command
```
</details>
@@ -172,6 +175,16 @@ By default, hidden files are skipped. If you want to include hidden files, use t
immich upload --include-hidden --recursive directory/
```
You can use the `--json-output` option to get a json printed which includes
three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging
output you will need to strip the first three lines of output to get the json.
For example to get a list of files that would be uploaded for further
processing:
```bash
immich upload --dry-run . | tail -n +4 | jq .newFiles[]
```
### Obtain the API Key
The API key can be obtained in the user setting panel on the web interface.

View File

@@ -13,6 +13,9 @@ import {
mdiTrashCan,
mdiWeb,
mdiWrap,
mdiCloudKeyOutline,
mdiRegex,
mdiCodeJson,
} from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
@@ -23,6 +26,30 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
const items: Item[] = [
{
icon: mdiRegex,
iconColor: 'purple',
title: 'Zitadel Actions are cursed',
description:
"Zitadel is cursed because its custom scripting feature is executed with a JS engine that doesn't support regex named capture groups.",
link: {
url: 'https://github.com/dop251/goja',
text: 'Go JS engine',
},
date: new Date(2025, 5, 4),
},
{
icon: mdiCloudKeyOutline,
iconColor: '#0078d4',
title: 'Entra is cursed',
description:
"Microsoft Entra supports PKCE, but doesn't include it in its OpenID discovery document. This leads to clients thinking PKCE isn't available.",
link: {
url: 'https://github.com/immich-app/immich/pull/18725',
text: '#18725',
},
date: new Date(2025, 4, 30),
},
{
icon: mdiCrop,
iconColor: 'tomato',
@@ -33,7 +60,18 @@ const items: Item[] = [
url: 'https://github.com/immich-app/immich/pull/17974',
text: '#17974',
},
date: new Date(2025, 5, 5),
date: new Date(2025, 4, 5),
},
{
icon: mdiCodeJson,
iconColor: 'yellow',
title: 'YAML whitespace is cursed',
description: 'YAML whitespaces are often handled in unintuitive ways.',
link: {
url: 'https://github.com/immich-app/immich/pull/17309',
text: '#17309',
},
date: new Date(2025, 3, 1),
},
{
icon: mdiMicrosoftWindows,

View File

@@ -28,8 +28,10 @@ services:
extra_hosts:
- 'auth-server:host-gateway'
depends_on:
- redis
- database
redis:
condition: service_started
database:
condition: service_healthy
ports:
- 2285:2285
@@ -37,7 +39,7 @@ services:
image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:e6d1209c1c13791c6f9fbf726c41865e3320dfe2445a6b4ffb03e25f904b3b37
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:9c704fb49ce27549df00f1b096cc93f8b0c959ef087507704d74954808f78a82
command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf
environment:
POSTGRES_PASSWORD: postgres
@@ -45,3 +47,9 @@ services:
POSTGRES_DB: immich
ports:
- 5435:5432
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres -d immich']
interval: 1s
timeout: 5s
retries: 30
start_period: 10s

View File

@@ -428,6 +428,15 @@ describe('/albums', () => {
order: AssetOrder.Desc,
});
});
it('should not be able to share album with owner', async () => {
const { status, body } = await request(app)
.post('/albums')
.send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] })
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot share album with owner'));
});
});
describe('PUT /albums/:id/assets', () => {

View File

@@ -75,8 +75,8 @@ describe('/timeline', () => {
expect(status).toBe(200);
expect(body).toEqual(
expect.arrayContaining([
{ count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
{ count: 3, timeBucket: '1970-02-01' },
{ count: 1, timeBucket: '1970-01-01' },
]),
);
});
@@ -167,7 +167,8 @@ describe('/timeline', () => {
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
localDateTime: [],
fileCreatedAt: [],
localOffsetHours: [],
ownerId: [],
projectionType: [],
ratio: [],
@@ -204,7 +205,8 @@ describe('/timeline', () => {
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
localDateTime: [],
fileCreatedAt: [],
localOffsetHours: [],
ownerId: [],
projectionType: [],
ratio: [],

View File

@@ -103,6 +103,7 @@ export const loginResponseDto = {
accessToken: expect.any(String),
name: 'Immich Admin',
isAdmin: true,
isOnboarded: false,
profileImagePath: '',
shouldChangePassword: true,
userEmail: 'admin@immich.cloud',

View File

@@ -33,7 +33,9 @@ test.describe('Registration', () => {
// onboarding
await expect(page).toHaveURL('/auth/onboarding');
await page.getByRole('button', { name: 'Theme' }).click();
await page.getByRole('button', { name: 'Privacy' }).click();
await page.getByRole('button', { name: 'Language' }).click();
await page.getByRole('button', { name: 'Server Privacy' }).click();
await page.getByRole('button', { name: 'User Privacy' }).click();
await page.getByRole('button', { name: 'Storage Template' }).click();
await page.getByRole('button', { name: 'Done' }).click();
@@ -77,6 +79,13 @@ test.describe('Registration', () => {
await page.getByLabel('Password').fill('new-password');
await page.getByRole('button', { name: 'Login' }).click();
// onboarding
await expect(page).toHaveURL('/auth/onboarding');
await page.getByRole('button', { name: 'Theme' }).click();
await page.getByRole('button', { name: 'Language' }).click();
await page.getByRole('button', { name: 'User Privacy' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// success
await expect(page).toHaveURL(/\/photos/);
});

View File

@@ -26,7 +26,6 @@
"add_to_album": "Add to album",
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"add_to_locked_folder": "Add to locked folder",
"add_to_shared_album": "Add to shared album",
"add_url": "Add URL",
"added_to_archive": "Added to archive",
@@ -44,9 +43,7 @@
"backup_database_enable_description": "Enable database dumps",
"backup_keep_last_amount": "Amount of previous dumps to keep",
"backup_settings": "Database Dump Settings",
"backup_settings_description": "Manage database dump settings. Note: These jobs are not monitored and you will not be notified of failure.",
"check_all": "Check All",
"cleanup": "Cleanup",
"backup_settings_description": "Manage database dump settings.",
"cleared_jobs": "Cleared jobs for: {job}",
"config_set_by_file": "Config is currently set by a config file",
"confirm_delete_library": "Are you sure you want to delete {library} library?",
@@ -62,14 +59,12 @@
"disable_login": "Disable login",
"duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search",
"exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
"external_library_created_at": "External library (created on {date})",
"external_library_management": "External Library Management",
"face_detection": "Face detection",
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
"facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
"failed_job_command": "Command {command} failed for job: {job}",
"force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.",
"forcing_refresh_library_files": "Forcing refresh of all library files",
"image_format": "Format",
"image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.",
"image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in",
@@ -210,8 +205,6 @@
"oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).",
"oauth_timeout": "Request Timeout",
"oauth_timeout_description": "Timeout for requests in milliseconds",
"offline_paths": "Offline Paths",
"offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.",
"password_enable_description": "Login with email and password",
"password_settings": "Password Login",
"password_settings_description": "Manage password login settings",
@@ -221,9 +214,6 @@
"refreshing_all_libraries": "Refreshing all libraries",
"registration": "Admin Registration",
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
"repair_all": "Repair All",
"repair_matched_items": "Matched {count, plural, one {# item} other {# items}}",
"repaired_items": "Repaired {count, plural, one {# item} other {# items}}",
"require_password_change_on_login": "Require user to change password on first login",
"reset_settings_to_default": "Reset settings to default",
"reset_settings_to_recent_saved": "Reset settings to the recent saved settings",
@@ -264,7 +254,6 @@
"template_email_invite_album": "Invite Album Template",
"template_email_preview": "Preview",
"template_email_settings": "Email Templates",
"template_email_settings_description": "Manage custom email notification templates",
"template_email_update_album": "Update Album Template",
"template_email_welcome": "Welcome email template",
"template_settings": "Notification Templates",
@@ -273,7 +262,6 @@
"theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.",
"theme_settings": "Theme Settings",
"theme_settings_description": "Manage customization of the Immich web interface",
"these_files_matched_by_checksum": "These files are matched by their checksums",
"thumbnail_generation_job": "Generate Thumbnails",
"thumbnail_generation_job_description": "Generate large, small and blurred thumbnails for each asset, as well as thumbnails for each person",
"transcoding_acceleration_api": "Acceleration API",
@@ -341,8 +329,6 @@
"trash_number_of_days_description": "Number of days to keep the assets in trash before permanently removing them",
"trash_settings": "Trash Settings",
"trash_settings_description": "Manage trash settings",
"untracked_files": "Untracked Files",
"untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
"user_cleanup_job": "User cleanup",
"user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
"user_delete_delay_settings": "Delete delay",
@@ -401,10 +387,6 @@
"album_remove_user": "Remove user?",
"album_remove_user_confirmation": "Are you sure you want to remove {user}?",
"album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{count} items",
"album_thumbnail_card_shared": " · Shared",
"album_thumbnail_shared_by": "Shared by {user}",
"album_updated": "Album updated",
"album_updated_setting_description": "Receive an email notification when a shared album has new assets",
"album_user_left": "Left {album}",
@@ -420,6 +402,9 @@
"album_with_link_access": "Let anyone with the link see photos and people in this album.",
"albums": "Albums",
"albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}",
"albums_default_sort_order": "Default album sort order",
"albums_default_sort_order_description": "Initial asset sort order when creating new albums.",
"albums_feature_description": "Collections of assets that can be shared with other users.",
"all": "All",
"all_albums": "All albums",
"all_people": "All people",
@@ -481,6 +466,8 @@
"assets_count": "{count, plural, one {# asset} other {# assets}}",
"assets_deleted_permanently": "{count} asset(s) deleted permanently",
"assets_deleted_permanently_from_server": "{count} asset(s) deleted permanently from the Immich server",
"assets_downloaded_failed": "{count, plural, one {Downloaded # file - {error} file failed} other {Downloaded # files - {error} files failed}}",
"assets_downloaded_successfully": "{count, plural, one {Downloaded # file successfully} other {Downloaded # files successfully}}",
"assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash",
"assets_permanently_deleted_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}",
"assets_removed_count": "Removed {count, plural, one {# asset} other {# assets}}",
@@ -495,6 +482,7 @@
"authorized_devices": "Authorized Devices",
"automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
"automatic_endpoint_switching_title": "Automatic URL switching",
"autoplay_slideshow": "Autoplay slideshow",
"back": "Back",
"back_close_deselect": "Back, close, or deselect",
"background_location_permission": "Background location permission",
@@ -576,21 +564,17 @@
"bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.",
"bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.",
"buy": "Purchase Immich",
"cache_settings_album_thumbnails": "Library page thumbnails ({count} assets)",
"cache_settings_clear_cache_button": "Clear cache",
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
"cache_settings_duplicated_assets_clear_button": "CLEAR",
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
"cache_settings_duplicated_assets_title": "Duplicated Assets ({count})",
"cache_settings_image_cache_size": "Image cache size ({count} assets)",
"cache_settings_statistics_album": "Library thumbnails",
"cache_settings_statistics_assets": "{count} assets ({size})",
"cache_settings_statistics_full": "Full images",
"cache_settings_statistics_shared": "Shared album thumbnails",
"cache_settings_statistics_thumbnail": "Thumbnails",
"cache_settings_statistics_title": "Cache usage",
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
"cache_settings_thumbnail_size": "Thumbnail cache size ({count} assets)",
"cache_settings_tile_subtitle": "Control the local storage behaviour",
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Caching Settings",
@@ -622,7 +606,6 @@
"change_pin_code": "Change PIN code",
"change_your_password": "Change your password",
"changed_visibility_successfully": "Changed visibility successfully",
"check_all": "Check All",
"check_corrupt_asset_backup": "Check for corrupt asset backups",
"check_corrupt_asset_backup_button": "Perform check",
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
@@ -668,7 +651,6 @@
"contain": "Contain",
"context": "Context",
"continue": "Continue",
"control_bottom_app_bar_album_info_shared": "{count} items · Shared",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
@@ -717,6 +699,7 @@
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"dark": "Dark",
"darkTheme": "Toggle dark theme",
"date_after": "Date after",
"date_and_time": "Date and Time",
"date_before": "Date before",
@@ -779,7 +762,6 @@
"download_enqueue": "Download enqueued",
"download_error": "Download Error",
"download_failed": "Download failed",
"download_filename": "file: {filename}",
"download_finished": "Download finished",
"download_include_embedded_motion_videos": "Embedded videos",
"download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file",
@@ -855,7 +837,6 @@
"cant_get_number_of_comments": "Can't get number of comments",
"cant_search_people": "Can't search people",
"cant_search_places": "Can't search places",
"cleared_jobs": "Cleared jobs for: {job}",
"error_adding_assets_to_album": "Error adding assets to album",
"error_adding_users_to_album": "Error adding users to album",
"error_deleting_shared_user": "Error deleting shared user",
@@ -864,7 +845,6 @@
"error_removing_assets_from_album": "Error removing assets from album, check console for more details",
"error_selecting_all_assets": "Error selecting all assets",
"exclusion_pattern_already_exists": "This exclusion pattern already exists.",
"failed_job_command": "Command {command} failed for job: {job}",
"failed_to_create_album": "Failed to create album",
"failed_to_create_shared_link": "Failed to create shared link",
"failed_to_edit_shared_link": "Failed to edit shared link",
@@ -883,7 +863,6 @@
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
"profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.",
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
"repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}",
"unable_to_add_album_users": "Unable to add users to album",
"unable_to_add_assets_to_shared_link": "Unable to add assets to shared link",
"unable_to_add_comment": "Unable to add comment",
@@ -902,7 +881,6 @@
"unable_to_change_visibility": "Unable to change the visibility for {count, plural, one {# person} other {# people}}",
"unable_to_complete_oauth_login": "Unable to complete OAuth login",
"unable_to_connect": "Unable to connect",
"unable_to_connect_to_server": "Unable to connect to server",
"unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https",
"unable_to_create_admin_account": "Unable to create admin account",
"unable_to_create_api_key": "Unable to create a new API Key",
@@ -926,14 +904,9 @@
"unable_to_hide_person": "Unable to hide person",
"unable_to_link_motion_video": "Unable to link motion video",
"unable_to_link_oauth_account": "Unable to link OAuth account",
"unable_to_load_album": "Unable to load album",
"unable_to_load_asset_activity": "Unable to load asset activity",
"unable_to_load_items": "Unable to load items",
"unable_to_load_liked_status": "Unable to load liked status",
"unable_to_log_out_all_devices": "Unable to log out all devices",
"unable_to_log_out_device": "Unable to log out device",
"unable_to_login_with_oauth": "Unable to login with OAuth",
"unable_to_move_to_locked_folder": "Unable to move to locked folder",
"unable_to_play_video": "Unable to play video",
"unable_to_reassign_assets_existing_person": "Unable to reassign assets to {name, select, null {an existing person} other {{name}}}",
"unable_to_reassign_assets_new_person": "Unable to reassign assets to a new person",
@@ -941,11 +914,9 @@
"unable_to_remove_album_users": "Unable to remove users from album",
"unable_to_remove_api_key": "Unable to remove API Key",
"unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link",
"unable_to_remove_deleted_assets": "Unable to remove offline files",
"unable_to_remove_library": "Unable to remove library",
"unable_to_remove_partner": "Unable to remove partner",
"unable_to_remove_reaction": "Unable to remove reaction",
"unable_to_repair_items": "Unable to repair items",
"unable_to_reset_password": "Unable to reset password",
"unable_to_reset_pin_code": "Unable to reset PIN code",
"unable_to_resolve_duplicate": "Unable to resolve duplicate",
@@ -1118,6 +1089,12 @@
"invalid_date_format": "Invalid date format",
"invite_people": "Invite People",
"invite_to_album": "Invite to album",
"ios_debug_info_fetch_ran_at": "Fetch ran {dateTime}",
"ios_debug_info_last_sync_at": "Last sync {dateTime}",
"ios_debug_info_no_processes_queued": "No background processes queued",
"ios_debug_info_no_sync_yet": "No background sync job has run yet",
"ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}",
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
"items_count": "{count, plural, one {# item} other {# items}}",
"jobs": "Jobs",
"keep": "Keep",
@@ -1126,6 +1103,9 @@
"kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}",
"keyboard_shortcuts": "Keyboard shortcuts",
"language": "Language",
"language_no_results_subtitle": "Try adjusting your search term",
"language_no_results_title": "No languages found",
"language_search_hint": "Search languages...",
"language_setting_description": "Select your preferred language",
"last_seen": "Last seen",
"latest_version": "Latest Version",
@@ -1161,7 +1141,7 @@
"location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Enter your longitude here",
"lock": "Lock",
"locked_folder": "Locked folder",
"locked_folder": "Locked Folder",
"log_out": "Log out",
"log_out_all_devices": "Log Out All Devices",
"logged_out_all_devices": "Logged out all devices",
@@ -1195,7 +1175,7 @@
"look": "Look",
"loop_videos": "Loop videos",
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
"main_branch_warning": "Youre using a development version; we strongly recommend using a release version!",
"main_branch_warning": "You're using a development version; we strongly recommend using a release version!",
"main_menu": "Main menu",
"make": "Make",
"manage_shared_links": "Manage shared links",
@@ -1319,15 +1299,15 @@
"oauth": "OAuth",
"official_immich_resources": "Official Immich Resources",
"offline": "Offline",
"offline_paths": "Offline paths",
"offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.",
"ok": "Ok",
"oldest_first": "Oldest first",
"on_this_device": "On this device",
"onboarding": "Onboarding",
"onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in the administration settings.",
"onboarding_locale_description": "Select your preferred language. You can change this later in your settings.",
"onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in settings.",
"onboarding_server_welcome_description": "Let's get your instance set up with some common settings.",
"onboarding_theme_description": "Choose a color theme for your instance. You can change this later in your settings.",
"onboarding_welcome_description": "Let's get your instance set up with some common settings.",
"onboarding_user_welcome_description": "Let's get you started!",
"onboarding_welcome_user": "Welcome, {user}",
"online": "Online",
"only_favorites": "Only favorites",
@@ -1460,7 +1440,7 @@
"purchase_lifetime_description": "Lifetime purchase",
"purchase_option_title": "PURCHASE OPTIONS",
"purchase_panel_info_1": "Building Immich takes a lot of time and effort, and we have full-time engineers working on it to make it as good as we possibly can. Our mission is for open-source software and ethical business practices to become a sustainable income source for developers and to create a privacy-respecting ecosystem with real alternatives to exploitative cloud services.",
"purchase_panel_info_2": "As were committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immichs ongoing development.",
"purchase_panel_info_2": "As we're committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immich's ongoing development.",
"purchase_panel_title": "Support the project",
"purchase_per_server": "Per server",
"purchase_per_user": "Per user",
@@ -1639,6 +1619,7 @@
"server_info_box_server_url": "Server URL",
"server_offline": "Server Offline",
"server_online": "Server Online",
"server_privacy": "Server Privacy",
"server_stats": "Server Stats",
"server_version": "Server Version",
"set": "Set",
@@ -1656,7 +1637,6 @@
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply",
"setting_languages_subtitle": "Change the app's language",
"setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {duration}",
"setting_notifications_notify_hours": "{count} hours",
"setting_notifications_notify_immediately": "immediately",
@@ -1843,7 +1823,6 @@
"to_parent": "Go to parent",
"to_trash": "Trash",
"toggle_settings": "Toggle settings",
"toggle_theme": "Toggle dark theme",
"total": "Total",
"total_usage": "Total usage",
"trash": "Trash",
@@ -1865,6 +1844,7 @@
"unable_to_setup_pin_code": "Unable to setup PIN code",
"unarchive": "Unarchive",
"unarchived_count": "{count, plural, other {Unarchived #}}",
"undo": "Undo",
"unfavorite": "Unfavorite",
"unhide_person": "Unhide person",
"unknown": "Unknown",
@@ -1883,8 +1863,6 @@
"unselect_all_duplicates": "Unselect all duplicates",
"unstack": "Un-stack",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"untracked_files": "Untracked files",
"untracked_files_decription": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
"up_next": "Up next",
"updated_at": "Updated",
"updated_password": "Updated password",
@@ -1912,6 +1890,7 @@
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
"user_pin_code_settings": "PIN Code",
"user_pin_code_settings_description": "Manage your PIN code",
"user_privacy": "User Privacy",
"user_purchase_settings": "Purchase",
"user_purchase_settings_description": "Manage your purchase",
"user_role_set": "Set {user} as {role}",
@@ -1927,11 +1906,6 @@
"version": "Version",
"version_announcement_closing": "Your friend, Alex",
"version_announcement_message": "Hi there! A new version of Immich is available. Please take some time to read the <link>release notes</link> to ensure your setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your Immich instance automatically.",
"version_announcement_overlay_release_notes": "release notes",
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available 🎉",
"version_history": "Version History",
"version_history_item": "Installed {version} on {date}",
"video": "Video",

View File

@@ -1,3 +1,3 @@
{
"flutter": "3.32.0"
}
"flutter": "3.29.3"
}

View File

@@ -56,6 +56,7 @@ custom_lint:
allowed:
# required / wanted
- 'lib/infrastructure/repositories/album_media.repository.dart'
- 'lib/infrastructure/repositories/storage.repository.dart'
- 'lib/repositories/{album,asset,file}_media.repository.dart'
# acceptable exceptions for the time being
- lib/entities/asset.entity.dart # to provide local AssetEntity for now

View File

@@ -247,6 +247,7 @@ interface NativeSyncApi {
fun getAlbums(): List<PlatformAlbum>
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset>
fun hashPaths(paths: List<String>): List<ByteArray?>
companion object {
/** The codec used by NativeSyncApi. */
@@ -388,6 +389,23 @@ interface NativeSyncApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashPaths$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val pathsArg = args[0] as List<String>
val wrapped: List<Any?> = try {
listOf(api.hashPaths(pathsArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -4,7 +4,10 @@ import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import android.provider.MediaStore
import android.util.Log
import java.io.File
import java.io.FileInputStream
import java.security.MessageDigest
sealed class AssetResult {
data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult()
@@ -16,6 +19,8 @@ open class NativeSyncApiImplBase(context: Context) {
private val ctx: Context = context.applicationContext
companion object {
private const val TAG = "NativeSyncApiImplBase"
const val MEDIA_SELECTION =
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
val MEDIA_SELECTION_ARGS = arrayOf(
@@ -34,6 +39,8 @@ open class NativeSyncApiImplBase(context: Context) {
MediaStore.MediaColumns.BUCKET_ID,
MediaStore.MediaColumns.DURATION
)
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
}
protected fun getCursor(
@@ -174,4 +181,24 @@ open class NativeSyncApiImplBase(context: Context) {
.mapNotNull { result -> (result as? AssetResult.ValidAsset)?.asset }
.toList()
}
fun hashPaths(paths: List<String>): List<ByteArray?> {
val buffer = ByteArray(HASH_BUFFER_SIZE)
val digest = MessageDigest.getInstance("SHA-1")
return paths.map { path ->
try {
FileInputStream(path).use { file ->
var bytesRead: Int
while (file.read(buffer).also { bytesRead = it } > 0) {
digest.update(buffer, 0, bytesRead)
}
}
digest.digest()
} catch (e: Exception) {
Log.w(TAG, "Failed to hash file $path: $e")
null
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -26,7 +26,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@@ -44,7 +43,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
disableMainThreadChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"

View File

@@ -307,6 +307,7 @@ protocol NativeSyncApi {
func getAlbums() throws -> [PlatformAlbum]
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset]
func hashPaths(paths: [String]) throws -> [FlutterStandardTypedData?]
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -442,5 +443,22 @@ class NativeSyncApiSetup {
} else {
getAssetsForAlbumChannel.setMessageHandler(nil)
}
let hashPathsChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashPaths\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashPaths\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
hashPathsChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let pathsArg = args[0] as! [String]
do {
let result = try api.hashPaths(paths: pathsArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
hashPathsChannel.setMessageHandler(nil)
}
}
}

View File

@@ -1,4 +1,5 @@
import Photos
import CryptoKit
struct AssetWrapper: Hashable, Equatable {
let asset: PlatformAsset
@@ -34,6 +35,8 @@ class NativeSyncApiImpl: NativeSyncApi {
private let changeTokenKey = "immich:changeToken"
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
private let hashBufferSize = 2 * 1024 * 1024
init(with defaults: UserDefaults = .standard) {
self.defaults = defaults
}
@@ -243,4 +246,24 @@ class NativeSyncApiImpl: NativeSyncApi {
}
return assets
}
func hashPaths(paths: [String]) throws -> [FlutterStandardTypedData?] {
return paths.map { path in
guard let file = FileHandle(forReadingAtPath: path) else {
print("Cannot open file: \(path)")
return nil
}
var hasher = Insecure.SHA1()
while autoreleasepool(invoking: {
let chunk = file.readData(ofLength: hashBufferSize)
guard !chunk.isEmpty else { return false }
hasher.update(data: chunk)
return true
}) { }
let digest = hasher.finalize()
return FlutterStandardTypedData(bytes: Data(digest))
}
}
}

View File

@@ -5,6 +5,7 @@ const Map<String, Locale> locales = {
'English (en)': Locale('en'),
// Additional locales
'Arabic (ar)': Locale('ar'),
'Bulgarian (bg)': Locale('bg'),
'Catalan (ca)': Locale('ca'),
'Chinese Simplified (zh_CN)':
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'),
@@ -31,6 +32,7 @@ const Map<String, Locale> locales = {
'Mongolian (mn)': Locale('mn'),
'Norwegian Bokmål (nb_NO)': Locale('nb', 'NO'),
'Polish (pl)': Locale('pl'),
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
'Portuguese (pt)': Locale('pt'),
'Romanian (ro)': Locale('ro'),
'Russian (ru)': Locale('ru'),
@@ -42,6 +44,8 @@ const Map<String, Locale> locales = {
'Slovenian (sl)': Locale('sl'),
'Spanish (es)': Locale('es'),
'Swedish (sv)': Locale('sv'),
'Tamil (ta)': Locale('ta'),
'Telugu (te)': Locale('te'),
'Thai (th)': Locale('th'),
'Turkish (tr)': Locale('tr'),
'Ukrainian (uk)': Locale('uk'),

View File

@@ -29,6 +29,8 @@ abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
String albumId,
Iterable<String> assetIdsToKeep,
);
Future<List<LocalAsset>> getAssetsToHash(String albumId);
}
enum SortLocalAlbumsBy { id }

View File

@@ -0,0 +1,6 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
abstract interface class ILocalAssetRepository implements IDatabaseRepository {
Future<void> updateHashes(Iterable<LocalAsset> hashes);
}

View File

@@ -0,0 +1,7 @@
import 'dart:io';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
abstract interface class IStorageRepository {
Future<File?> getFileForAsset(LocalAsset asset);
}

View File

@@ -1,9 +1,17 @@
part of 'base_asset.model.dart';
enum AssetVisibility {
timeline,
hidden,
archive,
locked,
}
// Model for an asset stored in the server
class Asset extends BaseAsset {
final String id;
final String? localId;
final AssetVisibility visibility;
const Asset({
required this.id,
@@ -17,6 +25,7 @@ class Asset extends BaseAsset {
super.height,
super.durationInSeconds,
super.isFavorite = false,
this.visibility = AssetVisibility.timeline,
});
@override
@@ -32,6 +41,7 @@ class Asset extends BaseAsset {
durationInSeconds: ${durationInSeconds ?? "<NA>"},
localId: ${localId ?? "<NA>"},
isFavorite: $isFavorite,
visibility: $visibility,
}''';
}
@@ -39,9 +49,13 @@ class Asset extends BaseAsset {
bool operator ==(Object other) {
if (other is! Asset) return false;
if (identical(this, other)) return true;
return super == other && id == other.id && localId == other.localId;
return super == other &&
id == other.id &&
localId == other.localId &&
visibility == other.visibility;
}
@override
int get hashCode => super.hashCode ^ id.hashCode ^ localId.hashCode;
int get hashCode =>
super.hashCode ^ id.hashCode ^ localId.hashCode ^ visibility.hashCode;
}

View File

@@ -1,13 +1,19 @@
enum BackupSelection {
none,
selected,
excluded,
none._(1),
selected._(0),
excluded._(2);
// Used to sort albums based on the backupSelection
// selected -> none -> excluded
final int sortOrder;
const BackupSelection._(this.sortOrder);
}
class LocalAlbum {
final String id;
final String name;
final DateTime updatedAt;
final bool isIosSharedAlbum;
final int assetCount;
final BackupSelection backupSelection;
@@ -18,6 +24,7 @@ class LocalAlbum {
required this.updatedAt,
this.assetCount = 0,
this.backupSelection = BackupSelection.none,
this.isIosSharedAlbum = false,
});
LocalAlbum copyWith({
@@ -26,6 +33,7 @@ class LocalAlbum {
DateTime? updatedAt,
int? assetCount,
BackupSelection? backupSelection,
bool? isIosSharedAlbum,
}) {
return LocalAlbum(
id: id ?? this.id,
@@ -33,6 +41,7 @@ class LocalAlbum {
updatedAt: updatedAt ?? this.updatedAt,
assetCount: assetCount ?? this.assetCount,
backupSelection: backupSelection ?? this.backupSelection,
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
);
}
@@ -45,7 +54,8 @@ class LocalAlbum {
other.name == name &&
other.updatedAt == updatedAt &&
other.assetCount == assetCount &&
other.backupSelection == backupSelection;
other.backupSelection == backupSelection &&
other.isIosSharedAlbum == isIosSharedAlbum;
}
@override
@@ -54,7 +64,8 @@ class LocalAlbum {
name.hashCode ^
updatedAt.hashCode ^
assetCount.hashCode ^
backupSelection.hashCode;
backupSelection.hashCode ^
isIosSharedAlbum.hashCode;
}
@override
@@ -65,6 +76,7 @@ name: $name,
updatedAt: $updatedAt,
assetCount: $assetCount,
backupSelection: $backupSelection,
isIosSharedAlbum: $isIosSharedAlbum
}''';
}
}

View File

@@ -0,0 +1,121 @@
import 'dart:convert';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart';
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:logging/logging.dart';
class HashService {
final int batchSizeLimit;
final int batchFileLimit;
final ILocalAlbumRepository _localAlbumRepository;
final ILocalAssetRepository _localAssetRepository;
final IStorageRepository _storageRepository;
final NativeSyncApi _nativeSyncApi;
final _log = Logger('HashService');
HashService({
required ILocalAlbumRepository localAlbumRepository,
required ILocalAssetRepository localAssetRepository,
required IStorageRepository storageRepository,
required NativeSyncApi nativeSyncApi,
this.batchSizeLimit = kBatchHashSizeLimit,
this.batchFileLimit = kBatchHashFileLimit,
}) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository,
_storageRepository = storageRepository,
_nativeSyncApi = nativeSyncApi;
Future<void> hashAssets() async {
final Stopwatch stopwatch = Stopwatch()..start();
// Sorted by backupSelection followed by isCloud
final localAlbums = await _localAlbumRepository.getAll();
localAlbums.sort((a, b) {
final backupComparison =
a.backupSelection.sortOrder.compareTo(b.backupSelection.sortOrder);
if (backupComparison != 0) {
return backupComparison;
}
// Local albums come before iCloud albums
return (a.isIosSharedAlbum ? 1 : 0).compareTo(b.isIosSharedAlbum ? 1 : 0);
});
for (final album in localAlbums) {
final assetsToHash =
await _localAlbumRepository.getAssetsToHash(album.id);
if (assetsToHash.isNotEmpty) {
await _hashAssets(assetsToHash);
}
}
stopwatch.stop();
_log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
DLog.log("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
}
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
/// with hash for those that were successfully hashed. Hashes are looked up in a table
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
Future<void> _hashAssets(List<LocalAsset> assetsToHash) async {
int bytesProcessed = 0;
final toHash = <_AssetToPath>[];
for (final asset in assetsToHash) {
final file = await _storageRepository.getFileForAsset(asset);
if (file == null) {
continue;
}
bytesProcessed += await file.length();
toHash.add(_AssetToPath(asset: asset, path: file.path));
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
await _processBatch(toHash);
toHash.clear();
bytesProcessed = 0;
}
}
await _processBatch(toHash);
}
/// Processes a batch of assets.
Future<void> _processBatch(List<_AssetToPath> toHash) async {
if (toHash.isEmpty) {
return;
}
_log.fine("Hashing ${toHash.length} files");
final hashed = <LocalAsset>[];
final hashes =
await _nativeSyncApi.hashPaths(toHash.map((e) => e.path).toList());
for (final (index, hash) in hashes.indexed) {
final asset = toHash[index].asset;
if (hash?.length == 20) {
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
} else {
_log.warning("Failed to hash file ${asset.id}");
}
}
_log.fine("Hashed ${hashed.length}/${toHash.length} assets");
DLog.log("Hashed ${hashed.length}/${toHash.length} assets");
await _localAssetRepository.updateHashes(hashed);
}
}
class _AssetToPath {
final LocalAsset asset;
final String path;
const _AssetToPath({required this.asset, required this.path});
}

View File

@@ -365,6 +365,7 @@ extension on Iterable<PlatformAsset> {
(e) => LocalAsset(
id: e.id,
name: e.name,
checksum: null,
type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other,
createdAt: e.createdAt == null
? DateTime.now()

View File

@@ -63,7 +63,6 @@ class SyncStreamService {
Iterable<dynamic> data,
) async {
_logger.fine("Processing sync data for $type of length ${data.length}");
// ignore: prefer-switch-expression
switch (type) {
case SyncEntityType.userV1:
return _syncStreamRepository.updateUsersV1(data.cast());

View File

@@ -7,6 +7,7 @@ import 'package:worker_manager/worker_manager.dart';
class BackgroundSyncManager {
Cancelable<void>? _syncTask;
Cancelable<void>? _deviceAlbumSyncTask;
Cancelable<void>? _hashTask;
BackgroundSyncManager();
@@ -45,6 +46,20 @@ class BackgroundSyncManager {
});
}
// No need to cancel the task, as it can also be run when the user logs out
Future<void> hashAssets() {
if (_hashTask != null) {
return _hashTask!.future;
}
_hashTask = runInIsolateGentle(
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
);
return _hashTask!.whenComplete(() {
_hashTask = null;
});
}
Future<void> syncRemote() {
if (_syncTask != null) {
return _syncTask!.future;

View File

@@ -19,6 +19,7 @@ class Album {
required this.name,
required this.createdAt,
required this.modifiedAt,
this.description,
this.startDate,
this.endDate,
this.lastModifiedAssetTimestamp,
@@ -34,6 +35,7 @@ class Album {
@Index(unique: false, replace: false, type: IndexType.hash)
String? localId;
String name;
String? description;
DateTime createdAt;
DateTime modifiedAt;
DateTime? startDate;
@@ -108,6 +110,7 @@ class Album {
remoteId == other.remoteId &&
localId == other.localId &&
name == other.name &&
description == other.description &&
createdAt.isAtSameMomentAs(other.createdAt) &&
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
isAtSameMomentAs(startDate, other.startDate) &&
@@ -135,6 +138,7 @@ class Album {
modifiedAt.hashCode ^
startDate.hashCode ^
endDate.hashCode ^
description.hashCode ^
lastModifiedAssetTimestamp.hashCode ^
shared.hashCode ^
activityEnabled.hashCode ^
@@ -150,6 +154,7 @@ class Album {
name: dto.albumName,
createdAt: dto.createdAt,
modifiedAt: dto.updatedAt,
description: dto.description,
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
shared: dto.shared,
startDate: dto.startDate,
@@ -184,7 +189,8 @@ class Album {
}
@override
String toString() => name;
String toString() =>
'remoteId: $remoteId name: $name description: $description';
}
extension AssetsHelper on IsarCollection<Album> {

View File

@@ -27,49 +27,54 @@ const AlbumSchema = CollectionSchema(
name: r'createdAt',
type: IsarType.dateTime,
),
r'endDate': PropertySchema(
r'description': PropertySchema(
id: 2,
name: r'description',
type: IsarType.string,
),
r'endDate': PropertySchema(
id: 3,
name: r'endDate',
type: IsarType.dateTime,
),
r'lastModifiedAssetTimestamp': PropertySchema(
id: 3,
id: 4,
name: r'lastModifiedAssetTimestamp',
type: IsarType.dateTime,
),
r'localId': PropertySchema(
id: 4,
id: 5,
name: r'localId',
type: IsarType.string,
),
r'modifiedAt': PropertySchema(
id: 5,
id: 6,
name: r'modifiedAt',
type: IsarType.dateTime,
),
r'name': PropertySchema(
id: 6,
id: 7,
name: r'name',
type: IsarType.string,
),
r'remoteId': PropertySchema(
id: 7,
id: 8,
name: r'remoteId',
type: IsarType.string,
),
r'shared': PropertySchema(
id: 8,
id: 9,
name: r'shared',
type: IsarType.bool,
),
r'sortOrder': PropertySchema(
id: 9,
id: 10,
name: r'sortOrder',
type: IsarType.byte,
enumMap: _AlbumsortOrderEnumValueMap,
),
r'startDate': PropertySchema(
id: 10,
id: 11,
name: r'startDate',
type: IsarType.dateTime,
)
@@ -146,6 +151,12 @@ int _albumEstimateSize(
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.description;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
{
final value = object.localId;
if (value != null) {
@@ -170,15 +181,16 @@ void _albumSerialize(
) {
writer.writeBool(offsets[0], object.activityEnabled);
writer.writeDateTime(offsets[1], object.createdAt);
writer.writeDateTime(offsets[2], object.endDate);
writer.writeDateTime(offsets[3], object.lastModifiedAssetTimestamp);
writer.writeString(offsets[4], object.localId);
writer.writeDateTime(offsets[5], object.modifiedAt);
writer.writeString(offsets[6], object.name);
writer.writeString(offsets[7], object.remoteId);
writer.writeBool(offsets[8], object.shared);
writer.writeByte(offsets[9], object.sortOrder.index);
writer.writeDateTime(offsets[10], object.startDate);
writer.writeString(offsets[2], object.description);
writer.writeDateTime(offsets[3], object.endDate);
writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp);
writer.writeString(offsets[5], object.localId);
writer.writeDateTime(offsets[6], object.modifiedAt);
writer.writeString(offsets[7], object.name);
writer.writeString(offsets[8], object.remoteId);
writer.writeBool(offsets[9], object.shared);
writer.writeByte(offsets[10], object.sortOrder.index);
writer.writeDateTime(offsets[11], object.startDate);
}
Album _albumDeserialize(
@@ -190,16 +202,18 @@ Album _albumDeserialize(
final object = Album(
activityEnabled: reader.readBool(offsets[0]),
createdAt: reader.readDateTime(offsets[1]),
endDate: reader.readDateTimeOrNull(offsets[2]),
lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[3]),
localId: reader.readStringOrNull(offsets[4]),
modifiedAt: reader.readDateTime(offsets[5]),
name: reader.readString(offsets[6]),
remoteId: reader.readStringOrNull(offsets[7]),
shared: reader.readBool(offsets[8]),
sortOrder: _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[9])] ??
SortOrder.desc,
startDate: reader.readDateTimeOrNull(offsets[10]),
description: reader.readStringOrNull(offsets[2]),
endDate: reader.readDateTimeOrNull(offsets[3]),
lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]),
localId: reader.readStringOrNull(offsets[5]),
modifiedAt: reader.readDateTime(offsets[6]),
name: reader.readString(offsets[7]),
remoteId: reader.readStringOrNull(offsets[8]),
shared: reader.readBool(offsets[9]),
sortOrder:
_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ??
SortOrder.desc,
startDate: reader.readDateTimeOrNull(offsets[11]),
);
object.id = id;
return object;
@@ -217,23 +231,25 @@ P _albumDeserializeProp<P>(
case 1:
return (reader.readDateTime(offset)) as P;
case 2:
return (reader.readDateTimeOrNull(offset)) as P;
return (reader.readStringOrNull(offset)) as P;
case 3:
return (reader.readDateTimeOrNull(offset)) as P;
case 4:
return (reader.readStringOrNull(offset)) as P;
return (reader.readDateTimeOrNull(offset)) as P;
case 5:
return (reader.readDateTime(offset)) as P;
case 6:
return (reader.readString(offset)) as P;
case 7:
return (reader.readStringOrNull(offset)) as P;
case 6:
return (reader.readDateTime(offset)) as P;
case 7:
return (reader.readString(offset)) as P;
case 8:
return (reader.readBool(offset)) as P;
return (reader.readStringOrNull(offset)) as P;
case 9:
return (reader.readBool(offset)) as P;
case 10:
return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ??
SortOrder.desc) as P;
case 10:
case 11:
return (reader.readDateTimeOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -535,6 +551,152 @@ extension AlbumQueryFilter on QueryBuilder<Album, Album, QFilterCondition> {
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'description',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'description',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'description',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'description',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'description',
value: '',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'description',
value: '',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> endDateIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@@ -1502,6 +1664,18 @@ extension AlbumQuerySortBy on QueryBuilder<Album, Album, QSortBy> {
});
}
QueryBuilder<Album, Album, QAfterSortBy> sortByDescription() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.asc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> sortByDescriptionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.desc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> sortByEndDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'endDate', Sort.asc);
@@ -1637,6 +1811,18 @@ extension AlbumQuerySortThenBy on QueryBuilder<Album, Album, QSortThenBy> {
});
}
QueryBuilder<Album, Album, QAfterSortBy> thenByDescription() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.asc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> thenByDescriptionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.desc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> thenByEndDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'endDate', Sort.asc);
@@ -1772,6 +1958,13 @@ extension AlbumQueryWhereDistinct on QueryBuilder<Album, Album, QDistinct> {
});
}
QueryBuilder<Album, Album, QDistinct> distinctByDescription(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'description', caseSensitive: caseSensitive);
});
}
QueryBuilder<Album, Album, QDistinct> distinctByEndDate() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'endDate');
@@ -1849,6 +2042,12 @@ extension AlbumQueryProperty on QueryBuilder<Album, Album, QQueryProperty> {
});
}
QueryBuilder<Album, String?, QQueryOperations> descriptionProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'description');
});
}
QueryBuilder<Album, DateTime?, QQueryOperations> endDateProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'endDate');

View File

@@ -1,7 +1,3 @@
import 'dart:typed_data';
import 'package:uuid/parsing.dart';
extension StringExtension on String {
String capitalize() {
return split(" ")
@@ -33,8 +29,3 @@ extension DurationExtension on String {
return int.parse(this);
}
}
extension UUIDExtension on String {
Uint8List toUuidByte({bool shouldValidate = false}) =>
UuidParsing.parseAsByteList(this, validate: shouldValidate);
}

View File

@@ -1,4 +1,7 @@
import 'package:drift/drift.dart' hide Query;
import 'package:immich_mobile/domain/models/exif.model.dart' as domain;
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';
import 'package:isar/isar.dart';
@@ -90,3 +93,53 @@ class ExifInfo {
exposureSeconds: exposureSeconds,
);
}
class RemoteExifEntity extends Table with DriftDefaultsMixin {
const RemoteExifEntity();
TextColumn get assetId =>
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get city => text().nullable()();
TextColumn get state => text().nullable()();
TextColumn get country => text().nullable()();
DateTimeColumn get dateTimeOriginal => dateTime().nullable()();
TextColumn get description => text().nullable()();
IntColumn get height => integer().nullable()();
IntColumn get width => integer().nullable()();
TextColumn get exposureTime => text().nullable()();
IntColumn get fNumber => integer().nullable()();
IntColumn get fileSize => integer().nullable()();
IntColumn get focalLength => integer().nullable()();
IntColumn get latitude => integer().nullable()();
IntColumn get longitude => integer().nullable()();
IntColumn get iso => integer().nullable()();
TextColumn get make => text().nullable()();
TextColumn get model => text().nullable()();
TextColumn get orientation => text().nullable()();
TextColumn get timeZone => text().nullable()();
IntColumn get rating => integer().nullable()();
TextColumn get projectionType => text().nullable()();
@override
Set<Column> get primaryKey => {assetId};
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,8 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin {
TextColumn get name => text()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get backupSelection => intEnum<BackupSelection>()();
BoolColumn get isIosSharedAlbum =>
boolean().withDefault(const Constant(false))();
// Used for mark & sweep
BoolColumn get marker_ => boolean().nullable()();

View File

@@ -14,6 +14,7 @@ typedef $$LocalAlbumEntityTableCreateCompanionBuilder
required String name,
i0.Value<DateTime> updatedAt,
required i2.BackupSelection backupSelection,
i0.Value<bool> isIosSharedAlbum,
i0.Value<bool?> marker_,
});
typedef $$LocalAlbumEntityTableUpdateCompanionBuilder
@@ -22,6 +23,7 @@ typedef $$LocalAlbumEntityTableUpdateCompanionBuilder
i0.Value<String> name,
i0.Value<DateTime> updatedAt,
i0.Value<i2.BackupSelection> backupSelection,
i0.Value<bool> isIosSharedAlbum,
i0.Value<bool?> marker_,
});
@@ -48,6 +50,10 @@ class $$LocalAlbumEntityTableFilterComposer
column: $table.backupSelection,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i0.ColumnFilters<bool> get isIosSharedAlbum => $composableBuilder(
column: $table.isIosSharedAlbum,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<bool> get marker_ => $composableBuilder(
column: $table.marker_, builder: (column) => i0.ColumnFilters(column));
}
@@ -75,6 +81,10 @@ class $$LocalAlbumEntityTableOrderingComposer
column: $table.backupSelection,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<bool> get isIosSharedAlbum => $composableBuilder(
column: $table.isIosSharedAlbum,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<bool> get marker_ => $composableBuilder(
column: $table.marker_, builder: (column) => i0.ColumnOrderings(column));
}
@@ -101,6 +111,9 @@ class $$LocalAlbumEntityTableAnnotationComposer
get backupSelection => $composableBuilder(
column: $table.backupSelection, builder: (column) => column);
i0.GeneratedColumn<bool> get isIosSharedAlbum => $composableBuilder(
column: $table.isIosSharedAlbum, builder: (column) => column);
i0.GeneratedColumn<bool> get marker_ =>
$composableBuilder(column: $table.marker_, builder: (column) => column);
}
@@ -139,6 +152,7 @@ class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<i2.BackupSelection> backupSelection =
const i0.Value.absent(),
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
i0.Value<bool?> marker_ = const i0.Value.absent(),
}) =>
i1.LocalAlbumEntityCompanion(
@@ -146,6 +160,7 @@ class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
name: name,
updatedAt: updatedAt,
backupSelection: backupSelection,
isIosSharedAlbum: isIosSharedAlbum,
marker_: marker_,
),
createCompanionCallback: ({
@@ -153,6 +168,7 @@ class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
required String name,
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
required i2.BackupSelection backupSelection,
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
i0.Value<bool?> marker_ = const i0.Value.absent(),
}) =>
i1.LocalAlbumEntityCompanion.insert(
@@ -160,6 +176,7 @@ class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
name: name,
updatedAt: updatedAt,
backupSelection: backupSelection,
isIosSharedAlbum: isIosSharedAlbum,
marker_: marker_,
),
withReferenceMapper: (p0) => p0
@@ -218,6 +235,16 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
type: i0.DriftSqlType.int, requiredDuringInsert: true)
.withConverter<i2.BackupSelection>(
i1.$LocalAlbumEntityTable.$converterbackupSelection);
static const i0.VerificationMeta _isIosSharedAlbumMeta =
const i0.VerificationMeta('isIosSharedAlbum');
@override
late final i0.GeneratedColumn<bool> isIosSharedAlbum =
i0.GeneratedColumn<bool>('is_ios_shared_album', aliasedName, false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_ios_shared_album" IN (0, 1))'),
defaultValue: const i4.Constant(false));
static const i0.VerificationMeta _marker_Meta =
const i0.VerificationMeta('marker_');
@override
@@ -229,7 +256,7 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
i0.GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))'));
@override
List<i0.GeneratedColumn> get $columns =>
[id, name, updatedAt, backupSelection, marker_];
[id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_];
@override
String get aliasedName => _alias ?? actualTableName;
@override
@@ -256,6 +283,12 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
context.handle(_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
}
if (data.containsKey('is_ios_shared_album')) {
context.handle(
_isIosSharedAlbumMeta,
isIosSharedAlbum.isAcceptableOrUnknown(
data['is_ios_shared_album']!, _isIosSharedAlbumMeta));
}
if (data.containsKey('marker')) {
context.handle(_marker_Meta,
marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta));
@@ -279,6 +312,8 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
.fromSql(attachedDatabase.typeMapping.read(i0.DriftSqlType.int,
data['${effectivePrefix}backup_selection'])!),
isIosSharedAlbum: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool, data['${effectivePrefix}is_ios_shared_album'])!,
marker_: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}marker']),
);
@@ -305,12 +340,14 @@ class LocalAlbumEntityData extends i0.DataClass
final String name;
final DateTime updatedAt;
final i2.BackupSelection backupSelection;
final bool isIosSharedAlbum;
final bool? marker_;
const LocalAlbumEntityData(
{required this.id,
required this.name,
required this.updatedAt,
required this.backupSelection,
required this.isIosSharedAlbum,
this.marker_});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -323,6 +360,7 @@ class LocalAlbumEntityData extends i0.DataClass
.$LocalAlbumEntityTable.$converterbackupSelection
.toSql(backupSelection));
}
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum);
if (!nullToAbsent || marker_ != null) {
map['marker'] = i0.Variable<bool>(marker_);
}
@@ -338,6 +376,7 @@ class LocalAlbumEntityData extends i0.DataClass
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
.fromJson(serializer.fromJson<int>(json['backupSelection'])),
isIosSharedAlbum: serializer.fromJson<bool>(json['isIosSharedAlbum']),
marker_: serializer.fromJson<bool?>(json['marker_']),
);
}
@@ -351,6 +390,7 @@ class LocalAlbumEntityData extends i0.DataClass
'backupSelection': serializer.toJson<int>(i1
.$LocalAlbumEntityTable.$converterbackupSelection
.toJson(backupSelection)),
'isIosSharedAlbum': serializer.toJson<bool>(isIosSharedAlbum),
'marker_': serializer.toJson<bool?>(marker_),
};
}
@@ -360,12 +400,14 @@ class LocalAlbumEntityData extends i0.DataClass
String? name,
DateTime? updatedAt,
i2.BackupSelection? backupSelection,
bool? isIosSharedAlbum,
i0.Value<bool?> marker_ = const i0.Value.absent()}) =>
i1.LocalAlbumEntityData(
id: id ?? this.id,
name: name ?? this.name,
updatedAt: updatedAt ?? this.updatedAt,
backupSelection: backupSelection ?? this.backupSelection,
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
marker_: marker_.present ? marker_.value : this.marker_,
);
LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) {
@@ -376,6 +418,9 @@ class LocalAlbumEntityData extends i0.DataClass
backupSelection: data.backupSelection.present
? data.backupSelection.value
: this.backupSelection,
isIosSharedAlbum: data.isIosSharedAlbum.present
? data.isIosSharedAlbum.value
: this.isIosSharedAlbum,
marker_: data.marker_.present ? data.marker_.value : this.marker_,
);
}
@@ -387,14 +432,15 @@ class LocalAlbumEntityData extends i0.DataClass
..write('name: $name, ')
..write('updatedAt: $updatedAt, ')
..write('backupSelection: $backupSelection, ')
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
..write('marker_: $marker_')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(id, name, updatedAt, backupSelection, marker_);
int get hashCode => Object.hash(
id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -403,6 +449,7 @@ class LocalAlbumEntityData extends i0.DataClass
other.name == this.name &&
other.updatedAt == this.updatedAt &&
other.backupSelection == this.backupSelection &&
other.isIosSharedAlbum == this.isIosSharedAlbum &&
other.marker_ == this.marker_);
}
@@ -412,12 +459,14 @@ class LocalAlbumEntityCompanion
final i0.Value<String> name;
final i0.Value<DateTime> updatedAt;
final i0.Value<i2.BackupSelection> backupSelection;
final i0.Value<bool> isIosSharedAlbum;
final i0.Value<bool?> marker_;
const LocalAlbumEntityCompanion({
this.id = const i0.Value.absent(),
this.name = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.backupSelection = const i0.Value.absent(),
this.isIosSharedAlbum = const i0.Value.absent(),
this.marker_ = const i0.Value.absent(),
});
LocalAlbumEntityCompanion.insert({
@@ -425,6 +474,7 @@ class LocalAlbumEntityCompanion
required String name,
this.updatedAt = const i0.Value.absent(),
required i2.BackupSelection backupSelection,
this.isIosSharedAlbum = const i0.Value.absent(),
this.marker_ = const i0.Value.absent(),
}) : id = i0.Value(id),
name = i0.Value(name),
@@ -434,6 +484,7 @@ class LocalAlbumEntityCompanion
i0.Expression<String>? name,
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? backupSelection,
i0.Expression<bool>? isIosSharedAlbum,
i0.Expression<bool>? marker_,
}) {
return i0.RawValuesInsertable({
@@ -441,6 +492,7 @@ class LocalAlbumEntityCompanion
if (name != null) 'name': name,
if (updatedAt != null) 'updated_at': updatedAt,
if (backupSelection != null) 'backup_selection': backupSelection,
if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum,
if (marker_ != null) 'marker': marker_,
});
}
@@ -450,12 +502,14 @@ class LocalAlbumEntityCompanion
i0.Value<String>? name,
i0.Value<DateTime>? updatedAt,
i0.Value<i2.BackupSelection>? backupSelection,
i0.Value<bool>? isIosSharedAlbum,
i0.Value<bool?>? marker_}) {
return i1.LocalAlbumEntityCompanion(
id: id ?? this.id,
name: name ?? this.name,
updatedAt: updatedAt ?? this.updatedAt,
backupSelection: backupSelection ?? this.backupSelection,
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
marker_: marker_ ?? this.marker_,
);
}
@@ -477,6 +531,9 @@ class LocalAlbumEntityCompanion
.$LocalAlbumEntityTable.$converterbackupSelection
.toSql(backupSelection.value));
}
if (isIosSharedAlbum.present) {
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum.value);
}
if (marker_.present) {
map['marker'] = i0.Variable<bool>(marker_.value);
}
@@ -490,6 +547,7 @@ class LocalAlbumEntityCompanion
..write('name: $name, ')
..write('updatedAt: $updatedAt, ')
..write('backupSelection: $backupSelection, ')
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
..write('marker_: $marker_')
..write(')'))
.toString();

View File

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex(name: 'local_asset_checksum', columns: {#checksum})
@TableIndex(name: 'idx_local_asset_checksum', columns: {#checksum})
class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const LocalAssetEntity();

View File

@@ -231,8 +231,8 @@ typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager<
),
i1.LocalAssetEntityData,
i0.PrefetchHooks Function()>;
i0.Index get localAssetChecksum => i0.Index('local_asset_checksum',
'CREATE INDEX local_asset_checksum ON local_asset_entity (checksum)');
i0.Index get idxLocalAssetChecksum => i0.Index('idx_local_asset_checksum',
'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)');
class $LocalAssetEntityTable extends i3.LocalAssetEntity
with i0.TableInfo<$LocalAssetEntityTable, i1.LocalAssetEntityData> {

View File

@@ -5,11 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class PartnerEntity extends Table with DriftDefaultsMixin {
const PartnerEntity();
BlobColumn get sharedById =>
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get sharedById =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
BlobColumn get sharedWithId =>
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get sharedWithId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
BoolColumn get inTimeline => boolean().withDefault(const Constant(false))();

View File

@@ -3,24 +3,23 @@
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i1;
import 'dart:typed_data' as i2;
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
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 i5;
import 'package:drift/internal/modular.dart' as i6;
as i4;
import 'package:drift/internal/modular.dart' as i5;
typedef $$PartnerEntityTableCreateCompanionBuilder = i1.PartnerEntityCompanion
Function({
required i2.Uint8List sharedById,
required i2.Uint8List sharedWithId,
required String sharedById,
required String sharedWithId,
i0.Value<bool> inTimeline,
});
typedef $$PartnerEntityTableUpdateCompanionBuilder = i1.PartnerEntityCompanion
Function({
i0.Value<i2.Uint8List> sharedById,
i0.Value<i2.Uint8List> sharedWithId,
i0.Value<String> sharedById,
i0.Value<String> sharedWithId,
i0.Value<bool> inTimeline,
});
@@ -29,25 +28,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences<
$$PartnerEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i5.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
static i4.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
i5.ReadDatabaseContainer(db)
.resultSet<i1.$PartnerEntityTable>('partner_entity')
.sharedById,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get sharedById {
final $_column = $_itemColumn<i2.Uint8List>('shared_by_id')!;
i4.$$UserEntityTableProcessedTableManager get sharedById {
final $_column = $_itemColumn<String>('shared_by_id')!;
final manager = i5
final manager = i4
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_sharedByIdTable($_db));
if (item == null) return manager;
@@ -55,25 +54,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences<
manager.$state.copyWith(prefetchedData: [item]));
}
static i5.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
static i4.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
i5.ReadDatabaseContainer(db)
.resultSet<i1.$PartnerEntityTable>('partner_entity')
.sharedWithId,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get sharedWithId {
final $_column = $_itemColumn<i2.Uint8List>('shared_with_id')!;
i4.$$UserEntityTableProcessedTableManager get sharedWithId {
final $_column = $_itemColumn<String>('shared_with_id')!;
final manager = i5
final manager = i4
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_sharedWithIdTable($_db));
if (item == null) return manager;
@@ -94,20 +93,20 @@ class $$PartnerEntityTableFilterComposer
i0.ColumnFilters<bool> get inTimeline => $composableBuilder(
column: $table.inTimeline, builder: (column) => i0.ColumnFilters(column));
i5.$$UserEntityTableFilterComposer get sharedById {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
i4.$$UserEntityTableFilterComposer get sharedById {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedById,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
i4.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -116,20 +115,20 @@ class $$PartnerEntityTableFilterComposer
return composer;
}
i5.$$UserEntityTableFilterComposer get sharedWithId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
i4.$$UserEntityTableFilterComposer get sharedWithId {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedWithId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
i4.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -152,20 +151,20 @@ class $$PartnerEntityTableOrderingComposer
column: $table.inTimeline,
builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get sharedById {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
i4.$$UserEntityTableOrderingComposer get sharedById {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedById,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
i4.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -174,20 +173,20 @@ class $$PartnerEntityTableOrderingComposer
return composer;
}
i5.$$UserEntityTableOrderingComposer get sharedWithId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
i4.$$UserEntityTableOrderingComposer get sharedWithId {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedWithId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
i4.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -209,20 +208,20 @@ class $$PartnerEntityTableAnnotationComposer
i0.GeneratedColumn<bool> get inTimeline => $composableBuilder(
column: $table.inTimeline, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get sharedById {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
i4.$$UserEntityTableAnnotationComposer get sharedById {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedById,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
i4.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -231,20 +230,20 @@ class $$PartnerEntityTableAnnotationComposer
return composer;
}
i5.$$UserEntityTableAnnotationComposer get sharedWithId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
i4.$$UserEntityTableAnnotationComposer get sharedWithId {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedWithId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
i4.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -278,8 +277,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager<
createComputedFieldComposer: () =>
i1.$$PartnerEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<i2.Uint8List> sharedById = const i0.Value.absent(),
i0.Value<i2.Uint8List> sharedWithId = const i0.Value.absent(),
i0.Value<String> sharedById = const i0.Value.absent(),
i0.Value<String> sharedWithId = const i0.Value.absent(),
i0.Value<bool> inTimeline = const i0.Value.absent(),
}) =>
i1.PartnerEntityCompanion(
@@ -288,8 +287,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager<
inTimeline: inTimeline,
),
createCompanionCallback: ({
required i2.Uint8List sharedById,
required i2.Uint8List sharedWithId,
required String sharedById,
required String sharedWithId,
i0.Value<bool> inTimeline = const i0.Value.absent(),
}) =>
i1.PartnerEntityCompanion.insert(
@@ -366,7 +365,7 @@ typedef $$PartnerEntityTableProcessedTableManager = i0.ProcessedTableManager<
i1.PartnerEntityData,
i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})>;
class $PartnerEntityTable extends i3.PartnerEntity
class $PartnerEntityTable extends i2.PartnerEntity
with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@@ -375,18 +374,18 @@ class $PartnerEntityTable extends i3.PartnerEntity
static const i0.VerificationMeta _sharedByIdMeta =
const i0.VerificationMeta('sharedById');
@override
late final i0.GeneratedColumn<i2.Uint8List> sharedById =
i0.GeneratedColumn<i2.Uint8List>('shared_by_id', aliasedName, false,
type: i0.DriftSqlType.blob,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
late final i0.GeneratedColumn<String> sharedById = i0.GeneratedColumn<String>(
'shared_by_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
static const i0.VerificationMeta _sharedWithIdMeta =
const i0.VerificationMeta('sharedWithId');
@override
late final i0.GeneratedColumn<i2.Uint8List> sharedWithId =
i0.GeneratedColumn<i2.Uint8List>('shared_with_id', aliasedName, false,
type: i0.DriftSqlType.blob,
late final i0.GeneratedColumn<String> sharedWithId =
i0.GeneratedColumn<String>('shared_with_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
@@ -399,7 +398,7 @@ class $PartnerEntityTable extends i3.PartnerEntity
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("in_timeline" IN (0, 1))'),
defaultValue: const i4.Constant(false));
defaultValue: const i3.Constant(false));
@override
List<i0.GeneratedColumn> get $columns =>
[sharedById, sharedWithId, inTimeline];
@@ -445,10 +444,10 @@ class $PartnerEntityTable extends i3.PartnerEntity
i1.PartnerEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.PartnerEntityData(
sharedById: attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}shared_by_id'])!,
sharedById: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!,
sharedWithId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.blob, data['${effectivePrefix}shared_with_id'])!,
i0.DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!,
inTimeline: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!,
);
@@ -467,8 +466,8 @@ class $PartnerEntityTable extends i3.PartnerEntity
class PartnerEntityData extends i0.DataClass
implements i0.Insertable<i1.PartnerEntityData> {
final i2.Uint8List sharedById;
final i2.Uint8List sharedWithId;
final String sharedById;
final String sharedWithId;
final bool inTimeline;
const PartnerEntityData(
{required this.sharedById,
@@ -477,8 +476,8 @@ class PartnerEntityData extends i0.DataClass
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['shared_by_id'] = i0.Variable<i2.Uint8List>(sharedById);
map['shared_with_id'] = i0.Variable<i2.Uint8List>(sharedWithId);
map['shared_by_id'] = i0.Variable<String>(sharedById);
map['shared_with_id'] = i0.Variable<String>(sharedWithId);
map['in_timeline'] = i0.Variable<bool>(inTimeline);
return map;
}
@@ -487,8 +486,8 @@ class PartnerEntityData extends i0.DataClass
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return PartnerEntityData(
sharedById: serializer.fromJson<i2.Uint8List>(json['sharedById']),
sharedWithId: serializer.fromJson<i2.Uint8List>(json['sharedWithId']),
sharedById: serializer.fromJson<String>(json['sharedById']),
sharedWithId: serializer.fromJson<String>(json['sharedWithId']),
inTimeline: serializer.fromJson<bool>(json['inTimeline']),
);
}
@@ -496,16 +495,14 @@ class PartnerEntityData extends i0.DataClass
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'sharedById': serializer.toJson<i2.Uint8List>(sharedById),
'sharedWithId': serializer.toJson<i2.Uint8List>(sharedWithId),
'sharedById': serializer.toJson<String>(sharedById),
'sharedWithId': serializer.toJson<String>(sharedWithId),
'inTimeline': serializer.toJson<bool>(inTimeline),
};
}
i1.PartnerEntityData copyWith(
{i2.Uint8List? sharedById,
i2.Uint8List? sharedWithId,
bool? inTimeline}) =>
{String? sharedById, String? sharedWithId, bool? inTimeline}) =>
i1.PartnerEntityData(
sharedById: sharedById ?? this.sharedById,
sharedWithId: sharedWithId ?? this.sharedWithId,
@@ -534,20 +531,19 @@ class PartnerEntityData extends i0.DataClass
}
@override
int get hashCode => Object.hash(i0.$driftBlobEquality.hash(sharedById),
i0.$driftBlobEquality.hash(sharedWithId), inTimeline);
int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.PartnerEntityData &&
i0.$driftBlobEquality.equals(other.sharedById, this.sharedById) &&
i0.$driftBlobEquality.equals(other.sharedWithId, this.sharedWithId) &&
other.sharedById == this.sharedById &&
other.sharedWithId == this.sharedWithId &&
other.inTimeline == this.inTimeline);
}
class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
final i0.Value<i2.Uint8List> sharedById;
final i0.Value<i2.Uint8List> sharedWithId;
final i0.Value<String> sharedById;
final i0.Value<String> sharedWithId;
final i0.Value<bool> inTimeline;
const PartnerEntityCompanion({
this.sharedById = const i0.Value.absent(),
@@ -555,14 +551,14 @@ class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
this.inTimeline = const i0.Value.absent(),
});
PartnerEntityCompanion.insert({
required i2.Uint8List sharedById,
required i2.Uint8List sharedWithId,
required String sharedById,
required String sharedWithId,
this.inTimeline = const i0.Value.absent(),
}) : sharedById = i0.Value(sharedById),
sharedWithId = i0.Value(sharedWithId);
static i0.Insertable<i1.PartnerEntityData> custom({
i0.Expression<i2.Uint8List>? sharedById,
i0.Expression<i2.Uint8List>? sharedWithId,
i0.Expression<String>? sharedById,
i0.Expression<String>? sharedWithId,
i0.Expression<bool>? inTimeline,
}) {
return i0.RawValuesInsertable({
@@ -573,8 +569,8 @@ class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
}
i1.PartnerEntityCompanion copyWith(
{i0.Value<i2.Uint8List>? sharedById,
i0.Value<i2.Uint8List>? sharedWithId,
{i0.Value<String>? sharedById,
i0.Value<String>? sharedWithId,
i0.Value<bool>? inTimeline}) {
return i1.PartnerEntityCompanion(
sharedById: sharedById ?? this.sharedById,
@@ -587,10 +583,10 @@ class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (sharedById.present) {
map['shared_by_id'] = i0.Variable<i2.Uint8List>(sharedById.value);
map['shared_by_id'] = i0.Variable<String>(sharedById.value);
}
if (sharedWithId.present) {
map['shared_with_id'] = i0.Variable<i2.Uint8List>(sharedWithId.value);
map['shared_with_id'] = i0.Variable<String>(sharedWithId.value);
}
if (inTimeline.present) {
map['in_timeline'] = i0.Variable<bool>(inTimeline.value);

View File

@@ -0,0 +1,35 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex(
name: 'UQ_remote_asset_owner_checksum',
columns: {#checksum, #ownerId},
unique: true,
)
class RemoteAssetEntity extends Table
with DriftDefaultsMixin, AssetEntityMixin {
const RemoteAssetEntity();
TextColumn get id => text()();
TextColumn get checksum => text()();
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
TextColumn get ownerId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
DateTimeColumn get localDateTime => dateTime().nullable()();
TextColumn get thumbHash => text().nullable()();
DateTimeColumn get deletedAt => dateTime().nullable()();
IntColumn get visibility => intEnum<AssetVisibility>()();
@override
Set<Column> get primaryKey => {id};
}

File diff suppressed because it is too large Load Diff

View File

@@ -78,7 +78,7 @@ class User {
class UserEntity extends Table with DriftDefaultsMixin {
const UserEntity();
BlobColumn get id => blob()();
TextColumn get id => text()();
TextColumn get name => text()();
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
TextColumn get email => text()();

View File

@@ -3,13 +3,12 @@
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i1;
import 'dart:typed_data' as i2;
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion
Function({
required i2.Uint8List id,
required String id,
required String name,
i0.Value<bool> isAdmin,
required String email,
@@ -20,7 +19,7 @@ typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion
});
typedef $$UserEntityTableUpdateCompanionBuilder = i1.UserEntityCompanion
Function({
i0.Value<i2.Uint8List> id,
i0.Value<String> id,
i0.Value<String> name,
i0.Value<bool> isAdmin,
i0.Value<String> email,
@@ -39,7 +38,7 @@ class $$UserEntityTableFilterComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<i2.Uint8List> get id => $composableBuilder(
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get name => $composableBuilder(
@@ -76,7 +75,7 @@ class $$UserEntityTableOrderingComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<i2.Uint8List> get id => $composableBuilder(
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get name => $composableBuilder(
@@ -114,7 +113,7 @@ class $$UserEntityTableAnnotationComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<i2.Uint8List> get id =>
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get name =>
@@ -167,7 +166,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager<
createComputedFieldComposer: () =>
i1.$$UserEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<i2.Uint8List> id = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(),
i0.Value<bool> isAdmin = const i0.Value.absent(),
i0.Value<String> email = const i0.Value.absent(),
@@ -187,7 +186,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager<
quotaUsageInBytes: quotaUsageInBytes,
),
createCompanionCallback: ({
required i2.Uint8List id,
required String id,
required String name,
i0.Value<bool> isAdmin = const i0.Value.absent(),
required String email,
@@ -230,7 +229,7 @@ typedef $$UserEntityTableProcessedTableManager = i0.ProcessedTableManager<
i1.UserEntityData,
i0.PrefetchHooks Function()>;
class $UserEntityTable extends i3.UserEntity
class $UserEntityTable extends i2.UserEntity
with i0.TableInfo<$UserEntityTable, i1.UserEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@@ -238,9 +237,9 @@ class $UserEntityTable extends i3.UserEntity
$UserEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<i2.Uint8List> id =
i0.GeneratedColumn<i2.Uint8List>('id', aliasedName, false,
type: i0.DriftSqlType.blob, requiredDuringInsert: true);
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _nameMeta =
const i0.VerificationMeta('name');
@override
@@ -256,7 +255,7 @@ class $UserEntityTable extends i3.UserEntity
requiredDuringInsert: false,
defaultConstraints:
i0.GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'),
defaultValue: const i4.Constant(false));
defaultValue: const i3.Constant(false));
static const i0.VerificationMeta _emailMeta =
const i0.VerificationMeta('email');
@override
@@ -276,7 +275,7 @@ class $UserEntityTable extends i3.UserEntity
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime);
defaultValue: i3.currentDateAndTime);
static const i0.VerificationMeta _quotaSizeInBytesMeta =
const i0.VerificationMeta('quotaSizeInBytes');
@override
@@ -290,7 +289,7 @@ class $UserEntityTable extends i3.UserEntity
i0.GeneratedColumn<int>('quota_usage_in_bytes', aliasedName, false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0));
defaultValue: const i3.Constant(0));
@override
List<i0.GeneratedColumn> get $columns => [
id,
@@ -366,7 +365,7 @@ class $UserEntityTable extends i3.UserEntity
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.UserEntityData(
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}id'])!,
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
name: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
isAdmin: attachedDatabase.typeMapping
@@ -397,7 +396,7 @@ class $UserEntityTable extends i3.UserEntity
class UserEntityData extends i0.DataClass
implements i0.Insertable<i1.UserEntityData> {
final i2.Uint8List id;
final String id;
final String name;
final bool isAdmin;
final String email;
@@ -417,7 +416,7 @@ class UserEntityData extends i0.DataClass
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<i2.Uint8List>(id);
map['id'] = i0.Variable<String>(id);
map['name'] = i0.Variable<String>(name);
map['is_admin'] = i0.Variable<bool>(isAdmin);
map['email'] = i0.Variable<String>(email);
@@ -436,7 +435,7 @@ class UserEntityData extends i0.DataClass
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return UserEntityData(
id: serializer.fromJson<i2.Uint8List>(json['id']),
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']),
isAdmin: serializer.fromJson<bool>(json['isAdmin']),
email: serializer.fromJson<String>(json['email']),
@@ -450,7 +449,7 @@ class UserEntityData extends i0.DataClass
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<i2.Uint8List>(id),
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name),
'isAdmin': serializer.toJson<bool>(isAdmin),
'email': serializer.toJson<String>(email),
@@ -462,7 +461,7 @@ class UserEntityData extends i0.DataClass
}
i1.UserEntityData copyWith(
{i2.Uint8List? id,
{String? id,
String? name,
bool? isAdmin,
String? email,
@@ -519,13 +518,13 @@ class UserEntityData extends i0.DataClass
}
@override
int get hashCode => Object.hash(i0.$driftBlobEquality.hash(id), name, isAdmin,
email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes);
int get hashCode => Object.hash(id, name, isAdmin, email, profileImagePath,
updatedAt, quotaSizeInBytes, quotaUsageInBytes);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.UserEntityData &&
i0.$driftBlobEquality.equals(other.id, this.id) &&
other.id == this.id &&
other.name == this.name &&
other.isAdmin == this.isAdmin &&
other.email == this.email &&
@@ -536,7 +535,7 @@ class UserEntityData extends i0.DataClass
}
class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
final i0.Value<i2.Uint8List> id;
final i0.Value<String> id;
final i0.Value<String> name;
final i0.Value<bool> isAdmin;
final i0.Value<String> email;
@@ -555,7 +554,7 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
this.quotaUsageInBytes = const i0.Value.absent(),
});
UserEntityCompanion.insert({
required i2.Uint8List id,
required String id,
required String name,
this.isAdmin = const i0.Value.absent(),
required String email,
@@ -567,7 +566,7 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
name = i0.Value(name),
email = i0.Value(email);
static i0.Insertable<i1.UserEntityData> custom({
i0.Expression<i2.Uint8List>? id,
i0.Expression<String>? id,
i0.Expression<String>? name,
i0.Expression<bool>? isAdmin,
i0.Expression<String>? email,
@@ -589,7 +588,7 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
}
i1.UserEntityCompanion copyWith(
{i0.Value<i2.Uint8List>? id,
{i0.Value<String>? id,
i0.Value<String>? name,
i0.Value<bool>? isAdmin,
i0.Value<String>? email,
@@ -613,7 +612,7 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<i2.Uint8List>(id.value);
map['id'] = i0.Variable<String>(id.value);
}
if (name.present) {
map['name'] = i0.Variable<String>(name.value);

View File

@@ -6,8 +6,8 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class UserMetadataEntity extends Table with DriftDefaultsMixin {
const UserMetadataEntity();
BlobColumn get userId =>
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get userId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get preferences => text().map(userPreferenceConverter)();
@override

View File

@@ -3,23 +3,22 @@
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i1;
import 'dart:typed_data' as i2;
import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i3;
import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'
as i4;
as i3;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
as i4;
import 'package:drift/internal/modular.dart' as i5;
typedef $$UserMetadataEntityTableCreateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
required i2.Uint8List userId,
required i3.UserPreferences preferences,
required String userId,
required i2.UserPreferences preferences,
});
typedef $$UserMetadataEntityTableUpdateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
i0.Value<i2.Uint8List> userId,
i0.Value<i3.UserPreferences> preferences,
i0.Value<String> userId,
i0.Value<i2.UserPreferences> preferences,
});
final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
@@ -29,26 +28,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
$$UserMetadataEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
i5.ReadDatabaseContainer(db)
.resultSet<i1.$UserMetadataEntityTable>(
'user_metadata_entity')
.userId,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<i2.Uint8List>('user_id')!;
i4.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<String>('user_id')!;
final manager = i5
final manager = i4
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_userIdTable($_db));
if (item == null) return manager;
@@ -66,26 +65,26 @@ class $$UserMetadataEntityTableFilterComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnWithTypeConverterFilters<i3.UserPreferences, i3.UserPreferences,
i0.ColumnWithTypeConverterFilters<i2.UserPreferences, i2.UserPreferences,
String>
get preferences => $composableBuilder(
column: $table.preferences,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i5.$$UserEntityTableFilterComposer get userId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
i4.$$UserEntityTableFilterComposer get userId {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
i4.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -108,20 +107,20 @@ class $$UserMetadataEntityTableOrderingComposer
column: $table.preferences,
builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get userId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
i4.$$UserEntityTableOrderingComposer get userId {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
i4.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -140,24 +139,24 @@ class $$UserMetadataEntityTableAnnotationComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumnWithTypeConverter<i3.UserPreferences, String>
i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
get preferences => $composableBuilder(
column: $table.preferences, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get userId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
i4.$$UserEntityTableAnnotationComposer get userId {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
i4.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -193,16 +192,16 @@ class $$UserMetadataEntityTableTableManager extends i0.RootTableManager<
i1.$$UserMetadataEntityTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<i2.Uint8List> userId = const i0.Value.absent(),
i0.Value<i3.UserPreferences> preferences = const i0.Value.absent(),
i0.Value<String> userId = const i0.Value.absent(),
i0.Value<i2.UserPreferences> preferences = const i0.Value.absent(),
}) =>
i1.UserMetadataEntityCompanion(
userId: userId,
preferences: preferences,
),
createCompanionCallback: ({
required i2.Uint8List userId,
required i3.UserPreferences preferences,
required String userId,
required i2.UserPreferences preferences,
}) =>
i1.UserMetadataEntityCompanion.insert(
userId: userId,
@@ -267,7 +266,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager
i1.UserMetadataEntityData,
i0.PrefetchHooks Function({bool userId})>;
class $UserMetadataEntityTable extends i4.UserMetadataEntity
class $UserMetadataEntityTable extends i3.UserMetadataEntity
with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@@ -276,18 +275,18 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
static const i0.VerificationMeta _userIdMeta =
const i0.VerificationMeta('userId');
@override
late final i0.GeneratedColumn<i2.Uint8List> userId =
i0.GeneratedColumn<i2.Uint8List>('user_id', aliasedName, false,
type: i0.DriftSqlType.blob,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
late final i0.GeneratedColumn<String> userId = i0.GeneratedColumn<String>(
'user_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
@override
late final i0.GeneratedColumnWithTypeConverter<i3.UserPreferences, String>
late final i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
preferences = i0.GeneratedColumn<String>(
'preferences', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true)
.withConverter<i3.UserPreferences>(
.withConverter<i2.UserPreferences>(
i1.$UserMetadataEntityTable.$converterpreferences);
@override
List<i0.GeneratedColumn> get $columns => [userId, preferences];
@@ -319,7 +318,7 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.UserMetadataEntityData(
userId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}user_id'])!,
.read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!,
preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!),
@@ -331,8 +330,8 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
return $UserMetadataEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i3.UserPreferences, String, Object?>
$converterpreferences = i4.userPreferenceConverter;
static i0.JsonTypeConverter2<i2.UserPreferences, String, Object?>
$converterpreferences = i3.userPreferenceConverter;
@override
bool get withoutRowId => true;
@override
@@ -341,14 +340,14 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
class UserMetadataEntityData extends i0.DataClass
implements i0.Insertable<i1.UserMetadataEntityData> {
final i2.Uint8List userId;
final i3.UserPreferences preferences;
final String userId;
final i2.UserPreferences preferences;
const UserMetadataEntityData(
{required this.userId, required this.preferences});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['user_id'] = i0.Variable<i2.Uint8List>(userId);
map['user_id'] = i0.Variable<String>(userId);
{
map['preferences'] = i0.Variable<String>(
i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences));
@@ -360,7 +359,7 @@ class UserMetadataEntityData extends i0.DataClass
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return UserMetadataEntityData(
userId: serializer.fromJson<i2.Uint8List>(json['userId']),
userId: serializer.fromJson<String>(json['userId']),
preferences: i1.$UserMetadataEntityTable.$converterpreferences
.fromJson(serializer.fromJson<Object?>(json['preferences'])),
);
@@ -369,7 +368,7 @@ class UserMetadataEntityData extends i0.DataClass
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<i2.Uint8List>(userId),
'userId': serializer.toJson<String>(userId),
'preferences': serializer.toJson<Object?>(i1
.$UserMetadataEntityTable.$converterpreferences
.toJson(preferences)),
@@ -377,7 +376,7 @@ class UserMetadataEntityData extends i0.DataClass
}
i1.UserMetadataEntityData copyWith(
{i2.Uint8List? userId, i3.UserPreferences? preferences}) =>
{String? userId, i2.UserPreferences? preferences}) =>
i1.UserMetadataEntityData(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
@@ -401,31 +400,30 @@ class UserMetadataEntityData extends i0.DataClass
}
@override
int get hashCode =>
Object.hash(i0.$driftBlobEquality.hash(userId), preferences);
int get hashCode => Object.hash(userId, preferences);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.UserMetadataEntityData &&
i0.$driftBlobEquality.equals(other.userId, this.userId) &&
other.userId == this.userId &&
other.preferences == this.preferences);
}
class UserMetadataEntityCompanion
extends i0.UpdateCompanion<i1.UserMetadataEntityData> {
final i0.Value<i2.Uint8List> userId;
final i0.Value<i3.UserPreferences> preferences;
final i0.Value<String> userId;
final i0.Value<i2.UserPreferences> preferences;
const UserMetadataEntityCompanion({
this.userId = const i0.Value.absent(),
this.preferences = const i0.Value.absent(),
});
UserMetadataEntityCompanion.insert({
required i2.Uint8List userId,
required i3.UserPreferences preferences,
required String userId,
required i2.UserPreferences preferences,
}) : userId = i0.Value(userId),
preferences = i0.Value(preferences);
static i0.Insertable<i1.UserMetadataEntityData> custom({
i0.Expression<i2.Uint8List>? userId,
i0.Expression<String>? userId,
i0.Expression<String>? preferences,
}) {
return i0.RawValuesInsertable({
@@ -435,8 +433,7 @@ class UserMetadataEntityCompanion
}
i1.UserMetadataEntityCompanion copyWith(
{i0.Value<i2.Uint8List>? userId,
i0.Value<i3.UserPreferences>? preferences}) {
{i0.Value<String>? userId, i0.Value<i2.UserPreferences>? preferences}) {
return i1.UserMetadataEntityCompanion(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
@@ -447,7 +444,7 @@ class UserMetadataEntityCompanion
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (userId.present) {
map['user_id'] = i0.Variable<i2.Uint8List>(userId.value);
map['user_id'] = i0.Variable<String>(userId.value);
}
if (preferences.present) {
map['preferences'] = i0.Variable<String>(i1

View File

@@ -3,10 +3,12 @@ import 'dart:async';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.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';
@@ -36,6 +38,8 @@ class IsarDatabaseRepository implements IDatabaseRepository {
LocalAlbumEntity,
LocalAssetEntity,
LocalAlbumAssetEntity,
RemoteAssetEntity,
RemoteExifEntity,
],
)
class Drift extends $Drift implements IDatabaseRepository {

View File

@@ -13,6 +13,10 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.d
as i5;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i6;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i7;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i8;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -28,6 +32,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
i5.$LocalAssetEntityTable(this);
late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
i6.$LocalAlbumAssetEntityTable(this);
late final i7.$RemoteAssetEntityTable remoteAssetEntity =
i7.$RemoteAssetEntityTable(this);
late final i8.$RemoteExifEntityTable remoteExifEntity =
i8.$RemoteExifEntityTable(this);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -39,7 +47,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
localAlbumEntity,
localAssetEntity,
localAlbumAssetEntity,
i5.localAssetChecksum
remoteAssetEntity,
remoteExifEntity,
i5.idxLocalAssetChecksum,
i7.uQRemoteAssetOwnerChecksum
];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>
@@ -83,6 +94,20 @@ 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('remote_asset_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete),
],
),
],
);
@override
@@ -105,4 +130,8 @@ class $DriftManager {
i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>
i7.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
i8.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
}

View File

@@ -98,12 +98,24 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
name: localAlbum.name,
updatedAt: Value(localAlbum.updatedAt),
backupSelection: localAlbum.backupSelection,
isIosSharedAlbum: Value(localAlbum.isIosSharedAlbum),
);
return _db.transaction(() async {
await _db.localAlbumEntity
.insertOne(companion, onConflict: DoUpdate((_) => companion));
await _addAssets(localAlbum.id, toUpsert);
if (toUpsert.isNotEmpty) {
await _upsertAssets(toUpsert);
await _db.localAlbumAssetEntity.insertAll(
toUpsert.map(
(a) => LocalAlbumAssetEntityCompanion.insert(
assetId: a.id,
albumId: localAlbum.id,
),
),
mode: InsertMode.insertOrIgnore,
);
}
await _removeAssets(localAlbum.id, toDelete);
});
}
@@ -122,6 +134,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
name: album.name,
updatedAt: Value(album.updatedAt),
backupSelection: album.backupSelection,
isIosSharedAlbum: Value(album.isIosSharedAlbum),
marker_: const Value(null),
);
@@ -226,21 +239,52 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
});
}
Future<void> _addAssets(String albumId, Iterable<LocalAsset> assets) {
if (assets.isEmpty) {
@override
Future<List<LocalAsset>> getAssetsToHash(String albumId) {
final query = _db.localAlbumAssetEntity.select().join(
[
innerJoin(
_db.localAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
),
],
)
..where(
_db.localAlbumAssetEntity.albumId.equals(albumId) &
_db.localAssetEntity.checksum.isNull(),
)
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]);
return query
.map((row) => row.readTable(_db.localAssetEntity).toDto())
.get();
}
Future<void> _upsertAssets(Iterable<LocalAsset> localAssets) {
if (localAssets.isEmpty) {
return Future.value();
}
return transaction(() async {
await _upsertAssets(assets);
await _db.localAlbumAssetEntity.insertAll(
assets.map(
(a) => LocalAlbumAssetEntityCompanion.insert(
assetId: a.id,
albumId: albumId,
return _db.batch((batch) async {
for (final asset in localAssets) {
final companion = LocalAssetEntityCompanion.insert(
name: asset.name,
type: asset.type,
createdAt: Value(asset.createdAt),
updatedAt: Value(asset.updatedAt),
durationInSeconds: Value.absentIfNull(asset.durationInSeconds),
id: asset.id,
checksum: const Value(null),
);
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
_db.localAssetEntity,
companion,
onConflict: DoUpdate(
(_) => companion,
where: (old) => old.updatedAt.isNotValue(asset.updatedAt),
),
),
mode: InsertMode.insertOrIgnore,
);
);
}
});
}
@@ -301,40 +345,14 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
return query.map((row) => row.read(assetId)!).get();
}
Future<void> _upsertAssets(Iterable<LocalAsset> localAssets) {
if (localAssets.isEmpty) {
return Future.value();
}
return _db.batch((batch) async {
batch.insertAllOnConflictUpdate(
_db.localAssetEntity,
localAssets.map(
(a) => LocalAssetEntityCompanion.insert(
name: a.name,
type: a.type,
createdAt: Value(a.createdAt),
updatedAt: Value(a.updatedAt),
durationInSeconds: Value.absentIfNull(a.durationInSeconds),
id: a.id,
checksum: Value.absentIfNull(a.checksum),
),
),
);
});
}
Future<void> _deleteAssets(Iterable<String> ids) {
if (ids.isEmpty) {
return Future.value();
}
return _db.batch(
(batch) => batch.deleteWhere(
_db.localAssetEntity,
(f) => f.id.isIn(ids),
),
);
return _db.batch((batch) {
batch.deleteWhere(_db.localAssetEntity, (f) => f.id.isIn(ids));
});
}
}

View File

@@ -0,0 +1,28 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/interfaces/local_asset.interface.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/repositories/db.repository.dart';
class DriftLocalAssetRepository extends DriftDatabaseRepository
implements ILocalAssetRepository {
final Drift _db;
const DriftLocalAssetRepository(this._db) : super(_db);
@override
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
if (hashes.isEmpty) {
return Future.value();
}
return _db.batch((batch) async {
for (final asset in hashes) {
batch.update(
_db.localAssetEntity,
LocalAssetEntityCompanion(checksum: Value(asset.checksum)),
where: (e) => e.id.equals(asset.id),
);
}
});
}
}

View File

@@ -0,0 +1,31 @@
import 'dart:io';
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';
class StorageRepository implements IStorageRepository {
final _log = Logger('StorageRepository');
@override
Future<File?> getFileForAsset(LocalAsset asset) async {
File? file;
try {
final entity = await AssetEntity.fromId(asset.id);
file = await entity?.originFile;
if (file == null) {
_log.warning(
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
);
}
} catch (error, stackTrace) {
_log.warning(
"Error getting file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
error,
stackTrace,
);
}
return file;
}
}

View File

@@ -5,6 +5,7 @@ import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -105,6 +106,7 @@ class SyncApiRepository implements ISyncApiRepository {
stopwatch.stop();
_logger
.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
DLog.log("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
}
List<SyncEvent> _parseLines(List<String> lines) {

View File

@@ -1,12 +1,14 @@
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
class DriftSyncStreamRepository extends DriftDatabaseRepository
implements ISyncStreamRepository {
@@ -22,7 +24,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
for (final user in data) {
batch.delete(
_db.userEntity,
UserEntityCompanion(id: Value(user.userId.toUuidByte())),
UserEntityCompanion(id: Value(user.userId)),
);
}
});
@@ -44,7 +46,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
batch.insert(
_db.userEntity,
companion.copyWith(id: Value(user.id.toUuidByte())),
companion.copyWith(id: Value(user.id)),
onConflict: DoUpdate((_) => companion),
);
}
@@ -63,8 +65,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
batch.delete(
_db.partnerEntity,
PartnerEntityCompanion(
sharedById: Value(partner.sharedById.toUuidByte()),
sharedWithId: Value(partner.sharedWithId.toUuidByte()),
sharedById: Value(partner.sharedById),
sharedWithId: Value(partner.sharedWithId),
),
);
}
@@ -86,8 +88,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
batch.insert(
_db.partnerEntity,
companion.copyWith(
sharedById: Value(partner.sharedById.toUuidByte()),
sharedWithId: Value(partner.sharedWithId.toUuidByte()),
sharedById: Value(partner.sharedById),
sharedWithId: Value(partner.sharedWithId),
),
onConflict: DoUpdate((_) => companion),
);
@@ -99,36 +101,153 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
}
}
// Assets
@override
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
debugPrint("updateAssetsV1 - ${data.length}");
}
@override
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
debugPrint("deleteAssetsV1 - ${data.length}");
try {
await _deleteAssetsV1(data);
} catch (e, s) {
_logger.severe('Error while processing deleteAssetsV1', e, s);
rethrow;
}
}
// Partner Assets
@override
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
debugPrint("updatePartnerAssetsV1 - ${data.length}");
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
} catch (e, s) {
_logger.severe('Error while processing updateAssetsV1', e, s);
rethrow;
}
}
@override
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
debugPrint("deletePartnerAssetsV1 - ${data.length}");
try {
await _deleteAssetsV1(data);
} catch (e, s) {
_logger.severe('Error while processing deletePartnerAssetsV1', e, s);
rethrow;
}
}
@override
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
} catch (e, s) {
_logger.severe('Error while processing updatePartnerAssetsV1', e, s);
rethrow;
}
}
// EXIF
@override
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
debugPrint("updateAssetsExifV1 - ${data.length}");
try {
await _updateAssetExifV1(data);
} catch (e, s) {
_logger.severe('Error while processing updateAssetsExifV1', e, s);
rethrow;
}
}
@override
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
debugPrint("updatePartnerAssetsExifV1 - ${data.length}");
try {
await _updateAssetExifV1(data);
} catch (e, s) {
_logger.severe('Error while processing updatePartnerAssetsExifV1', e, s);
rethrow;
}
}
Future<void> _updateAssetsV1(Iterable<SyncAssetV1> data) =>
_db.batch((batch) {
for (final asset in data) {
final companion = RemoteAssetEntityCompanion(
name: Value(asset.originalFileName),
type: Value(asset.type.toAssetType()),
createdAt: Value.absentIfNull(asset.fileCreatedAt),
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
durationInSeconds: const Value(0),
checksum: Value(asset.checksum),
isFavorite: Value(asset.isFavorite),
ownerId: Value(asset.ownerId),
localDateTime: Value(asset.localDateTime),
thumbHash: Value(asset.thumbhash),
deletedAt: Value(asset.deletedAt),
visibility: Value(asset.visibility.toAssetVisibility()),
);
batch.insert(
_db.remoteAssetEntity,
companion.copyWith(id: Value(asset.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
Future<void> _deleteAssetsV1(Iterable<SyncAssetDeleteV1> assets) =>
_db.batch((batch) {
for (final asset in assets) {
batch.delete(
_db.remoteAssetEntity,
RemoteAssetEntityCompanion(id: Value(asset.assetId)),
);
}
});
Future<void> _updateAssetExifV1(Iterable<SyncAssetExifV1> data) =>
_db.batch((batch) {
for (final exif in data) {
final companion = RemoteExifEntityCompanion(
city: Value(exif.city),
state: Value(exif.state),
country: Value(exif.country),
dateTimeOriginal: Value(exif.dateTimeOriginal),
description: Value(exif.description),
height: Value(exif.exifImageHeight),
width: Value(exif.exifImageWidth),
exposureTime: Value(exif.exposureTime),
fNumber: Value(exif.fNumber),
fileSize: Value(exif.fileSizeInByte),
focalLength: Value(exif.focalLength),
latitude: Value(exif.latitude),
longitude: Value(exif.longitude),
iso: Value(exif.iso),
make: Value(exif.make),
model: Value(exif.model),
orientation: Value(exif.orientation),
timeZone: Value(exif.timeZone),
rating: Value(exif.rating),
projectionType: Value(exif.projectionType),
);
batch.insert(
_db.remoteExifEntity,
companion.copyWith(assetId: Value(exif.assetId)),
onConflict: DoUpdate((_) => companion),
);
}
});
}
extension on AssetTypeEnum {
AssetType toAssetType() => switch (this) {
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown AssetType value: $this'),
};
}
extension on api.AssetVisibility {
AssetVisibility toAssetVisibility() => switch (this) {
api.AssetVisibility.timeline => AssetVisibility.timeline,
api.AssetVisibility.hidden => AssetVisibility.hidden,
api.AssetVisibility.archive => AssetVisibility.archive,
api.AssetVisibility.locked => AssetVisibility.locked,
_ => throw Exception('Unknown AssetVisibility value: $this'),
};
}

View File

@@ -7,7 +7,8 @@ abstract interface class IDownloadRepository {
void Function(TaskProgressUpdate)? onTaskProgress;
Future<List<TaskRecord>> getLiveVideoTasks();
Future<bool> download(DownloadTask task);
Future<List<bool>> downloadAll(List<DownloadTask> tasks);
Future<bool> cancel(String id);
Future<void> deleteAllTrackingRecords();
Future<void> deleteRecordsWithIds(List<String> id);

View File

@@ -14,7 +14,7 @@ abstract class ITimelineRepository {
Album album,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchAllVideosTimeline();
Stream<RenderList> watchAllVideosTimeline(String userId);
Stream<RenderList> watchHomeTimeline(
String userId,

View File

@@ -18,8 +18,8 @@ import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/theme.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/app_navigation_observer.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/services/local_notification.service.dart';
import 'package:immich_mobile/theme/dynamic_theme.dart';
@@ -89,18 +89,6 @@ Future<void> initApp() async {
initializeTimeZones();
FileDownloader().configureNotification(
running: TaskNotification(
'downloading_media'.tr(),
'file: {filename}',
),
complete: TaskNotification(
'download_finished'.tr(),
'file: {filename}',
),
progressBar: true,
);
await FileDownloader().trackTasksInGroup(
downloadGroupLivePhoto,
markDownloadedComplete: false,
@@ -167,10 +155,27 @@ class ImmichAppState extends ConsumerState<ImmichApp>
await ref.read(localNotificationService).setup();
}
void _configureFileDownloaderNotifications() {
FileDownloader().configureNotification(
running: TaskNotification(
'downloading_media'.tr(),
'${'file_name'.tr()}: {filename}',
),
complete: TaskNotification(
'download_finished'.tr(),
'${'file_name'.tr()}: {filename}',
),
progressBar: true,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
Intl.defaultLocale = context.locale.toLanguageTag();
WidgetsBinding.instance.addPostFrameCallback((_) {
_configureFileDownloaderNotifications();
});
}
@override

View File

@@ -3,18 +3,23 @@ import 'dart:convert';
class AlbumViewerPageState {
final bool isEditAlbum;
final String editTitleText;
final String editDescriptionText;
AlbumViewerPageState({
required this.isEditAlbum,
required this.editTitleText,
required this.editDescriptionText,
});
AlbumViewerPageState copyWith({
bool? isEditAlbum,
String? editTitleText,
String? editDescriptionText,
}) {
return AlbumViewerPageState(
isEditAlbum: isEditAlbum ?? this.isEditAlbum,
editTitleText: editTitleText ?? this.editTitleText,
editDescriptionText: editDescriptionText ?? this.editDescriptionText,
);
}
@@ -23,6 +28,7 @@ class AlbumViewerPageState {
result.addAll({'isEditAlbum': isEditAlbum});
result.addAll({'editTitleText': editTitleText});
result.addAll({'editDescriptionText': editDescriptionText});
return result;
}
@@ -31,6 +37,7 @@ class AlbumViewerPageState {
return AlbumViewerPageState(
isEditAlbum: map['isEditAlbum'] ?? false,
editTitleText: map['editTitleText'] ?? '',
editDescriptionText: map['editDescriptionText'] ?? '',
);
}
@@ -41,7 +48,7 @@ class AlbumViewerPageState {
@override
String toString() =>
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)';
@override
bool operator ==(Object other) {
@@ -49,9 +56,13 @@ class AlbumViewerPageState {
return other is AlbumViewerPageState &&
other.isEditAlbum == isEditAlbum &&
other.editTitleText == editTitleText;
other.editTitleText == editTitleText &&
other.editDescriptionText == editDescriptionText;
}
@override
int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode;
int get hashCode =>
isEditAlbum.hashCode ^
editTitleText.hashCode ^
editDescriptionText.hashCode;
}

View File

@@ -35,7 +35,7 @@ class AssetSelectionState {
@override
String toString() =>
'SelectionAssetState(hasRemote: $hasRemote, hasMerged: $hasMerged, hasMerged: $hasMerged, selectedCount: $selectedCount)';
'SelectionAssetState(hasRemote: $hasRemote, hasLocal: $hasLocal, hasMerged: $hasMerged, selectedCount: $selectedCount)';
@override
bool operator ==(covariant AssetSelectionState other) {

View File

@@ -26,9 +26,9 @@ class AlbumControlButton extends ConsumerWidget {
);
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
padding: const EdgeInsets.only(left: 16.0),
child: SizedBox(
height: 40,
height: 36,
child: ListView(
scrollDirection: Axis.horizontal,
children: [

View File

@@ -30,15 +30,12 @@ class AlbumDateRange extends ConsumerWidget {
final (startDate, endDate, shared) = data;
return Padding(
padding: shared
? const EdgeInsets.only(
left: 16.0,
bottom: 0.0,
)
: const EdgeInsets.only(left: 16.0, bottom: 8.0),
padding: const EdgeInsets.only(left: 16.0),
child: Text(
_getDateRangeText(startDate, endDate),
style: context.textTheme.labelLarge,
style: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
);
}

View File

@@ -0,0 +1,45 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
class AlbumDescription extends ConsumerWidget {
const AlbumDescription({super.key, required this.descriptionFocusNode});
final FocusNode descriptionFocusNode;
@override
Widget build(BuildContext context, WidgetRef ref) {
final userId = ref.watch(authProvider).userId;
final (isOwner, isRemote, albumDescription) = ref.watch(
currentAlbumProvider.select((album) {
if (album == null) {
return const (false, false, '');
}
return (album.ownerId == userId, album.isRemote, album.description);
}),
);
if (isOwner && isRemote) {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: AlbumViewerEditableDescription(
albumDescription: albumDescription ?? 'add_a_description'.tr(),
descriptionFocusNode: descriptionFocusNode,
),
);
}
return Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Text(
albumDescription ?? 'add_a_description'.tr(),
style: context.textTheme.bodyLarge,
),
);
}
}

View File

@@ -36,7 +36,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget {
child: SizedBox(
height: 50,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
padding: const EdgeInsets.only(left: 16, bottom: 8),
scrollDirection: Axis.horizontal,
itemBuilder: ((context, index) {
return Padding(

View File

@@ -19,7 +19,11 @@ class AlbumTitle extends ConsumerWidget {
return const (false, false, '');
}
return (album.ownerId == userId, album.isRemote, album.name);
return (
album.ownerId == userId,
album.isRemote,
album.name,
);
}),
);
@@ -35,7 +39,12 @@ class AlbumTitle extends ConsumerWidget {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Text(albumName, style: context.textTheme.headlineMedium),
child: Text(
albumName,
style: context.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
);
}
}

View File

@@ -10,10 +10,12 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
import 'package:immich_mobile/pages/album/album_control_button.dart';
import 'package:immich_mobile/pages/album/album_date_range.dart';
import 'package:immich_mobile/pages/album/album_description.dart';
import 'package:immich_mobile/pages/album/album_shared_user_icons.dart';
import 'package:immich_mobile/pages/album/album_title.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/timeline.provider.dart';
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
@@ -35,6 +37,7 @@ class AlbumViewer extends HookConsumerWidget {
}
final titleFocusNode = useFocusNode();
final descriptionFocusNode = useFocusNode();
final userId = ref.watch(authProvider).userId;
final isMultiselecting = ref.watch(multiselectProvider);
final isProcessing = useProcessingOverlay();
@@ -93,6 +96,7 @@ class AlbumViewer extends HookConsumerWidget {
onActivitiesPressed() {
if (album.remoteId != null) {
ref.read(currentAssetProvider.notifier).set(null);
context.pushRoute(
const ActivitiesRoute(),
);
@@ -104,23 +108,44 @@ class AlbumViewer extends HookConsumerWidget {
MultiselectGrid(
key: const ValueKey("albumViewerMultiselectGrid"),
renderListProvider: albumTimelineProvider(album.id),
topWidget: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AlbumTitle(
key: const ValueKey("albumTitle"),
titleFocusNode: titleFocusNode,
topWidget: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
context.primaryColor.withValues(alpha: 0.06),
context.primaryColor.withValues(alpha: 0.04),
Colors.indigo.withValues(alpha: 0.02),
Colors.transparent,
],
stops: const [0.0, 0.3, 0.7, 1.0],
),
const AlbumDateRange(),
const AlbumSharedUserIcons(),
if (album.isRemote)
AlbumControlButton(
key: const ValueKey("albumControlButton"),
onAddPhotosPressed: onAddPhotosPressed,
onAddUsersPressed: onAddUsersPressed,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 32),
const AlbumDateRange(),
AlbumTitle(
key: const ValueKey("albumTitle"),
titleFocusNode: titleFocusNode,
),
],
AlbumDescription(
key: const ValueKey("albumDescription"),
descriptionFocusNode: descriptionFocusNode,
),
const AlbumSharedUserIcons(),
if (album.isRemote)
AlbumControlButton(
key: const ValueKey("albumControlButton"),
onAddPhotosPressed: onAddPhotosPressed,
onAddUsersPressed: onAddUsersPressed,
),
const SizedBox(height: 8),
],
),
),
onRemoveFromAlbum: onRemoveFromAlbumPressed,
editEnabled: album.ownerId == userId,
@@ -134,6 +159,7 @@ class AlbumViewer extends HookConsumerWidget {
child: AlbumViewerAppbar(
key: const ValueKey("albumViewerAppbar"),
titleFocusNode: titleFocusNode,
descriptionFocusNode: descriptionFocusNode,
userId: userId,
onAddPhotos: onAddPhotosPressed,
onAddUsers: onAddUsersPressed,

View File

@@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/translation.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
@@ -229,13 +230,11 @@ class AlbumsPage extends HookConsumerWidget {
),
subtitle: sorted[index].ownerId != null
? Text(
'${(sorted[index].assetCount == 1 ? 'album_thumbnail_card_item'.tr() : 'album_thumbnail_card_items'.tr(
namedArgs: {
'count': sorted[index]
.assetCount
.toString(),
},
))} ${sorted[index].ownerId != userId ? 'album_thumbnail_shared_by'.tr(namedArgs: {'user': sorted[index].ownerName!}) : 'owned'.tr()}',
'${t('items_count', {
'count': sorted[index].assetCount,
})} • ${sorted[index].ownerId != userId ? t('shared_by_user', {
'user': sorted[index].ownerName!,
}) : 'owned'.tr()}',
overflow: TextOverflow.ellipsis,
style:
context.textTheme.bodyMedium?.copyWith(

View File

@@ -8,9 +8,11 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_title.provider.dart';
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
import 'package:immich_mobile/widgets/album/album_title_text_field.dart';
import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart';
import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
@RoutePage()
@@ -28,6 +30,7 @@ class CreateAlbumPage extends HookConsumerWidget {
final albumTitleController =
useTextEditingController.fromValue(TextEditingValue.empty);
final albumTitleTextFieldFocusNode = useFocusNode();
final albumDescriptionTextFieldFocusNode = useFocusNode();
final isAlbumTitleTextFieldFocus = useState(false);
final isAlbumTitleEmpty = useState(true);
final selectedAssets = useState<Set<Asset>>(
@@ -36,6 +39,7 @@ class CreateAlbumPage extends HookConsumerWidget {
void onBackgroundTapped() {
albumTitleTextFieldFocusNode.unfocus();
albumDescriptionTextFieldFocusNode.unfocus();
isAlbumTitleTextFieldFocus.value = false;
if (albumTitleController.text.isEmpty) {
@@ -77,6 +81,19 @@ class CreateAlbumPage extends HookConsumerWidget {
);
}
buildDescriptionInputField() {
return Padding(
padding: const EdgeInsets.only(
right: 10,
left: 10,
),
child: AlbumViewerEditableDescription(
albumDescription: '',
descriptionFocusNode: albumDescriptionTextFieldFocusNode,
),
);
}
buildTitle() {
if (selectedAssets.value.isEmpty) {
return SliverToBoxAdapter(
@@ -178,18 +195,18 @@ class CreateAlbumPage extends HookConsumerWidget {
return const SliverToBoxAdapter();
}
createNonSharedAlbum() async {
Future<void> createAlbum() async {
onBackgroundTapped();
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
ref.watch(albumTitleProvider),
ref.read(albumTitleProvider),
selectedAssets.value,
);
if (newAlbum != null) {
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
ref.read(albumProvider.notifier).refreshRemoteAlbums();
selectedAssets.value = {};
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
ref.read(albumTitleProvider.notifier).clearAlbumTitle();
ref.read(albumViewerProvider.notifier).disableEditAlbum();
context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id));
}
}
@@ -211,9 +228,8 @@ class CreateAlbumPage extends HookConsumerWidget {
).tr(),
actions: [
TextButton(
onPressed: albumTitleController.text.isNotEmpty
? createNonSharedAlbum
: null,
onPressed:
albumTitleController.text.isNotEmpty ? createAlbum : null,
child: Text(
'create'.tr(),
style: TextStyle(
@@ -237,10 +253,11 @@ class CreateAlbumPage extends HookConsumerWidget {
pinned: true,
floating: false,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(96.0),
preferredSize: const Size.fromHeight(125.0),
child: Column(
children: [
buildTitleInputField(),
buildDescriptionInputField(),
if (selectedAssets.value.isNotEmpty) buildControlButton(),
],
),

View File

@@ -331,7 +331,10 @@ class GalleryViewerPage extends HookConsumerWidget {
pageController: controller,
scrollPhysics: isZoomed.value
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
: const ClampingScrollPhysics(),
: (Platform.isIOS
? const FastScrollPhysics() // Use bouncing physics for iOS
: const FastClampingScrollPhysics() // Use heavy physics for Android
),
itemCount: totalAssets.value,
scrollDirection: Axis.horizontal,
onPageChanged: (value) {

View File

@@ -30,7 +30,7 @@ enum SettingSection {
"backup_setting_subtitle",
),
languages(
'setting_languages_title',
'language',
Icons.language,
"setting_languages_subtitle",
),

View File

@@ -72,7 +72,9 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
return;
}
context.replaceRoute(const TabControllerRoute());
if (context.router.current.name != ShareIntentRoute.name) {
context.replaceRoute(const TabControllerRoute());
}
final hasPermission =
await ref.read(galleryPermissionNotifier.notifier).hasPermission;

View File

@@ -9,6 +9,7 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
import 'package:immich_mobile/models/folder/root_folder.model.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/folder.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
@@ -219,12 +220,15 @@ class FolderContent extends HookConsumerWidget {
list.allAssets!.isNotEmpty)
...list.allAssets!.map(
(asset) => LargeLeadingTile(
onTap: () => context.pushRoute(
GalleryViewerRoute(
renderList: list,
initialIndex: list.allAssets!.indexOf(asset),
),
),
onTap: () {
ref.read(currentAssetProvider.notifier).set(asset);
context.pushRoute(
GalleryViewerRoute(
renderList: list,
initialIndex: list.allAssets!.indexOf(asset),
),
);
},
leading: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(15),

View File

@@ -133,7 +133,7 @@ class PeopleCollectionPage extends HookConsumerWidget {
);
},
error: (error, stack) => const Text("error"),
loading: () => const CircularProgressIndicator(),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
},

View File

@@ -395,6 +395,7 @@ class _MapWithMarker extends StatelessWidget {
children: [
style.widgetWhen(
onData: (style) => MapLibreMap(
attributionButtonMargins: const Point(8, kToolbarHeight),
initialCameraPosition: CameraPosition(
target: initialLocation ?? const LatLng(0, 0),
zoom: initialLocation != null ? 12 : 0,

View File

@@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/url_helper.dart';
@RoutePage()
@@ -20,6 +21,11 @@ class ShareIntentPage extends HookConsumerWidget {
final currentEndpoint = getServerUrl() ?? '--';
final candidates = ref.watch(shareIntentUploadProvider);
final isUploaded = useState(false);
useOnAppLifecycleStateChange((previous, current) {
if (current == AppLifecycleState.resumed) {
isUploaded.value = false;
}
});
void removeAttachment(ShareIntentAttachment attachment) {
ref.read(shareIntentUploadProvider.notifier).removeAttachment(attachment);
@@ -66,6 +72,14 @@ class ShareIntentPage extends HookConsumerWidget {
),
],
),
leading: IconButton(
onPressed: () {
context.navigateTo(
const TabControllerRoute(),
);
},
icon: const Icon(Icons.arrow_back),
),
),
body: ListView.builder(
itemCount: attachments.length,

View File

@@ -498,4 +498,35 @@ class NativeSyncApi {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAsset>();
}
}
Future<List<Uint8List?>> hashPaths(List<String> paths) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashPaths$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture =
pigeonVar_channel.send(<Object?>[paths]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<Uint8List?>();
}
}
}

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
@@ -15,7 +16,6 @@ abstract final class DLog {
static Stream<List<LogMessage>> watchLog() {
final db = Isar.getInstance();
if (db == null) {
debugPrint('Isar is not initialized');
return const Stream.empty();
}
@@ -30,7 +30,6 @@ abstract final class DLog {
static void clearLog() {
final db = Isar.getInstance();
if (db == null) {
debugPrint('Isar is not initialized');
return;
}
@@ -40,7 +39,9 @@ abstract final class DLog {
}
static void log(String message, [Object? error, StackTrace? stackTrace]) {
debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message');
if (!Platform.environment.containsKey('FLUTTER_TEST')) {
debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message');
}
if (error != null) {
debugPrint('Error: $error');
}
@@ -50,7 +51,6 @@ abstract final class DLog {
final isar = Isar.getInstance();
if (isar == null) {
debugPrint('Isar is not initialized');
return;
}

View File

@@ -26,6 +26,11 @@ final _features = [
icon: Icons.photo_library_rounded,
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true),
),
_Feature(
name: 'Hash Local Assets',
icon: Icons.numbers_outlined,
onTap: (_, ref) => ref.read(backgroundSyncProvider).hashAssets(),
),
_Feature(
name: 'Sync Remote',
icon: Icons.refresh_rounded,
@@ -53,11 +58,38 @@ final _features = [
await db.localAlbumAssetEntity.deleteAll();
},
),
_Feature(
name: 'Clear Remote Data',
icon: Icons.delete_sweep_rounded,
onTap: (_, ref) async {
final db = ref.read(driftProvider);
await db.remoteAssetEntity.deleteAll();
await db.remoteExifEntity.deleteAll();
},
),
_Feature(
name: 'Local Media Summary',
icon: Icons.table_chart_rounded,
onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()),
),
_Feature(
name: 'Remote Media Summary',
icon: Icons.summarize_rounded,
onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()),
),
_Feature(
name: 'Reset Sqlite',
icon: Icons.table_view_rounded,
onTap: (_, ref) async {
final drift = ref.read(driftProvider);
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
final migrator = drift.createMigrator();
for (final entity in drift.allSchemaEntities) {
await migrator.drop(entity);
await migrator.create(entity);
}
},
),
];
@RoutePage()

View File

@@ -1,14 +1,48 @@
// ignore_for_file: prefer-single-widget-per-file
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final _stats = [
class _Stat {
const _Stat({required this.name, required this.load});
final String name;
final Future<int> Function(Drift _) load;
}
class _Summary extends StatelessWidget {
final String name;
final Future<int> countFuture;
const _Summary({required this.name, required this.countFuture});
@override
Widget build(BuildContext context) {
return FutureBuilder<int>(
future: countFuture,
builder: (ctx, snapshot) {
final Widget subtitle;
if (snapshot.connectionState == ConnectionState.waiting) {
subtitle = const CircularProgressIndicator();
} else if (snapshot.hasError) {
subtitle = const Icon(Icons.error_rounded);
} else {
subtitle = Text('${snapshot.data ?? 0}');
}
return ListTile(title: Text(name), trailing: subtitle);
},
);
}
}
final _localStats = [
_Stat(
name: 'Local Assets',
load: (db) => db.managers.localAssetEntity.count(),
@@ -36,11 +70,11 @@ class LocalMediaSummaryPage extends StatelessWidget {
slivers: [
SliverList.builder(
itemBuilder: (_, index) {
final stat = _stats[index];
final stat = _localStats[index];
final countFuture = stat.load(db);
return _Summary(name: stat.name, countFuture: countFuture);
},
itemCount: _stats.length,
itemCount: _localStats.length,
),
SliverToBoxAdapter(
child: Column(
@@ -59,9 +93,8 @@ class LocalMediaSummaryPage extends StatelessWidget {
),
FutureBuilder(
future: albumsFuture,
initialData: <LocalAlbum>[],
builder: (_, snap) {
final albums = snap.data!;
final albums = snap.data ?? [];
if (albums.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
@@ -90,36 +123,43 @@ class LocalMediaSummaryPage extends StatelessWidget {
}
}
// ignore: prefer-single-widget-per-file
class _Summary extends StatelessWidget {
final String name;
final Future<int> countFuture;
final _remoteStats = [
_Stat(
name: 'Remote Assets',
load: (db) => db.managers.remoteAssetEntity.count(),
),
_Stat(
name: 'Exif Entities',
load: (db) => db.managers.remoteExifEntity.count(),
),
];
const _Summary({required this.name, required this.countFuture});
@RoutePage()
class RemoteMediaSummaryPage extends StatelessWidget {
const RemoteMediaSummaryPage({super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder<int>(
future: countFuture,
builder: (ctx, snapshot) {
final Widget subtitle;
return Scaffold(
appBar: AppBar(title: const Text('Remote Media Summary')),
body: Consumer(
builder: (ctx, ref, __) {
final db = ref.watch(driftProvider);
if (snapshot.connectionState == ConnectionState.waiting) {
subtitle = const CircularProgressIndicator();
} else if (snapshot.hasError) {
subtitle = const Icon(Icons.error_rounded);
} else {
subtitle = Text('${snapshot.data ?? 0}');
}
return ListTile(title: Text(name), trailing: subtitle);
},
return CustomScrollView(
slivers: [
SliverList.builder(
itemBuilder: (_, index) {
final stat = _remoteStats[index];
final countFuture = stat.load(db);
return _Summary(name: stat.name, countFuture: countFuture);
},
itemCount: _remoteStats.length,
),
],
);
},
),
);
}
}
class _Stat {
const _Stat({required this.name, required this.load});
final String name;
final Future<int> Function(Drift _) load;
}

View File

@@ -5,7 +5,13 @@ import 'package:immich_mobile/entities/album.entity.dart';
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
AlbumViewerNotifier(this.ref)
: super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
: super(
AlbumViewerPageState(
editTitleText: "",
isEditAlbum: false,
editDescriptionText: "",
),
);
final Ref ref;
@@ -21,12 +27,24 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
state = state.copyWith(editTitleText: newTitle);
}
void setEditDescriptionText(String newDescription) {
state = state.copyWith(editDescriptionText: newDescription);
}
void remoteEditTitleText() {
state = state.copyWith(editTitleText: "");
}
void remoteEditDescriptionText() {
state = state.copyWith(editDescriptionText: "");
}
void resetState() {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
state = state.copyWith(
editTitleText: "",
isEditAlbum: false,
editDescriptionText: "",
);
}
Future<bool> changeAlbumTitle(
@@ -46,6 +64,28 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
return false;
}
Future<bool> changeAlbumDescription(
Album album,
String newAlbumDescription,
) async {
AlbumService service = ref.watch(albumServiceProvider);
bool isSuccess = await service.changeDescriptionAlbum(
album,
newAlbumDescription,
);
if (isSuccess) {
state = state.copyWith(editDescriptionText: "", isEditAlbum: false);
return true;
}
state = state.copyWith(editDescriptionText: "", isEditAlbum: false);
return false;
}
}
final albumViewerProvider =

View File

@@ -140,6 +140,10 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
});
}
Future<List<bool>> downloadAllAsset(List<Asset> assets) async {
return await _downloadService.downloadAll(assets);
}
void downloadAsset(Asset asset, BuildContext context) async {
await _downloadService.download(asset);
}

View File

@@ -0,0 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final localAssetRepository = Provider<ILocalAssetRepository>(
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
);

View File

@@ -0,0 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
final storageRepositoryProvider = Provider<IStorageRepository>(
(ref) => StorageRepository(),
);

View File

@@ -1,13 +1,16 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/hash.service.dart';
import 'package:immich_mobile/domain/services/local_sync.service.dart';
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.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/infrastructure/storage.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
final syncStreamServiceProvider = Provider(
@@ -33,3 +36,12 @@ final localSyncServiceProvider = Provider(
storeService: ref.watch(storeServiceProvider),
),
);
final hashServiceProvider = Provider(
(ref) => HashService(
localAlbumRepository: ref.watch(localAlbumRepository),
localAssetRepository: ref.watch(localAssetRepository),
storageRepository: ref.watch(storageRepositoryProvider),
nativeSyncApi: ref.watch(nativeSyncApiProvider),
),
);

View File

@@ -36,6 +36,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
String name, {
required Iterable<String> assetIds,
Iterable<String> sharedUserIds = const [],
String? description,
}) async {
final users = sharedUserIds.map(
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
@@ -44,6 +45,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
_api.createAlbum(
CreateAlbumDto(
albumName: name,
description: description,
assetIds: assetIds.toList(),
albumUsers: users.toList(),
),
@@ -161,6 +163,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
shared: dto.shared,
startDate: dto.startDate,
description: dto.description,
endDate: dto.endDate,
activityEnabled: dto.isActivityEnabled,
sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc,
@@ -174,6 +177,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
album.sharedUsers.addAll(users.map(entity.User.fromDto));
final assets = dto.assets.map(Asset.remote).toList();
album.assets.addAll(assets);
return album;
}
}

View File

@@ -17,6 +17,30 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
bool get useCustomFilter =>
Store.get(StoreKey.photoManagerCustomFilter, false);
FilterOptionGroup? _getAlbumFilter({
DateTimeCond? updateTimeCond,
bool? containsPathModified,
List<OrderOption>? orderBy,
}) =>
useCustomFilter
? FilterOptionGroup(
imageOption: const FilterOption(
needTitle: true,
sizeConstraint: SizeConstraint(ignoreSize: true),
),
videoOption: const FilterOption(
needTitle: true,
sizeConstraint: SizeConstraint(ignoreSize: true),
durationConstraint: DurationConstraint(allowNullable: true),
),
containsPathModified: containsPathModified ?? false,
createTimeCond: DateTimeCond.def().copyWith(ignore: true),
updateTimeCond:
updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true),
orders: orderBy ?? [],
)
: null;
@override
Future<List<Album>> getAll() async {
final filter = useCustomFilter
@@ -30,7 +54,8 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
@override
Future<List<String>> getAssetIds(String albumId) async {
final album = await AssetPathEntity.fromId(albumId);
final album =
await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
final List<AssetEntity> assets =
await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
return assets.map((e) => e.id).toList();
@@ -38,7 +63,8 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
@override
Future<int> getAssetCount(String albumId) async {
final album = await AssetPathEntity.fromId(albumId);
final album =
await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
return album.assetCountAsync;
}
@@ -53,17 +79,14 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
}) async {
final onDevice = await AssetPathEntity.fromId(
albumId,
filterOption: FilterOptionGroup(
imageOption: const FilterOption(needTitle: true),
videoOption: const FilterOption(needTitle: true),
containsPathModified: true,
filterOption: _getAlbumFilter(
updateTimeCond: modifiedFrom == null && modifiedUntil == null
? null
: DateTimeCond(
min: modifiedFrom ?? DateTime.utc(-271820),
max: modifiedUntil ?? DateTime.utc(275760),
),
orders: orderByModificationDate
orderBy: orderByModificationDate
? [const OrderOption(type: OrderOptionType.updateDate)]
: [],
),
@@ -80,7 +103,10 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
DateTime? modifiedFrom,
DateTime? modifiedUntil,
}) async {
final assetPathEntity = await AssetPathEntity.fromId(id);
final assetPathEntity = await AssetPathEntity.fromId(
id,
filterOption: _getAlbumFilter(containsPathModified: true),
);
return _toAlbum(assetPathEntity);
}

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -8,17 +9,22 @@ import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/interfaces/auth.interface.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
final authRepositoryProvider = Provider<IAuthRepository>(
(ref) => AuthRepository(ref.watch(dbProvider)),
(ref) =>
AuthRepository(ref.watch(dbProvider), drift: ref.watch(driftProvider)),
);
class AuthRepository extends DatabaseRepository implements IAuthRepository {
AuthRepository(super.db);
final Drift _drift;
AuthRepository(super.db, {required Drift drift}) : _drift = drift;
@override
Future<void> clearLocalData() {
@@ -29,6 +35,8 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository {
db.albums.clear(),
db.eTags.clear(),
db.users.clear(),
_drift.remoteAssetEntity.deleteAll(),
_drift.remoteExifEntity.deleteAll(),
]);
});
}

View File

@@ -39,8 +39,8 @@ class DownloadRepository implements IDownloadRepository {
}
@override
Future<bool> download(DownloadTask task) {
return FileDownloader().enqueue(task);
Future<List<bool>> downloadAll(List<DownloadTask> tasks) {
return FileDownloader().enqueueAll(tasks);
}
@override

View File

@@ -98,8 +98,10 @@ class TimelineRepository extends DatabaseRepository
}
@override
Stream<RenderList> watchAllVideosTimeline() {
Stream<RenderList> watchAllVideosTimeline(String userId) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.isTrashedEqualTo(false)
.visibilityEqualTo(AssetVisibilityEnum.timeline)

View File

@@ -64,7 +64,7 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart';
import 'package:immich_mobile/pages/search/search.page.dart';
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
import 'package:immich_mobile/presentation/pages/dev/local_media_stat.page.dart';
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/routing/auth_guard.dart';
@@ -326,5 +326,9 @@ class AppRouter extends RootStackRouter {
page: LocalMediaSummaryRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: RemoteMediaSummaryRoute.page,
guards: [_authGuard, _duplicateGuard],
),
];
}

View File

@@ -1356,6 +1356,22 @@ class RecentlyTakenRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [RemoteMediaSummaryPage]
class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
const RemoteMediaSummaryRoute({List<PageRouteInfo>? children})
: super(RemoteMediaSummaryRoute.name, initialChildren: children);
static const String name = 'RemoteMediaSummaryRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const RemoteMediaSummaryPage();
},
);
}
/// generated route for
/// [SearchPage]
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {

View File

@@ -422,6 +422,25 @@ class AlbumService {
}
}
Future<bool> changeDescriptionAlbum(
Album album,
String newAlbumDescription,
) async {
try {
final updatedAlbum = await _albumApiRepository.update(
album.remoteId!,
description: newAlbumDescription,
);
album.description = updatedAlbum.description;
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error changeDescriptionAlbum ${e.toString()}");
return false;
}
}
Future<Album?> getAlbumByName(
String name, {
bool? remote,

View File

@@ -104,7 +104,7 @@ class AppSettingsService {
return Store.get(setting.storeKey, setting.defaultValue);
}
void setSetting<T>(AppSettingsEnum<T> setting, T value) {
Store.put(setting.storeKey, value);
Future<void> setSetting<T>(AppSettingsEnum<T> setting, T value) {
return Store.put(setting.storeKey, value);
}
}

View File

@@ -159,9 +159,19 @@ class DownloadService {
return await FileDownloader().cancelTaskWithId(id);
}
Future<List<bool>> downloadAll(List<Asset> assets) async {
return await _downloadRepository
.downloadAll(assets.expand(_createDownloadTasks).toList());
}
Future<void> download(Asset asset) async {
final tasks = _createDownloadTasks(asset);
await _downloadRepository.downloadAll(tasks);
}
List<DownloadTask> _createDownloadTasks(Asset asset) {
if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
await _downloadRepository.download(
return [
_buildDownloadTask(
asset.remoteId!,
asset.fileName,
@@ -171,9 +181,6 @@ class DownloadService {
id: asset.remoteId!,
).toJson(),
),
);
await _downloadRepository.download(
_buildDownloadTask(
asset.livePhotoVideoId!,
asset.fileName
@@ -185,16 +192,20 @@ class DownloadService {
id: asset.remoteId!,
).toJson(),
),
);
} else {
await _downloadRepository.download(
_buildDownloadTask(
asset.remoteId!,
asset.fileName,
group: asset.isImage ? downloadGroupImage : downloadGroupVideo,
),
);
];
}
if (asset.remoteId == null) {
return [];
}
return [
_buildDownloadTask(
asset.remoteId!,
asset.fileName,
group: asset.isImage ? downloadGroupImage : downloadGroupVideo,
),
];
}
DownloadTask _buildDownloadTask(

View File

@@ -451,6 +451,7 @@ class SyncService {
final usersToLink = await _userRepository.getByUserIds(userIdsToAdd);
album.name = dto.name;
album.description = dto.description;
album.shared = dto.shared;
album.createdAt = dto.createdAt;
album.modifiedAt = dto.modifiedAt;
@@ -643,6 +644,7 @@ class SyncService {
toUpdate.isEmpty &&
toDelete.isEmpty &&
dbAlbum.name == deviceAlbum.name &&
dbAlbum.description == deviceAlbum.description &&
dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
// changes only affeted excluded albums
_log.info(
@@ -670,6 +672,7 @@ class SyncService {
deleteCandidates.addAll(toDelete);
existing.addAll(existingInDb);
dbAlbum.name = deviceAlbum.name;
dbAlbum.description = deviceAlbum.description;
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
if (dbAlbum.thumbnail.value != null &&
toDelete.contains(dbAlbum.thumbnail.value)) {
@@ -943,6 +946,7 @@ class SyncService {
Album dbAlbum,
) async {
return deviceAlbum.name != dbAlbum.name ||
deviceAlbum.description != dbAlbum.description ||
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
@@ -1101,6 +1105,7 @@ class SyncService {
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
remoteAlbum.name != dbAlbum.name ||
remoteAlbum.description != dbAlbum.description ||
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
remoteAlbum.shared != dbAlbum.shared ||
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||

View File

@@ -75,7 +75,9 @@ class TimelineService {
}
Stream<RenderList> watchAllVideosTimeline() {
return _timelineRepository.watchAllVideosTimeline();
final user = _userService.getMyUser();
return _timelineRepository.watchAllVideosTimeline(user.id);
}
Future<RenderList> getTimelineFromAssets(

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