Compare commits

...

61 Commits

Author SHA1 Message Date
Alex The Bot
da1710bcd2 Version v1.59.1 2023-05-30 17:56:47 +00:00
Jason Rasmussen
2dfd56b49b fix: reload assets from typesense results (#2615)
* fix: reload assets from typesense results

* chore: coverage
2023-05-30 12:55:06 -05:00
Alex The Bot
6538e599dd Version v1.59.0 2023-05-30 15:27:35 +00:00
Michel Heusschen
789e3e3924 refactor(server): use date type for entities (#2602) 2023-05-30 08:15:56 -05:00
Michel Heusschen
3d505e425d fix(web): show icons for empty album (#2604) 2023-05-29 13:58:09 -05:00
Manuel Taberna
e7122d7a72 feat(web): add zoom to photo viewer (#2577)
* feat(web): add zoom to photo viewer

* reduce asset viewer next/prev button div width

* add wrap to block statement
2023-05-29 09:12:58 -05:00
Michel Heusschen
94d0705607 refactor(server): change asset entity to date type (#2599)
* refactor(server): change asset entity to date type

* lower coverage threshold
2023-05-29 09:05:14 -05:00
Jason Rasmussen
caba462703 fix(server): library folder missing on new install (#2597) 2023-05-28 20:48:07 -05:00
Jason Rasmussen
ffe397247e refactor(server): auth decorator (#2588) 2023-05-28 11:30:01 -05:00
Michel Heusschen
e7ad622c02 refactor(web): user avatar (#2585)
* refactor(web): user avatar

* change user settings link

* update package lock json

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-28 08:10:55 -05:00
Jason Rasmussen
bca4626708 feat(server): return asset checksum (#2582)
* feat: return asset checksum

* chore: generate open api

* chore: coverage

* feat(server): support base64 hashes in bulk upload check:

* chore: generate open api
2023-05-27 20:56:17 -05:00
Michel Heusschen
7f0ad8e2d2 fix(web+mobile): consistent filename handling (#2534) 2023-05-27 20:53:29 -05:00
Sergey Kondrikov
6c6c5ef651 chore(web): generate API functions with a single argument (#2568) 2023-05-27 20:52:22 -05:00
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
Alex The Bot
cd7fc7e026 Version v1.57.0 2023-05-23 02:03:49 +00:00
Alex
b4d312efb6 fix(web): revert justify layout - improve gallery view load time (#2522)
* fix(web): revert justify layout - improve gallery view load time

* Remove package
2023-05-22 21:01:32 -05:00
Mert
e9722710ac feat(server): transcode bitrate and thread settings (#2488)
* support for two-pass transcoding

* added max bitrate and thread to transcode api

* admin page setting desc+bitrate and thread options

* Update web/src/lib/components/admin-page/settings/setting-input-field.svelte

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

* Update web/src/lib/components/admin-page/settings/setting-input-field.svelte

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

* two-pass slider, `crf` and `threads` as numbers

* updated and added transcode tests

* refactored `getFfmpegOptions`

* default `threads`, `maxBitrate` now 0, more tests

* vp9 constant quality mode

* fixed nullable `crf` and `threads`

* fixed two-pass slider, added apiproperty

* optional `desc` for `SettingSelect`

* disable two-pass if settings are incompatible

* fixed test

* transcode interface

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2023-05-22 13:07:43 -05:00
Michel Heusschen
f1384fea58 feat(server): pagination for asset queries in jobs (#2516)
* feat(server): pagination for asset queries in jobs

* default mock value for getAll

* remove live photo name correction

* order paginated results by createdAt

* change log level

* move usePagination to domain
2023-05-22 13:05:06 -05:00
Alex
feadc45e75 chore(server): Remove dist directory in command script (#2518) 2023-05-22 10:27:08 -05:00
Jason Rasmussen
eefe5266a8 chore(server): remove unused filename (#2517) 2023-05-22 10:26:56 -05:00
Jason Rasmussen
74353193f8 feat(web,server): user storage label (#2418)
* feat: user storage label

* chore: open api

* fix: checks

* fix: api update validation and tests

* feat: default admin storage label

* fix: linting

* fix: user create/update dto

* fix: delete library with custom label
2023-05-21 23:18:10 -04:00
Jason Rasmussen
0ccb73cf2b feat(server): add missing thumbnail check to nightly jobs (#2510) 2023-05-21 21:24:21 -05:00
Mert
356f4424df chore(server): queue handlers shouldn't increase concurrency (#2508) 2023-05-21 21:11:26 -05:00
Michel Heusschen
85c6cf4309 fix(web): context menu overlap + outclick types (#2506) 2023-05-21 11:01:08 -05:00
Michel Heusschen
96fb68135e fix(nginx): enable gzip and show error logs (#2504) 2023-05-21 08:23:46 -05:00
Michel Heusschen
a7b9adc692 feat(web+server): map improvements (#2498)
* feat(web+server): map improvements

* add number format double to fix mobile
2023-05-21 01:26:06 -05:00
Jason Rasmussen
e028cf9002 fix(server): reverse geocoding crash loop (#2489) 2023-05-20 21:39:12 -05:00
Jason Rasmussen
f984be8ea0 docs: update contributing pages (#2503) 2023-05-20 20:46:09 -05:00
Jason Rasmussen
3d426b55d3 chore(server): auth request type (#2502) 2023-05-20 20:44:26 -05:00
Fynn Petersen-Frey
02b8b2c125 chore(mobile): remove hive (#2497) 2023-05-20 20:42:19 -05:00
Fynn Petersen-Frey
dc7b0f75bb chore(mobile): use Record instead of custom pair+triple (#2483) 2023-05-20 20:41:34 -05:00
Jason Rasmussen
a089d9891d feat: confirm before deleting all faces and people (#2496) 2023-05-20 20:40:53 -05:00
431 changed files with 10763 additions and 5719 deletions

View File

@@ -1,17 +1,17 @@
dev:
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-new:
rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-new-update:
rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
dev-update:
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
dev-scale:
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
stage:
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans

View File

@@ -1,32 +0,0 @@
# Development Setup
## Lint / format extensions
Setting these in the IDE give a better developer experience auto-formatting code on save and providing instant feedback on lint issues.
### VSCode
Install Prettier, ESLint and Svelte extensions.
in User `settings.json` (`cmd + shift + p` and search for Open User Settings JSON) add the following:
```json
{
"editor.formatOnSave": true,
"[javascript][typescript][css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.tabSize": 2
},
"svelte.enable-ts-plugin": true,
"eslint.validate": ["javascript", "svelte"]
}
```
## Running tests / checks
In both server and web:
`npm run check:all`

View File

@@ -135,8 +135,6 @@ services:
dockerfile: Dockerfile
ports:
- 2283:8080
logging:
driver: none
depends_on:
- immich-server
restart: always

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
@@ -87,8 +86,6 @@ services:
- IMMICH_WEB_URL
ports:
- 2283:8080
logging:
driver: none
depends_on:
- immich-server
restart: always

View File

@@ -0,0 +1,14 @@
# Database Migrations
After making any changes in the `server/libs/database/src/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration.
1. Run the command
```bash
npm run typeorm:migrations:generate ./libs/infra/src/<migration-name>
```
2. Check if the migration file makes sense.
3. Move the migration file to folder `server/libs/database/src/migrations` in your code editor.
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.

View File

@@ -1,7 +1,17 @@
---
sidebar_position: 5
---
# Open API
Immich uses the [Open API](https://swagger.io/specification/) standard to generate API documentation. To view the published docs see [here](/docs/api).
## Generator
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). When you add a new or modify an existing endpoint, you must run the command below to update the client SDK.
```bash
npm run api:generate # Run from the `server/` directory
```
You can find the generated client SDK in the `web/src/api` for Typescript SDK and `mobile/openapi` for Dart SDK.
:::tip
This can also be run via `make api` from the project root directory (not in the `server` folder)
:::

View File

@@ -1,16 +1,8 @@
---
sidebar_position: 3
---
# Contributing
Contributions are welcome!
## PR Checklist
# PR Checklist
When contributing code through a pull request, please check the following:
### Web Checks
## Web Checks
- [ ] `npm run lint` (linting via ESLint)
- [ ] `npm run format` (formatting via Prettier)
@@ -21,7 +13,7 @@ When contributing code through a pull request, please check the following:
Run all web checks with `npm run check:all`
:::
### Server Checks
## Server Checks
- [ ] `npm run lint` (linting via ESLint)
- [ ] `npm run format` (formatting via Prettier)
@@ -32,12 +24,10 @@ Run all web checks with `npm run check:all`
Run all server checks with `npm run check:all`
:::
### Open API
## Open API
The Open API client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file.
The Open API client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. See [Open API](/docs/developer/open-api.md) for more details.
- [ ] `npm run api:generate`
## Database Migrations
:::tip
This can also be run via `make api` from the project root directory (not in the `server` folder)
:::
A database migration needs to be generated whenever there are changes to `server/libs/infra/src/entities`. See [Database Migration](/docs/developer/database-migrations.md) for more details.

View File

@@ -92,27 +92,3 @@ in User `settings.json` (`cmd + shift + p` and search for `Open User Settings JS
}
}
```
## OpenAPI generator
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). When you add a new or modify an existing endpoint, you must run the command below to update the client SDK.
```bash
npm run api:generate # Run from the `server` directory
```
You can find the generated client SDK in the `web/src/api` for Typescript SDK and `mobile/openapi` for Dart SDK.
## Database migrations
After making any changes in the `server/libs/database/src/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration.
1. Attached to the server container shell.
2. Run
```bash
npm run typeorm:migrations:generate ./libs/infra/src/<migration-name>
```
3. Check if the migration file makes sense.
4. Move the migration file to folder `server/libs/database/src/migrations` in your code editor.

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' />

View File

@@ -22,6 +22,7 @@
{ "source": "/docs/features/password-login", "destination": "/docs/administration/password-login" },
{ "source": "/docs/features/server-commands", "destination": "/docs/administration/server-commands" },
{ "source": "/docs/features/storage-template", "destination": "/docs/administration/storage-template" },
{ "source": "/docs/features/user-management", "destination": "/docs/administration/user-management" }
{ "source": "/docs/features/user-management", "destination": "/docs/administration/user-management" },
{ "source": "/docs/developer/contributing", "destination": "/docs/developer/pr-checklist" }
]
}

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" => 79,
"android.injected.version.name" => "1.56.2",
"android.injected.version.code" => 82,
"android.injected.version.name" => "1.59.1",
}
)
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.56.2"
version_number: "1.59.1"
)
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

@@ -1,37 +0,0 @@
// Access token
const String userInfoBox = "immichBoxUserInfo"; // Box
const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1
const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3
const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4
const String assetEtagKey = 'immichAssetEtagKey'; // Key 5
const String userIdKey = 'immichUserIdKey'; // Key 6
// Login Info
const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box
const String savedLoginInfoKey = "immichSavedLoginInfoKey"; // Key 1
// Backup Info
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox"; // Box
const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1
// Github Release Info
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
// User Setting Info
const String userSettingInfoBox = "immichUserSettingInfoBox";
// Background backup Info
const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
const String backupFailedSince = "immichBackupFailedSince"; // Key 1
const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2
const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3
const String backupTriggerDelay = "immichBackupTriggerDelay"; // Key 4
// Duplicate asset
const String duplicatedAssetsBox = "immichDuplicatedAssetsBox"; // Box
const String duplicatedAssetsKey = "immichDuplicatedAssetsKey"; // Key 1
// In app logger
const String immichLoggerBox = "immichInAppLogger"; // Box

View File

@@ -6,17 +6,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart';
@@ -24,8 +20,8 @@ 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/immich_logger_message.model.dart';
import 'package:immich_mobile/shared/models/logger_message.model.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
@@ -50,18 +46,11 @@ void main() async {
final db = await loadDb();
await initApp();
await migrateHiveToStoreIfNecessary();
await migrateJsonCacheIfNecessary();
await migrateDatabaseIfNeeded(db);
runApp(getMainWidget(db));
}
Future<void> initApp() async {
await Hive.initFlutter();
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
Hive.registerAdapter(HiveBackupAlbumsAdapter());
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
Hive.registerAdapter(ImmichLoggerMessageAdapter());
await EasyLocalization.ensureInitialized();
if (kReleaseMode && Platform.isAndroid) {
@@ -101,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,23 +0,0 @@
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/services/json_cache.dart';
@Deprecated("only kept to remove its files after migration")
class _BaseAlbumCacheService extends JsonCache<List<Album>> {
_BaseAlbumCacheService(super.cacheFileName);
@override
void put(List<Album> data) {}
@override
Future<List<Album>?> get() => Future.value(null);
}
@Deprecated("only kept to remove its files after migration")
class AlbumCacheService extends _BaseAlbumCacheService {
AlbumCacheService() : super("album_cache");
}
@Deprecated("only kept to remove its files after migration")
class SharedAlbumCacheService extends _BaseAlbumCacheService {
SharedAlbumCacheService() : super("shared_album_cache");
}

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

@@ -1,105 +0,0 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:hive/hive.dart';
part 'hive_backup_albums.model.g.dart';
@HiveType(typeId: 1)
class HiveBackupAlbums {
@HiveField(0)
List<String> selectedAlbumIds;
@HiveField(1)
List<String> excludedAlbumsIds;
@HiveField(2, defaultValue: [])
List<DateTime> lastSelectedBackupTime;
@HiveField(3, defaultValue: [])
List<DateTime> lastExcludedBackupTime;
HiveBackupAlbums({
required this.selectedAlbumIds,
required this.excludedAlbumsIds,
required this.lastSelectedBackupTime,
required this.lastExcludedBackupTime,
});
@override
String toString() =>
'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
HiveBackupAlbums copyWith({
List<String>? selectedAlbumIds,
List<String>? excludedAlbumsIds,
List<DateTime>? lastSelectedBackupTime,
List<DateTime>? lastExcludedBackupTime,
}) {
return HiveBackupAlbums(
selectedAlbumIds: selectedAlbumIds ?? this.selectedAlbumIds,
excludedAlbumsIds: excludedAlbumsIds ?? this.excludedAlbumsIds,
lastSelectedBackupTime:
lastSelectedBackupTime ?? this.lastSelectedBackupTime,
lastExcludedBackupTime:
lastExcludedBackupTime ?? this.lastExcludedBackupTime,
);
}
/// Returns a deep copy to allow safe modification without changing the global
/// state of [HiveBackupAlbums] before actually saving the changes
HiveBackupAlbums deepCopy() {
return HiveBackupAlbums(
selectedAlbumIds: selectedAlbumIds.toList(),
excludedAlbumsIds: excludedAlbumsIds.toList(),
lastSelectedBackupTime: lastSelectedBackupTime.toList(),
lastExcludedBackupTime: lastExcludedBackupTime.toList(),
);
}
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
result.addAll({'selectedAlbumIds': selectedAlbumIds});
result.addAll({'excludedAlbumsIds': excludedAlbumsIds});
result.addAll({'lastSelectedBackupTime': lastSelectedBackupTime});
result.addAll({'lastExcludedBackupTime': lastExcludedBackupTime});
return result;
}
factory HiveBackupAlbums.fromMap(Map<String, dynamic> map) {
return HiveBackupAlbums(
selectedAlbumIds: List<String>.from(map['selectedAlbumIds']),
excludedAlbumsIds: List<String>.from(map['excludedAlbumsIds']),
lastSelectedBackupTime:
List<DateTime>.from(map['lastSelectedBackupTime']),
lastExcludedBackupTime:
List<DateTime>.from(map['lastExcludedBackupTime']),
);
}
String toJson() => json.encode(toMap());
factory HiveBackupAlbums.fromJson(String source) =>
HiveBackupAlbums.fromMap(json.decode(source));
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;
return other is HiveBackupAlbums &&
listEquals(other.selectedAlbumIds, selectedAlbumIds) &&
listEquals(other.excludedAlbumsIds, excludedAlbumsIds) &&
listEquals(other.lastSelectedBackupTime, lastSelectedBackupTime) &&
listEquals(other.lastExcludedBackupTime, lastExcludedBackupTime);
}
@override
int get hashCode =>
selectedAlbumIds.hashCode ^
excludedAlbumsIds.hashCode ^
lastSelectedBackupTime.hashCode ^
lastExcludedBackupTime.hashCode;
}

View File

@@ -1,52 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hive_backup_albums.model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class HiveBackupAlbumsAdapter extends TypeAdapter<HiveBackupAlbums> {
@override
final int typeId = 1;
@override
HiveBackupAlbums read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return HiveBackupAlbums(
selectedAlbumIds: (fields[0] as List).cast<String>(),
excludedAlbumsIds: (fields[1] as List).cast<String>(),
lastSelectedBackupTime:
fields[2] == null ? [] : (fields[2] as List).cast<DateTime>(),
lastExcludedBackupTime:
fields[3] == null ? [] : (fields[3] as List).cast<DateTime>(),
);
}
@override
void write(BinaryWriter writer, HiveBackupAlbums obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.selectedAlbumIds)
..writeByte(1)
..write(obj.excludedAlbumsIds)
..writeByte(2)
..write(obj.lastSelectedBackupTime)
..writeByte(3)
..write(obj.lastExcludedBackupTime);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HiveBackupAlbumsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -1,57 +0,0 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:hive/hive.dart';
part 'hive_duplicated_assets.model.g.dart';
@HiveType(typeId: 2)
class HiveDuplicatedAssets {
@HiveField(0, defaultValue: [])
List<String> duplicatedAssetIds;
HiveDuplicatedAssets({
required this.duplicatedAssetIds,
});
HiveDuplicatedAssets copyWith({
List<String>? duplicatedAssetIds,
}) {
return HiveDuplicatedAssets(
duplicatedAssetIds: duplicatedAssetIds ?? this.duplicatedAssetIds,
);
}
Map<String, dynamic> toMap() {
return {
'duplicatedAssetIds': duplicatedAssetIds,
};
}
factory HiveDuplicatedAssets.fromMap(Map<String, dynamic> map) {
return HiveDuplicatedAssets(
duplicatedAssetIds: List<String>.from(map['duplicatedAssetIds']),
);
}
String toJson() => json.encode(toMap());
factory HiveDuplicatedAssets.fromJson(String source) =>
HiveDuplicatedAssets.fromMap(json.decode(source));
@override
String toString() =>
'HiveDuplicatedAssets(duplicatedAssetIds: $duplicatedAssetIds)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;
return other is HiveDuplicatedAssets &&
listEquals(other.duplicatedAssetIds, duplicatedAssetIds);
}
@override
int get hashCode => duplicatedAssetIds.hashCode;
}

View File

@@ -1,42 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hive_duplicated_assets.model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class HiveDuplicatedAssetsAdapter extends TypeAdapter<HiveDuplicatedAssets> {
@override
final int typeId = 2;
@override
HiveDuplicatedAssets read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return HiveDuplicatedAssets(
duplicatedAssetIds:
fields[0] == null ? [] : (fields[0] as List).cast<String>(),
);
}
@override
void write(BinaryWriter writer, HiveDuplicatedAssets obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.duplicatedAssetIds);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HiveDuplicatedAssetsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -219,8 +219,6 @@ class BackupService {
if (file != null) {
String originalFileName = await entity.titleAsync;
String fileNameWithoutPath =
originalFileName.toString().split(".")[0];
var fileExtension = p.extension(file.path);
var mimeType = FileHelper.getMimeType(file.path);
var fileStream = file.openRead();
@@ -228,7 +226,7 @@ class BackupService {
"assetData",
fileStream,
file.lengthSync(),
filename: fileNameWithoutPath,
filename: originalFileName,
contentType: MediaType(
mimeType["type"],
mimeType["subType"],
@@ -334,14 +332,13 @@ class BackupService {
var motionFile = File(validPath);
var fileStream = motionFile.openRead();
String originalFileName = await entity.titleAsync;
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
var mimeType = FileHelper.getMimeType(validPath);
return http.MultipartFile(
"livePhotoData",
fileStream,
motionFile.lengthSync(),
filename: fileNameWithoutPath,
filename: originalFileName,
contentType: MediaType(
mimeType["type"],
mimeType["subType"],

View File

@@ -364,7 +364,7 @@ class BackupControllerPage extends HookConsumerWidget {
.read(backgroundServiceProvider)
.getIOSBackgroundAppRefreshEnabled(),
builder: (context, snapshot) {
final enabled = snapshot.data as bool?;
final enabled = snapshot.data;
// If it's not enabled, show them some kind of alert that says
// background refresh is not enabled
if (enabled != null && !enabled) {}

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

@@ -1,25 +0,0 @@
import 'package:hive/hive.dart';
part 'hive_saved_login_info.model.g.dart';
@HiveType(typeId: 0)
class HiveSavedLoginInfo {
@HiveField(0)
String email; // DEPRECATED
@HiveField(1)
String password; // DEPRECATED
@HiveField(2)
String serverUrl;
@HiveField(4, defaultValue: "")
String accessToken;
HiveSavedLoginInfo({
required this.email,
required this.password,
required this.serverUrl,
required this.accessToken,
});
}

View File

@@ -1,50 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hive_saved_login_info.model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class HiveSavedLoginInfoAdapter extends TypeAdapter<HiveSavedLoginInfo> {
@override
final int typeId = 0;
@override
HiveSavedLoginInfo read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return HiveSavedLoginInfo(
email: fields[0] as String,
password: fields[1] as String,
serverUrl: fields[2] as String,
accessToken: fields[4] == null ? '' : fields[4] as String,
);
}
@override
void write(BinaryWriter writer, HiveSavedLoginInfo obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.email)
..writeByte(1)
..write(obj.password)
..writeByte(2)
..write(obj.serverUrl)
..writeByte(4)
..write(obj.accessToken);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HiveSavedLoginInfoAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

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 &&
@@ -128,8 +128,8 @@ class Album {
final Album a = Album(
remoteId: dto.id,
name: dto.albumName,
createdAt: DateTime.parse(dto.createdAt),
modifiedAt: DateTime.parse(dto.updatedAt),
createdAt: dto.createdAt,
modifiedAt: dto.updatedAt,
shared: dto.shared,
);
a.owner.value = await db.users.getById(dto.ownerId);

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

@@ -15,9 +15,9 @@ class Asset {
Asset.remote(AssetResponseDto remote)
: remoteId = remote.id,
isLocal = false,
fileCreatedAt = DateTime.parse(remote.fileCreatedAt),
fileModifiedAt = DateTime.parse(remote.fileModifiedAt),
updatedAt = DateTime.parse(remote.updatedAt),
fileCreatedAt = remote.fileCreatedAt,
fileModifiedAt = remote.fileModifiedAt,
updatedAt = remote.updatedAt,
durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0,
type = remote.type.toAssetType(),
fileName = p.basename(remote.originalPath),
@@ -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

@@ -1,34 +0,0 @@
import 'package:hive/hive.dart';
part 'immich_logger_message.model.g.dart';
@HiveType(typeId: 3)
class ImmichLoggerMessage {
@HiveField(0)
String message;
@HiveField(1, defaultValue: "INFO")
String level;
@HiveField(2)
DateTime createdAt;
@HiveField(3)
String? context1;
@HiveField(4)
String? context2;
ImmichLoggerMessage({
required this.message,
required this.level,
required this.createdAt,
required this.context1,
required this.context2,
});
@override
String toString() {
return 'InAppLoggerMessage(message: $message, level: $level, createdAt: $createdAt)';
}
}

View File

@@ -1,53 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'immich_logger_message.model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ImmichLoggerMessageAdapter extends TypeAdapter<ImmichLoggerMessage> {
@override
final int typeId = 3;
@override
ImmichLoggerMessage read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ImmichLoggerMessage(
message: fields[0] as String,
level: fields[1] == null ? 'INFO' : fields[1] as String,
createdAt: fields[2] as DateTime,
context1: fields[3] as String?,
context2: fields[4] as String?,
);
}
@override
void write(BinaryWriter writer, ImmichLoggerMessage obj) {
writer
..writeByte(5)
..writeByte(0)
..write(obj.message)
..writeByte(1)
..write(obj.level)
..writeByte(2)
..write(obj.createdAt)
..writeByte(3)
..write(obj.context1)
..writeByte(4)
..write(obj.context2);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ImmichLoggerMessageAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

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,18 +14,20 @@ class User {
required this.firstName,
required this.lastName,
required this.isAdmin,
this.isPartnerSharedBy = false,
this.isPartnerSharedWith = false,
});
Id get isarId => fastHash(id);
User.fromDto(UserResponseDto dto)
: id = dto.id,
updatedAt = dto.updatedAt != null
? DateTime.parse(dto.updatedAt!).toUtc()
: DateTime.now().toUtc(),
updatedAt = dto.updatedAt,
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 +36,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 +48,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 +65,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,14 +3,15 @@ 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';
import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:immich_mobile/utils/openapi_extensions.dart';
import 'package:immich_mobile/utils/tuple.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -37,38 +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 Pair<List<AssetResponseDto>, String?>? remote =
await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
if (remote == null) {
final etag = hasCache ? _db.eTags.getByIdSync(user.id)?.value : null;
final (List<AssetResponseDto>? assets, String? newETag) =
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) {
_db.writeTxn(() => _db.eTags.put(ETag(id: user.id, value: newETag)));
}
if (remote.second != null && remote.second != etag) {
Store.put(StoreKey.assetETag, remote.second);
}
return remote.first;
return assets.map(Asset.remote).toList();
} catch (e, stack) {
log.severe('Error while getting remote assets', e, stack);
return null;

View File

@@ -1,13 +0,0 @@
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/services/json_cache.dart';
@Deprecated("only kept to remove its files after migration")
class AssetCacheService extends JsonCache<List<Asset>> {
AssetCacheService() : super("asset_cache");
@override
void put(List<Asset> data) {}
@override
Future<List<Asset>?> get() => Future.value(null);
}

View File

@@ -1,36 +0,0 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
@Deprecated("only kept to remove its files after migration")
abstract class JsonCache<T> {
final String cacheFileName;
JsonCache(this.cacheFileName);
Future<File> _getCacheFile() async {
final basePath = await getTemporaryDirectory();
final basePathName = basePath.path;
final file = File("$basePathName/$cacheFileName.bin");
return file;
}
Future<bool> isValid() async {
final file = await _getCacheFile();
return await file.exists();
}
Future<void> invalidate() async {
try {
final file = await _getCacheFile();
await file.delete();
} on FileSystemException {
// file is already deleted
}
}
void put(T data);
Future<T?> get();
}

View File

@@ -11,7 +11,6 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/utils/builtin_extensions.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/tuple.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -41,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;
}
@@ -62,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
@@ -94,7 +96,7 @@ class SyncService {
deleteCandidates.sort(Asset.compareById);
existing.sort(Asset.compareById);
return _diffAssets(existing, deleteCandidates, compare: Asset.compareById)
.third
.$3
.map((e) => e.id)
.toList();
}
@@ -150,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)
@@ -165,14 +167,14 @@ class SyncService {
.thenByFileModifiedAt()
.findAll();
remote.sort(Asset.compareByOwnerDeviceLocalIdModified);
final diff = _diffAssets(remote, inDb, remote: true);
if (diff.first.isEmpty && diff.second.isEmpty && diff.third.isEmpty) {
final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true);
if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) {
return false;
}
final idsToDelete = diff.third.map((e) => e.id).toList();
final idsToDelete = toRemove.map((e) => e.id).toList();
try {
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
await upsertAssetsWithExif(diff.first + diff.second);
await upsertAssetsWithExif(toAdd + toUpdate);
} on IsarError catch (e) {
_log.severe("Failed to sync remote assets to db: $e");
}
@@ -252,8 +254,7 @@ class SyncService {
.findAll();
final List<Asset> assetsOnRemote = dto.getAssets();
assetsOnRemote.sort(Asset.compareByOwnerDeviceLocalIdModified);
final d = _diffAssets(assetsOnRemote, assetsInDb);
final List<Asset> toAdd = d.first, toUpdate = d.second, toUnlink = d.third;
final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb);
// update shared users
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
@@ -271,14 +272,14 @@ class SyncService {
);
// for shared album: put missing album assets into local DB
final resultPair = await _linkWithExistingFromDb(toAdd);
await upsertAssetsWithExif(resultPair.second);
final assetsToLink = resultPair.first + resultPair.second;
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
await upsertAssetsWithExif(updated);
final assetsToLink = existingInDb + updated;
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
album.name = dto.albumName;
album.shared = dto.shared;
album.modifiedAt = DateTime.parse(dto.updatedAt);
album.modifiedAt = dto.updatedAt;
if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
album.thumbnail.value = await _db.assets
.where()
@@ -327,9 +328,10 @@ class SyncService {
if (dto.assetCount == dto.assets.length) {
// in case an album contains assets not yet present in local DB:
// put missing album assets into local DB
final result = await _linkWithExistingFromDb(dto.getAssets());
existing.addAll(result.first);
await upsertAssetsWithExif(result.second);
final (existingInDb, updated) =
await _linkWithExistingFromDb(dto.getAssets());
existing.addAll(existingInDb);
await upsertAssetsWithExif(updated);
final Album a = await Album.remote(dto);
await _db.writeTxn(() => _db.albums.store(a));
@@ -350,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));
@@ -393,18 +404,19 @@ class SyncService {
_log.fine(
"Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete",
);
final pair = _handleAssetRemoval(deleteCandidates, existing, remote: false);
final (toDelete, toUpdate) =
_handleAssetRemoval(deleteCandidates, existing, remote: false);
_log.fine(
"${pair.first.length} assets to delete, ${pair.second.length} to update",
"${toDelete.length} assets to delete, ${toUpdate.length} to update",
);
if (pair.first.isNotEmpty || pair.second.isNotEmpty) {
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
await _db.writeTxn(() async {
await _db.assets.deleteAll(pair.first);
await _db.exifInfos.deleteAll(pair.first);
await _db.assets.putAll(pair.second);
await _db.assets.deleteAll(toDelete);
await _db.exifInfos.deleteAll(toDelete);
await _db.assets.putAll(toUpdate);
});
_log.info(
"Removed ${pair.first.length} and updated ${pair.second.length} local assets from DB",
"Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB",
);
}
return anyChanges;
@@ -441,8 +453,8 @@ class SyncService {
final List<Asset> onDevice =
await ape.getAssets(excludedAssets: excludedAssets);
onDevice.sort(Asset.compareByLocalId);
final d = _diffAssets(onDevice, inDb, compare: Asset.compareByLocalId);
final List<Asset> toAdd = d.first, toUpdate = d.second, toDelete = d.third;
final (toAdd, toUpdate, toDelete) =
_diffAssets(onDevice, inDb, compare: Asset.compareByLocalId);
if (toAdd.isEmpty &&
toUpdate.isEmpty &&
toDelete.isEmpty &&
@@ -458,12 +470,12 @@ class SyncService {
_log.fine(
"Syncing local album ${ape.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
);
final result = await _linkWithExistingFromDb(toAdd);
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
_log.fine(
"Linking assets to add with existing from db. ${result.first.length} existing, ${result.second.length} to update",
"Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update",
);
deleteCandidates.addAll(toDelete);
existing.addAll(result.first);
existing.addAll(existingInDb);
album.name = ape.name;
album.modifiedAt = ape.lastModified ?? DateTime.now();
if (album.thumbnail.value != null &&
@@ -472,10 +484,10 @@ class SyncService {
}
try {
await _db.writeTxn(() async {
await _db.assets.putAll(result.second);
await _db.assets.putAll(updated);
await _db.assets.putAll(toUpdate);
await album.assets
.update(link: result.first + result.second, unlink: toDelete);
.update(link: existingInDb + updated, unlink: toDelete);
await _db.albums.put(album);
album.thumbnail.value ??= await album.assets.filter().findFirst();
await album.thumbnail.save();
@@ -510,11 +522,11 @@ class SyncService {
return false;
}
album.modifiedAt = ape.lastModified ?? DateTime.now();
final result = await _linkWithExistingFromDb(newAssets);
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
try {
await _db.writeTxn(() async {
await _db.assets.putAll(result.second);
await album.assets.update(link: result.first + result.second);
await _db.assets.putAll(updated);
await album.assets.update(link: existingInDb + updated);
await _db.albums.put(album);
});
_log.info("Fast synced local album ${ape.name} to DB");
@@ -536,15 +548,15 @@ class SyncService {
_log.info("Syncing a new local album to DB: ${ape.name}");
final Album a = Album.local(ape);
final assets = await ape.getAssets(excludedAssets: excludedAssets);
final result = await _linkWithExistingFromDb(assets);
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
_log.info(
"${result.first.length} assets already existed in DB, to upsert ${result.second.length}",
"${existingInDb.length} assets already existed in DB, to upsert ${updated.length}",
);
await upsertAssetsWithExif(result.second);
existing.addAll(result.first);
a.assets.addAll(result.first);
a.assets.addAll(result.second);
final thumb = result.first.firstOrNull ?? result.second.firstOrNull;
await upsertAssetsWithExif(updated);
existing.addAll(existingInDb);
a.assets.addAll(existingInDb);
a.assets.addAll(updated);
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
a.thumbnail.value = thumb;
try {
await _db.writeTxn(() => _db.albums.store(a));
@@ -555,11 +567,11 @@ class SyncService {
}
/// Returns a tuple (existing, updated)
Future<Pair<List<Asset>, List<Asset>>> _linkWithExistingFromDb(
Future<(List<Asset> existing, List<Asset> updated)> _linkWithExistingFromDb(
List<Asset> assets,
) async {
if (assets.isEmpty) {
return const Pair([], []);
return ([].cast<Asset>(), [].cast<Asset>());
}
final List<Asset> inDb = await _db.assets
.where()
@@ -596,7 +608,7 @@ class SyncService {
),
onlySecond: (Asset b) => toUpsert.add(b),
);
return Pair(existing, toUpsert);
return (existing, toUpsert);
}
/// Inserts or updates the assets in the database with their ExifInfo (if any)
@@ -623,7 +635,7 @@ class SyncService {
}
/// Returns a triple(toAdd, toUpdate, toRemove)
Triple<List<Asset>, List<Asset>, List<Asset>> _diffAssets(
(List<Asset> toAdd, List<Asset> toUpdate, List<Asset> toRemove) _diffAssets(
List<Asset> assets,
List<Asset> inDb, {
bool? remote,
@@ -660,30 +672,30 @@ Triple<List<Asset>, List<Asset>, List<Asset>> _diffAssets(
},
onlySecond: (Asset b) => toAdd.add(b),
);
return Triple(toAdd, toUpdate, toRemove);
return (toAdd, toUpdate, toRemove);
}
/// returns a tuple (toDelete toUpdate) when assets are to be deleted
Pair<List<int>, List<Asset>> _handleAssetRemoval(
(List<int> toDelete, List<Asset> toUpdate) _handleAssetRemoval(
List<Asset> deleteCandidates,
List<Asset> existing, {
bool? remote,
}) {
if (deleteCandidates.isEmpty) {
return const Pair([], []);
return const ([], []);
}
deleteCandidates.sort(Asset.compareById);
deleteCandidates.uniqueConsecutive((a) => a.id);
existing.sort(Asset.compareById);
existing.uniqueConsecutive((a) => a.id);
final triple = _diffAssets(
final (tooAdd, toUpdate, toRemove) = _diffAssets(
existing,
deleteCandidates,
compare: Asset.compareById,
remote: remote,
);
assert(triple.first.isEmpty, "toAdd should be empty in _handleAssetRemoval");
return Pair(triple.third.map((e) => e.id).toList(), triple.second);
assert(tooAdd.isEmpty, "toAdd should be empty in _handleAssetRemoval");
return (toRemove.map((e) => e.id).toList(), toUpdate);
}
/// returns `true` if the albums differ on the surface
@@ -701,5 +713,5 @@ bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
dto.shared != a.shared ||
dto.sharedUsers.length != a.sharedUsers.length ||
!DateTime.parse(dto.updatedAt).isAtSameMomentAs(a.modifiedAt);
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt);
}

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

@@ -1,151 +1,9 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/services/asset_cache.service.dart';
import 'package:immich_mobile/utils/db.dart';
import 'package:isar/isar.dart';
Future<void> migrateHiveToStoreIfNecessary() async {
await _migrateHiveBoxIfNecessary(userInfoBox, _migrateHiveUserInfoBox);
await _migrateHiveBoxIfNecessary(
backgroundBackupInfoBox,
_migrateHiveBackgroundBackupInfoBox,
);
await _migrateHiveBoxIfNecessary(hiveBackupInfoBox, _migrateBackupInfoBox);
await _migrateHiveBoxIfNecessary(
duplicatedAssetsBox,
_migrateDuplicatedAssetsBox,
);
await _migrateHiveBoxIfNecessary(
hiveGithubReleaseInfoBox,
_migrateReleaseInfoBox,
);
await _migrateHiveBoxIfNecessary(hiveLoginInfoBox, _migrateLoginInfoBox);
await _migrateHiveBoxIfNecessary(
immichLoggerBox,
(Box<ImmichLoggerMessage> box) => box.deleteFromDisk(),
);
await _migrateHiveBoxIfNecessary(userSettingInfoBox, _migrateAppSettingsBox);
}
FutureOr<void> _migrateReleaseInfoBox(Box box) =>
_migrateKey(box, githubReleaseInfoKey, StoreKey.githubReleaseInfo);
Future<void> _migrateLoginInfoBox(Box<HiveSavedLoginInfo> box) async {
final HiveSavedLoginInfo? info = box.get(savedLoginInfoKey);
if (info != null) {
await Store.put(StoreKey.serverUrl, info.serverUrl);
await Store.put(StoreKey.accessToken, info.accessToken);
}
}
Future<void> _migrateHiveUserInfoBox(Box box) async {
await _migrateKey(box, assetEtagKey, StoreKey.assetETag);
if (Store.tryGet(StoreKey.deviceId) == null) {
await _migrateKey(box, deviceIdKey, StoreKey.deviceId);
}
await _migrateKey(box, serverEndpointKey, StoreKey.serverEndpoint);
}
Future<void> _migrateHiveBackgroundBackupInfoBox(Box box) async {
await _migrateKey(box, backupFailedSince, StoreKey.backupFailedSince);
await _migrateKey(box, backupRequireWifi, StoreKey.backupRequireWifi);
await _migrateKey(box, backupRequireCharging, StoreKey.backupRequireCharging);
await _migrateKey(box, backupTriggerDelay, StoreKey.backupTriggerDelay);
}
FutureOr<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) {
final HiveBackupAlbums? infos = box.get(backupInfoKey);
if (infos != null) {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
List<BackupAlbum> albums = [];
for (int i = 0; i < infos.selectedAlbumIds.length; i++) {
final album = BackupAlbum(
infos.selectedAlbumIds[i],
infos.lastSelectedBackupTime[i],
BackupSelection.select,
);
albums.add(album);
}
for (int i = 0; i < infos.excludedAlbumsIds.length; i++) {
final album = BackupAlbum(
infos.excludedAlbumsIds[i],
infos.lastExcludedBackupTime[i],
BackupSelection.exclude,
);
albums.add(album);
}
return db.writeTxn(() => db.backupAlbums.putAll(albums));
}
}
FutureOr<void> _migrateDuplicatedAssetsBox(Box<HiveDuplicatedAssets> box) {
final HiveDuplicatedAssets? duplicatedAssets = box.get(duplicatedAssetsKey);
if (duplicatedAssets != null) {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
final duplicatedAssetIds = duplicatedAssets.duplicatedAssetIds
.map((id) => DuplicatedAsset(id))
.toList();
return db.writeTxn(() => db.duplicatedAssets.putAll(duplicatedAssetIds));
}
}
Future<void> _migrateAppSettingsBox(Box box) async {
for (AppSettingsEnum s in AppSettingsEnum.values) {
if (s.hiveKey != null) {
await _migrateKey(box, s.hiveKey!, s.storeKey);
}
}
}
Future<void> _migrateHiveBoxIfNecessary<T>(
String boxName,
FutureOr<void> Function(Box<T>) migrate,
) async {
try {
if (await Hive.boxExists(boxName)) {
final box = await Hive.openBox<T>(boxName);
await migrate(box);
await box.deleteFromDisk();
}
} catch (e) {
debugPrint("Error while migrating $boxName $e");
}
}
FutureOr<void> _migrateKey<T>(Box box, String hiveKey, StoreKey<T> key) {
final T? value = box.get(hiveKey);
if (value != null) {
return Store.put(key, value);
}
}
Future<void> migrateJsonCacheIfNecessary() async {
await AlbumCacheService().invalidate();
await SharedAlbumCacheService().invalidate();
await AssetCacheService().invalidate();
}
Future<void> migrateDatabaseIfNeeded(Isar db) async {
final int version = Store.get(StoreKey.version, 1);
switch (version) {

View File

@@ -4,8 +4,6 @@ import 'dart:io';
import 'package:http/http.dart';
import 'package:openapi/api.dart';
import 'tuple.dart';
/// Extension methods to retrieve ETag together with the API call
extension WithETag on AssetApi {
/// Get all AssetEntity belong to the user
@@ -14,11 +12,13 @@ extension WithETag on AssetApi {
///
/// * [String] eTag:
/// ETag of data already cached on the client
Future<Pair<List<AssetResponseDto>, String?>?> getAllAssetsWithETag({
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));
@@ -36,9 +36,9 @@ extension WithETag on AssetApi {
) as List)
.cast<AssetResponseDto>()
.toList();
return Pair(data, etag);
return (data, etag);
}
return null;
return (null, null);
}
}

View File

@@ -1,18 +0,0 @@
/// An immutable pair or 2-tuple
/// TODO replace with Record once Dart 2.19 is available
class Pair<T1, T2> {
final T1 first;
final T2 second;
const Pair(this.first, this.second);
}
/// An immutable triple or 3-tuple
/// TODO replace with Record once Dart 2.19 is available
class Triple<T1, T2, T3> {
final T1 first;
final T2 second;
final T3 third;
const Triple(this.first, this.second, this.third);
}

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.56.2
- API version: 1.59.1
- 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

@@ -10,8 +10,8 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | |
**name** | **String** | |
**createdAt** | **String** | |
**updatedAt** | **String** | |
**createdAt** | [**DateTime**](DateTime.md) | |
**updatedAt** | [**DateTime**](DateTime.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

@@ -12,7 +12,7 @@ Name | Type | Description | Notes
**email** | **String** | |
**firstName** | **String** | |
**lastName** | **String** | |
**createdAt** | **String** | |
**createdAt** | [**DateTime**](DateTime.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

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

@@ -12,8 +12,8 @@ Name | Type | Description | Notes
**id** | **String** | |
**ownerId** | **String** | |
**albumName** | **String** | |
**createdAt** | **String** | |
**updatedAt** | **String** | |
**createdAt** | [**DateTime**](DateTime.md) | |
**updatedAt** | [**DateTime**](DateTime.md) | |
**albumThumbnailAssetId** | **String** | |
**shared** | **bool** | |
**sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | | [default to const []]

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,12 +1101,10 @@ 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, isArchived, skip)
> List<MapMarkerResponseDto> getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore)
Get all assets that have GPS information embedded
### Example
```dart
import 'package:openapi/api.dart';
@@ -1067,11 +1125,11 @@ import 'package:openapi/api.dart';
final api_instance = AssetApi();
final isFavorite = true; // bool |
final isArchived = true; // bool |
final skip = 8.14; // num |
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, isArchived, skip);
final result = api_instance.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getMapMarkers: $e\n');
@@ -1083,8 +1141,8 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**isFavorite** | **bool**| | [optional]
**isArchived** | **bool**| | [optional]
**skip** | **num**| | [optional]
**fileCreatedAfter** | **DateTime**| | [optional]
**fileCreatedBefore** | **DateTime**| | [optional]
### Return type
@@ -1391,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,18 +1476,19 @@ final assetType = ; // AssetTypeEnum |
final assetData = BINARY_DATA_HERE; // MultipartFile |
final deviceAssetId = deviceAssetId_example; // String |
final deviceId = deviceId_example; // String |
final fileCreatedAt = fileCreatedAt_example; // String |
final fileModifiedAt = fileModifiedAt_example; // String |
final fileCreatedAt = 2013-10-20T19:20:30+01:00; // DateTime |
final fileModifiedAt = 2013-10-20T19:20:30+01:00; // DateTime |
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,12 +1503,13 @@ Name | Type | Description | Notes
**assetData** | **MultipartFile**| |
**deviceAssetId** | **String**| |
**deviceId** | **String**| |
**fileCreatedAt** | **String**| |
**fileModifiedAt** | **String**| |
**fileCreatedAt** | **DateTime**| |
**fileModifiedAt** | **DateTime**| |
**isFavorite** | **bool**| |
**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** | base64 or hex encoded sha1 hash |
[[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

@@ -15,21 +15,20 @@ Name | Type | Description | Notes
**deviceId** | **String** | |
**originalPath** | **String** | |
**originalFileName** | **String** | |
**resizePath** | **String** | |
**fileCreatedAt** | **String** | |
**fileModifiedAt** | **String** | |
**updatedAt** | **String** | |
**resized** | **bool** | |
**fileCreatedAt** | [**DateTime**](DateTime.md) | |
**fileModifiedAt** | [**DateTime**](DateTime.md) | |
**updatedAt** | [**DateTime**](DateTime.md) | |
**isFavorite** | **bool** | |
**isArchived** | **bool** | |
**mimeType** | **String** | |
**duration** | **String** | |
**webpPath** | **String** | |
**encodedVideoPath** | **String** | | [optional]
**exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) | | [optional]
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
**livePhotoVideoId** | **String** | | [optional]
**tags** | [**List<TagResponseDto>**](TagResponseDto.md) | | [optional] [default to const []]
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
**checksum** | **String** | base64 encoded sha1 hash |
[[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

@@ -9,7 +9,7 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assetIds** | **List<String>** | | [default to const []]
**expiresAt** | **String** | | [optional]
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**allowUpload** | **bool** | | [optional]
**allowDownload** | **bool** | | [optional]
**showExif** | **bool** | | [optional]

View File

@@ -12,6 +12,7 @@ Name | Type | Description | Notes
**password** | **String** | |
**firstName** | **String** | |
**lastName** | **String** | |
**storageLabel** | **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

@@ -9,7 +9,7 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**description** | **String** | | [optional]
**expiresAt** | **String** | | [optional]
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**allowUpload** | **bool** | | [optional]
**allowDownload** | **bool** | | [optional]
**showExif** | **bool** | | [optional]

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

@@ -8,10 +8,9 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | |
**id** | **String** | |
**lat** | **double** | |
**lon** | **double** | |
**id** | **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

@@ -13,8 +13,8 @@ Name | Type | Description | Notes
**description** | **String** | | [optional]
**userId** | **String** | |
**key** | **String** | |
**createdAt** | **String** | |
**expiresAt** | **String** | |
**createdAt** | [**DateTime**](DateTime.md) | |
**expiresAt** | [**DateTime**](DateTime.md) | |
**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
**album** | [**AlbumResponseDto**](AlbumResponseDto.md) | | [optional]
**allowUpload** | **bool** | |

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