Compare commits

...

30 Commits

Author SHA1 Message Date
Alex The Bot
a460940430 Version v1.58.0 2023-05-27 21:56:06 +00:00
Alex
fc2455be80 fix(server): missing metadata extraction job (#2586)
* fix(server): missing metadata extraction job

* format

* fix test

* fix test

* Added source to upload

* fix test

* OK! CODE COVERAGE
2023-05-27 16:49:57 -05:00
Michel Heusschen
fd4357cf23 fix(server): exif time extraction (#2583) 2023-05-27 16:24:07 -05:00
Jason Rasmussen
e41e0df27e fix(server): invalid exif date string (#2580) 2023-05-26 21:13:09 -05:00
Michel Heusschen
f370dc3929 fix(web): small style issues (#2578) 2023-05-26 14:44:06 -05:00
Jason Rasmussen
1c2d83e2c7 refactor(server): job handlers (#2572)
* refactor(server): job handlers

* chore: remove comment

* chore: add comments for
2023-05-26 14:43:24 -05:00
Jason Rasmussen
d6756f3d81 feat(web): improved action bar actions (#2553)
* feat(web): improved action bar actions

* Update web/src/lib/components/photos-page/actions/delete-assets.svelte

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* update archive and favorite actions

* feat: add un archive/favorite on associated pages

* fix favorite action + use isAllArchived for photos

* remove unneeded unarchive check

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2023-05-26 08:11:10 -05:00
Fynn Petersen-Frey
71ef7685c5 chore(mobile): update isar (#2571) 2023-05-26 08:09:44 -05:00
Jason Rasmussen
b7516f31c6 refactor(server): delete album (#2570) 2023-05-26 08:04:09 -05:00
Jason Rasmussen
065fb166c2 refactor(server): bull jobs (#2569)
* refactor(server): bull jobs

* chore: add comment

* chore: metadata test coverage

* fix typo

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-26 07:52:52 -05:00
Jason Rasmussen
4cc6e3b966 refactor(server): update album (#2562)
* refactor: update album

* fix: remove unnecessary decorator
2023-05-25 14:37:19 -05:00
Michel Heusschen
1c293a2759 fix(web): back button on person page (#2566) 2023-05-25 11:48:36 -05:00
Michel Heusschen
062e2eca6f feat(web+server): map date filters + small changes (#2565) 2023-05-25 11:47:52 -05:00
Fynn Petersen-Frey
bcc2c34eef feat(mobile): partner sharing (#2541)
* feat(mobile): partner sharing

* getAllAssets for other users

* i18n

* fix tests

* try to fix web tests

* shared with/by confusion

* error logging

* guard against outdated server version
2023-05-24 22:52:43 -05:00
Jason Rasmussen
1613ae9185 feat(web): show assets without thumbs (#2561)
* feat(web): show assets without thumbnails

* chore: open api
2023-05-24 21:13:02 -05:00
Jason Rasmussen
d827a6182b refactor: create album (#2555) 2023-05-24 21:10:45 -05:00
Jason Rasmussen
83df14d379 feat: disk stats for library folder (#2560) 2023-05-24 21:05:31 -05:00
Alex Phillips
7c1dae918d feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support

* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards

* didn't mean to commit default log level during testing

* new sidecar logic for video metadata as well

* Added xml mimetype for sidecars only

* don't need capture group for this regex

* wrong default value reverted

* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway

* simplified setter logic

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>

* simplified logic per suggestions

* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing

* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar

* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync

* simplified logic of filename extraction and asset instantiation

* not sure how that got deleted..

* updated code per suggestions and comments in the PR

* stat was not being used, removed the variable set

* better type checking, using in-scope variables for exif getter instead of passing in every time

* removed commented out test

* ran and resolved all lints, formats, checks, and tests

* resolved suggested change in PR

* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function  for better type checking

* better error handling and moving files back to positions on move or save failure

* regenerated api

* format fixes

* Added XMP documentation

* documentation typo

* Merged in main

* missed merge conflict

* more changes due to a merge

* Resolving conflicts

* added icon for sidecar jobs

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 20:59:30 -05:00
Jonathan Jogenfors
1b54c4f8e7 feat(server): Add support for client-side hashing (#2072)
* Modify controller DTOs

* Can check duplicates on server side

* Remove deviceassetid and deviceid

* Remove device ids from file uploader

* Add db migration for removed device ids

* Don't sanitize checksum

* Convert asset checksum to string

* Make checksum not optional for asset

* Use enums when rejecting duplicates

* Cleanup

* Return of the device id, but optional

* Don't use deviceId for upload folder

* Use checksum in thumb path

* Only use asset id in thumb path

* Openapi generation

* Put deviceAssetId back in asset response dto

* Add missing checksum in test fixture

* Add another missing checksum in test fixture

* Cleanup asset repository

* Add back previous /exists endpoint

* Require checksum to not be null

* Correctly set deviceId in db

* Remove index

* Fix compilation errors

* Make device id nullabel in asset response dto

* Reduce PR scope

* Revert asset service

* Reorder imports

* Reorder imports

* Reduce PR scope

* Reduce PR scope

* Reduce PR scope

* Reduce PR scope

* Reduce PR scope

* Update openapi

* Reduce PR scope

* refactor: asset bulk upload check

* chore: regenreate open-api

* chore: fix tests

* chore: tests

* update migrations and regenerate api

* Feat: use checksum in web file uploader

* Change to wasm-crypto

* Use crypto api for checksumming in web uploader

* Minor cleanup of file upload

* feat(web): pause and resume jobs

* Make device asset id not nullable again

* Cleanup

* Device id not nullable in response dto

* Update API specs

* Bump api specs

* Remove old TODO comment

* Remove NOT NULL constraint on checksum index

* Fix requested pubspec changes

* Remove unneeded import

* Update server/apps/immich/src/api-v1/asset/asset.service.ts

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* Update server/apps/immich/src/api-v1/asset/asset-repository.ts

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* Remove unneeded check

* Update server/apps/immich/src/api-v1/asset/asset-repository.ts

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* Remove hashing in the web uploader

* Cleanup file uploader

* Remove varchar from asset entity fields

* Return 200 from bulk upload check

* Put device asset id back into asset repository

* Merge migrations

* Revert pubspec lock

* Update openapi specs

* Merge upstream changes

* Fix failing asset service tests

* Fix formatting issue

* Cleanup migrations

* Remove newline from pubspec

* Revert newline

* Checkout main version

* Revert again

* Only return AssetCheck

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2023-05-24 16:08:21 -05:00
Jason Rasmussen
49b74e9091 refactor(server): album controller (#2539)
* refactor: album controller/service

* chore: open-api

* fix: tests
2023-05-24 09:30:13 -05:00
Jason Rasmussen
a1f1e5bc37 fix(server): reverse geocoding delete dump logic (#2551) 2023-05-23 20:36:36 -05:00
Jason Rasmussen
2dc8a93685 feat(web): use user layout on admin pages (#2550) 2023-05-23 19:02:12 -05:00
Jason Rasmussen
c2145cbe11 fix: hide album context menu (#2543) 2023-05-23 15:40:32 -05:00
Jason Rasmussen
50a792a81a refactor(server): use cascades for keys and tokens (#2544) 2023-05-23 15:40:04 -05:00
Jason Rasmussen
e2bd7e1e08 feat(web): job tile icons (#2546) 2023-05-23 15:04:24 -05:00
Thomas
11a5a990d0 docker: use default entrypoint and command where applicable (#2529)
A default entrypoint and command make it just a bit easier to use the images as
there is no longer a need for an explicit entrypoint. The exception is the
server image, which still requires the shell script to be specified.
2023-05-23 09:02:47 -05:00
Alex The Bot
ecc894ac82 Version v1.57.1 2023-05-23 09:21:22 +00:00
Michel Heusschen
50b649cd3e fix(web): small fixes for album selection modal (#2527) 2023-05-23 04:15:48 -05:00
Michel Heusschen
99b018cd49 fix(web): loading leaflet in production builds (#2526) 2023-05-23 04:14:00 -05:00
Alex
6aa2800275 chore: post release tasks 2023-05-22 22:43:06 -05:00
242 changed files with 6419 additions and 3172 deletions

View File

@@ -4,7 +4,7 @@ services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:release
entrypoint: ["/bin/sh", "./start-server.sh"]
command: ["start-server.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
@@ -18,7 +18,7 @@ services:
immich-microservices:
container_name: immich_microservices
image: ghcr.io/immich-app/immich-server:release
entrypoint: ["/bin/sh", "./start-microservices.sh"]
command: ["start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
@@ -42,7 +42,6 @@ services:
immich-web:
container_name: immich_web
image: ghcr.io/immich-app/immich-web:release
entrypoint: ["/bin/sh", "./entrypoint.sh"]
env_file:
- .env
restart: always

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,13 @@
# XMP Sidecars
Immich can ingest XMP sidecars on file upload (via the CLI) as well as detect new sidecars that are placed in the filesystem for existing images.
<img src={require('./img/xmp-sidecars.png').default} title='XMP sidecars' />
XMP sidecars are external XML files that contain metadata related to media files. Many applications read and write these files either exclusively or in addition to the metadata written to image files. They can be a powerful tool for editing and storing metadata of a media file without modifying the mdia file itself. When Immich receives or detects an XMP sidecar for a media file, it will attempt to extract the metadata from both the sidecar as well as the media file. It will prioritize the metadata for fields in the sidecar but will fall back and use the metadata in the media file if necessary.
When importing files via the CLI bulk uploader, Immich will automatically detect XMP sidecar files as files that exist next to the original media file and have the exact same name with an additional `.xmp` file extension (i.e., `PXL_20230401_203352928.MP.jpg` and `PXL_20230401_203352928.MP.jpg.xmp`).
There are 2 administrator jobs associated with sidecar files: `SYNC` and `DISCOVER`. The sync job will re-scan all media with existing sidecar files and queue them for a metadata refresh. This is a great use case when third-party applications are used to modify the metadata of media. The discover job will attempt to scan the filesystem for new sidecar files for all media that does not currently have a sidecar file associated with it.
<img src={require('./img/sidecar-jobs.png').default} title='Sidecar Administrator Jobs' />

5
mobile/.gitignore vendored
View File

@@ -49,3 +49,8 @@ app.*.map.json
# Fastlane
ios/fastlane/report.xml
# Isar
default.isar
default.isar.lock
libisar.so

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 80,
"android.injected.version.name" => "1.57.0",
"android.injected.version.code" => 81,
"android.injected.version.name" => "1.58.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -0,0 +1 @@
* Remove Hive box

View File

@@ -5,19 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00032">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000296">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="29.247439">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="64.042552">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="22.794249">
<failure message="/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:42:in `block (2 levels) in parsing_binding&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/bin/fastlane:25:in `load&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Google Api Error: Invalid request - APK specifies a version code that has already been used." />
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.676557">
</testcase>

View File

@@ -257,6 +257,15 @@
"sharing_page_empty_list": "EMPTY LIST",
"sharing_silver_appbar_create_shared_album": "Create shared album",
"sharing_silver_appbar_share_partner": "Share with partner",
"partner_page_title": "Partner",
"partner_page_no_more_users": "No more users to add",
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
"partner_page_shared_to_title": "Shared to",
"partner_page_select_partner": "Select partner",
"partner_page_add_partner": "Add partner",
"partner_page_partner_add_failed": "Failed to add partner",
"partner_page_stop_sharing_title": "Stop sharing your photos?",
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"tab_controller_nav_library": "Library",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Search",

View File

@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 95;
CURRENT_PROJECT_VERSION = 97;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 95;
CURRENT_PROJECT_VERSION = 97;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 95;
CURRENT_PROJECT_VERSION = 97;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -45,11 +45,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.55.0</string>
<string>1.57.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>95</string>
<string>97</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.57.0"
version_number: "1.58.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -5,29 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000282">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000407">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.815995">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.988375">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="30.927419">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="45.42439">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.464698">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.381359">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="66.988561">
<testcase classname="fastlane.lanes" name="4: build_app" time="94.653021">
<failure message="/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:27:in `block (2 levels) in parsing_binding&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/bin/fastlane:25:in `load&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Error packaging up the application" />
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="58.237354">
</testcase>

View File

@@ -20,6 +20,7 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/etag.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/logger_message.model.dart';
import 'package:immich_mobile/shared/models/store.dart';
@@ -89,6 +90,7 @@ Future<Isar> loadDb() async {
BackupAlbumSchema,
DuplicatedAssetSchema,
LoggerMessageSchema,
ETagSchema,
],
directory: dir.path,
maxSizeMiB: 256,

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:isar/isar.dart';
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
@@ -73,7 +74,9 @@ final sharedAlbumProvider =
});
final sharedAlbumDetailProvider =
StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
StreamProvider.family<Album, int>((ref, albumId) async* {
final user = ref.watch(currentUserProvider);
if (user == null) return;
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
await for (final a in sharedAlbumService.watchAlbum(albumId)) {

View File

@@ -2,8 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/services/user.service.dart';
final suggestedSharedUsersProvider =
FutureProvider.autoDispose<List<User>>((ref) {
final otherUsersProvider = FutureProvider.autoDispose<List<User>>((ref) {
UserService userService = ref.watch(userServiceProvider);
return userService.getUsersInDb();

View File

@@ -1,85 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/routing/router.dart';
class SharingSliverAppBar extends StatelessWidget {
const SharingSliverAppBar({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SliverAppBar(
centerTitle: true,
floating: false,
pinned: true,
snap: false,
automaticallyImplyLeading: false,
title: Text(
'IMMICH',
style: TextStyle(
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
fontSize: 22,
color: Theme.of(context).primaryColor,
),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50.0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 4.0),
child: ElevatedButton.icon(
onPressed: () {
AutoRouter.of(context)
.push(CreateAlbumRoute(isSharedAlbum: true));
},
icon: const Icon(
Icons.photo_album_outlined,
size: 20,
),
label: const Text(
"sharing_silver_appbar_create_shared_album",
maxLines: 1,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
// color: Theme.of(context).primaryColor,
),
).tr(),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: ElevatedButton.icon(
onPressed: null,
icon: const Icon(
Icons.swap_horizontal_circle_outlined,
size: 20,
),
label: const Text(
"sharing_silver_appbar_share_partner",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
),
maxLines: 1,
).tr(),
),
),
)
],
),
),
),
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
class AssetSelectionPage extends HookConsumerWidget {
const AssetSelectionPage({
@@ -21,7 +22,8 @@ class AssetSelectionPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final renderList = ref.watch(remoteAssetsProvider);
final currentUser = ref.watch(currentUserProvider);
final renderList = ref.watch(remoteAssetsProvider(currentUser?.isarId));
final selected = useState<Set<Asset>>(existingAssets);
final selectionEnabledHook = useState(true);

View File

@@ -17,7 +17,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<List<User>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider);
ref.watch(otherUsersProvider);
final sharedUsersList = useState<Set<User>>({});
addNewUsersHandler() {

View File

@@ -20,8 +20,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedUsersList = useState<Set<User>>({});
AsyncValue<List<User>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider);
final suggestedShareUsers = ref.watch(otherUsersProvider);
createSharedAlbum() async {
var newAlbum =

View File

@@ -5,10 +5,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
import 'package:immich_mobile/modules/partner/ui/partner_list.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/store.dart' as store;
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
class SharingPage extends HookConsumerWidget {
@@ -17,7 +18,8 @@ class SharingPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
final userId = store.Store.get(store.StoreKey.currentUser).id;
final userId = ref.watch(currentUserProvider)?.id;
final partner = ref.watch(partnerSharedWithProvider);
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
useEffect(
@@ -63,8 +65,7 @@ class SharingPage extends HookConsumerWidget {
final isOwner = album.ownerId == userId;
return ListTile(
contentPadding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: ImmichImage(
@@ -93,7 +94,8 @@ class SharingPage extends HookConsumerWidget {
)
: album.ownerName != null
? Text(
'album_thumbnail_shared_by'.tr(args: [album.ownerName!]),
'album_thumbnail_shared_by'
.tr(args: [album.ownerName!]),
style: const TextStyle(
fontSize: 12.0,
),
@@ -110,6 +112,75 @@ class SharingPage extends HookConsumerWidget {
);
}
buildTopBottons() {
return Padding(
padding: const EdgeInsets.only(
left: 12.0,
right: 12.0,
bottom: 12.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
AutoRouter.of(context)
.push(CreateAlbumRoute(isSharedAlbum: true));
},
icon: const Icon(
Icons.photo_album_outlined,
size: 20,
),
label: const Text(
"sharing_silver_appbar_create_shared_album",
maxLines: 1,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
),
).tr(),
),
),
const SizedBox(width: 12.0),
Expanded(
child: ElevatedButton.icon(
onPressed: () =>
AutoRouter.of(context).push(const PartnerRoute()),
icon: const Icon(
Icons.swap_horizontal_circle_outlined,
size: 20,
),
label: const Text(
"sharing_silver_appbar_share_partner",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
),
maxLines: 1,
).tr(),
),
)
],
),
);
}
AppBar buildAppBar() {
return AppBar(
centerTitle: true,
automaticallyImplyLeading: false,
title: const Text(
'IMMICH',
style: TextStyle(
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
fontSize: 22,
),
),
);
}
buildEmptyListIndication() {
return SliverToBoxAdapter(
child: Padding(
@@ -123,7 +194,6 @@ class SharingPage extends HookConsumerWidget {
width: 0.5,
),
),
// color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Column(
@@ -160,11 +230,27 @@ class SharingPage extends HookConsumerWidget {
}
return Scaffold(
appBar: buildAppBar(),
body: CustomScrollView(
slivers: [
const SharingSliverAppBar(),
SliverToBoxAdapter(child: buildTopBottons()),
if (partner.isNotEmpty)
SliverPadding(
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 4),
sliver: SliverToBoxAdapter(
child: const Text(
"partner_page_title",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
),
if (partner.isNotEmpty) PartnerList(partner: partner),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
padding: EdgeInsets.only(
left: 12,
right: 12,
top: partner.isEmpty ? 0 : 16,
),
sliver: SliverToBoxAdapter(
child: const Text(
"sharing_page_album",

View File

@@ -3,16 +3,18 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:isar/isar.dart';
final archiveProvider = StreamProvider<RenderList>((ref) async* {
final user = ref.watch(currentUserProvider);
if (user == null) return;
final query = ref
.watch(dbProvider)
.assets
.filter()
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.ownerIdEqualTo(user.isarId)
.isArchivedEqualTo(true)
.sortByFileCreatedAt();
final settings = ref.watch(appSettingsServiceProvider);

View File

@@ -4,9 +4,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/asset_description.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/shared/models/store.dart' as store;
class DescriptionInput extends HookConsumerWidget {
DescriptionInput({
@@ -25,9 +25,10 @@ class DescriptionInput extends HookConsumerWidget {
final focusNode = useFocusNode();
final isFocus = useState(false);
final isTextEmpty = useState(controller.text.isEmpty);
final descriptionProvider = ref.watch(assetDescriptionProvider(asset).notifier);
final descriptionProvider =
ref.watch(assetDescriptionProvider(asset).notifier);
final description = ref.watch(assetDescriptionProvider(asset));
final owner = store.Store.get(store.StoreKey.currentUser);
final owner = ref.watch(currentUserProvider);
final hasError = useState(false);
controller.text = description;
@@ -67,7 +68,7 @@ class DescriptionInput extends HookConsumerWidget {
}
return TextField(
enabled: owner.isarId == asset.ownerId,
enabled: owner?.isarId == asset.ownerId,
focusNode: focusNode,
onTap: () => isFocus.value = true,
onChanged: (value) {

View File

@@ -7,7 +7,7 @@ part of 'backup_album.model.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetBackupAlbumCollection on Isar {
IsarCollection<BackupAlbum> get backupAlbums => this.collection();
@@ -45,7 +45,7 @@ const BackupAlbumSchema = CollectionSchema(
getId: _backupAlbumGetId,
getLinks: _backupAlbumGetLinks,
attach: _backupAlbumAttach,
version: '3.0.5',
version: '3.1.0+1',
);
int _backupAlbumEstimateSize(

View File

@@ -7,7 +7,7 @@ part of 'duplicated_asset.model.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetDuplicatedAssetCollection on Isar {
IsarCollection<DuplicatedAsset> get duplicatedAssets => this.collection();
@@ -34,7 +34,7 @@ const DuplicatedAssetSchema = CollectionSchema(
getId: _duplicatedAssetGetId,
getLinks: _duplicatedAssetGetLinks,
attach: _duplicatedAssetAttach,
version: '3.0.5',
version: '3.1.0+1',
);
int _duplicatedAssetEstimateSize(

View File

@@ -3,16 +3,18 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:isar/isar.dart';
final favoriteAssetsProvider = StreamProvider<RenderList>((ref) async* {
final user = ref.watch(currentUserProvider);
if (user == null) return;
final query = ref
.watch(dbProvider)
.assets
.filter()
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.ownerIdEqualTo(user.isarId)
.isFavoriteEqualTo(true)
.sortByFileCreatedAt();
final settings = ref.watch(appSettingsServiceProvider);

View File

@@ -1,47 +1,16 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
class DeleteDialog extends ConsumerWidget {
class DeleteDialog extends ConfirmDialog {
final Function onDelete;
const DeleteDialog({Key? key, required this.onDelete}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return AlertDialog(
// backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
title: const Text("delete_dialog_title").tr(),
content: const Text("delete_dialog_alert").tr(),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
"delete_dialog_cancel",
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(),
),
TextButton(
onPressed: () {
onDelete();
Navigator.of(context).pop();
},
child: Text(
"delete_dialog_ok",
style: TextStyle(
color: Colors.red[400],
fontWeight: FontWeight.bold,
),
).tr(),
),
],
);
}
const DeleteDialog({Key? key, required this.onDelete})
: super(
key: key,
title: "delete_dialog_title",
content: "delete_dialog_alert",
cancel: "delete_dialog_cancel",
ok: "delete_dialog_ok",
onOk: onDelete,
);
}

View File

@@ -20,6 +20,7 @@ import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
@@ -38,6 +39,7 @@ class HomePage extends HookConsumerWidget {
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
final sharedAlbums = ref.watch(sharedAlbumProvider);
final albumService = ref.watch(albumServiceProvider);
final currentUser = ref.watch(currentUserProvider);
final tipOneOpacity = useState(0.0);
final refreshCount = useState(0);
@@ -300,7 +302,7 @@ class HomePage extends HookConsumerWidget {
bottom: false,
child: Stack(
children: [
ref.watch(assetsProvider).when(
ref.watch(assetsProvider(currentUser?.isarId)).when(
data: (data) => data.isEmpty
? buildLoadingIndicator()
: ImmichAssetGrid(

View File

@@ -0,0 +1,50 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';
class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
PartnerSharedWithNotifier(Isar db) : super([]) {
final query = db.users.filter().isPartnerSharedWithEqualTo(true);
query.findAll().then((partners) => state = partners);
query.watch().listen((partners) => state = partners);
}
}
final partnerSharedWithProvider =
StateNotifierProvider<PartnerSharedWithNotifier, List<User>>((ref) {
return PartnerSharedWithNotifier(ref.watch(dbProvider));
});
class PartnerSharedByNotifier extends StateNotifier<List<User>> {
PartnerSharedByNotifier(Isar db) : super([]) {
final query = db.users.filter().isPartnerSharedByEqualTo(true);
query.findAll().then((partners) => state = partners);
streamSub = query.watch().listen((partners) => state = partners);
}
late final StreamSubscription<List<User>> streamSub;
@override
void dispose() {
streamSub.cancel();
super.dispose();
}
}
final partnerSharedByProvider =
StateNotifierProvider<PartnerSharedByNotifier, List<User>>((ref) {
return PartnerSharedByNotifier(ref.watch(dbProvider));
});
final partnerAvailableProvider =
FutureProvider.autoDispose<List<User>>((ref) async {
final otherUsers = await ref.watch(otherUsersProvider.future);
final currentPartners = ref.watch(partnerSharedByProvider);
final available = Set<User>.of(otherUsers);
available.removeAll(currentPartners);
return available.toList();
});

View File

@@ -0,0 +1,72 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
final partnerServiceProvider = Provider(
(ref) => PartnerService(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
),
);
enum PartnerDirection {
sharedWith("shared-with"),
sharedBy("shared-by");
const PartnerDirection(
this._value,
);
final String _value;
}
class PartnerService {
final ApiService _apiService;
final Isar _db;
final Logger _log = Logger("PartnerService");
PartnerService(this._apiService, this._db);
Future<List<User>?> getPartners(PartnerDirection direction) async {
try {
final userDtos =
await _apiService.partnerApi.getPartners(direction._value);
if (userDtos != null) {
return userDtos.map((u) => User.fromDto(u)).toList();
}
} catch (e) {
_log.warning("failed to get partners for direction $direction:\n$e");
}
return null;
}
Future<bool> removePartner(User partner) async {
try {
await _apiService.partnerApi.removePartner(partner.id);
partner.isPartnerSharedBy = false;
await _db.writeTxn(() => _db.users.put(partner));
} catch (e) {
_log.warning("failed to remove partner ${partner.id}:\n$e");
return false;
}
return true;
}
Future<bool> addPartner(User partner) async {
try {
final dto = await _apiService.partnerApi.createPartner(partner.id);
if (dto != null) {
partner.isPartnerSharedBy = true;
await _db.writeTxn(() => _db.users.put(partner));
return true;
}
} catch (e) {
_log.warning("failed to add partner ${partner.id}:\n$e");
}
return false;
}
}

View File

@@ -0,0 +1,30 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/ui/user_avatar.dart';
class PartnerList extends HookConsumerWidget {
const PartnerList({Key? key, required this.partner}) : super(key: key);
final List<User> partner;
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverList(
delegate:
SliverChildBuilderDelegate(listEntry, childCount: partner.length),
);
}
Widget listEntry(BuildContext context, int index) {
final User p = partner[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
leading: userAvatar(context, p, radius: 30),
title: Text("${p.firstName} ${p.lastName}"),
onTap: () => AutoRouter.of(context).push(PartnerDetailRoute(partner: p)),
);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class PartnerDetailPage extends HookConsumerWidget {
const PartnerDetailPage({Key? key, required this.partner}) : super(key: key);
final User partner;
@override
Widget build(BuildContext context, WidgetRef ref) {
final assets = ref.watch(assetsProvider(partner.isarId));
return Scaffold(
appBar: AppBar(
title: Text("${partner.firstName} ${partner.lastName}"),
elevation: 0,
centerTitle: false,
),
body: assets.when(
data: (renderList) => renderList.isEmpty
? Padding(
padding: const EdgeInsets.all(16),
child: Text(
"It seems ${partner.firstName} does not have any photos...\n"
"Or your server version does not match the app version."),
)
: ImmichAssetGrid(
renderList: renderList,
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
),
error: (e, _) => Text("Error loading partners:\n$e"),
loading: () => const Center(child: ImmichLoadingIndicator()),
),
);
}
}

View File

@@ -0,0 +1,160 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
import 'package:immich_mobile/modules/partner/services/partner.service.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/user_avatar.dart';
class PartnerPage extends HookConsumerWidget {
const PartnerPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final List<User> partners = ref.watch(partnerSharedByProvider);
final availableUsers = ref.watch(partnerAvailableProvider);
addNewUsersHandler() async {
final users = availableUsers.value;
if (users == null || users.isEmpty) {
ImmichToast.show(
context: context,
msg: "partner_page_no_more_users".tr(),
);
return;
}
final selectedUser = await showDialog<User>(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text("partner_page_select_partner").tr(),
children: [
for (User u in users)
SimpleDialogOption(
onPressed: () => Navigator.pop(context, u),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: userAvatar(context, u),
),
Text("${u.firstName} ${u.lastName}"),
],
),
)
],
);
},
);
if (selectedUser != null) {
final ok =
await ref.read(partnerServiceProvider).addPartner(selectedUser);
if (ok) {
ref.invalidate(partnerSharedByProvider);
} else {
ImmichToast.show(
context: context,
msg: "partner_page_partner_add_failed".tr(),
toastType: ToastType.error,
);
}
}
}
onDeleteUser(User u) {
return showDialog(
context: context,
builder: (BuildContext context) {
return ConfirmDialog(
title: "partner_page_stop_sharing_title",
content:
"partner_page_stop_sharing_content".tr(args: [u.firstName]),
onOk: () => ref.read(partnerServiceProvider).removePartner(u),
);
},
);
}
buildUserList(List<User> users) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
child: const Text(
"partner_page_shared_to_title",
style: TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold,
),
).tr(),
),
if (users.isNotEmpty)
ListView.builder(
shrinkWrap: true,
itemCount: users.length,
itemBuilder: ((context, index) {
return ListTile(
leading: userAvatar(context, users[index]),
title: Text(
users[index].email,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
trailing: IconButton(
icon: const Icon(Icons.person_remove),
onPressed: () => onDeleteUser(users[index]),
),
);
}),
),
if (users.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: const Text(
"partner_page_empty_message",
style: TextStyle(fontSize: 14),
).tr(),
),
ElevatedButton.icon(
onPressed: availableUsers.whenOrNull(
data: (data) => addNewUsersHandler,
),
icon: const Icon(Icons.person_add),
label: const Text("partner_page_add_partner").tr(),
),
],
),
),
],
);
}
return Scaffold(
appBar: AppBar(
title: const Text("partner_page_title").tr(),
elevation: 0,
centerTitle: false,
actions: [
IconButton(
onPressed:
availableUsers.whenOrNull(data: (data) => addNewUsersHandler),
icon: const Icon(Icons.person_add),
tooltip: "partner_page_add_partner".tr(),
)
],
),
body: buildUserList(partners),
);
}
}

View File

@@ -6,6 +6,8 @@ import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
import 'package:immich_mobile/modules/album/views/library_page.dart';
import 'package:immich_mobile/modules/partner/views/partner_detail_page.dart';
import 'package:immich_mobile/modules/partner/views/partner_page.dart';
import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart';
import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart';
import 'package:immich_mobile/modules/album/views/sharing_page.dart';
@@ -35,6 +37,7 @@ import 'package:immich_mobile/routing/duplicate_guard.dart';
import 'package:immich_mobile/routing/gallery_permission_guard.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/models/logger_message.model.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
@@ -136,6 +139,8 @@ part 'router.gr.dart';
DuplicateGuard,
],
),
AutoRoute(page: PartnerPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: PartnerDetailPage, guards: [AuthGuard, DuplicateGuard])
],
)
class AppRouter extends _$AppRouter {

View File

@@ -256,6 +256,22 @@ class _$AppRouter extends RootStackRouter {
child: const ArchivePage(),
);
},
PartnerRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const PartnerPage(),
);
},
PartnerDetailRoute.name: (routeData) {
final args = routeData.argsAs<PartnerDetailRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData,
child: PartnerDetailPage(
key: args.key,
partner: args.partner,
),
);
},
HomeRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
@@ -523,6 +539,22 @@ class _$AppRouter extends RootStackRouter {
duplicateGuard,
],
),
RouteConfig(
PartnerRoute.name,
path: '/partner-page',
guards: [
authGuard,
duplicateGuard,
],
),
RouteConfig(
PartnerDetailRoute.name,
path: '/partner-detail-page',
guards: [
authGuard,
duplicateGuard,
],
),
];
}
@@ -1113,6 +1145,52 @@ class ArchiveRoute extends PageRouteInfo<void> {
static const String name = 'ArchiveRoute';
}
/// generated route for
/// [PartnerPage]
class PartnerRoute extends PageRouteInfo<void> {
const PartnerRoute()
: super(
PartnerRoute.name,
path: '/partner-page',
);
static const String name = 'PartnerRoute';
}
/// generated route for
/// [PartnerDetailPage]
class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> {
PartnerDetailRoute({
Key? key,
required User partner,
}) : super(
PartnerDetailRoute.name,
path: '/partner-detail-page',
args: PartnerDetailRouteArgs(
key: key,
partner: partner,
),
);
static const String name = 'PartnerDetailRoute';
}
class PartnerDetailRouteArgs {
const PartnerDetailRouteArgs({
this.key,
required this.partner,
});
final Key? key;
final User partner;
@override
String toString() {
return 'PartnerDetailRouteArgs{key: $key, partner: $partner}';
}
}
/// generated route for
/// [HomePage]
class HomeRoute extends PageRouteInfo<void> {

View File

@@ -87,8 +87,8 @@ class Album {
remoteId == other.remoteId &&
localId == other.localId &&
name == other.name &&
createdAt == other.createdAt &&
modifiedAt == other.modifiedAt &&
createdAt.isAtSameMomentAs(other.createdAt) &&
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
shared == other.shared &&
owner.value == other.owner.value &&
thumbnail.value == other.thumbnail.value &&

View File

@@ -7,7 +7,7 @@ part of 'album.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetAlbumCollection on Isar {
IsarCollection<Album> get albums => this.collection();
@@ -111,7 +111,7 @@ const AlbumSchema = CollectionSchema(
getId: _albumGetId,
getLinks: _albumGetLinks,
attach: _albumAttach,
version: '3.0.5',
version: '3.1.0+1',
);
int _albumEstimateSize(

View File

@@ -179,9 +179,9 @@ class Asset {
localId == other.localId &&
deviceId == other.deviceId &&
ownerId == other.ownerId &&
fileCreatedAt == other.fileCreatedAt &&
fileModifiedAt == other.fileModifiedAt &&
updatedAt == other.updatedAt &&
fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) &&
fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) &&
updatedAt.isAtSameMomentAs(other.updatedAt) &&
durationInSeconds == other.durationInSeconds &&
type == other.type &&
width == other.width &&

View File

@@ -7,7 +7,7 @@ part of 'asset.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetAssetCollection on Isar {
IsarCollection<Asset> get assets => this.collection();
@@ -142,7 +142,7 @@ const AssetSchema = CollectionSchema(
getId: _assetGetId,
getLinks: _assetGetLinks,
attach: _assetAttach,
version: '3.0.5',
version: '3.1.0+1',
);
int _assetEstimateSize(

View File

@@ -0,0 +1,13 @@
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
part 'etag.g.dart';
@Collection(inheritance: false)
class ETag {
ETag({required this.id, this.value});
Id get isarId => fastHash(id);
@Index(unique: true, replace: true, type: IndexType.hash)
String id;
String? value;
}

View File

@@ -0,0 +1,724 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'etag.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetETagCollection on Isar {
IsarCollection<ETag> get eTags => this.collection();
}
const ETagSchema = CollectionSchema(
name: r'ETag',
id: -644290296585643859,
properties: {
r'id': PropertySchema(
id: 0,
name: r'id',
type: IsarType.string,
),
r'value': PropertySchema(
id: 1,
name: r'value',
type: IsarType.string,
)
},
estimateSize: _eTagEstimateSize,
serialize: _eTagSerialize,
deserialize: _eTagDeserialize,
deserializeProp: _eTagDeserializeProp,
idName: r'isarId',
indexes: {
r'id': IndexSchema(
id: -3268401673993471357,
name: r'id',
unique: true,
replace: true,
properties: [
IndexPropertySchema(
name: r'id',
type: IndexType.hash,
caseSensitive: true,
)
],
)
},
links: {},
embeddedSchemas: {},
getId: _eTagGetId,
getLinks: _eTagGetLinks,
attach: _eTagAttach,
version: '3.1.0+1',
);
int _eTagEstimateSize(
ETag object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.id.length * 3;
{
final value = object.value;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
void _eTagSerialize(
ETag object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.id);
writer.writeString(offsets[1], object.value);
}
ETag _eTagDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = ETag(
id: reader.readString(offsets[0]),
value: reader.readStringOrNull(offsets[1]),
);
return object;
}
P _eTagDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readString(offset)) as P;
case 1:
return (reader.readStringOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _eTagGetId(ETag object) {
return object.isarId;
}
List<IsarLinkBase<dynamic>> _eTagGetLinks(ETag object) {
return [];
}
void _eTagAttach(IsarCollection<dynamic> col, Id id, ETag object) {}
extension ETagByIndex on IsarCollection<ETag> {
Future<ETag?> getById(String id) {
return getByIndex(r'id', [id]);
}
ETag? getByIdSync(String id) {
return getByIndexSync(r'id', [id]);
}
Future<bool> deleteById(String id) {
return deleteByIndex(r'id', [id]);
}
bool deleteByIdSync(String id) {
return deleteByIndexSync(r'id', [id]);
}
Future<List<ETag?>> getAllById(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return getAllByIndex(r'id', values);
}
List<ETag?> getAllByIdSync(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return getAllByIndexSync(r'id', values);
}
Future<int> deleteAllById(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return deleteAllByIndex(r'id', values);
}
int deleteAllByIdSync(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return deleteAllByIndexSync(r'id', values);
}
Future<Id> putById(ETag object) {
return putByIndex(r'id', object);
}
Id putByIdSync(ETag object, {bool saveLinks = true}) {
return putByIndexSync(r'id', object, saveLinks: saveLinks);
}
Future<List<Id>> putAllById(List<ETag> objects) {
return putAllByIndex(r'id', objects);
}
List<Id> putAllByIdSync(List<ETag> objects, {bool saveLinks = true}) {
return putAllByIndexSync(r'id', objects, saveLinks: saveLinks);
}
}
extension ETagQueryWhereSort on QueryBuilder<ETag, ETag, QWhere> {
QueryBuilder<ETag, ETag, QAfterWhere> anyIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension ETagQueryWhere on QueryBuilder<ETag, ETag, QWhereClause> {
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdEqualTo(Id isarId) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: isarId,
upper: isarId,
));
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdNotEqualTo(Id isarId) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
);
}
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdGreaterThan(Id isarId,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: include),
);
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdLessThan(Id isarId,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: include),
);
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdBetween(
Id lowerIsarId,
Id upperIsarId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: lowerIsarId,
includeLower: includeLower,
upper: upperIsarId,
includeUpper: includeUpper,
));
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> idEqualTo(String id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IndexWhereClause.equalTo(
indexName: r'id',
value: [id],
));
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> idNotEqualTo(String id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(IndexWhereClause.between(
indexName: r'id',
lower: [],
upper: [id],
includeUpper: false,
))
.addWhereClause(IndexWhereClause.between(
indexName: r'id',
lower: [id],
includeLower: false,
upper: [],
));
} else {
return query
.addWhereClause(IndexWhereClause.between(
indexName: r'id',
lower: [id],
includeLower: false,
upper: [],
))
.addWhereClause(IndexWhereClause.between(
indexName: r'id',
lower: [],
upper: [id],
includeUpper: false,
));
}
});
}
}
extension ETagQueryFilter on QueryBuilder<ETag, ETag, QFilterCondition> {
QueryBuilder<ETag, ETag, QAfterFilterCondition> idEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'id',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idBetween(
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'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idContains(String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'id',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idMatches(String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'id',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'id',
value: '',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'id',
value: '',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isarId',
value: value,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'isarId',
value: value,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'isarId',
value: value,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'isarId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'value',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'value',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'value',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'value',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'value',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueBetween(
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'value',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'value',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'value',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueContains(String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'value',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueMatches(String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'value',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'value',
value: '',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'value',
value: '',
));
});
}
}
extension ETagQueryObject on QueryBuilder<ETag, ETag, QFilterCondition> {}
extension ETagQueryLinks on QueryBuilder<ETag, ETag, QFilterCondition> {}
extension ETagQuerySortBy on QueryBuilder<ETag, ETag, QSortBy> {
QueryBuilder<ETag, ETag, QAfterSortBy> sortById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByValue() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByValueDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.desc);
});
}
}
extension ETagQuerySortThenBy on QueryBuilder<ETag, ETag, QSortThenBy> {
QueryBuilder<ETag, ETag, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByIsarIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByValue() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByValueDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.desc);
});
}
}
extension ETagQueryWhereDistinct on QueryBuilder<ETag, ETag, QDistinct> {
QueryBuilder<ETag, ETag, QDistinct> distinctById(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
});
}
QueryBuilder<ETag, ETag, QDistinct> distinctByValue(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'value', caseSensitive: caseSensitive);
});
}
}
extension ETagQueryProperty on QueryBuilder<ETag, ETag, QQueryProperty> {
QueryBuilder<ETag, int, QQueryOperations> isarIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isarId');
});
}
QueryBuilder<ETag, String, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<ETag, String?, QQueryOperations> valueProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'value');
});
}
}

View File

@@ -7,7 +7,7 @@ part of 'exif_info.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetExifInfoCollection on Isar {
IsarCollection<ExifInfo> get exifInfos => this.collection();
@@ -99,7 +99,7 @@ const ExifInfoSchema = CollectionSchema(
getId: _exifInfoGetId,
getLinks: _exifInfoGetLinks,
attach: _exifInfoAttach,
version: '3.0.5',
version: '3.1.0+1',
);
int _exifInfoEstimateSize(

View File

@@ -7,7 +7,7 @@ part of 'logger_message.model.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetLoggerMessageCollection on Isar {
IsarCollection<LoggerMessage> get loggerMessages => this.collection();
@@ -55,7 +55,7 @@ const LoggerMessageSchema = CollectionSchema(
getId: _loggerMessageGetId,
getLinks: _loggerMessageGetLinks,
attach: _loggerMessageAttach,
version: '3.0.5',
version: '3.1.0+1',
);
int _loggerMessageEstimateSize(

View File

@@ -7,7 +7,7 @@ part of 'store.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetStoreValueCollection on Isar {
IsarCollection<StoreValue> get storeValues => this.collection();
@@ -39,7 +39,7 @@ const StoreValueSchema = CollectionSchema(
getId: _storeValueGetId,
getLinks: _storeValueGetLinks,
attach: _storeValueAttach,
version: '3.0.5',
version: '3.1.0+1',
);
int _storeValueEstimateSize(

View File

@@ -14,6 +14,8 @@ class User {
required this.firstName,
required this.lastName,
required this.isAdmin,
this.isPartnerSharedBy = false,
this.isPartnerSharedWith = false,
});
Id get isarId => fastHash(id);
@@ -26,6 +28,8 @@ class User {
email = dto.email,
firstName = dto.firstName,
lastName = dto.lastName,
isPartnerSharedBy = false,
isPartnerSharedWith = false,
isAdmin = dto.isAdmin;
@Index(unique: true, replace: false, type: IndexType.hash)
@@ -34,6 +38,8 @@ class User {
String email;
String firstName;
String lastName;
bool isPartnerSharedBy;
bool isPartnerSharedWith;
bool isAdmin;
@Backlink(to: 'owner')
final IsarLinks<Album> albums = IsarLinks<Album>();
@@ -44,10 +50,12 @@ class User {
bool operator ==(other) {
if (other is! User) return false;
return id == other.id &&
updatedAt == other.updatedAt &&
updatedAt.isAtSameMomentAs(other.updatedAt) &&
email == other.email &&
firstName == other.firstName &&
lastName == other.lastName &&
isPartnerSharedBy == other.isPartnerSharedBy &&
isPartnerSharedWith == other.isPartnerSharedWith &&
isAdmin == other.isAdmin;
}
@@ -59,5 +67,7 @@ class User {
email.hashCode ^
firstName.hashCode ^
lastName.hashCode ^
isPartnerSharedBy.hashCode ^
isPartnerSharedWith.hashCode ^
isAdmin.hashCode;
}

View File

@@ -7,7 +7,7 @@ part of 'user.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetUserCollection on Isar {
IsarCollection<User> get users => this.collection();
@@ -37,13 +37,23 @@ const UserSchema = CollectionSchema(
name: r'isAdmin',
type: IsarType.bool,
),
r'lastName': PropertySchema(
r'isPartnerSharedBy': PropertySchema(
id: 4,
name: r'isPartnerSharedBy',
type: IsarType.bool,
),
r'isPartnerSharedWith': PropertySchema(
id: 5,
name: r'isPartnerSharedWith',
type: IsarType.bool,
),
r'lastName': PropertySchema(
id: 6,
name: r'lastName',
type: IsarType.string,
),
r'updatedAt': PropertySchema(
id: 5,
id: 7,
name: r'updatedAt',
type: IsarType.dateTime,
)
@@ -88,7 +98,7 @@ const UserSchema = CollectionSchema(
getId: _userGetId,
getLinks: _userGetLinks,
attach: _userAttach,
version: '3.0.5',
version: '3.1.0+1',
);
int _userEstimateSize(
@@ -114,8 +124,10 @@ void _userSerialize(
writer.writeString(offsets[1], object.firstName);
writer.writeString(offsets[2], object.id);
writer.writeBool(offsets[3], object.isAdmin);
writer.writeString(offsets[4], object.lastName);
writer.writeDateTime(offsets[5], object.updatedAt);
writer.writeBool(offsets[4], object.isPartnerSharedBy);
writer.writeBool(offsets[5], object.isPartnerSharedWith);
writer.writeString(offsets[6], object.lastName);
writer.writeDateTime(offsets[7], object.updatedAt);
}
User _userDeserialize(
@@ -129,8 +141,10 @@ User _userDeserialize(
firstName: reader.readString(offsets[1]),
id: reader.readString(offsets[2]),
isAdmin: reader.readBool(offsets[3]),
lastName: reader.readString(offsets[4]),
updatedAt: reader.readDateTime(offsets[5]),
isPartnerSharedBy: reader.readBoolOrNull(offsets[4]) ?? false,
isPartnerSharedWith: reader.readBoolOrNull(offsets[5]) ?? false,
lastName: reader.readString(offsets[6]),
updatedAt: reader.readDateTime(offsets[7]),
);
return object;
}
@@ -151,8 +165,12 @@ P _userDeserializeProp<P>(
case 3:
return (reader.readBool(offset)) as P;
case 4:
return (reader.readString(offset)) as P;
return (reader.readBoolOrNull(offset) ?? false) as P;
case 5:
return (reader.readBoolOrNull(offset) ?? false) as P;
case 6:
return (reader.readString(offset)) as P;
case 7:
return (reader.readDateTime(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -741,6 +759,26 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
});
}
QueryBuilder<User, User, QAfterFilterCondition> isPartnerSharedByEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isPartnerSharedBy',
value: value,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> isPartnerSharedWithEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isPartnerSharedWith',
value: value,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> isarIdEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
@@ -1140,6 +1178,30 @@ extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
});
}
QueryBuilder<User, User, QAfterSortBy> sortByIsPartnerSharedBy() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isPartnerSharedBy', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByIsPartnerSharedByDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isPartnerSharedBy', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByIsPartnerSharedWith() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isPartnerSharedWith', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByIsPartnerSharedWithDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isPartnerSharedWith', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByLastName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastName', Sort.asc);
@@ -1214,6 +1276,30 @@ extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
});
}
QueryBuilder<User, User, QAfterSortBy> thenByIsPartnerSharedBy() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isPartnerSharedBy', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByIsPartnerSharedByDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isPartnerSharedBy', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByIsPartnerSharedWith() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isPartnerSharedWith', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByIsPartnerSharedWithDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isPartnerSharedWith', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.asc);
@@ -1279,6 +1365,18 @@ extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> {
});
}
QueryBuilder<User, User, QDistinct> distinctByIsPartnerSharedBy() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isPartnerSharedBy');
});
}
QueryBuilder<User, User, QDistinct> distinctByIsPartnerSharedWith() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isPartnerSharedWith');
});
}
QueryBuilder<User, User, QDistinct> distinctByLastName(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@@ -1324,6 +1422,18 @@ extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> {
});
}
QueryBuilder<User, bool, QQueryOperations> isPartnerSharedByProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isPartnerSharedBy');
});
}
QueryBuilder<User, bool, QQueryOperations> isPartnerSharedWithProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isPartnerSharedWith');
});
}
QueryBuilder<User, String, QQueryOperations> lastNameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'lastName');

View File

@@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/asset.service.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
@@ -10,6 +11,7 @@ import 'package:immich_mobile/modules/settings/providers/app_settings.provider.d
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:immich_mobile/shared/services/user.service.dart';
import 'package:immich_mobile/utils/db.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
@@ -23,6 +25,7 @@ class AssetsState {}
class AssetNotifier extends StateNotifier<AssetsState> {
final AssetService _assetService;
final AlbumService _albumService;
final UserService _userService;
final SyncService _syncService;
final Isar _db;
final log = Logger('AssetNotifier');
@@ -32,6 +35,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
AssetNotifier(
this._assetService,
this._albumService,
this._userService,
this._syncService,
this._db,
) : super(AssetsState());
@@ -51,6 +55,12 @@ class AssetNotifier extends StateNotifier<AssetsState> {
final bool newRemote = await _assetService.refreshRemoteAssets();
final bool newLocal = await _albumService.refreshDeviceAlbums();
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
await _userService.refreshUsers();
final List<User> partners =
await _db.users.filter().isPartnerSharedWithEqualTo(true).findAll();
for (User u in partners) {
await _assetService.refreshRemoteAssets(u);
}
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
} finally {
_getAllAssetInProgress = false;
@@ -147,6 +157,7 @@ final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
return AssetNotifier(
ref.watch(assetServiceProvider),
ref.watch(albumServiceProvider),
ref.watch(userServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(dbProvider),
);
@@ -161,12 +172,14 @@ final assetDetailProvider =
}
});
final assetsProvider = StreamProvider.autoDispose<RenderList>((ref) async* {
final assetsProvider =
StreamProvider.family<RenderList, int?>((ref, userId) async* {
if (userId == null) return;
final query = ref
.watch(dbProvider)
.assets
.filter()
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.ownerIdEqualTo(userId)
.isArchivedEqualTo(false)
.sortByFileCreatedAtDesc();
final settings = ref.watch(appSettingsServiceProvider);
@@ -179,14 +192,15 @@ final assetsProvider = StreamProvider.autoDispose<RenderList>((ref) async* {
});
final remoteAssetsProvider =
StreamProvider.autoDispose<RenderList>((ref) async* {
StreamProvider.family<RenderList, int?>((ref, userId) async* {
if (userId == null) return;
final query = ref
.watch(dbProvider)
.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.ownerIdEqualTo(userId)
.sortByFileCreatedAt();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =

View File

@@ -0,0 +1,26 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
class CurrentUserProvider extends StateNotifier<User?> {
CurrentUserProvider() : super(null) {
state = Store.tryGet(StoreKey.currentUser);
streamSub =
Store.watch(StoreKey.currentUser).listen((user) => state = user);
}
late final StreamSubscription<User?> streamSub;
@override
void dispose() {
streamSub.cancel();
super.dispose();
}
}
final currentUserProvider =
StateNotifierProvider<CurrentUserProvider, User?>((ref) {
return CurrentUserProvider();
});

View File

@@ -16,6 +16,7 @@ class ApiService {
late AssetApi assetApi;
late SearchApi searchApi;
late ServerInfoApi serverInfoApi;
late PartnerApi partnerApi;
ApiService() {
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
@@ -37,6 +38,7 @@ class ApiService {
assetApi = AssetApi(_apiClient);
serverInfoApi = ServerInfoApi(_apiClient);
searchApi = SearchApi(_apiClient);
partnerApi = PartnerApi(_apiClient);
}
Future<String> resolveAndSetEndpoint(String serverUrl) async {

View File

@@ -3,8 +3,10 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/etag.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
@@ -36,37 +38,47 @@ class AssetService {
/// Checks the server for updated assets and updates the local database if
/// required. Returns `true` if there were any changes.
Future<bool> refreshRemoteAssets() async {
Future<bool> refreshRemoteAssets([User? user]) async {
user ??= Store.get(StoreKey.currentUser);
final Stopwatch sw = Stopwatch()..start();
final int numOwnedRemoteAssets = await _db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.ownerIdEqualTo(user!.isarId)
.count();
final bool changes = await _syncService.syncRemoteAssetsToDb(
() async => (await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0))
?.map(Asset.remote)
.toList(),
user,
() async => (await _getRemoteAssets(
hasCache: numOwnedRemoteAssets > 0,
user: user!,
)),
);
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
return changes;
}
/// Returns `null` if the server state did not change, else list of assets
Future<List<AssetResponseDto>?> _getRemoteAssets({
Future<List<Asset>?> _getRemoteAssets({
required bool hasCache,
required User user,
}) async {
try {
final etag = hasCache ? Store.tryGet(StoreKey.assetETag) : null;
final etag = hasCache ? _db.eTags.getByIdSync(user.id)?.value : null;
final (List<AssetResponseDto>? assets, String? newETag) =
await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
await _apiService.assetApi
.getAllAssetsWithETag(eTag: etag, userId: user.id);
if (assets == null) {
return null;
} else if (assets.isNotEmpty && assets.first.ownerId != user.id) {
log.warning("Make sure that server and app versions match!"
" The server returned assets for user ${assets.first.ownerId}"
" while requesting assets of user ${user.id}");
return null;
} else if (newETag != etag) {
Store.put(StoreKey.assetETag, newETag);
_db.writeTxn(() => _db.eTags.put(ETag(id: user.id, value: newETag)));
}
return assets;
return assets.map(Asset.remote).toList();
} catch (e, stack) {
log.severe('Error while getting remote assets', e, stack);
return null;

View File

@@ -40,7 +40,9 @@ class SyncService {
dbUsers,
compare: (User a, User b) => a.id.compareTo(b.id),
both: (User a, User b) {
if (!a.updatedAt.isAtSameMomentAs(b.updatedAt)) {
if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) ||
a.isPartnerSharedBy != b.isPartnerSharedBy ||
a.isPartnerSharedWith != b.isPartnerSharedWith) {
toUpsert.add(a);
return true;
}
@@ -61,9 +63,10 @@ class SyncService {
/// Syncs remote assets owned by the logged-in user to the DB
/// Returns `true` if there were any changes
Future<bool> syncRemoteAssetsToDb(
User user,
FutureOr<List<Asset>?> Function() loadAssets,
) =>
_lock.run(() => _syncRemoteAssetsToDb(loadAssets));
_lock.run(() => _syncRemoteAssetsToDb(user, loadAssets));
/// Syncs remote albums to the database
/// returns `true` if there were any changes
@@ -149,13 +152,13 @@ class SyncService {
/// Syncs remote assets to the databas
/// returns `true` if there were any changes
Future<bool> _syncRemoteAssetsToDb(
User user,
FutureOr<List<Asset>?> Function() loadAssets,
) async {
final List<Asset>? remote = await loadAssets();
if (remote == null) {
return false;
}
final User user = Store.get(StoreKey.currentUser);
final List<Asset> inDb = await _db.assets
.filter()
.ownerIdEqualTo(user.isarId)
@@ -349,10 +352,19 @@ class SyncService {
);
} else if (album.shared) {
final User user = Store.get(StoreKey.currentUser);
// delete assets in DB unless they belong to this user or are part of some other shared album
deleteCandidates.addAll(
await album.assets.filter().not().ownerIdEqualTo(user.isarId).findAll(),
);
// delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner
final userIds = await _db.users
.filter()
.isPartnerSharedWithEqualTo(true)
.isarIdProperty()
.findAll();
userIds.add(user.isarId);
final orphanedAssets = await album.assets
.filter()
.not()
.anyOf(userIds, (q, int id) => q.ownerIdEqualTo(id))
.findAll();
deleteCandidates.addAll(orphanedAssets);
}
try {
final bool ok = await _db.writeTxn(() => _db.albums.delete(album.id));

View File

@@ -1,16 +1,19 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/modules/partner/services/partner.service.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/files_helper.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final userServiceProvider = Provider(
@@ -18,6 +21,7 @@ final userServiceProvider = Provider(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(syncServiceProvider),
ref.watch(partnerServiceProvider),
),
);
@@ -25,15 +29,22 @@ class UserService {
final ApiService _apiService;
final Isar _db;
final SyncService _syncService;
final PartnerService _partnerService;
final Logger _log = Logger("UserService");
UserService(this._apiService, this._db, this._syncService);
UserService(
this._apiService,
this._db,
this._syncService,
this._partnerService,
);
Future<List<User>?> _getAllUsers({required bool isAll}) async {
try {
final dto = await _apiService.userApi.getAllUsers(isAll);
return dto?.map(User.fromDto).toList();
} catch (e) {
debugPrint("Error [getAllUsersInfo] ${e.toString()}");
_log.warning("Failed get all users:\n$e");
return null;
}
}
@@ -62,16 +73,45 @@ class UserService {
),
);
} catch (e) {
debugPrint("Error [uploadProfileImage] ${e.toString()}");
_log.warning("Failed to upload profile image:\n$e");
return null;
}
}
Future<bool> refreshUsers() async {
final List<User>? users = await _getAllUsers(isAll: true);
if (users == null) {
final List<User>? sharedBy =
await _partnerService.getPartners(PartnerDirection.sharedBy);
final List<User>? sharedWith =
await _partnerService.getPartners(PartnerDirection.sharedWith);
if (users == null || sharedBy == null || sharedWith == null) {
_log.warning("Failed to refresh users");
return false;
}
users.sortBy((u) => u.id);
sharedBy.sortBy((u) => u.id);
sharedWith.sortBy((u) => u.id);
diffSortedListsSync(
users,
sharedBy,
compare: (User a, User b) => a.id.compareTo(b.id),
both: (User a, User b) => a.isPartnerSharedBy = true,
onlyFirst: (_) {},
onlySecond: (_) {},
);
diffSortedListsSync(
users,
sharedWith,
compare: (User a, User b) => a.id.compareTo(b.id),
both: (User a, User b) => a.isPartnerSharedWith = true,
onlyFirst: (_) {},
onlySecond: (_) {},
);
return _syncService.syncUsersFromServer(users);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ConfirmDialog extends ConsumerWidget {
final Function onOk;
final String title;
final String content;
final String cancel;
final String ok;
const ConfirmDialog({
Key? key,
required this.onOk,
required this.title,
required this.content,
this.cancel = "delete_dialog_cancel",
this.ok = "backup_controller_page_background_battery_info_ok",
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
title: Text(title).tr(),
content: Text(content).tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
cancel,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(),
),
TextButton(
onPressed: () {
onOk();
Navigator.of(context).pop();
},
child: Text(
ok,
style: TextStyle(
color: Colors.red[400],
fontWeight: FontWeight.bold,
),
).tr(),
),
],
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
Widget userAvatar(BuildContext context, User u, {double? radius}) {
final url =
"${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${u.id}";
return CircleAvatar(
radius: radius,
backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
foregroundImage: CachedNetworkImageProvider(
url,
headers: {"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"},
cacheKey: "user-${u.id}-profile",
),
// silence errors if user has no profile image, use initials as fallback
onForegroundImageError: (exception, stackTrace) {},
child: Text((u.firstName[0] + u.lastName[0]).toUpperCase()),
);
}

View File

@@ -1,7 +1,9 @@
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/etag.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:isar/isar.dart';
Future<void> clearAssetsAndAlbums(Isar db) async {
@@ -10,5 +12,7 @@ Future<void> clearAssetsAndAlbums(Isar db) async {
await db.assets.clear();
await db.exifInfos.clear();
await db.albums.clear();
await db.eTags.clear();
await db.users.clear();
});
}

View File

@@ -14,9 +14,11 @@ extension WithETag on AssetApi {
/// ETag of data already cached on the client
Future<(List<AssetResponseDto>? assets, String? eTag)> getAllAssetsWithETag({
String? eTag,
String? userId,
}) async {
final response = await getAllAssetsWithHttpInfo(
ifNoneMatch: eTag,
userId: userId,
);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));

View File

@@ -17,6 +17,10 @@ doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md
doc/AllJobStatusResponseDto.md
doc/AssetApi.md
doc/AssetBulkUploadCheckDto.md
doc/AssetBulkUploadCheckItem.md
doc/AssetBulkUploadCheckResponseDto.md
doc/AssetBulkUploadCheckResult.md
doc/AssetCountByTimeBucket.md
doc/AssetCountByTimeBucketResponseDto.md
doc/AssetCountByUserIdResponseDto.md
@@ -142,6 +146,10 @@ lib/model/api_key_create_dto.dart
lib/model/api_key_create_response_dto.dart
lib/model/api_key_response_dto.dart
lib/model/api_key_update_dto.dart
lib/model/asset_bulk_upload_check_dto.dart
lib/model/asset_bulk_upload_check_item.dart
lib/model/asset_bulk_upload_check_response_dto.dart
lib/model/asset_bulk_upload_check_result.dart
lib/model/asset_count_by_time_bucket.dart
lib/model/asset_count_by_time_bucket_response_dto.dart
lib/model/asset_count_by_user_id_response_dto.dart
@@ -236,6 +244,10 @@ test/api_key_create_response_dto_test.dart
test/api_key_response_dto_test.dart
test/api_key_update_dto_test.dart
test/asset_api_test.dart
test/asset_bulk_upload_check_dto_test.dart
test/asset_bulk_upload_check_item_test.dart
test/asset_bulk_upload_check_response_dto_test.dart
test/asset_bulk_upload_check_result_test.dart
test/asset_count_by_time_bucket_response_dto_test.dart
test/asset_count_by_time_bucket_test.dart
test/asset_count_by_user_id_response_dto_test.dart

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.57.0
- API version: 1.58.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -77,19 +77,20 @@ Class | Method | HTTP request | Description
*APIKeyApi* | [**getKey**](doc//APIKeyApi.md#getkey) | **GET** /api-key/{id} |
*APIKeyApi* | [**getKeys**](doc//APIKeyApi.md#getkeys) | **GET** /api-key |
*APIKeyApi* | [**updateKey**](doc//APIKeyApi.md#updatekey) | **PUT** /api-key/{id} |
*AlbumApi* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{albumId}/assets |
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
*AlbumApi* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets |
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users |
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album |
*AlbumApi* | [**createAlbumSharedLink**](doc//AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link |
*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
*AlbumApi* | [**downloadArchive**](doc//AlbumApi.md#downloadarchive) | **GET** /album/{albumId}/download |
*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{id} |
*AlbumApi* | [**downloadArchive**](doc//AlbumApi.md#downloadarchive) | **GET** /album/{id}/download |
*AlbumApi* | [**getAlbumCountByUserId**](doc//AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
*AlbumApi* | [**getAlbumInfo**](doc//AlbumApi.md#getalbuminfo) | **GET** /album/{albumId} |
*AlbumApi* | [**getAlbumInfo**](doc//AlbumApi.md#getalbuminfo) | **GET** /album/{id} |
*AlbumApi* | [**getAllAlbums**](doc//AlbumApi.md#getallalbums) | **GET** /album |
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} |
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} |
*AssetApi* | [**addAssetsToSharedLink**](doc//AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |
*AssetApi* | [**bulkUploadCheck**](doc//AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check |
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
*AssetApi* | [**createAssetsSharedLink**](doc//AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |
@@ -183,6 +184,10 @@ Class | Method | HTTP request | Description
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
- [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md)
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
- [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
- [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
- [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md)

View File

@@ -9,22 +9,22 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**addAssetsToAlbum**](AlbumApi.md#addassetstoalbum) | **PUT** /album/{albumId}/assets |
[**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
[**addAssetsToAlbum**](AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets |
[**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users |
[**createAlbum**](AlbumApi.md#createalbum) | **POST** /album |
[**createAlbumSharedLink**](AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link |
[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
[**downloadArchive**](AlbumApi.md#downloadarchive) | **GET** /album/{albumId}/download |
[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{id} |
[**downloadArchive**](AlbumApi.md#downloadarchive) | **GET** /album/{id}/download |
[**getAlbumCountByUserId**](AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
[**getAlbumInfo**](AlbumApi.md#getalbuminfo) | **GET** /album/{albumId} |
[**getAlbumInfo**](AlbumApi.md#getalbuminfo) | **GET** /album/{id} |
[**getAllAlbums**](AlbumApi.md#getallalbums) | **GET** /album |
[**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
[**removeUserFromAlbum**](AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |
[**updateAlbumInfo**](AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |
[**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |
[**removeUserFromAlbum**](AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} |
[**updateAlbumInfo**](AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} |
# **addAssetsToAlbum**
> AddAssetsResponseDto addAssetsToAlbum(albumId, addAssetsDto, key)
> AddAssetsResponseDto addAssetsToAlbum(id, addAssetsDto, key)
@@ -47,12 +47,12 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AlbumApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final addAssetsDto = AddAssetsDto(); // AddAssetsDto |
final key = key_example; // String |
try {
final result = api_instance.addAssetsToAlbum(albumId, addAssetsDto, key);
final result = api_instance.addAssetsToAlbum(id, addAssetsDto, key);
print(result);
} catch (e) {
print('Exception when calling AlbumApi->addAssetsToAlbum: $e\n');
@@ -63,7 +63,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**id** | **String**| |
**addAssetsDto** | [**AddAssetsDto**](AddAssetsDto.md)| |
**key** | **String**| | [optional]
@@ -83,7 +83,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **addUsersToAlbum**
> AlbumResponseDto addUsersToAlbum(albumId, addUsersDto)
> AlbumResponseDto addUsersToAlbum(id, addUsersDto)
@@ -106,11 +106,11 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AlbumApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final addUsersDto = AddUsersDto(); // AddUsersDto |
try {
final result = api_instance.addUsersToAlbum(albumId, addUsersDto);
final result = api_instance.addUsersToAlbum(id, addUsersDto);
print(result);
} catch (e) {
print('Exception when calling AlbumApi->addUsersToAlbum: $e\n');
@@ -121,7 +121,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**id** | **String**| |
**addUsersDto** | [**AddUsersDto**](AddUsersDto.md)| |
### Return type
@@ -250,7 +250,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **deleteAlbum**
> deleteAlbum(albumId)
> deleteAlbum(id)
@@ -273,10 +273,10 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AlbumApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
api_instance.deleteAlbum(albumId);
api_instance.deleteAlbum(id);
} catch (e) {
print('Exception when calling AlbumApi->deleteAlbum: $e\n');
}
@@ -286,7 +286,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**id** | **String**| |
### Return type
@@ -304,7 +304,7 @@ void (empty response body)
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **downloadArchive**
> MultipartFile downloadArchive(albumId, name, skip, key)
> MultipartFile downloadArchive(id, name, skip, key)
@@ -327,13 +327,13 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AlbumApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final name = name_example; // String |
final skip = 8.14; // num |
final key = key_example; // String |
try {
final result = api_instance.downloadArchive(albumId, name, skip, key);
final result = api_instance.downloadArchive(id, name, skip, key);
print(result);
} catch (e) {
print('Exception when calling AlbumApi->downloadArchive: $e\n');
@@ -344,7 +344,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**id** | **String**| |
**name** | **String**| | [optional]
**skip** | **num**| | [optional]
**key** | **String**| | [optional]
@@ -416,7 +416,7 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAlbumInfo**
> AlbumResponseDto getAlbumInfo(albumId, key)
> AlbumResponseDto getAlbumInfo(id, key)
@@ -439,11 +439,11 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AlbumApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final key = key_example; // String |
try {
final result = api_instance.getAlbumInfo(albumId, key);
final result = api_instance.getAlbumInfo(id, key);
print(result);
} catch (e) {
print('Exception when calling AlbumApi->getAlbumInfo: $e\n');
@@ -454,7 +454,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**id** | **String**| |
**key** | **String**| | [optional]
### Return type
@@ -530,7 +530,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **removeAssetFromAlbum**
> AlbumResponseDto removeAssetFromAlbum(albumId, removeAssetsDto)
> AlbumResponseDto removeAssetFromAlbum(id, removeAssetsDto)
@@ -553,11 +553,11 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AlbumApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto |
try {
final result = api_instance.removeAssetFromAlbum(albumId, removeAssetsDto);
final result = api_instance.removeAssetFromAlbum(id, removeAssetsDto);
print(result);
} catch (e) {
print('Exception when calling AlbumApi->removeAssetFromAlbum: $e\n');
@@ -568,7 +568,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**id** | **String**| |
**removeAssetsDto** | [**RemoveAssetsDto**](RemoveAssetsDto.md)| |
### Return type
@@ -587,7 +587,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **removeUserFromAlbum**
> removeUserFromAlbum(albumId, userId)
> removeUserFromAlbum(id, userId)
@@ -610,11 +610,11 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AlbumApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final userId = userId_example; // String |
try {
api_instance.removeUserFromAlbum(albumId, userId);
api_instance.removeUserFromAlbum(id, userId);
} catch (e) {
print('Exception when calling AlbumApi->removeUserFromAlbum: $e\n');
}
@@ -624,7 +624,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**id** | **String**| |
**userId** | **String**| |
### Return type
@@ -643,7 +643,7 @@ void (empty response body)
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **updateAlbumInfo**
> AlbumResponseDto updateAlbumInfo(albumId, updateAlbumDto)
> AlbumResponseDto updateAlbumInfo(id, updateAlbumDto)
@@ -666,11 +666,11 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AlbumApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final updateAlbumDto = UpdateAlbumDto(); // UpdateAlbumDto |
try {
final result = api_instance.updateAlbumInfo(albumId, updateAlbumDto);
final result = api_instance.updateAlbumInfo(id, updateAlbumDto);
print(result);
} catch (e) {
print('Exception when calling AlbumApi->updateAlbumInfo: $e\n');
@@ -681,7 +681,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**id** | **String**| |
**updateAlbumDto** | [**UpdateAlbumDto**](UpdateAlbumDto.md)| |
### Return type

View File

@@ -17,6 +17,7 @@ Name | Type | Description | Notes
**backgroundTaskQueue** | [**JobStatusDto**](JobStatusDto.md) | |
**searchQueue** | [**JobStatusDto**](JobStatusDto.md) | |
**recognizeFacesQueue** | [**JobStatusDto**](JobStatusDto.md) | |
**sidecarQueue** | [**JobStatusDto**](JobStatusDto.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -10,6 +10,7 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**addAssetsToSharedLink**](AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |
[**bulkUploadCheck**](AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check |
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
[**createAssetsSharedLink**](AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |
@@ -93,6 +94,63 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **bulkUploadCheck**
> AssetBulkUploadCheckResponseDto bulkUploadCheck(assetBulkUploadCheckDto)
Checks if assets exist by checksums
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final assetBulkUploadCheckDto = AssetBulkUploadCheckDto(); // AssetBulkUploadCheckDto |
try {
final result = api_instance.bulkUploadCheck(assetBulkUploadCheckDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->bulkUploadCheck: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**assetBulkUploadCheckDto** | [**AssetBulkUploadCheckDto**](AssetBulkUploadCheckDto.md)| |
### Return type
[**AssetBulkUploadCheckResponseDto**](AssetBulkUploadCheckResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **checkDuplicateAsset**
> CheckDuplicateAssetResponseDto checkDuplicateAsset(checkDuplicateAssetDto, key)
@@ -495,7 +553,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAllAssets**
> List<AssetResponseDto> getAllAssets(isFavorite, isArchived, skip, ifNoneMatch)
> List<AssetResponseDto> getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch)
@@ -520,13 +578,14 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final isFavorite = true; // bool |
final isArchived = true; // bool |
final skip = 8.14; // num |
final ifNoneMatch = ifNoneMatch_example; // String | ETag of data already cached on the client
try {
final result = api_instance.getAllAssets(isFavorite, isArchived, skip, ifNoneMatch);
final result = api_instance.getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getAllAssets: $e\n');
@@ -537,6 +596,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**userId** | **String**| | [optional]
**isFavorite** | **bool**| | [optional]
**isArchived** | **bool**| | [optional]
**skip** | **num**| | [optional]
@@ -1041,7 +1101,7 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getMapMarkers**
> List<MapMarkerResponseDto> getMapMarkers(isFavorite)
> List<MapMarkerResponseDto> getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore)
@@ -1065,9 +1125,11 @@ import 'package:openapi/api.dart';
final api_instance = AssetApi();
final isFavorite = true; // bool |
final fileCreatedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final fileCreatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
try {
final result = api_instance.getMapMarkers(isFavorite);
final result = api_instance.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getMapMarkers: $e\n');
@@ -1079,6 +1141,8 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**isFavorite** | **bool**| | [optional]
**fileCreatedAfter** | **DateTime**| | [optional]
**fileCreatedBefore** | **DateTime**| | [optional]
### Return type
@@ -1385,7 +1449,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **uploadFile**
> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isArchived, isVisible, duration)
> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration)
@@ -1418,12 +1482,13 @@ final isFavorite = true; // bool |
final fileExtension = fileExtension_example; // String |
final key = key_example; // String |
final livePhotoData = BINARY_DATA_HERE; // MultipartFile |
final sidecarData = BINARY_DATA_HERE; // MultipartFile |
final isArchived = true; // bool |
final isVisible = true; // bool |
final duration = duration_example; // String |
try {
final result = api_instance.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isArchived, isVisible, duration);
final result = api_instance.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration);
print(result);
} catch (e) {
print('Exception when calling AssetApi->uploadFile: $e\n');
@@ -1444,6 +1509,7 @@ Name | Type | Description | Notes
**fileExtension** | **String**| |
**key** | **String**| | [optional]
**livePhotoData** | **MultipartFile**| | [optional]
**sidecarData** | **MultipartFile**| | [optional]
**isArchived** | **bool**| | [optional]
**isVisible** | **bool**| | [optional]
**duration** | **String**| | [optional]

View File

@@ -0,0 +1,15 @@
# openapi.model.AssetBulkUploadCheckDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assets** | [**List<AssetBulkUploadCheckItem>**](AssetBulkUploadCheckItem.md) | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,16 @@
# openapi.model.AssetBulkUploadCheckItem
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | |
**checksum** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,15 @@
# openapi.model.AssetBulkUploadCheckResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**results** | [**List<AssetBulkUploadCheckResult>**](AssetBulkUploadCheckResult.md) | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,18 @@
# openapi.model.AssetBulkUploadCheckResult
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | |
**action** | **String** | |
**reason** | **String** | | [optional]
**assetId** | **String** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -10,6 +10,7 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**timeBucket** | **List<String>** | | [default to const []]
**userId** | **String** | | [optional]
**withoutThumbs** | **bool** | Include assets without thumbnails | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -54,6 +54,10 @@ part 'model/admin_signup_response_dto.dart';
part 'model/album_count_response_dto.dart';
part 'model/album_response_dto.dart';
part 'model/all_job_status_response_dto.dart';
part 'model/asset_bulk_upload_check_dto.dart';
part 'model/asset_bulk_upload_check_item.dart';
part 'model/asset_bulk_upload_check_response_dto.dart';
part 'model/asset_bulk_upload_check_result.dart';
part 'model/asset_count_by_time_bucket.dart';
part 'model/asset_count_by_time_bucket_response_dto.dart';
part 'model/asset_count_by_user_id_response_dto.dart';

View File

@@ -16,18 +16,18 @@ class AlbumApi {
final ApiClient apiClient;
/// Performs an HTTP 'PUT /album/{albumId}/assets' operation and returns the [Response].
/// Performs an HTTP 'PUT /album/{id}/assets' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [AddAssetsDto] addAssetsDto (required):
///
/// * [String] key:
Future<Response> addAssetsToAlbumWithHttpInfo(String albumId, AddAssetsDto addAssetsDto, { String? key, }) async {
Future<Response> addAssetsToAlbumWithHttpInfo(String id, AddAssetsDto addAssetsDto, { String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/album/{albumId}/assets'
.replaceAll('{albumId}', albumId);
final path = r'/album/{id}/assets'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = addAssetsDto;
@@ -56,13 +56,13 @@ class AlbumApi {
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [AddAssetsDto] addAssetsDto (required):
///
/// * [String] key:
Future<AddAssetsResponseDto?> addAssetsToAlbum(String albumId, AddAssetsDto addAssetsDto, { String? key, }) async {
final response = await addAssetsToAlbumWithHttpInfo(albumId, addAssetsDto, key: key, );
Future<AddAssetsResponseDto?> addAssetsToAlbum(String id, AddAssetsDto addAssetsDto, { String? key, }) async {
final response = await addAssetsToAlbumWithHttpInfo(id, addAssetsDto, key: key, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -76,16 +76,16 @@ class AlbumApi {
return null;
}
/// Performs an HTTP 'PUT /album/{albumId}/users' operation and returns the [Response].
/// Performs an HTTP 'PUT /album/{id}/users' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [AddUsersDto] addUsersDto (required):
Future<Response> addUsersToAlbumWithHttpInfo(String albumId, AddUsersDto addUsersDto,) async {
Future<Response> addUsersToAlbumWithHttpInfo(String id, AddUsersDto addUsersDto,) async {
// ignore: prefer_const_declarations
final path = r'/album/{albumId}/users'
.replaceAll('{albumId}', albumId);
final path = r'/album/{id}/users'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = addUsersDto;
@@ -110,11 +110,11 @@ class AlbumApi {
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [AddUsersDto] addUsersDto (required):
Future<AlbumResponseDto?> addUsersToAlbum(String albumId, AddUsersDto addUsersDto,) async {
final response = await addUsersToAlbumWithHttpInfo(albumId, addUsersDto,);
Future<AlbumResponseDto?> addUsersToAlbum(String id, AddUsersDto addUsersDto,) async {
final response = await addUsersToAlbumWithHttpInfo(id, addUsersDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -222,14 +222,14 @@ class AlbumApi {
return null;
}
/// Performs an HTTP 'DELETE /album/{albumId}' operation and returns the [Response].
/// Performs an HTTP 'DELETE /album/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
Future<Response> deleteAlbumWithHttpInfo(String albumId,) async {
/// * [String] id (required):
Future<Response> deleteAlbumWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/album/{albumId}'
.replaceAll('{albumId}', albumId);
final path = r'/album/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
@@ -254,28 +254,28 @@ class AlbumApi {
/// Parameters:
///
/// * [String] albumId (required):
Future<void> deleteAlbum(String albumId,) async {
final response = await deleteAlbumWithHttpInfo(albumId,);
/// * [String] id (required):
Future<void> deleteAlbum(String id,) async {
final response = await deleteAlbumWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'GET /album/{albumId}/download' operation and returns the [Response].
/// Performs an HTTP 'GET /album/{id}/download' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [String] name:
///
/// * [num] skip:
///
/// * [String] key:
Future<Response> downloadArchiveWithHttpInfo(String albumId, { String? name, num? skip, String? key, }) async {
Future<Response> downloadArchiveWithHttpInfo(String id, { String? name, num? skip, String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/album/{albumId}/download'
.replaceAll('{albumId}', albumId);
final path = r'/album/{id}/download'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
@@ -310,15 +310,15 @@ class AlbumApi {
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [String] name:
///
/// * [num] skip:
///
/// * [String] key:
Future<MultipartFile?> downloadArchive(String albumId, { String? name, num? skip, String? key, }) async {
final response = await downloadArchiveWithHttpInfo(albumId, name: name, skip: skip, key: key, );
Future<MultipartFile?> downloadArchive(String id, { String? name, num? skip, String? key, }) async {
final response = await downloadArchiveWithHttpInfo(id, name: name, skip: skip, key: key, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -373,16 +373,16 @@ class AlbumApi {
return null;
}
/// Performs an HTTP 'GET /album/{albumId}' operation and returns the [Response].
/// Performs an HTTP 'GET /album/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [String] key:
Future<Response> getAlbumInfoWithHttpInfo(String albumId, { String? key, }) async {
Future<Response> getAlbumInfoWithHttpInfo(String id, { String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/album/{albumId}'
.replaceAll('{albumId}', albumId);
final path = r'/album/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
@@ -411,11 +411,11 @@ class AlbumApi {
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [String] key:
Future<AlbumResponseDto?> getAlbumInfo(String albumId, { String? key, }) async {
final response = await getAlbumInfoWithHttpInfo(albumId, key: key, );
Future<AlbumResponseDto?> getAlbumInfo(String id, { String? key, }) async {
final response = await getAlbumInfoWithHttpInfo(id, key: key, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -492,16 +492,16 @@ class AlbumApi {
return null;
}
/// Performs an HTTP 'DELETE /album/{albumId}/assets' operation and returns the [Response].
/// Performs an HTTP 'DELETE /album/{id}/assets' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [RemoveAssetsDto] removeAssetsDto (required):
Future<Response> removeAssetFromAlbumWithHttpInfo(String albumId, RemoveAssetsDto removeAssetsDto,) async {
Future<Response> removeAssetFromAlbumWithHttpInfo(String id, RemoveAssetsDto removeAssetsDto,) async {
// ignore: prefer_const_declarations
final path = r'/album/{albumId}/assets'
.replaceAll('{albumId}', albumId);
final path = r'/album/{id}/assets'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = removeAssetsDto;
@@ -526,11 +526,11 @@ class AlbumApi {
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [RemoveAssetsDto] removeAssetsDto (required):
Future<AlbumResponseDto?> removeAssetFromAlbum(String albumId, RemoveAssetsDto removeAssetsDto,) async {
final response = await removeAssetFromAlbumWithHttpInfo(albumId, removeAssetsDto,);
Future<AlbumResponseDto?> removeAssetFromAlbum(String id, RemoveAssetsDto removeAssetsDto,) async {
final response = await removeAssetFromAlbumWithHttpInfo(id, removeAssetsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -544,16 +544,16 @@ class AlbumApi {
return null;
}
/// Performs an HTTP 'DELETE /album/{albumId}/user/{userId}' operation and returns the [Response].
/// Performs an HTTP 'DELETE /album/{id}/user/{userId}' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [String] userId (required):
Future<Response> removeUserFromAlbumWithHttpInfo(String albumId, String userId,) async {
Future<Response> removeUserFromAlbumWithHttpInfo(String id, String userId,) async {
// ignore: prefer_const_declarations
final path = r'/album/{albumId}/user/{userId}'
.replaceAll('{albumId}', albumId)
final path = r'/album/{id}/user/{userId}'
.replaceAll('{id}', id)
.replaceAll('{userId}', userId);
// ignore: prefer_final_locals
@@ -579,26 +579,26 @@ class AlbumApi {
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [String] userId (required):
Future<void> removeUserFromAlbum(String albumId, String userId,) async {
final response = await removeUserFromAlbumWithHttpInfo(albumId, userId,);
Future<void> removeUserFromAlbum(String id, String userId,) async {
final response = await removeUserFromAlbumWithHttpInfo(id, userId,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'PATCH /album/{albumId}' operation and returns the [Response].
/// Performs an HTTP 'PATCH /album/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [UpdateAlbumDto] updateAlbumDto (required):
Future<Response> updateAlbumInfoWithHttpInfo(String albumId, UpdateAlbumDto updateAlbumDto,) async {
Future<Response> updateAlbumInfoWithHttpInfo(String id, UpdateAlbumDto updateAlbumDto,) async {
// ignore: prefer_const_declarations
final path = r'/album/{albumId}'
.replaceAll('{albumId}', albumId);
final path = r'/album/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = updateAlbumDto;
@@ -623,11 +623,11 @@ class AlbumApi {
/// Parameters:
///
/// * [String] albumId (required):
/// * [String] id (required):
///
/// * [UpdateAlbumDto] updateAlbumDto (required):
Future<AlbumResponseDto?> updateAlbumInfo(String albumId, UpdateAlbumDto updateAlbumDto,) async {
final response = await updateAlbumInfoWithHttpInfo(albumId, updateAlbumDto,);
Future<AlbumResponseDto?> updateAlbumInfo(String id, UpdateAlbumDto updateAlbumDto,) async {
final response = await updateAlbumInfoWithHttpInfo(id, updateAlbumDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -71,6 +71,58 @@ class AssetApi {
return null;
}
/// Checks if assets exist by checksums
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [AssetBulkUploadCheckDto] assetBulkUploadCheckDto (required):
Future<Response> bulkUploadCheckWithHttpInfo(AssetBulkUploadCheckDto assetBulkUploadCheckDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/bulk-upload-check';
// ignore: prefer_final_locals
Object? postBody = assetBulkUploadCheckDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Checks if assets exist by checksums
///
/// Parameters:
///
/// * [AssetBulkUploadCheckDto] assetBulkUploadCheckDto (required):
Future<AssetBulkUploadCheckResponseDto?> bulkUploadCheck(AssetBulkUploadCheckDto assetBulkUploadCheckDto,) async {
final response = await bulkUploadCheckWithHttpInfo(assetBulkUploadCheckDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetBulkUploadCheckResponseDto',) as AssetBulkUploadCheckResponseDto;
}
return null;
}
/// Check duplicated asset before uploading - for Web upload used
///
/// Note: This method returns the HTTP [Response].
@@ -467,6 +519,8 @@ class AssetApi {
///
/// Parameters:
///
/// * [String] userId:
///
/// * [bool] isFavorite:
///
/// * [bool] isArchived:
@@ -475,7 +529,7 @@ class AssetApi {
///
/// * [String] ifNoneMatch:
/// ETag of data already cached on the client
Future<Response> getAllAssetsWithHttpInfo({ bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async {
Future<Response> getAllAssetsWithHttpInfo({ String? userId, bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async {
// ignore: prefer_const_declarations
final path = r'/asset';
@@ -486,6 +540,9 @@ class AssetApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
@@ -518,6 +575,8 @@ class AssetApi {
///
/// Parameters:
///
/// * [String] userId:
///
/// * [bool] isFavorite:
///
/// * [bool] isArchived:
@@ -526,8 +585,8 @@ class AssetApi {
///
/// * [String] ifNoneMatch:
/// ETag of data already cached on the client
Future<List<AssetResponseDto>?> getAllAssets({ bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async {
final response = await getAllAssetsWithHttpInfo( isFavorite: isFavorite, isArchived: isArchived, skip: skip, ifNoneMatch: ifNoneMatch, );
Future<List<AssetResponseDto>?> getAllAssets({ String? userId, bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async {
final response = await getAllAssetsWithHttpInfo( userId: userId, isFavorite: isFavorite, isArchived: isArchived, skip: skip, ifNoneMatch: ifNoneMatch, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -983,7 +1042,11 @@ class AssetApi {
/// Parameters:
///
/// * [bool] isFavorite:
Future<Response> getMapMarkersWithHttpInfo({ bool? isFavorite, }) async {
///
/// * [DateTime] fileCreatedAfter:
///
/// * [DateTime] fileCreatedBefore:
Future<Response> getMapMarkersWithHttpInfo({ bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/map-marker';
@@ -997,6 +1060,12 @@ class AssetApi {
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (fileCreatedAfter != null) {
queryParams.addAll(_queryParams('', 'fileCreatedAfter', fileCreatedAfter));
}
if (fileCreatedBefore != null) {
queryParams.addAll(_queryParams('', 'fileCreatedBefore', fileCreatedBefore));
}
const contentTypes = <String>[];
@@ -1015,8 +1084,12 @@ class AssetApi {
/// Parameters:
///
/// * [bool] isFavorite:
Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isFavorite, }) async {
final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, );
///
/// * [DateTime] fileCreatedAfter:
///
/// * [DateTime] fileCreatedBefore:
Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -1344,12 +1417,14 @@ class AssetApi {
///
/// * [MultipartFile] livePhotoData:
///
/// * [MultipartFile] sidecarData:
///
/// * [bool] isArchived:
///
/// * [bool] isVisible:
///
/// * [String] duration:
Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String fileCreatedAt, String fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, bool? isArchived, bool? isVisible, String? duration, }) async {
Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String fileCreatedAt, String fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isArchived, bool? isVisible, String? duration, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/upload';
@@ -1382,6 +1457,11 @@ class AssetApi {
mp.fields[r'livePhotoData'] = livePhotoData.field;
mp.files.add(livePhotoData);
}
if (sidecarData != null) {
hasFields = true;
mp.fields[r'sidecarData'] = sidecarData.field;
mp.files.add(sidecarData);
}
if (deviceAssetId != null) {
hasFields = true;
mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId);
@@ -1455,13 +1535,15 @@ class AssetApi {
///
/// * [MultipartFile] livePhotoData:
///
/// * [MultipartFile] sidecarData:
///
/// * [bool] isArchived:
///
/// * [bool] isVisible:
///
/// * [String] duration:
Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String fileCreatedAt, String fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, bool? isArchived, bool? isVisible, String? duration, }) async {
final response = await uploadFileWithHttpInfo(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key: key, livePhotoData: livePhotoData, isArchived: isArchived, isVisible: isVisible, duration: duration, );
Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String fileCreatedAt, String fileModifiedAt, bool isFavorite, String fileExtension, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isArchived, bool? isVisible, String? duration, }) async {
final response = await uploadFileWithHttpInfo(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isArchived: isArchived, isVisible: isVisible, duration: duration, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -203,6 +203,14 @@ class ApiClient {
return AlbumResponseDto.fromJson(value);
case 'AllJobStatusResponseDto':
return AllJobStatusResponseDto.fromJson(value);
case 'AssetBulkUploadCheckDto':
return AssetBulkUploadCheckDto.fromJson(value);
case 'AssetBulkUploadCheckItem':
return AssetBulkUploadCheckItem.fromJson(value);
case 'AssetBulkUploadCheckResponseDto':
return AssetBulkUploadCheckResponseDto.fromJson(value);
case 'AssetBulkUploadCheckResult':
return AssetBulkUploadCheckResult.fromJson(value);
case 'AssetCountByTimeBucket':
return AssetCountByTimeBucket.fromJson(value);
case 'AssetCountByTimeBucketResponseDto':

View File

@@ -22,6 +22,7 @@ class AllJobStatusResponseDto {
required this.backgroundTaskQueue,
required this.searchQueue,
required this.recognizeFacesQueue,
required this.sidecarQueue,
});
JobStatusDto thumbnailGenerationQueue;
@@ -42,6 +43,8 @@ class AllJobStatusResponseDto {
JobStatusDto recognizeFacesQueue;
JobStatusDto sidecarQueue;
@override
bool operator ==(Object other) => identical(this, other) || other is AllJobStatusResponseDto &&
other.thumbnailGenerationQueue == thumbnailGenerationQueue &&
@@ -52,7 +55,8 @@ class AllJobStatusResponseDto {
other.storageTemplateMigrationQueue == storageTemplateMigrationQueue &&
other.backgroundTaskQueue == backgroundTaskQueue &&
other.searchQueue == searchQueue &&
other.recognizeFacesQueue == recognizeFacesQueue;
other.recognizeFacesQueue == recognizeFacesQueue &&
other.sidecarQueue == sidecarQueue;
@override
int get hashCode =>
@@ -65,10 +69,11 @@ class AllJobStatusResponseDto {
(storageTemplateMigrationQueue.hashCode) +
(backgroundTaskQueue.hashCode) +
(searchQueue.hashCode) +
(recognizeFacesQueue.hashCode);
(recognizeFacesQueue.hashCode) +
(sidecarQueue.hashCode);
@override
String toString() => 'AllJobStatusResponseDto[thumbnailGenerationQueue=$thumbnailGenerationQueue, metadataExtractionQueue=$metadataExtractionQueue, videoConversionQueue=$videoConversionQueue, objectTaggingQueue=$objectTaggingQueue, clipEncodingQueue=$clipEncodingQueue, storageTemplateMigrationQueue=$storageTemplateMigrationQueue, backgroundTaskQueue=$backgroundTaskQueue, searchQueue=$searchQueue, recognizeFacesQueue=$recognizeFacesQueue]';
String toString() => 'AllJobStatusResponseDto[thumbnailGenerationQueue=$thumbnailGenerationQueue, metadataExtractionQueue=$metadataExtractionQueue, videoConversionQueue=$videoConversionQueue, objectTaggingQueue=$objectTaggingQueue, clipEncodingQueue=$clipEncodingQueue, storageTemplateMigrationQueue=$storageTemplateMigrationQueue, backgroundTaskQueue=$backgroundTaskQueue, searchQueue=$searchQueue, recognizeFacesQueue=$recognizeFacesQueue, sidecarQueue=$sidecarQueue]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -81,6 +86,7 @@ class AllJobStatusResponseDto {
json[r'background-task-queue'] = this.backgroundTaskQueue;
json[r'search-queue'] = this.searchQueue;
json[r'recognize-faces-queue'] = this.recognizeFacesQueue;
json[r'sidecar-queue'] = this.sidecarQueue;
return json;
}
@@ -112,6 +118,7 @@ class AllJobStatusResponseDto {
backgroundTaskQueue: JobStatusDto.fromJson(json[r'background-task-queue'])!,
searchQueue: JobStatusDto.fromJson(json[r'search-queue'])!,
recognizeFacesQueue: JobStatusDto.fromJson(json[r'recognize-faces-queue'])!,
sidecarQueue: JobStatusDto.fromJson(json[r'sidecar-queue'])!,
);
}
return null;
@@ -168,6 +175,7 @@ class AllJobStatusResponseDto {
'background-task-queue',
'search-queue',
'recognize-faces-queue',
'sidecar-queue',
};
}

View File

@@ -0,0 +1,109 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class AssetBulkUploadCheckDto {
/// Returns a new [AssetBulkUploadCheckDto] instance.
AssetBulkUploadCheckDto({
this.assets = const [],
});
List<AssetBulkUploadCheckItem> assets;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckDto &&
other.assets == assets;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assets.hashCode);
@override
String toString() => 'AssetBulkUploadCheckDto[assets=$assets]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'assets'] = this.assets;
return json;
}
/// Returns a new [AssetBulkUploadCheckDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetBulkUploadCheckDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "AssetBulkUploadCheckDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetBulkUploadCheckDto[$key]" has a null value in JSON.');
});
return true;
}());
return AssetBulkUploadCheckDto(
assets: AssetBulkUploadCheckItem.listFromJson(json[r'assets']),
);
}
return null;
}
static List<AssetBulkUploadCheckDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetBulkUploadCheckDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetBulkUploadCheckDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetBulkUploadCheckDto> mapFromJson(dynamic json) {
final map = <String, AssetBulkUploadCheckDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetBulkUploadCheckDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetBulkUploadCheckDto-objects as value to a dart map
static Map<String, List<AssetBulkUploadCheckDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetBulkUploadCheckDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetBulkUploadCheckDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assets',
};
}

View File

@@ -0,0 +1,117 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class AssetBulkUploadCheckItem {
/// Returns a new [AssetBulkUploadCheckItem] instance.
AssetBulkUploadCheckItem({
required this.id,
required this.checksum,
});
String id;
String checksum;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckItem &&
other.id == id &&
other.checksum == checksum;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(id.hashCode) +
(checksum.hashCode);
@override
String toString() => 'AssetBulkUploadCheckItem[id=$id, checksum=$checksum]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'id'] = this.id;
json[r'checksum'] = this.checksum;
return json;
}
/// Returns a new [AssetBulkUploadCheckItem] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetBulkUploadCheckItem? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "AssetBulkUploadCheckItem[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetBulkUploadCheckItem[$key]" has a null value in JSON.');
});
return true;
}());
return AssetBulkUploadCheckItem(
id: mapValueOfType<String>(json, r'id')!,
checksum: mapValueOfType<String>(json, r'checksum')!,
);
}
return null;
}
static List<AssetBulkUploadCheckItem> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetBulkUploadCheckItem>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetBulkUploadCheckItem.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetBulkUploadCheckItem> mapFromJson(dynamic json) {
final map = <String, AssetBulkUploadCheckItem>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetBulkUploadCheckItem.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetBulkUploadCheckItem-objects as value to a dart map
static Map<String, List<AssetBulkUploadCheckItem>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetBulkUploadCheckItem>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetBulkUploadCheckItem.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'id',
'checksum',
};
}

View File

@@ -0,0 +1,109 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class AssetBulkUploadCheckResponseDto {
/// Returns a new [AssetBulkUploadCheckResponseDto] instance.
AssetBulkUploadCheckResponseDto({
this.results = const [],
});
List<AssetBulkUploadCheckResult> results;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckResponseDto &&
other.results == results;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(results.hashCode);
@override
String toString() => 'AssetBulkUploadCheckResponseDto[results=$results]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'results'] = this.results;
return json;
}
/// Returns a new [AssetBulkUploadCheckResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetBulkUploadCheckResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "AssetBulkUploadCheckResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetBulkUploadCheckResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
return AssetBulkUploadCheckResponseDto(
results: AssetBulkUploadCheckResult.listFromJson(json[r'results']),
);
}
return null;
}
static List<AssetBulkUploadCheckResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetBulkUploadCheckResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetBulkUploadCheckResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetBulkUploadCheckResponseDto> mapFromJson(dynamic json) {
final map = <String, AssetBulkUploadCheckResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetBulkUploadCheckResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetBulkUploadCheckResponseDto-objects as value to a dart map
static Map<String, List<AssetBulkUploadCheckResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetBulkUploadCheckResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetBulkUploadCheckResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'results',
};
}

View File

@@ -0,0 +1,293 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class AssetBulkUploadCheckResult {
/// Returns a new [AssetBulkUploadCheckResult] instance.
AssetBulkUploadCheckResult({
required this.id,
required this.action,
this.reason,
this.assetId,
});
String id;
AssetBulkUploadCheckResultActionEnum action;
AssetBulkUploadCheckResultReasonEnum? reason;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? assetId;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckResult &&
other.id == id &&
other.action == action &&
other.reason == reason &&
other.assetId == assetId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(id.hashCode) +
(action.hashCode) +
(reason == null ? 0 : reason!.hashCode) +
(assetId == null ? 0 : assetId!.hashCode);
@override
String toString() => 'AssetBulkUploadCheckResult[id=$id, action=$action, reason=$reason, assetId=$assetId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'id'] = this.id;
json[r'action'] = this.action;
if (this.reason != null) {
json[r'reason'] = this.reason;
} else {
// json[r'reason'] = null;
}
if (this.assetId != null) {
json[r'assetId'] = this.assetId;
} else {
// json[r'assetId'] = null;
}
return json;
}
/// Returns a new [AssetBulkUploadCheckResult] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetBulkUploadCheckResult? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "AssetBulkUploadCheckResult[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetBulkUploadCheckResult[$key]" has a null value in JSON.');
});
return true;
}());
return AssetBulkUploadCheckResult(
id: mapValueOfType<String>(json, r'id')!,
action: AssetBulkUploadCheckResultActionEnum.fromJson(json[r'action'])!,
reason: AssetBulkUploadCheckResultReasonEnum.fromJson(json[r'reason']),
assetId: mapValueOfType<String>(json, r'assetId'),
);
}
return null;
}
static List<AssetBulkUploadCheckResult> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetBulkUploadCheckResult>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetBulkUploadCheckResult.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetBulkUploadCheckResult> mapFromJson(dynamic json) {
final map = <String, AssetBulkUploadCheckResult>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetBulkUploadCheckResult.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetBulkUploadCheckResult-objects as value to a dart map
static Map<String, List<AssetBulkUploadCheckResult>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetBulkUploadCheckResult>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetBulkUploadCheckResult.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'id',
'action',
};
}
class AssetBulkUploadCheckResultActionEnum {
/// Instantiate a new enum with the provided [value].
const AssetBulkUploadCheckResultActionEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const accept = AssetBulkUploadCheckResultActionEnum._(r'accept');
static const reject = AssetBulkUploadCheckResultActionEnum._(r'reject');
/// List of all possible values in this [enum][AssetBulkUploadCheckResultActionEnum].
static const values = <AssetBulkUploadCheckResultActionEnum>[
accept,
reject,
];
static AssetBulkUploadCheckResultActionEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultActionEnumTypeTransformer().decode(value);
static List<AssetBulkUploadCheckResultActionEnum>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetBulkUploadCheckResultActionEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetBulkUploadCheckResultActionEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultActionEnum] to String,
/// and [decode] dynamic data back to [AssetBulkUploadCheckResultActionEnum].
class AssetBulkUploadCheckResultActionEnumTypeTransformer {
factory AssetBulkUploadCheckResultActionEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultActionEnumTypeTransformer._();
const AssetBulkUploadCheckResultActionEnumTypeTransformer._();
String encode(AssetBulkUploadCheckResultActionEnum data) => data.value;
/// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultActionEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
AssetBulkUploadCheckResultActionEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'accept': return AssetBulkUploadCheckResultActionEnum.accept;
case r'reject': return AssetBulkUploadCheckResultActionEnum.reject;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [AssetBulkUploadCheckResultActionEnumTypeTransformer] instance.
static AssetBulkUploadCheckResultActionEnumTypeTransformer? _instance;
}
class AssetBulkUploadCheckResultReasonEnum {
/// Instantiate a new enum with the provided [value].
const AssetBulkUploadCheckResultReasonEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const duplicate = AssetBulkUploadCheckResultReasonEnum._(r'duplicate');
static const unsupportedFormat = AssetBulkUploadCheckResultReasonEnum._(r'unsupported-format');
/// List of all possible values in this [enum][AssetBulkUploadCheckResultReasonEnum].
static const values = <AssetBulkUploadCheckResultReasonEnum>[
duplicate,
unsupportedFormat,
];
static AssetBulkUploadCheckResultReasonEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultReasonEnumTypeTransformer().decode(value);
static List<AssetBulkUploadCheckResultReasonEnum>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetBulkUploadCheckResultReasonEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetBulkUploadCheckResultReasonEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultReasonEnum] to String,
/// and [decode] dynamic data back to [AssetBulkUploadCheckResultReasonEnum].
class AssetBulkUploadCheckResultReasonEnumTypeTransformer {
factory AssetBulkUploadCheckResultReasonEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultReasonEnumTypeTransformer._();
const AssetBulkUploadCheckResultReasonEnumTypeTransformer._();
String encode(AssetBulkUploadCheckResultReasonEnum data) => data.value;
/// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultReasonEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
AssetBulkUploadCheckResultReasonEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'duplicate': return AssetBulkUploadCheckResultReasonEnum.duplicate;
case r'unsupported-format': return AssetBulkUploadCheckResultReasonEnum.unsupportedFormat;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [AssetBulkUploadCheckResultReasonEnumTypeTransformer] instance.
static AssetBulkUploadCheckResultReasonEnumTypeTransformer? _instance;
}

View File

@@ -15,6 +15,7 @@ class GetAssetByTimeBucketDto {
GetAssetByTimeBucketDto({
this.timeBucket = const [],
this.userId,
this.withoutThumbs,
});
List<String> timeBucket;
@@ -27,19 +28,30 @@ class GetAssetByTimeBucketDto {
///
String? userId;
/// Include assets without thumbnails
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? withoutThumbs;
@override
bool operator ==(Object other) => identical(this, other) || other is GetAssetByTimeBucketDto &&
other.timeBucket == timeBucket &&
other.userId == userId;
other.userId == userId &&
other.withoutThumbs == withoutThumbs;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(timeBucket.hashCode) +
(userId == null ? 0 : userId!.hashCode);
(userId == null ? 0 : userId!.hashCode) +
(withoutThumbs == null ? 0 : withoutThumbs!.hashCode);
@override
String toString() => 'GetAssetByTimeBucketDto[timeBucket=$timeBucket, userId=$userId]';
String toString() => 'GetAssetByTimeBucketDto[timeBucket=$timeBucket, userId=$userId, withoutThumbs=$withoutThumbs]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -49,6 +61,11 @@ class GetAssetByTimeBucketDto {
} else {
// json[r'userId'] = null;
}
if (this.withoutThumbs != null) {
json[r'withoutThumbs'] = this.withoutThumbs;
} else {
// json[r'withoutThumbs'] = null;
}
return json;
}
@@ -75,6 +92,7 @@ class GetAssetByTimeBucketDto {
? (json[r'timeBucket'] as Iterable).cast<String>().toList(growable: false)
: const [],
userId: mapValueOfType<String>(json, r'userId'),
withoutThumbs: mapValueOfType<bool>(json, r'withoutThumbs'),
);
}
return null;

View File

@@ -32,6 +32,7 @@ class JobName {
static const backgroundTaskQueue = JobName._(r'background-task-queue');
static const storageTemplateMigrationQueue = JobName._(r'storage-template-migration-queue');
static const searchQueue = JobName._(r'search-queue');
static const sidecarQueue = JobName._(r'sidecar-queue');
/// List of all possible values in this [enum][JobName].
static const values = <JobName>[
@@ -44,6 +45,7 @@ class JobName {
backgroundTaskQueue,
storageTemplateMigrationQueue,
searchQueue,
sidecarQueue,
];
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
@@ -91,6 +93,7 @@ class JobNameTypeTransformer {
case r'background-task-queue': return JobName.backgroundTaskQueue;
case r'storage-template-migration-queue': return JobName.storageTemplateMigrationQueue;
case r'search-queue': return JobName.searchQueue;
case r'sidecar-queue': return JobName.sidecarQueue;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');

View File

@@ -17,12 +17,12 @@ void main() {
// final instance = AlbumApi();
group('tests for AlbumApi', () {
//Future<AddAssetsResponseDto> addAssetsToAlbum(String albumId, AddAssetsDto addAssetsDto, { String key }) async
//Future<AddAssetsResponseDto> addAssetsToAlbum(String id, AddAssetsDto addAssetsDto, { String key }) async
test('test addAssetsToAlbum', () async {
// TODO
});
//Future<AlbumResponseDto> addUsersToAlbum(String albumId, AddUsersDto addUsersDto) async
//Future<AlbumResponseDto> addUsersToAlbum(String id, AddUsersDto addUsersDto) async
test('test addUsersToAlbum', () async {
// TODO
});
@@ -37,12 +37,12 @@ void main() {
// TODO
});
//Future deleteAlbum(String albumId) async
//Future deleteAlbum(String id) async
test('test deleteAlbum', () async {
// TODO
});
//Future<MultipartFile> downloadArchive(String albumId, { String name, num skip, String key }) async
//Future<MultipartFile> downloadArchive(String id, { String name, num skip, String key }) async
test('test downloadArchive', () async {
// TODO
});
@@ -52,7 +52,7 @@ void main() {
// TODO
});
//Future<AlbumResponseDto> getAlbumInfo(String albumId, { String key }) async
//Future<AlbumResponseDto> getAlbumInfo(String id, { String key }) async
test('test getAlbumInfo', () async {
// TODO
});
@@ -62,17 +62,17 @@ void main() {
// TODO
});
//Future<AlbumResponseDto> removeAssetFromAlbum(String albumId, RemoveAssetsDto removeAssetsDto) async
//Future<AlbumResponseDto> removeAssetFromAlbum(String id, RemoveAssetsDto removeAssetsDto) async
test('test removeAssetFromAlbum', () async {
// TODO
});
//Future removeUserFromAlbum(String albumId, String userId) async
//Future removeUserFromAlbum(String id, String userId) async
test('test removeUserFromAlbum', () async {
// TODO
});
//Future<AlbumResponseDto> updateAlbumInfo(String albumId, UpdateAlbumDto updateAlbumDto) async
//Future<AlbumResponseDto> updateAlbumInfo(String id, UpdateAlbumDto updateAlbumDto) async
test('test updateAlbumInfo', () async {
// TODO
});

View File

@@ -61,6 +61,11 @@ void main() {
// TODO
});
// JobStatusDto sidecarQueue
test('to test the property `sidecarQueue`', () async {
// TODO
});
});

View File

@@ -22,6 +22,13 @@ void main() {
// TODO
});
// Checks if assets exist by checksums
//
//Future<AssetBulkUploadCheckResponseDto> bulkUploadCheck(AssetBulkUploadCheckDto assetBulkUploadCheckDto) async
test('test bulkUploadCheck', () async {
// TODO
});
// Check duplicated asset before uploading - for Web upload used
//
//Future<CheckDuplicateAssetResponseDto> checkDuplicateAsset(CheckDuplicateAssetDto checkDuplicateAssetDto, { String key }) async
@@ -65,7 +72,7 @@ void main() {
// Get all AssetEntity belong to the user
//
//Future<List<AssetResponseDto>> getAllAssets({ bool isFavorite, bool isArchived, num skip, String ifNoneMatch }) async
//Future<List<AssetResponseDto>> getAllAssets({ String userId, bool isFavorite, bool isArchived, num skip, String ifNoneMatch }) async
test('test getAllAssets', () async {
// TODO
});
@@ -117,7 +124,7 @@ void main() {
// TODO
});
//Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite }) async
//Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite, DateTime fileCreatedAfter, DateTime fileCreatedBefore }) async
test('test getMapMarkers', () async {
// TODO
});
@@ -151,7 +158,7 @@ void main() {
// TODO
});
//Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String fileCreatedAt, String fileModifiedAt, bool isFavorite, String fileExtension, { String key, MultipartFile livePhotoData, bool isArchived, bool isVisible, String duration }) async
//Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String fileCreatedAt, String fileModifiedAt, bool isFavorite, String fileExtension, { String key, MultipartFile livePhotoData, MultipartFile sidecarData, bool isArchived, bool isVisible, String duration }) async
test('test uploadFile', () async {
// TODO
});

View File

@@ -0,0 +1,27 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for AssetBulkUploadCheckDto
void main() {
// final instance = AssetBulkUploadCheckDto();
group('test AssetBulkUploadCheckDto', () {
// List<AssetBulkUploadCheckItem> assets (default value: const [])
test('to test the property `assets`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,32 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for AssetBulkUploadCheckItem
void main() {
// final instance = AssetBulkUploadCheckItem();
group('test AssetBulkUploadCheckItem', () {
// String id
test('to test the property `id`', () async {
// TODO
});
// String checksum
test('to test the property `checksum`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,27 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for AssetBulkUploadCheckResponseDto
void main() {
// final instance = AssetBulkUploadCheckResponseDto();
group('test AssetBulkUploadCheckResponseDto', () {
// List<AssetBulkUploadCheckResult> results (default value: const [])
test('to test the property `results`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,42 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for AssetBulkUploadCheckResult
void main() {
// final instance = AssetBulkUploadCheckResult();
group('test AssetBulkUploadCheckResult', () {
// String id
test('to test the property `id`', () async {
// TODO
});
// String action
test('to test the property `action`', () async {
// TODO
});
// String reason
test('to test the property `reason`', () async {
// TODO
});
// String assetId
test('to test the property `assetId`', () async {
// TODO
});
});
}

View File

@@ -26,6 +26,12 @@ void main() {
// TODO
});
// Include assets without thumbnails
// bool withoutThumbs
test('to test the property `withoutThumbs`', () async {
// TODO
});
});

View File

@@ -624,26 +624,26 @@ packages:
dependency: "direct main"
description:
name: isar
sha256: "5be35dbc489880fccc535da3d1c4b3f5fdeee6ebfcacd4b149e39e803c4029cd"
sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.1.0+1"
isar_flutter_libs:
dependency: "direct main"
description:
name: isar_flutter_libs
sha256: "9794524734856a8a3629652f9f359b66e3fea3cebeec4dbdeb3e3a8fb253073e"
sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.1.0+1"
isar_generator:
dependency: "direct dev"
description:
name: isar_generator
sha256: ee4ab5d5b251bc7e86e1257793b57af100065831f00f3a12404b177ae53c2d69
sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.1.0+1"
js:
dependency: transitive
description:

View File

@@ -2,8 +2,8 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.57.0+80
isar_version: &isar_version 3.0.5
version: 1.58.0+81
isar_version: &isar_version 3.1.0+1
environment:
sdk: ">=3.0.0-0 <4.0.0"

View File

@@ -47,11 +47,20 @@ void main() {
LoggerMessageSchema
],
maxSizeMiB: 256,
directory: ".",
);
}
group('Test SyncService grouped', () {
late final Isar db;
final owner = User(
id: "1",
updatedAt: DateTime.now(),
email: "a@b.c",
firstName: "first",
lastName: "last",
isAdmin: false,
);
setUpAll(() async {
WidgetsFlutterBinding.ensureInitialized();
await Isar.initializeIsarCore(download: true);
@@ -59,17 +68,7 @@ void main() {
ImmichLogger();
db.writeTxnSync(() => db.clearSync());
Store.init(db);
await Store.put(
StoreKey.currentUser,
User(
id: "1",
updatedAt: DateTime.now(),
email: "a@b.c",
firstName: "first",
lastName: "last",
isAdmin: false,
),
);
await Store.put(StoreKey.currentUser, owner);
});
final List<Asset> initialAssets = [
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
@@ -92,7 +91,7 @@ void main() {
makeAsset(localId: "1", remoteId: "1-1"),
];
expect(db.assets.countSync(), 5);
final bool c1 = await s.syncRemoteAssetsToDb(() => remoteAssets);
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
expect(c1, false);
expect(db.assets.countSync(), 5);
});
@@ -108,7 +107,7 @@ void main() {
makeAsset(localId: "1", remoteId: "3-1", deviceId: 3),
];
expect(db.assets.countSync(), 5);
final bool c1 = await s.syncRemoteAssetsToDb(() => remoteAssets);
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
expect(c1, true);
expect(db.assets.countSync(), 7);
});
@@ -124,19 +123,19 @@ void main() {
makeAsset(localId: "1", remoteId: "2-1d", deviceId: 2),
];
expect(db.assets.countSync(), 5);
final bool c1 = await s.syncRemoteAssetsToDb(() => remoteAssets);
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
expect(c1, true);
expect(db.assets.countSync(), 8);
final bool c2 = await s.syncRemoteAssetsToDb(() => remoteAssets);
final bool c2 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
expect(c2, false);
expect(db.assets.countSync(), 8);
remoteAssets.removeAt(4);
final bool c3 = await s.syncRemoteAssetsToDb(() => remoteAssets);
final bool c3 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
expect(c3, true);
expect(db.assets.countSync(), 7);
remoteAssets.add(makeAsset(localId: "1", remoteId: "2-1e", deviceId: 2));
remoteAssets.add(makeAsset(localId: "2", remoteId: "2-2", deviceId: 2));
final bool c4 = await s.syncRemoteAssetsToDb(() => remoteAssets);
final bool c4 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
expect(c4, true);
expect(db.assets.countSync(), 9);
});

View File

@@ -39,3 +39,5 @@ RUN npm link && npm cache clean --force
VOLUME /usr/src/app/upload
EXPOSE 3001
ENTRYPOINT ["/bin/sh"]

View File

@@ -5,21 +5,16 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AddAssetsDto } from './dto/add-assets.dto';
import { AddUsersDto } from './dto/add-users.dto';
import { CreateAlbumDto } from './dto/create-album.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
export interface IAlbumRepository {
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
get(albumId: string): Promise<AlbumEntity | null>;
delete(album: AlbumEntity): Promise<void>;
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
removeUser(album: AlbumEntity, userId: string): Promise<void>;
removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<number>;
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>;
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
updateThumbnails(): Promise<number | undefined>;
getCountByUserId(userId: string): Promise<AlbumCountResponseDto>;
getSharedWithUserAlbumCount(userId: string, assetId: string): Promise<number>;
@@ -45,19 +40,6 @@ export class AlbumRepository implements IAlbumRepository {
return new AlbumCountResponseDto(ownedAlbums.length, sharedAlbums, sharedAlbumCount);
}
async create(ownerId: string, dto: CreateAlbumDto): Promise<AlbumEntity> {
const album = await this.albumRepository.save({
ownerId,
albumName: dto.albumName,
sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value } as UserEntity)) ?? [],
assets: dto.assetIds?.map((value) => ({ id: value } as AssetEntity)) ?? [],
albumThumbnailAssetId: dto.assetIds?.[0] || null,
});
// need to re-load the relations
return this.get(album.id) as Promise<AlbumEntity>;
}
async get(albumId: string): Promise<AlbumEntity | null> {
return this.albumRepository.findOne({
where: { id: albumId },
@@ -77,10 +59,6 @@ export class AlbumRepository implements IAlbumRepository {
});
}
async delete(album: AlbumEntity): Promise<void> {
await this.albumRepository.delete({ id: album.id, ownerId: album.ownerId });
}
async addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity> {
album.sharedUsers.push(...addUsersDto.sharedUserIds.map((id) => ({ id } as UserEntity)));
album.updatedAt = new Date().toISOString();
@@ -143,13 +121,6 @@ export class AlbumRepository implements IAlbumRepository {
};
}
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity> {
album.albumName = updateAlbumDto.albumName || album.albumName;
album.albumThumbnailAssetId = updateAlbumDto.albumThumbnailAssetId || album.albumThumbnailAssetId;
return this.albumRepository.save(album);
}
/**
* Makes sure all thumbnails for albums are updated by:
* - Removing thumbnails from albums without assets

View File

@@ -1,13 +1,11 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Put, Query, Response } from '@nestjs/common';
import { Controller, Get, Post, Body, Param, Delete, Put, Query, Response } from '@nestjs/common';
import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe';
import { AlbumService } from './album.service';
import { CreateAlbumDto } from './dto/create-album.dto';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { AddAssetsDto } from './dto/add-assets.dto';
import { AddUsersDto } from './dto/add-users.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AlbumResponseDto } from '@app/domain';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
@@ -20,124 +18,91 @@ import {
} from '../../constants/download.constant';
import { DownloadDto } from '../asset/dto/download-library.dto';
import { CreateAlbumShareLinkDto as CreateAlbumSharedLinkDto } from './dto/create-album-shared-link.dto';
import { AlbumIdDto } from './dto/album-id.dto';
import { UseValidation } from '../../decorators/use-validation.decorator';
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
import { DownloadArchive } from '../../modules/download/download.service';
const handleDownload = (download: DownloadArchive, res: Res) => {
res.attachment(download.fileName);
res.setHeader(IMMICH_CONTENT_LENGTH_HINT, download.fileSize);
res.setHeader(IMMICH_ARCHIVE_FILE_COUNT, download.fileCount);
res.setHeader(IMMICH_ARCHIVE_COMPLETE, `${download.complete}`);
return download.stream;
};
@ApiTags('Album')
@Controller('album')
@UseValidation()
export class AlbumController {
constructor(private readonly albumService: AlbumService) {}
constructor(private readonly service: AlbumService) {}
@Authenticated()
@Get('count-by-user-id')
async getAlbumCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
return this.albumService.getAlbumCountByUserId(authUser);
getAlbumCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
return this.service.getCountByUserId(authUser);
}
@Authenticated()
@Post()
async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body() createAlbumDto: CreateAlbumDto) {
// TODO: Handle nonexistent sharedWithUserIds and assetIds.
return this.albumService.create(authUser, createAlbumDto);
}
@Authenticated()
@Put('/:albumId/users')
async addUsersToAlbum(
@GetAuthUser() authUser: AuthUserDto,
@Body() addUsersDto: AddUsersDto,
@Param() { albumId }: AlbumIdDto,
) {
@Put(':id/users')
addUsersToAlbum(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: AddUsersDto) {
// TODO: Handle nonexistent sharedUserIds.
return this.albumService.addUsersToAlbum(authUser, addUsersDto, albumId);
return this.service.addUsers(authUser, id, dto);
}
@Authenticated({ isShared: true })
@Put('/:albumId/assets')
async addAssetsToAlbum(
@Put(':id/assets')
addAssetsToAlbum(
@GetAuthUser() authUser: AuthUserDto,
@Body() addAssetsDto: AddAssetsDto,
@Param() { albumId }: AlbumIdDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AddAssetsDto,
): Promise<AddAssetsResponseDto> {
// TODO: Handle nonexistent assetIds.
// TODO: Disallow adding assets of another user to an album.
return this.albumService.addAssetsToAlbum(authUser, addAssetsDto, albumId);
return this.service.addAssets(authUser, id, dto);
}
@Authenticated({ isShared: true })
@Get('/:albumId')
async getAlbumInfo(@GetAuthUser() authUser: AuthUserDto, @Param() { albumId }: AlbumIdDto) {
return this.albumService.getAlbumInfo(authUser, albumId);
@Get(':id')
getAlbumInfo(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
return this.service.get(authUser, id);
}
@Authenticated()
@Delete('/:albumId/assets')
async removeAssetFromAlbum(
@Delete(':id/assets')
removeAssetFromAlbum(
@GetAuthUser() authUser: AuthUserDto,
@Body() removeAssetsDto: RemoveAssetsDto,
@Param() { albumId }: AlbumIdDto,
@Body() dto: RemoveAssetsDto,
@Param() { id }: UUIDParamDto,
): Promise<AlbumResponseDto> {
return this.albumService.removeAssetsFromAlbum(authUser, removeAssetsDto, albumId);
return this.service.removeAssets(authUser, id, dto);
}
@Authenticated()
@Delete('/:albumId')
async deleteAlbum(@GetAuthUser() authUser: AuthUserDto, @Param() { albumId }: AlbumIdDto) {
return this.albumService.deleteAlbum(authUser, albumId);
}
@Authenticated()
@Delete('/:albumId/user/:userId')
async removeUserFromAlbum(
@Delete(':id/user/:userId')
removeUserFromAlbum(
@GetAuthUser() authUser: AuthUserDto,
@Param() { albumId }: AlbumIdDto,
@Param() { id }: UUIDParamDto,
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
) {
return this.albumService.removeUserFromAlbum(authUser, albumId, userId);
}
@Authenticated()
@Patch('/:albumId')
async updateAlbumInfo(
@GetAuthUser() authUser: AuthUserDto,
@Body() updateAlbumInfoDto: UpdateAlbumDto,
@Param() { albumId }: AlbumIdDto,
) {
// TODO: Handle nonexistent albumThumbnailAssetId.
// TODO: Disallow setting asset from other user as albumThumbnailAssetId.
return this.albumService.updateAlbumInfo(authUser, updateAlbumInfoDto, albumId);
return this.service.removeUser(authUser, id, userId);
}
@Authenticated({ isShared: true })
@Get('/:albumId/download')
@Get(':id/download')
@ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } })
async downloadArchive(
downloadArchive(
@GetAuthUser() authUser: AuthUserDto,
@Param() { albumId }: AlbumIdDto,
@Param() { id }: UUIDParamDto,
@Query() dto: DownloadDto,
@Response({ passthrough: true }) res: Res,
) {
this.albumService.checkDownloadAccess(authUser);
const { stream, fileName, fileSize, fileCount, complete } = await this.albumService.downloadArchive(
authUser,
albumId,
dto,
);
res.attachment(fileName);
res.setHeader(IMMICH_CONTENT_LENGTH_HINT, fileSize);
res.setHeader(IMMICH_ARCHIVE_FILE_COUNT, fileCount);
res.setHeader(IMMICH_ARCHIVE_COMPLETE, `${complete}`);
return stream;
this.service.checkDownloadAccess(authUser);
return this.service.downloadArchive(authUser, id, dto).then((download) => handleDownload(download, res));
}
@Authenticated()
@Post('/create-shared-link')
async createAlbumSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body() createAlbumShareLinkDto: CreateAlbumSharedLinkDto,
) {
return this.albumService.createAlbumSharedLink(authUser, createAlbumShareLinkDto);
@Post('create-shared-link')
createAlbumSharedLink(@GetAuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumSharedLinkDto) {
return this.service.createSharedLink(authUser, dto);
}
}

View File

@@ -2,7 +2,7 @@ import { AlbumService } from './album.service';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
import { AlbumEntity, UserEntity } from '@app/infra/entities';
import { AlbumResponseDto, ICryptoRepository, IJobRepository, JobName, mapUser } from '@app/domain';
import { AlbumResponseDto, ICryptoRepository, IJobRepository, mapUser } from '@app/domain';
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
import { IAlbumRepository } from './album-repository';
import { DownloadService } from '../../modules/download/download.service';
@@ -121,12 +121,9 @@ describe('Album service', () => {
albumRepositoryMock = {
addAssets: jest.fn(),
addSharedUsers: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
get: jest.fn(),
removeAssets: jest.fn(),
removeUser: jest.fn(),
updateAlbum: jest.fn(),
updateThumbnails: jest.fn(),
getCountByUserId: jest.fn(),
getSharedWithUserAlbumCount: jest.fn(),
@@ -150,19 +147,6 @@ describe('Album service', () => {
);
});
it('creates album', async () => {
const albumEntity = _getOwnedAlbum();
albumRepositoryMock.create.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
const result = await sut.create(authUser, {
albumName: albumEntity.albumName,
});
expect(result.id).toEqual(albumEntity.id);
expect(result.albumName).toEqual(albumEntity.albumName);
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [albumEntity.id] } });
});
it('gets an owned album', async () => {
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
@@ -182,14 +166,14 @@ describe('Album service', () => {
shared: false,
assetCount: 0,
};
await expect(sut.getAlbumInfo(authUser, albumId)).resolves.toEqual(expectedResult);
await expect(sut.get(authUser, albumId)).resolves.toEqual(expectedResult);
});
it('gets a shared album', async () => {
const albumEntity = _getSharedWithAuthUserAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
const result = await sut.getAlbumInfo(authUser, albumId);
const result = await sut.get(authUser, albumId);
expect(result.id).toEqual(albumId);
expect(result.ownerId).toEqual(sharedAlbumOwnerId);
expect(result.shared).toEqual(true);
@@ -203,34 +187,19 @@ describe('Album service', () => {
const albumId = albumEntity.id;
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect(sut.getAlbumInfo(authUser, albumId)).rejects.toBeInstanceOf(ForbiddenException);
await expect(sut.get(authUser, albumId)).rejects.toBeInstanceOf(ForbiddenException);
});
it('throws a not found exception if the album is not found', async () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve(null));
await expect(sut.getAlbumInfo(authUser, '0002')).rejects.toBeInstanceOf(NotFoundException);
});
it('deletes an owned album', async () => {
const albumEntity = _getOwnedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.delete.mockImplementation(() => Promise.resolve());
await sut.deleteAlbum(authUser, albumId);
expect(albumRepositoryMock.delete).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.delete).toHaveBeenCalledWith(albumEntity);
});
it('prevents deleting a shared album (shared with auth user)', async () => {
const albumEntity = _getSharedWithAuthUserAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect(sut.deleteAlbum(authUser, albumId)).rejects.toBeInstanceOf(ForbiddenException);
await expect(sut.get(authUser, '0002')).rejects.toBeInstanceOf(NotFoundException);
});
it('removes a shared user from an owned album', async () => {
const albumEntity = _getOwnedSharedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeUser.mockImplementation(() => Promise.resolve());
await expect(sut.removeUserFromAlbum(authUser, albumEntity.id, ownedAlbumSharedWithId)).resolves.toBeUndefined();
await expect(sut.removeUser(authUser, albumEntity.id, ownedAlbumSharedWithId)).resolves.toBeUndefined();
expect(albumRepositoryMock.removeUser).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.removeUser).toHaveBeenCalledWith(albumEntity, ownedAlbumSharedWithId);
});
@@ -242,7 +211,7 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect(sut.removeUserFromAlbum(authUser, albumId, userIdToRemove)).rejects.toBeInstanceOf(ForbiddenException);
await expect(sut.removeUser(authUser, albumId, userIdToRemove)).rejects.toBeInstanceOf(ForbiddenException);
expect(albumRepositoryMock.removeUser).not.toHaveBeenCalled();
});
@@ -251,7 +220,7 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeUser.mockImplementation(() => Promise.resolve());
await sut.removeUserFromAlbum(authUser, albumEntity.id, authUser.id);
await sut.removeUser(authUser, albumEntity.id, authUser.id);
expect(albumRepositoryMock.removeUser).toHaveReturnedTimes(1);
expect(albumRepositoryMock.removeUser).toHaveBeenCalledWith(albumEntity, authUser.id);
});
@@ -261,7 +230,7 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeUser.mockImplementation(() => Promise.resolve());
await sut.removeUserFromAlbum(authUser, albumEntity.id, 'me');
await sut.removeUser(authUser, albumEntity.id, 'me');
expect(albumRepositoryMock.removeUser).toHaveReturnedTimes(1);
expect(albumRepositoryMock.removeUser).toHaveBeenCalledWith(albumEntity, authUser.id);
});
@@ -270,55 +239,7 @@ describe('Album service', () => {
const albumEntity = _getOwnedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect(sut.removeUserFromAlbum(authUser, albumEntity.id, authUser.id)).rejects.toBeInstanceOf(
BadRequestException,
);
});
it('updates a owned album', async () => {
const albumEntity = _getOwnedAlbum();
const albumId = albumEntity.id;
const updatedAlbumName = 'new album name';
const updatedAlbumThumbnailAssetId = '69d2f917-0b31-48d8-9d7d-673b523f1aac';
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
const updatedAlbum = { ...albumEntity, albumName: updatedAlbumName };
albumRepositoryMock.updateAlbum.mockResolvedValue(updatedAlbum);
const result = await sut.updateAlbumInfo(
authUser,
{
albumName: updatedAlbumName,
albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
},
albumId,
);
expect(result.id).toEqual(albumId);
expect(result.albumName).toEqual(updatedAlbumName);
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
albumName: updatedAlbumName,
albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
});
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } });
});
it('prevents updating a not owned album (shared with auth user)', async () => {
const albumEntity = _getSharedWithAuthUserAlbum();
const albumId = albumEntity.id;
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect(
sut.updateAlbumInfo(
authUser,
{
albumName: 'new album name',
albumThumbnailAssetId: '69d2f917-0b31-48d8-9d7d-673b523f1aac',
},
albumId,
),
).rejects.toBeInstanceOf(ForbiddenException);
await expect(sut.removeUser(authUser, albumEntity.id, authUser.id)).rejects.toBeInstanceOf(BadRequestException);
});
it('adds assets to owned album', async () => {
@@ -334,13 +255,7 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
const result = (await sut.addAssetsToAlbum(
authUser,
{
assetIds: ['1'],
},
albumId,
)) as AddAssetsResponseDto;
const result = (await sut.addAssets(authUser, albumId, { assetIds: ['1'] })) as AddAssetsResponseDto;
// TODO: stub and expect album rendered
expect(result.album?.id).toEqual(albumId);
@@ -359,13 +274,7 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
const result = (await sut.addAssetsToAlbum(
authUser,
{
assetIds: ['1'],
},
albumId,
)) as AddAssetsResponseDto;
const result = (await sut.addAssets(authUser, albumId, { assetIds: ['1'] })) as AddAssetsResponseDto;
// TODO: stub and expect album rendered
expect(result.album?.id).toEqual(albumId);
@@ -384,15 +293,7 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
await expect(
sut.addAssetsToAlbum(
authUser,
{
assetIds: ['1'],
},
albumId,
),
).rejects.toBeInstanceOf(ForbiddenException);
await expect(sut.addAssets(authUser, albumId, { assetIds: ['1'] })).rejects.toBeInstanceOf(ForbiddenException);
});
// it('removes assets from owned album', async () => {
@@ -448,14 +349,6 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
await expect(
sut.removeAssetsFromAlbum(
authUser,
{
assetIds: ['1'],
},
albumId,
),
).rejects.toBeInstanceOf(ForbiddenException);
await expect(sut.removeAssets(authUser, albumId, { assetIds: ['1'] })).rejects.toBeInstanceOf(ForbiddenException);
});
});

View File

@@ -1,11 +1,9 @@
import { BadRequestException, Inject, Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAlbumDto } from './dto/create-album.dto';
import { AlbumEntity, SharedLinkType } from '@app/infra/entities';
import { AddUsersDto } from './dto/add-users.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto';
import { AlbumResponseDto, IJobRepository, JobName, mapAlbum } from '@app/domain';
import { AlbumResponseDto, IJobRepository, mapAlbum } from '@app/domain';
import { IAlbumRepository } from './album-repository';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
@@ -55,35 +53,18 @@ export class AlbumService {
return album;
}
async create(authUser: AuthUserDto, createAlbumDto: CreateAlbumDto): Promise<AlbumResponseDto> {
const albumEntity = await this.albumRepository.create(authUser.id, createAlbumDto);
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [albumEntity.id] } });
return mapAlbum(albumEntity);
}
async getAlbumInfo(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> {
async get(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> {
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
return mapAlbum(album);
}
async addUsersToAlbum(authUser: AuthUserDto, addUsersDto: AddUsersDto, albumId: string): Promise<AlbumResponseDto> {
async addUsers(authUser: AuthUserDto, albumId: string, dto: AddUsersDto): Promise<AlbumResponseDto> {
const album = await this._getAlbum({ authUser, albumId });
const updatedAlbum = await this.albumRepository.addSharedUsers(album, addUsersDto);
const updatedAlbum = await this.albumRepository.addSharedUsers(album, dto);
return mapAlbum(updatedAlbum);
}
async deleteAlbum(authUser: AuthUserDto, albumId: string): Promise<void> {
const album = await this._getAlbum({ authUser, albumId });
for (const sharedLink of album.sharedLinks) {
await this.shareCore.remove(authUser.id, sharedLink.id);
}
await this.albumRepository.delete(album);
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ALBUM, data: { ids: [albumId] } });
}
async removeUserFromAlbum(authUser: AuthUserDto, albumId: string, userId: string | 'me'): Promise<void> {
async removeUser(authUser: AuthUserDto, albumId: string, userId: string | 'me'): Promise<void> {
const sharedUserId = userId == 'me' ? authUser.id : userId;
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
if (album.ownerId != authUser.id && authUser.id != sharedUserId) {
@@ -95,34 +76,26 @@ export class AlbumService {
await this.albumRepository.removeUser(album, sharedUserId);
}
async removeAssetsFromAlbum(
authUser: AuthUserDto,
removeAssetsDto: RemoveAssetsDto,
albumId: string,
): Promise<AlbumResponseDto> {
async removeAssets(authUser: AuthUserDto, albumId: string, dto: RemoveAssetsDto): Promise<AlbumResponseDto> {
const album = await this._getAlbum({ authUser, albumId });
const deletedCount = await this.albumRepository.removeAssets(album, removeAssetsDto);
const deletedCount = await this.albumRepository.removeAssets(album, dto);
const newAlbum = await this._getAlbum({ authUser, albumId });
if (deletedCount !== removeAssetsDto.assetIds.length) {
if (deletedCount !== dto.assetIds.length) {
throw new BadRequestException('Some assets were not found in the album');
}
return mapAlbum(newAlbum);
}
async addAssetsToAlbum(
authUser: AuthUserDto,
addAssetsDto: AddAssetsDto,
albumId: string,
): Promise<AddAssetsResponseDto> {
async addAssets(authUser: AuthUserDto, albumId: string, dto: AddAssetsDto): Promise<AddAssetsResponseDto> {
if (authUser.isPublicUser && !authUser.isAllowUpload) {
this.logger.warn('Deny public user attempt to add asset to album');
throw new ForbiddenException('Public user is not allowed to upload');
}
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
const result = await this.albumRepository.addAssets(album, addAssetsDto);
const result = await this.albumRepository.addAssets(album, dto);
const newAlbum = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
return {
@@ -131,25 +104,7 @@ export class AlbumService {
};
}
async updateAlbumInfo(
authUser: AuthUserDto,
updateAlbumDto: UpdateAlbumDto,
albumId: string,
): Promise<AlbumResponseDto> {
const album = await this._getAlbum({ authUser, albumId });
if (authUser.id != album.ownerId) {
throw new BadRequestException('Unauthorized to change album info');
}
const updatedAlbum = await this.albumRepository.updateAlbum(album, updateAlbumDto);
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } });
return mapAlbum(updatedAlbum);
}
async getAlbumCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
async getCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
return this.albumRepository.getCountByUserId(authUser.id);
}
@@ -160,7 +115,7 @@ export class AlbumService {
return this.downloadService.downloadArchive(album.albumName, assets);
}
async createAlbumSharedLink(authUser: AuthUserDto, dto: CreateAlbumShareLinkDto): Promise<SharedLinkResponseDto> {
async createSharedLink(authUser: AuthUserDto, dto: CreateAlbumShareLinkDto): Promise<SharedLinkResponseDto> {
const album = await this._getAlbum({ authUser, albumId: dto.albumId });
const sharedLink = await this.shareCore.create(authUser.id, {

View File

@@ -1,6 +0,0 @@
import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
export class AlbumIdDto {
@ValidateUUID()
albumId!: string;
}

View File

@@ -10,13 +10,17 @@ import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
import { In } from 'typeorm/find-options/operator/In';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { ITagRepository } from '../tag/tag.repository';
import { IsNull, Not } from 'typeorm';
import { AssetSearchDto } from './dto/asset-search.dto';
export interface AssetCheck {
id: string;
checksum: Buffer;
}
export interface IAssetRepository {
get(id: string): Promise<AssetEntity | null>;
create(
@@ -38,11 +42,8 @@ export interface IAssetRepository {
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
getArchivedAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
getExistingAssets(
userId: string,
checkDuplicateAssetDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto>;
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
countByIdAndUser(assetId: string, userId: string): Promise<number>;
}
@@ -103,19 +104,23 @@ export class AssetRepository implements IAssetRepository {
return this.getAssetCount(items);
}
async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
async getAssetByTimeBucket(userId: string, dto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
// Get asset entity from a list of time buckets
return await this.assetRepository
let builder = this.assetRepository
.createQueryBuilder('asset')
.where('asset.ownerId = :userId', { userId: userId })
.andWhere(`date_trunc('month', "fileCreatedAt") IN (:...buckets)`, {
buckets: [...getAssetByTimeBucketDto.timeBucket],
buckets: [...dto.timeBucket],
})
.andWhere('asset.resizePath is not NULL')
.andWhere('asset.isVisible = true')
.andWhere('asset.isArchived = false')
.orderBy('asset.fileCreatedAt', 'DESC')
.getMany();
.orderBy('asset.fileCreatedAt', 'DESC');
if (!dto.withoutThumbs) {
builder = builder.andWhere('asset.resizePath is not NULL');
}
return builder.getMany();
}
async getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum) {
@@ -310,41 +315,39 @@ export class AssetRepository implements IAssetRepository {
* @returns Promise<string[]> - Array of assetIds belong to the device
*/
async getAllByDeviceId(ownerId: string, deviceId: string): Promise<string[]> {
const rows = await this.assetRepository.find({
const items = await this.assetRepository.find({
select: { deviceAssetId: true },
where: {
ownerId,
deviceId,
isVisible: true,
},
select: ['deviceAssetId'],
});
const res: string[] = [];
rows.forEach((v) => res.push(v.deviceAssetId));
return res;
return items.map((asset) => asset.deviceAssetId);
}
/**
* Get asset by checksum on the database
* Get assets by checksums on the database
* @param ownerId
* @param checksum
* @param checksums
*
*/
getAssetByChecksum(ownerId: string, checksum: Buffer): Promise<AssetEntity> {
return this.assetRepository.findOneOrFail({
async getAssetsByChecksums(ownerId: string, checksums: Buffer[]): Promise<AssetCheck[]> {
return this.assetRepository.find({
select: {
id: true,
checksum: true,
},
where: {
ownerId,
checksum,
checksum: In(checksums),
},
relations: ['exifInfo'],
});
}
async getExistingAssets(
ownerId: string,
checkDuplicateAssetDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
const existingAssets = await this.assetRepository.find({
async getExistingAssets(ownerId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]> {
const assets = await this.assetRepository.find({
select: { deviceAssetId: true },
where: {
deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds),
@@ -352,7 +355,7 @@ export class AssetRepository implements IAssetRepository {
ownerId,
},
});
return new CheckExistingAssetsResponseDto(existingAssets.map((a) => a.deviceAssetId));
return assets.map((asset) => asset.deviceAssetId);
}
async countByIdAndUser(assetId: string, ownerId: string): Promise<number> {

View File

@@ -57,6 +57,8 @@ import { AssetSearchDto } from './dto/asset-search.dto';
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto';
import { AssetIdDto } from './dto/asset-id.dto';
import { DeviceIdDto } from './dto/device-id.dto';
@@ -76,6 +78,7 @@ export class AssetController {
[
{ name: 'assetData', maxCount: 1 },
{ name: 'livePhotoData', maxCount: 1 },
{ name: 'sidecarData', maxCount: 1 },
],
assetUploadOption,
),
@@ -88,18 +91,24 @@ export class AssetController {
async uploadFile(
@GetAuthUser() authUser: AuthUserDto,
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] }))
files: { assetData: ImmichFile[]; livePhotoData?: ImmichFile[] },
files: { assetData: ImmichFile[]; livePhotoData?: ImmichFile[]; sidecarData: ImmichFile[] },
@Body(new ValidationPipe()) dto: CreateAssetDto,
@Response({ passthrough: true }) res: Res,
): Promise<AssetFileUploadResponseDto> {
const file = mapToUploadFile(files.assetData[0]);
const _livePhotoFile = files.livePhotoData?.[0];
const _sidecarFile = files.sidecarData?.[0];
let livePhotoFile;
if (_livePhotoFile) {
livePhotoFile = mapToUploadFile(_livePhotoFile);
}
const responseDto = await this.assetService.uploadFile(authUser, dto, file, livePhotoFile);
let sidecarFile;
if (_sidecarFile) {
sidecarFile = mapToUploadFile(_sidecarFile);
}
const responseDto = await this.assetService.uploadFile(authUser, dto, file, livePhotoFile, sidecarFile);
if (responseDto.duplicate) {
res.status(200);
}
@@ -332,6 +341,19 @@ export class AssetController {
return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto);
}
/**
* Checks if assets exist by checksums
*/
@Authenticated()
@Post('/bulk-upload-check')
@HttpCode(200)
bulkUploadCheck(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: AssetBulkUploadCheckDto,
): Promise<AssetBulkUploadCheckResponseDto> {
return this.assetService.bulkUploadCheck(authUser, dto);
}
@Authenticated()
@Post('/shared-link')
async createAssetsSharedLink(

View File

@@ -1,5 +1,5 @@
import { AuthUserDto, IJobRepository, JobName } from '@app/domain';
import { AssetEntity, UserEntity } from '@app/infra/entities';
import { AssetEntity, AssetType, UserEntity } from '@app/infra/entities';
import { IAssetRepository } from './asset-repository';
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
import { parse } from 'node:path';
@@ -12,12 +12,13 @@ export class AssetCore {
dto: CreateAssetDto,
file: UploadFile,
livePhotoAssetId?: string,
sidecarFile?: UploadFile,
): Promise<AssetEntity> {
const asset = await this.repository.create({
owner: { id: authUser.id } as UserEntity,
mimeType: file.mimeType,
checksum: file.checksum || null,
checksum: file.checksum,
originalPath: file.originalPath,
deviceAssetId: dto.deviceAssetId,
@@ -39,9 +40,13 @@ export class AssetCore {
sharedLinks: [],
originalFileName: parse(file.originalName).name,
faces: [],
sidecarPath: sidecarFile?.originalPath || null,
});
await this.jobRepository.queue({ name: JobName.ASSET_UPLOADED, data: { asset } });
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
if (asset.type === AssetType.VIDEO) {
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: asset.id } });
}
return asset;
}

View File

@@ -157,7 +157,7 @@ describe('AssetService', () => {
getLocationsByUserId: jest.fn(),
getSearchPropertiesByUserId: jest.fn(),
getAssetByTimeBucket: jest.fn(),
getAssetByChecksum: jest.fn(),
getAssetsByChecksums: jest.fn(),
getAssetCountByUserId: jest.fn(),
getArchivedAssetCountByUserId: jest.fn(),
getExistingAssets: jest.fn(),
@@ -299,13 +299,13 @@ describe('AssetService', () => {
(error as any).constraint = 'UQ_userid_checksum';
assetRepositoryMock.create.mockRejectedValue(error);
assetRepositoryMock.getAssetByChecksum.mockResolvedValue(_getAsset_1());
assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES,
data: { files: ['fake_path/asset_1.jpeg', undefined] },
data: { files: ['fake_path/asset_1.jpeg', undefined, undefined] },
});
expect(storageMock.moveFile).not.toHaveBeenCalled();
});
@@ -328,8 +328,14 @@ describe('AssetService', () => {
});
expect(jobMock.queue.mock.calls).toEqual([
[{ name: JobName.ASSET_UPLOADED, data: { asset: assetEntityStub.livePhotoMotionAsset } }],
[{ name: JobName.ASSET_UPLOADED, data: { asset: assetEntityStub.livePhotoStillAsset } }],
[
{
name: JobName.METADATA_EXTRACTION,
data: { id: assetEntityStub.livePhotoMotionAsset.id, source: 'upload' },
},
],
[{ name: JobName.VIDEO_CONVERSION, data: { id: assetEntityStub.livePhotoMotionAsset.id } }],
[{ name: JobName.METADATA_EXTRACTION, data: { id: assetEntityStub.livePhotoStillAsset.id, source: 'upload' } }],
]);
});
});
@@ -413,10 +419,12 @@ describe('AssetService', () => {
undefined,
undefined,
undefined,
undefined,
'fake_path/asset_1.mp4',
undefined,
undefined,
undefined,
undefined,
],
},
});
@@ -462,10 +470,12 @@ describe('AssetService', () => {
'web-path-1',
'resize-path-1',
undefined,
undefined,
'original-path-2',
'web-path-2',
'resize-path-2',
'encoded-video-path-2',
undefined,
],
},
},

View File

@@ -63,6 +63,12 @@ import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
import { AssetSearchDto } from './dto/asset-search.dto';
import { AddAssetsDto } from '../album/dto/add-assets.dto';
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
import {
AssetUploadAction,
AssetRejectReason,
AssetBulkUploadCheckResponseDto,
} from './response-dto/asset-check-response.dto';
const fileInfo = promisify(stat);
@@ -100,6 +106,7 @@ export class AssetService {
dto: CreateAssetDto,
file: UploadFile,
livePhotoFile?: UploadFile,
sidecarFile?: UploadFile,
): Promise<AssetFileUploadResponseDto> {
if (livePhotoFile) {
livePhotoFile = {
@@ -116,19 +123,20 @@ export class AssetService {
livePhotoAsset = await this.assetCore.create(authUser, livePhotoDto, livePhotoFile);
}
const asset = await this.assetCore.create(authUser, dto, file, livePhotoAsset?.id);
const asset = await this.assetCore.create(authUser, dto, file, livePhotoAsset?.id, sidecarFile);
return { id: asset.id, duplicate: false };
} catch (error: any) {
// clean up files
await this.jobRepository.queue({
name: JobName.DELETE_FILES,
data: { files: [file.originalPath, livePhotoFile?.originalPath] },
data: { files: [file.originalPath, livePhotoFile?.originalPath, sidecarFile?.originalPath] },
});
// handle duplicates with a success response
if (error instanceof QueryFailedError && (error as any).constraint === 'UQ_userid_checksum') {
const duplicate = await this.getAssetByChecksum(authUser.id, file.checksum);
const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum);
const [duplicate] = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums);
return { id: duplicate.id, duplicate: true };
}
@@ -142,7 +150,10 @@ export class AssetService {
}
public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
const assets = await this._assetRepository.getAllByUserId(authUser.id, dto);
if (dto.userId && dto.userId !== authUser.id) {
await this.checkUserAccess(authUser, dto.userId);
}
const assets = await this._assetRepository.getAllByUserId(dto.userId || authUser.id, dto);
return assets.map((asset) => mapAsset(asset));
}
@@ -359,7 +370,13 @@ export class AssetService {
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [id] } });
result.push({ id, status: DeleteAssetStatusEnum.SUCCESS });
deleteQueue.push(asset.originalPath, asset.webpPath, asset.resizePath, asset.encodedVideoPath);
deleteQueue.push(
asset.originalPath,
asset.webpPath,
asset.resizePath,
asset.encodedVideoPath,
asset.sidecarPath,
);
// TODO refactor this to use cascades
if (asset.livePhotoVideoId && !ids.includes(asset.livePhotoVideoId)) {
@@ -463,7 +480,40 @@ export class AssetService {
authUser: AuthUserDto,
checkExistingAssetsDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
return this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto);
return {
existingIds: await this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto),
};
}
async bulkUploadCheck(authUser: AuthUserDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
const checksums: Buffer[] = dto.assets.map((asset) => Buffer.from(asset.checksum, 'hex'));
const results = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums);
const resultsMap: Record<string, string> = {};
for (const { id, checksum } of results) {
resultsMap[checksum.toString('hex')] = id;
}
return {
results: dto.assets.map(({ id, checksum }) => {
const duplicate = resultsMap[checksum];
if (duplicate) {
return {
id,
assetId: duplicate,
action: AssetUploadAction.REJECT,
reason: AssetRejectReason.DUPLICATE,
};
}
// TODO mime-check
return {
id,
action: AssetUploadAction.ACCEPT,
};
}),
};
}
async getAssetCountByTimeBucket(
@@ -482,10 +532,6 @@ export class AssetService {
return mapAssetCountByTimeBucket(result);
}
getAssetByChecksum(userId: string, checksum: Buffer) {
return this._assetRepository.getAssetByChecksum(userId, checksum);
}
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this._assetRepository.getAssetCountByUserId(authUser.id);
}

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