Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 388e132fca | |||
| 43e0a59f93 | |||
| da34bd714e | |||
| 77da10e3ab | |||
| 64aac239f0 | |||
| d6823b128c | |||
| 508f32c08a | |||
| 8ed6ed4d2b | |||
| 1abb0bdae8 | |||
| 5ef6215546 | |||
| 95fb9c4365 | |||
| fa0a5107c2 | |||
| dc3c329431 | |||
| 2a9f2b4515 | |||
| 793049388b | |||
| 382b63954c | |||
| 87ccba7f9d | |||
| e21c96c0ef | |||
| 4de0b2f44e | |||
| b588a87d4a | |||
| 44ed1f0919 | |||
| 16d0df796c | |||
| 9fd5d2ad9c | |||
| 28ad004b01 | |||
| ef4a492cb1 | |||
| 6d9e7694b1 | |||
| 0c13c63bb6 | |||
| 907eb869bc | |||
| c1402eee8e | |||
| 84f7ca855a | |||
| 2dcce03352 | |||
| 96a22ec3c1 | |||
| 4b29bccc7c | |||
| 40e079a247 | |||
| 81f0265095 | |||
| 92cc647cf6 | |||
| 048d437b0b | |||
| ec9a6bca14 | |||
| bd5952b943 | |||
| 3f0d54c752 | |||
| dab4595a4e | |||
| 6d9ca82b19 | |||
| 373a03e819 | |||
| d97b0259fa | |||
| 2267ca1949 | |||
| 29be53e70d | |||
| 851fe4a49f | |||
| 30f499cf2e | |||
| 591a641d8d | |||
| 5b314ffd46 | |||
| 0b078c9f99 | |||
| 0d5584ecbb | |||
| 5e090646ba | |||
| c4e910dd3d | |||
| 5a2394af7c | |||
| 48e32269f4 | |||
| dd9d90d21e | |||
| 0544c687b9 | |||
| e810aae212 | |||
| 9c6a26de9f | |||
| e6f2bb9f89 | |||
| f908bd4a64 | |||
| 7395b03b1f | |||
| 63b4fc6f65 | |||
| f392fe7702 | |||
| 2daed747cd | |||
| 9e4bab7494 | |||
| 9274c0701b | |||
| 0bc773fd00 | |||
| c6d2408517 | |||
| 033f83a55a | |||
| 51841d627c | |||
| 50924f0b3d | |||
| 4aae1da841 | |||
| 1a2554548a | |||
| 40262c30cb | |||
| 761e7fdd2d | |||
| cd8a124b25 | |||
| 148428a564 | |||
| 14da671bf9 | |||
| e8f0f82db0 | |||
| b8278404a0 | |||
| 45671b0b8b | |||
| 321525ead5 | |||
| 1d24e20d22 | |||
| 3a045b33ca | |||
| a9438a9c2d | |||
| eea0a98090 | |||
| a491240aeb | |||
| 8c24a994e1 | |||
| 64f53e674c | |||
| 54fdf33fd9 | |||
| 997e9c5877 | |||
| e21c586cc5 |
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.16.9"
|
||||
flutter-version: "3.19.3"
|
||||
cache: true
|
||||
|
||||
- name: Create the Keystore
|
||||
|
||||
@@ -4,16 +4,16 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
serverBump:
|
||||
description: "Bump server version"
|
||||
description: 'Bump server version'
|
||||
required: true
|
||||
default: "false"
|
||||
default: 'false'
|
||||
type: choice
|
||||
options:
|
||||
- "false"
|
||||
- 'false'
|
||||
- minor
|
||||
- patch
|
||||
mobileBump:
|
||||
description: "Bump mobile build number"
|
||||
description: 'Bump mobile build number'
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
@@ -46,8 +46,8 @@ jobs:
|
||||
with:
|
||||
author_name: Alex The Bot
|
||||
author_email: alex.tran1502@gmail.com
|
||||
default_author: user_info
|
||||
message: "Version ${{ env.IMMICH_VERSION }}"
|
||||
default_author: user_info
|
||||
message: 'Version ${{ env.IMMICH_VERSION }}'
|
||||
tag: ${{ env.IMMICH_VERSION }}
|
||||
push: true
|
||||
|
||||
@@ -85,4 +85,5 @@ jobs:
|
||||
docker/example.env
|
||||
docker/hwaccel.ml.yml
|
||||
docker/hwaccel.transcoding.yml
|
||||
docker/prometheus.yml
|
||||
*.apk
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.16.9"
|
||||
flutter-version: "3.19.3"
|
||||
|
||||
- name: Install dependencies
|
||||
run: dart pub get
|
||||
|
||||
@@ -329,14 +329,14 @@ jobs:
|
||||
|
||||
- name: Generate new migrations
|
||||
continue-on-error: true
|
||||
run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
|
||||
run: npm run typeorm:migrations:generate ./src/migrations/TestMigration
|
||||
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@v19
|
||||
id: verify-changed-files
|
||||
with:
|
||||
files: |
|
||||
server/src/infra/migrations/
|
||||
server/src/migrations/
|
||||
- name: Verify migration files have not changed
|
||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||
run: |
|
||||
@@ -354,7 +354,7 @@ jobs:
|
||||
id: verify-changed-sql-files
|
||||
with:
|
||||
files: |
|
||||
server/src/infra/sql
|
||||
server/src/queries
|
||||
|
||||
- name: Verify SQL files have not changed
|
||||
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"[dart]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.selectionHighlight": false,
|
||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
"editor.wordBasedSuggestions": "off",
|
||||
"editor.defaultFormatter": "Dart-Code.dart-code"
|
||||
},
|
||||
"cSpell.words": [
|
||||
"immich"
|
||||
],
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
|
||||
}
|
||||
}
|
||||
@@ -11,24 +11,24 @@
|
||||
<p align="center">
|
||||
<img src="design/immich-logo-stacked-light.svg" width="300" title="Login With Custom URL">
|
||||
</p>
|
||||
<h3 align="center">High performance self-hosted photo and video backup solution</h3>
|
||||
<h3 align="center">High performance self-hosted photo and video management solution</h3>
|
||||
<br/>
|
||||
<a href="https://immich.app">
|
||||
<img src="design/immich-screenshots.png" title="Main Screenshot">
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_ko_KR.md">한국어</a>
|
||||
<a href="README_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="readme_i18n/README_ca_ES.md">Català</a>
|
||||
<a href="readme_i18n/README_es_ES.md">Español</a>
|
||||
<a href="readme_i18n/README_fr_FR.md">Français</a>
|
||||
<a href="readme_i18n/README_it_IT.md">Italiano</a>
|
||||
<a href="readme_i18n/README_ja_JP.md">日本語</a>
|
||||
<a href="readme_i18n/README_ko_KR.md">한국어</a>
|
||||
<a href="readme_i18n/README_de_DE.md">Deutsch</a>
|
||||
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
|
||||
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
|
||||
<a href="readme_i18n/README_zh_CN.md">中文</a>
|
||||
<a href="readme_i18n/README_ru_RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine3.19@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c as core
|
||||
FROM node:20-alpine3.19@sha256:bf77dc26e48ea95fca9d1aceb5acfa69d2e546b765ec2abfb502975f1a2d4def as core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.2",
|
||||
"version": "1.99.0",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
||||
@@ -99,7 +99,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
restart: always
|
||||
|
||||
database:
|
||||
@@ -78,7 +78,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:bc1794e85c9e00293351b967efa267ce6af1c824ac875a9d0c7ac84700a8b53e
|
||||
image: prom/prometheus@sha256:5ccad477d0057e62a7cd1981ffcc43785ac10c5a35522dc207466ff7e7ec845f
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -11,7 +11,7 @@ Unable to set `app.immich:/` as a valid redirect URI? See [Mobile Redirect URI](
|
||||
Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an identity layer built on top of OAuth2. OIDC is supported by most identity providers, including:
|
||||
|
||||
- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
|
||||
- [Authelia](https://www.authelia.com/configuration/identity-providers/open-id-connect/)
|
||||
- [Authelia](https://www.authelia.com/configuration/identity-providers/openid-connect/clients/)
|
||||
- [Okta](https://www.okta.com/openid-connect/)
|
||||
- [Google](https://developers.google.com/identity/openid-connect/openid-connect)
|
||||
|
||||
@@ -67,14 +67,20 @@ Once you have a new OAuth client application configured, Immich can be configure
|
||||
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label |
|
||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage |
|
||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
||||
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
||||
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
||||
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
||||
|
||||
:::note Claim Options [1]
|
||||
|
||||
Claim is only used on user creation and not synchronized after that.
|
||||
|
||||
:::
|
||||
|
||||
:::info
|
||||
The Issuer URL should look something like the following, and return a valid json document.
|
||||
|
||||
|
||||
@@ -13,12 +13,57 @@ Immich supports multiple users, each with their own library.
|
||||
|
||||
<UserCreate />
|
||||
|
||||
## Delete a User
|
||||
## Set Storage Quota For User
|
||||
|
||||
If you need to remove a user from Immich, head to "Administration", where users can be scheduled for deletion. The user account will immediately become disabled and their library and all associated data will be removed after 7 days.
|
||||
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.
|
||||
|
||||
In order to select a storage quota, click on the pencil icon and enter the storage quota in GiB. You can choose an unlimited quota using the value 0 (default).
|
||||
|
||||
:::tip
|
||||
The system administrator can see the usage quota percentage of all users in Server Stats page.
|
||||
:::
|
||||
|
||||
:::info
|
||||
External libraries don't take up space from the storage quota.
|
||||
:::
|
||||
|
||||
<img src={require('./img/user-quota-size.png').default} width="40%" title="Set Quota Size" />
|
||||
|
||||
## Set Storage Label For User
|
||||
|
||||
The admin can add a custom label for each user, so instead of `upload/{userId}/your-template` it will be `upload/{custom_user_label}/your-template`.
|
||||
To apply a storage template, go to the Administration page -> click on the pencil button next to the user.
|
||||
:::note
|
||||
To apply the Storage Label to previously uploaded assets, run the Storage Migration Job.
|
||||
:::
|
||||
|
||||
<img src={require('./img/user-storage-label.png').default} width="40%" title="Delete User" />
|
||||
|
||||
## Password Reset
|
||||
|
||||
To reset a user's password, click the pencil icon to edit a user, then click "Reset Password". The user's password will be reset to "password" and they have to change it next time the sign in.
|
||||
To reset a user's password, click the pencil icon to edit a user, then click "Reset Password". The user's password will be reset to random password and they have to change it next time the sign in.
|
||||
|
||||

|
||||
<img src={require('./img/user-management-update.png').default} width="40%" title="Reset Password" />
|
||||
|
||||
## Delete a User
|
||||
|
||||
If you need to remove a user from Immich, head to "Administration", where users can be scheduled for deletion. The user account will immediately become disabled and their library and all associated data will be removed after 7 days by default.
|
||||
|
||||
<img src={require('./img/delete-user.webp').default} width="40%" title="Delete User" />
|
||||
|
||||
### Delete Delay
|
||||
|
||||
You can customize the time of the deletion of the users from the Administration -> Settings -> User Settings.
|
||||
:::info user deletion job
|
||||
The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.
|
||||
:::
|
||||
|
||||
<img src={require('./img/customize-delete-user.png').default} width="80%" title="Customize Delete User" />
|
||||
|
||||
### Immediately Remove User
|
||||
|
||||
You can choose to delete a user immediately by checking the box
|
||||
`Queue user and assets for immediate deletion` in the deletion process, this will immediately remove the user and all assets.
|
||||
This cannot be undone and the files cannot be recovered.
|
||||
|
||||
<img src={require('./img/immediately-remove-user.png').default} width="40%" title="Customize Delete User" />
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Database Migrations
|
||||
|
||||
After making any changes in the `server/src/infra/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.
|
||||
After making any changes in the `server/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 ./src/infra/<migration-name>
|
||||
npm run typeorm:migrations:generate <migration-name>
|
||||
```
|
||||
|
||||
2. Check if the migration file makes sense.
|
||||
3. Move the migration file to folder `./server/src/infra/migrations` in your code editor.
|
||||
3. Move the migration file to folder `./server/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.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Monitoring
|
||||
|
||||
## Overview
|
||||
|
||||
Immich provides a variety of performance metrics to allow for local monitoring and insights. This integration is primarily in the form of Prometheus metrics. However, exporting traces is also possible due to the use of OpenTelemetry instrumentation.
|
||||
|
||||
:::note
|
||||
This is an opt-in feature intended for you to monitor immich's performance. This data isn't sent anywhere beyond what you've configured.
|
||||
:::
|
||||
|
||||
## Prometheus
|
||||
|
||||
Prometheus is a tool that collects metrics from a number of sources you configure. It operates in a "pull" strategy - that is, it periodically requests metrics from each defined source. This means that the source doesn't send anything until it's requested. It also means that the source -- immich, in this case -- has to expose an endpoint for Prometheus to target when it requests metrics.
|
||||
|
||||
### Metrics
|
||||
|
||||
These metrics come in a variety of forms:
|
||||
|
||||
- Counters, which can only increase. Example: the number of times an endpoint has been called.
|
||||
- Gauges, which can increase or decrease within a certain range. Example: CPU utilization.
|
||||
- Histograms, where each observation is assigned to a certain number of "buckets". Example: response time, where each bucket is a number of milliseconds. This one is a bit more complicated.
|
||||
- Buckets in this case are _cumulative_; that is, an observation is placed not only into the smallest bucket that contains it, but also to all buckets larger than this. For example, if a histogram has three buckets for 1ms, 5ms and 10ms, an observation of 3ms will be bucketed into both 5ms and 10ms.
|
||||
|
||||
The metrics in immich are grouped into API (endpoint calls and response times), host (memory and CPU utilization), and IO (internal database queries, image processing, and so on). Each group of metrics can be enabled or disabled independently.
|
||||
|
||||
### Configuration
|
||||
|
||||
Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_METRICS=true` environmental variable to your `.env` file. Note that only the server and microservices containers currently use this variable.
|
||||
|
||||
:::note
|
||||
`IMMICH_METRICS` is equivalent to enabling the following three environmental variables: `IMMICH_API_METRICS`, `IMMICH_HOST_METRICS`, and `IMMICH_IO_METRICS`. If you would like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group.
|
||||
:::
|
||||
|
||||
The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way.
|
||||
|
||||
You can start by defining a Prometheus service in the Compose file:
|
||||
|
||||
```yaml
|
||||
immich-prometheus:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
# this exposes the default port for Prometheus so you can interact with it
|
||||
- 9090:9090
|
||||
image: prom/prometheus
|
||||
volumes:
|
||||
# the Prometheus configuration file - a barebones one is provided to get started
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
# a named volume defined in the bottom of the Compose file; it can also be a mounted folder
|
||||
- prometheus-data:/prometheus
|
||||
```
|
||||
|
||||
You will also need to add `prometheus-data` to the list of volumes in the bottom of the Compose file:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
model-cache:
|
||||
prometheus-data:
|
||||
```
|
||||
|
||||
The last piece is the [configuration file][prom-file]. This file defines (among other things) the sources Prometheus should target. Download it and place it in the same folder as the Compose file.
|
||||
|
||||
:::tip
|
||||
The provided file is just a starting point. There are a ton of ways to configure Prometheus, so feel free to experiment!
|
||||
:::
|
||||
|
||||
After bringing down the containers with `docker compose down` and back up with `docker compose up -d`, a Prometheus instance will now collect metrics from the immich server and microservices containers. Note that we didn't need to expose any new ports for these containers - the communication is handled in the internal Docker network.
|
||||
|
||||
:::note
|
||||
To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8081` to the microservices container's ports. Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects.
|
||||
:::
|
||||
|
||||
### Usage
|
||||
|
||||
So after setting up Prometheus, how do you actually view the metrics? The simplest way is to use Prometheus directly. Visiting Prometheus will show you a web UI where you can search for and visualize metrics. You can also view the status of your data sources and configure settings, but this is beyond the scope of this guide.
|
||||
|
||||
## Grafana
|
||||
|
||||
For a dedicated tool with nice presentation, you can use Grafana instead. This connects to Prometheus (and possibly other sources) for sophisticated data visualization.
|
||||
|
||||
Setting up Grafana is similar to Prometheus. You can add a service for it:
|
||||
|
||||
```yaml
|
||||
immich-grafana:
|
||||
container_name: immich_grafana
|
||||
command: ['./run.sh', '-disable-reporting'] # this is to disable Grafana's telemetry
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana
|
||||
volumes:
|
||||
# stores your pretty dashboards and panels
|
||||
- grafana-data:/var/lib/grafana
|
||||
```
|
||||
|
||||
And add another volume for it:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
model-cache:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
```
|
||||
|
||||
After bringing down the services and back up again, you can now visit Grafana to view your metrics. On the first login, enter `admin` for both username and password and update your password. You can then go to the settings and add a data source with `http://immich-prometheus:9090` to point Grafana to your Prometheus instance.
|
||||
|
||||
### Usage
|
||||
|
||||
You can make your first dashboard to get started. Don't forget to save it frequently, or you'll lose all your progress!
|
||||
|
||||
You can then make a new panel, specifying Prometheus as the data source for it.
|
||||
|
||||
-- TODO: add images and more details here
|
||||
|
||||
[prom-file]: https://github.com/immich-app/immich/releases/latest/download/prometheus.yml
|
||||
@@ -16,7 +16,12 @@ version: '3.8'
|
||||
services:
|
||||
immich-machine-learning:
|
||||
container_name: immich_machine_learning
|
||||
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
|
||||
# Example tag: ${IMMICH_VERSION:-release}-cuda
|
||||
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
||||
# extends:
|
||||
# file: hwaccel.ml.yml
|
||||
# service: # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||
volumes:
|
||||
- model-cache:/cache
|
||||
restart: always
|
||||
|
||||
@@ -9,8 +9,8 @@ The database is saved to your Immich upload folder in the `database-backup` subd
|
||||
### Prerequisites
|
||||
|
||||
- Borg needs to be installed on your server as well as the remote machine. You can find instructions to install Borg [here](https://borgbackup.readthedocs.io/en/latest/installation.html).
|
||||
- To run this sript as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/).
|
||||
- To run this script non-interactively, set up [passwordless ssh](https://www.redhat.com/sysadmin/passwordless-ssh) to your remote machine from your server.
|
||||
- (Optional) To run this sript as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/).
|
||||
- To run this script non-interactively, set up [passwordless ssh](https://www.redhat.com/sysadmin/passwordless-ssh) to your remote machine from your server. If you skipped the previous step, make sure this step is done from your root account.
|
||||
|
||||
To initialize the borg repository, run the following commands once.
|
||||
|
||||
@@ -19,16 +19,13 @@ UPLOAD_LOCATION="/path/to/immich/directory" # Immich database location, as
|
||||
BACKUP_PATH="/path/to/local/backup/directory"
|
||||
|
||||
mkdir "$UPLOAD_LOCATION/database-backup"
|
||||
mkdir "$BACKUP_PATH/immich-borg"
|
||||
|
||||
borg init --encryption=none "$BACKUP_PATH/immich-borg"
|
||||
|
||||
## Remote set up
|
||||
REMOTE_HOST="remote_host@IP"
|
||||
REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
|
||||
|
||||
ssh "$REMOTE_HOST" "mkdir \"$REMOTE_BACKUP_PATH\"/immich-borg"
|
||||
ssh "$REMOTE_HOST" "borg init --encryption=none \"$REMOTE_BACKUP_PATH\"/immich-borg"
|
||||
borg init --encryption=none "$REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg"
|
||||
```
|
||||
|
||||
Edit the following script as necessary and add it to your crontab. Note that this script assumes there are no `:`, `@`, or `"` characters in your paths. If these characters exist, you will need to escape and/or rename the paths.
|
||||
|
||||
@@ -36,7 +36,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning, web |
|
||||
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload` | server, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
|
||||
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
|
||||
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ function HomepageHeader() {
|
||||
<section className="text-center m-6 p-12 border border-red-400 rounded-[50px] bg-slate-200 dark:bg-immich-dark-gray">
|
||||
<img
|
||||
src={isDarkTheme ? 'img/immich-logo-stacked-dark.svg' : 'img/immich-logo-stacked-light.svg'}
|
||||
className="md:h-60 h-12 mb-2 antialiased"
|
||||
className="md:h-60 h-44 mb-2 antialiased"
|
||||
alt="Immich logo"
|
||||
/>
|
||||
<div className="sm:text-base md:text-4xl mb-12 sm:leading-tight">
|
||||
<div className="sm:text-2xl text-lg md:text-4xl mb-12 sm:leading-tight">
|
||||
<p className="mb-1 font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
Self-hosted photo and <span className="block"></span>
|
||||
video management solution<span className="block"></span>
|
||||
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
<<: *server-common
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
|
||||
database:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.98.2",
|
||||
"version": "1.99.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -12,7 +12,8 @@ describe('/audit', () => {
|
||||
admin = await utils.adminSetup();
|
||||
});
|
||||
|
||||
describe('GET :/file-report', () => {
|
||||
// TODO: Enable these tests again once #7436 is resolved as these were flaky
|
||||
describe.skip('GET :/file-report', () => {
|
||||
it('excludes assets without issues from report', async () => {
|
||||
const [trashedAsset, archivedAsset] = await Promise.all([
|
||||
utils.createAsset(admin.accessToken),
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('/library', () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
||||
library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
||||
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
|
||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('/library', () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({ type: LibraryType.External });
|
||||
.send({ ownerId: admin.userId, type: LibraryType.External });
|
||||
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
@@ -92,7 +92,7 @@ describe('/library', () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ type: LibraryType.External });
|
||||
.send({ ownerId: admin.userId, type: LibraryType.External });
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(
|
||||
@@ -113,6 +113,7 @@ describe('/library', () => {
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
name: 'My Awesome Library',
|
||||
importPaths: ['/path/to/import'],
|
||||
@@ -133,6 +134,7 @@ describe('/library', () => {
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
name: 'My Awesome Library',
|
||||
importPaths: ['/path', '/path'],
|
||||
@@ -148,6 +150,7 @@ describe('/library', () => {
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
name: 'My Awesome Library',
|
||||
importPaths: ['/path/to/import'],
|
||||
@@ -162,7 +165,7 @@ describe('/library', () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ type: LibraryType.Upload });
|
||||
.send({ ownerId: admin.userId, type: LibraryType.Upload });
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(
|
||||
@@ -182,7 +185,7 @@ describe('/library', () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ type: LibraryType.Upload, name: 'My Awesome Library' });
|
||||
.send({ ownerId: admin.userId, type: LibraryType.Upload, name: 'My Awesome Library' });
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(
|
||||
@@ -196,7 +199,7 @@ describe('/library', () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ type: LibraryType.Upload, importPaths: ['/path/to/import'] });
|
||||
.send({ ownerId: admin.userId, type: LibraryType.Upload, importPaths: ['/path/to/import'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have import paths'));
|
||||
@@ -206,7 +209,7 @@ describe('/library', () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
|
||||
.send({ ownerId: admin.userId, type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
|
||||
@@ -330,7 +333,10 @@ describe('/library', () => {
|
||||
});
|
||||
|
||||
it('should get library by id', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get(`/library/${library.id}`)
|
||||
@@ -386,7 +392,10 @@ describe('/library', () => {
|
||||
});
|
||||
|
||||
it('should delete an external library', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
@@ -407,6 +416,7 @@ describe('/library', () => {
|
||||
|
||||
it('should delete an external library with assets', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
@@ -455,6 +465,7 @@ describe('/library', () => {
|
||||
|
||||
it('should not scan an upload library', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.Upload,
|
||||
});
|
||||
|
||||
@@ -468,6 +479,7 @@ describe('/library', () => {
|
||||
|
||||
it('should scan external library', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
|
||||
});
|
||||
@@ -483,6 +495,7 @@ describe('/library', () => {
|
||||
|
||||
it('should scan external library with exclusion pattern', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
exclusionPatterns: ['**/directoryA'],
|
||||
@@ -499,6 +512,7 @@ describe('/library', () => {
|
||||
|
||||
it('should scan multiple import paths', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
|
||||
});
|
||||
@@ -515,6 +529,7 @@ describe('/library', () => {
|
||||
|
||||
it('should pick up new files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { LoginResponseDto } from '@immich/sdk';
|
||||
import { LoginResponseDto, getConfig } from '@immich/sdk';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) });
|
||||
|
||||
describe('/system-config', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let nonAdmin: LoginResponseDto;
|
||||
@@ -60,4 +62,25 @@ describe('/system-config', () => {
|
||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /system-config', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put('/system-config');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should reject an invalid config entry', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put('/system-config')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({
|
||||
...(await getSystemConfig(admin.accessToken)),
|
||||
storageTemplate: { enabled: true, hashVerificationEnabled: true, template: '{{foo}}' },
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(expect.stringContaining('Invalid storage template')));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ ip_address=$(hostname -I | awk '{print $1}')
|
||||
|
||||
create_immich_directory() {
|
||||
echo "Creating Immich directory..."
|
||||
mkdir -p ./immich-app/immich-data
|
||||
mkdir -p ./immich-app
|
||||
cd ./immich-app || exit
|
||||
}
|
||||
|
||||
@@ -20,21 +20,6 @@ download_dot_env_file() {
|
||||
curl -L https://github.com/immich-app/immich/releases/latest/download/example.env -o ./.env >/dev/null 2>&1
|
||||
}
|
||||
|
||||
replace_env_value() {
|
||||
KERNEL="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
if [ "$KERNEL" = "darwin" ]; then
|
||||
sed -i '' "s|$1=.*|$1=$2|" ./.env
|
||||
else
|
||||
sed -i "s|$1=.*|$1=$2|" ./.env
|
||||
fi
|
||||
}
|
||||
|
||||
populate_upload_location() {
|
||||
echo "Populating default UPLOAD_LOCATION value..."
|
||||
upload_location=$(pwd)/immich-data
|
||||
replace_env_value "UPLOAD_LOCATION" "$upload_location"
|
||||
}
|
||||
|
||||
start_docker_compose() {
|
||||
echo "Starting Immich's docker containers"
|
||||
|
||||
@@ -59,7 +44,6 @@ start_docker_compose() {
|
||||
show_friendly_message() {
|
||||
echo "Successfully deployed Immich!"
|
||||
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
|
||||
echo "The library location is $upload_location"
|
||||
echo "---------------------------------------------------"
|
||||
echo "If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
|
||||
|
||||
@@ -75,5 +59,4 @@ show_friendly_message() {
|
||||
create_immich_directory
|
||||
download_docker_compose_file
|
||||
download_dot_env_file
|
||||
populate_upload_location
|
||||
start_docker_compose
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:8e697181d24bd77cc4251fdd37e4cdd6d725c5de2ed63b9bc8db77357400c5e2 as builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:991e20a11120277e977cadbc104e7a9b196a68a346597879821b19034285a403 as builder-cpu
|
||||
|
||||
FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as builder-openvino
|
||||
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino
|
||||
USER root
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3-dev
|
||||
|
||||
@@ -21,10 +21,12 @@ FROM builder-${DEVICE} as builder
|
||||
|
||||
ARG DEVICE
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=true \
|
||||
VIRTUAL_ENV="/opt/venv" \
|
||||
PATH="/opt/venv/bin:${PATH}"
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=true \
|
||||
VIRTUAL_ENV="/opt/venv" \
|
||||
PATH="/opt/venv/bin:${PATH}"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
||||
|
||||
RUN pip install --upgrade pip && pip install poetry
|
||||
RUN poetry config installer.max-workers 10 && \
|
||||
@@ -34,9 +36,9 @@ RUN python3 -m venv /opt/venv
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:ce81dc539f0aedc9114cae640f8352fad83d37461c24a3615b01f081d0c0583a as prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:a2eb07f336e4f194358382611b4fea136c632b40baa6314cb27a366deeaf0144 as prod-cpu
|
||||
|
||||
FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as prod-openvino
|
||||
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino
|
||||
USER root
|
||||
|
||||
FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:2d913b09e6be8387e1a10976933642c73c840c0b735f0bf3c28d97fc9bc422e0 as prod-cuda
|
||||
@@ -56,14 +58,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends ocl-icd-libopen
|
||||
mkdir /opt/armnn
|
||||
|
||||
COPY --from=builder-armnn \
|
||||
/opt/armnn/libarmnn.so.?? \
|
||||
/opt/armnn/libarmnnOnnxParser.so.?? \
|
||||
/opt/armnn/libarmnnDeserializer.so.?? \
|
||||
/opt/armnn/libarmnnTfLiteParser.so.?? \
|
||||
/opt/armnn/libprotobuf.so.?.??.?.? \
|
||||
/opt/ann/libann.s[o] \
|
||||
/opt/ann/build.sh \
|
||||
/opt/armnn/
|
||||
/opt/armnn/libarmnn.so.?? \
|
||||
/opt/armnn/libarmnnOnnxParser.so.?? \
|
||||
/opt/armnn/libarmnnDeserializer.so.?? \
|
||||
/opt/armnn/libarmnnTfLiteParser.so.?? \
|
||||
/opt/armnn/libprotobuf.so.?.??.?.? \
|
||||
/opt/ann/libann.s[o] \
|
||||
/opt/ann/build.sh \
|
||||
/opt/armnn/
|
||||
|
||||
FROM prod-${DEVICE} as prod
|
||||
|
||||
@@ -73,11 +75,12 @@ RUN apt-get update && \
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:$PATH" \
|
||||
PYTHONPATH=/usr/src
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:$PATH" \
|
||||
PYTHONPATH=/usr/src \
|
||||
DEVICE=${DEVICE}
|
||||
|
||||
# prevent core dumps
|
||||
RUN echo "hard core 0" >> /etc/security/limits.conf && \
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
@@ -115,17 +114,12 @@ class InferenceModel(ABC):
|
||||
case ".armnn":
|
||||
session = AnnSession(model_path)
|
||||
case ".onnx":
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(model_path.parent)
|
||||
session = ort.InferenceSession(
|
||||
model_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
session = ort.InferenceSession(
|
||||
model_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported model file type: {model_path.suffix}")
|
||||
return session
|
||||
@@ -192,7 +186,7 @@ class InferenceModel(ABC):
|
||||
case "CPUExecutionProvider" | "CUDAExecutionProvider":
|
||||
option = {"arena_extend_strategy": "kSameAsRequested"}
|
||||
case "OpenVINOExecutionProvider":
|
||||
option = {"device_type": "GPU_FP32"}
|
||||
option = {"device_type": "GPU_FP32", "cache_dir": (self.cache_dir / "openvino").as_posix()}
|
||||
case _:
|
||||
option = {}
|
||||
options.append(option)
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestBase:
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
|
||||
|
||||
assert encoder.provider_options == [
|
||||
{"device_type": "GPU_FP32"},
|
||||
{"device_type": "GPU_FP32", "cache_dir": (encoder.cache_dir / "openvino").as_posix()},
|
||||
{"arena_extend_strategy": "kSameAsRequested"},
|
||||
]
|
||||
|
||||
@@ -262,7 +262,6 @@ class TestBase:
|
||||
|
||||
mock_ann = mocker.patch("app.models.base.AnnSession")
|
||||
mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
|
||||
mocker.patch("app.models.base.os.chdir")
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
encoder._make_session(mock_armnn_path)
|
||||
@@ -285,26 +284,6 @@ class TestBase:
|
||||
mock_ann.assert_not_called()
|
||||
mock_ort.assert_not_called()
|
||||
|
||||
def test_make_session_changes_cwd(self, mocker: MockerFixture) -> None:
|
||||
mock_model_path = mocker.Mock()
|
||||
mock_model_path.is_file.return_value = True
|
||||
mock_model_path.suffix = ".onnx"
|
||||
mock_model_path.parent = "model_parent"
|
||||
mock_model_path.with_suffix.return_value = mock_model_path
|
||||
mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
|
||||
mock_chdir = mocker.patch("app.models.base.os.chdir")
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
encoder._make_session(mock_model_path)
|
||||
|
||||
mock_chdir.assert_has_calls(
|
||||
[
|
||||
mock.call(mock_model_path.parent),
|
||||
mock.call(os.getcwd()),
|
||||
]
|
||||
)
|
||||
mock_ort.assert_called_once()
|
||||
|
||||
def test_download(self, mocker: MockerFixture) -> None:
|
||||
mock_snapshot_download = mocker.patch("app.models.base.snapshot_download")
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:96586e238e2fed914b839e50cf91943b5655262348d141466b34ced2e0b5b155 as builder
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:881dbb68d115182b2c12e7e77dc54ea5005fd4e0123ca009d822adb5b0631785 as builder
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiocache"
|
||||
@@ -877,13 +877,13 @@ tqdm = ["tqdm"]
|
||||
|
||||
[[package]]
|
||||
name = "ftfy"
|
||||
version = "6.1.3"
|
||||
version = "6.2.0"
|
||||
description = "Fixes mojibake and other problems with Unicode, after the fact"
|
||||
optional = false
|
||||
python-versions = ">=3.8,<4"
|
||||
files = [
|
||||
{file = "ftfy-6.1.3-py3-none-any.whl", hash = "sha256:e49c306c06a97f4986faa7a8740cfe3c13f3106e85bcec73eb629817e671557c"},
|
||||
{file = "ftfy-6.1.3.tar.gz", hash = "sha256:693274aead811cff24c1e8784165aa755cd2f6e442a5ec535c7d697f6422a422"},
|
||||
{file = "ftfy-6.2.0-py3-none-any.whl", hash = "sha256:f94a2c34b76e07475720e3096f5ca80911d152406fbde66fdb45c4d0c9150026"},
|
||||
{file = "ftfy-6.2.0.tar.gz", hash = "sha256:5e42143c7025ef97944ca2619d6b61b0619fc6654f98771d39e862c1424c75c0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1828,38 +1828,38 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"},
|
||||
{file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"},
|
||||
{file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"},
|
||||
{file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"},
|
||||
{file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"},
|
||||
{file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"},
|
||||
{file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"},
|
||||
{file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"},
|
||||
{file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"},
|
||||
{file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"},
|
||||
{file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"},
|
||||
{file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"},
|
||||
{file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"},
|
||||
{file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"},
|
||||
{file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"},
|
||||
{file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"},
|
||||
{file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"},
|
||||
{file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"},
|
||||
{file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"},
|
||||
{file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"},
|
||||
{file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"},
|
||||
{file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"},
|
||||
{file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"},
|
||||
{file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"},
|
||||
{file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"},
|
||||
{file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"},
|
||||
{file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"},
|
||||
{file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"},
|
||||
{file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"},
|
||||
{file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"},
|
||||
{file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"},
|
||||
{file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"},
|
||||
{file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"},
|
||||
{file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2064,21 +2064,22 @@ reference = "cuda12"
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime-openvino"
|
||||
version = "1.15.0"
|
||||
version = "1.17.1"
|
||||
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "onnxruntime_openvino-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9bfe245312e897f219dfef619c0d98f4797ffb008ad55aa41aedb32b522f72"},
|
||||
{file = "onnxruntime_openvino-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:a31cd9c9848dc196803d74ea46152fe0f3dd876bc5769eff7e3776fef4c654de"},
|
||||
{file = "onnxruntime_openvino-1.15.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c9bc1614f9d267d62023287035d204d9840ac0057d1c7a770a27acdd1642662"},
|
||||
{file = "onnxruntime_openvino-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5808b7b876e5f6a083228bd43fc1028096cb9b485f466bf980d8f72d8424d"},
|
||||
{file = "onnxruntime_openvino-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ed693011b472f9a617b2d5c4785d5fa1e1b77f7cb2b02e47b899534ec6c6396"},
|
||||
{file = "onnxruntime_openvino-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:5152b5e56e83e022ced2986700d68dd8ba7b1466761725ce774f679c5710ab87"},
|
||||
{file = "onnxruntime_openvino-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce3b1aa06d6b8b732d314d217028ec4735de5806215c44d3bdbcad03b9260d5"},
|
||||
{file = "onnxruntime_openvino-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:21133a701bb07ea19e01f48b8c23beee575f2e879f49173843f275d7c91a625a"},
|
||||
{file = "onnxruntime_openvino-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76824dac3c392ad4b812f29c18be2055ab3bba2e3c111e44baae847b33d5b081"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
coloredlogs = "*"
|
||||
flatbuffers = "*"
|
||||
numpy = ">=1.21.6"
|
||||
numpy = ">=1.25.2"
|
||||
packaging = "*"
|
||||
protobuf = "*"
|
||||
sympy = "*"
|
||||
@@ -2289,13 +2290,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
|
||||
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
|
||||
{file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
|
||||
{file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -2472,13 +2473,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.0.2"
|
||||
version = "8.1.1"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"},
|
||||
{file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"},
|
||||
{file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
|
||||
{file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2486,21 +2487,21 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.3.0,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
pluggy = ">=1.4,<2.0"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.23.5"
|
||||
version = "0.23.5.post1"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"},
|
||||
{file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"},
|
||||
{file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"},
|
||||
{file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2843,28 +2844,28 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"},
|
||||
{file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"},
|
||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"},
|
||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"},
|
||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"},
|
||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"},
|
||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"},
|
||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"},
|
||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"},
|
||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"},
|
||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"},
|
||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"},
|
||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"},
|
||||
{file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"},
|
||||
{file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"},
|
||||
{file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"},
|
||||
{file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"},
|
||||
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"},
|
||||
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"},
|
||||
{file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"},
|
||||
{file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"},
|
||||
{file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"},
|
||||
{file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3288,13 +3289,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.27.1"
|
||||
version = "0.28.0"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"},
|
||||
{file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"},
|
||||
{file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"},
|
||||
{file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3626,4 +3627,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.12"
|
||||
content-hash = "c947090d326e81179054b7ce4dded311df8b7ca5a56680d5e9459cf8ca18df1a"
|
||||
content-hash = "1b014276ec94f9389459a70d31f0d96d1dd5a138bcc988900865e5f07a72bc62"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.98.2"
|
||||
version = "1.99.0"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
@@ -51,7 +51,7 @@ onnxruntime-gpu = {version = "^1.17.0", source = "cuda12"}
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.openvino.dependencies]
|
||||
onnxruntime-openvino = ">=1.15.0,<1.16.0"
|
||||
onnxruntime-openvino = "^1.17.1"
|
||||
|
||||
[tool.poetry.group.armnn]
|
||||
optional = true
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
|
||||
export LD_PRELOAD="$lib_path"
|
||||
export LD_BIND_NOW=1
|
||||
# mimalloc seems to increase memory usage dramatically with openvino, need to investigate
|
||||
if ! [ "$DEVICE" = "openvino" ]; then
|
||||
export LD_PRELOAD="$lib_path"
|
||||
export LD_BIND_NOW=1
|
||||
fi
|
||||
|
||||
: "${MACHINE_LEARNING_HOST:=[::]}"
|
||||
: "${MACHINE_LEARNING_PORT:=3003}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"flutterSdkVersion": "3.16.9",
|
||||
"flutterSdkVersion": "3.19.3",
|
||||
"flavors": {}
|
||||
}
|
||||
@@ -58,6 +58,7 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
@@ -75,4 +76,4 @@
|
||||
<data android:scheme="geo" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 128,
|
||||
"android.injected.version.name" => "1.98.2",
|
||||
"android.injected.version.code" => 129,
|
||||
"android.injected.version.name" => "1.99.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000208">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00024">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="76.932126">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="81.32752">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.616364">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="26.041597">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
"exif_bottom_sheet_location": "LOCATION",
|
||||
"exif_bottom_sheet_location_add": "Add a location",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||
"experimental_settings_subtitle": "Use at your own risk!",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Router_Medium_x5F_Black_00000159464448132936669960000002337362428709113490_"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 792 266.25"
|
||||
style="enable-background:new 0 0 792 266.25;" xml:space="preserve">
|
||||
|
||||
<g>
|
||||
<path class="st0" style="fill:#ACCBFA;" d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||
c5.84,0,10.52,4.67,10.52,10.68c0,2.84-0.83,7.68-0.83,10.68v38.73c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||
c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||
<path class="st0" style="fill:#ACCBFA;" d="M394.28,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||
c0-2.84,0.83-7.68,0.83-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C399.12,182.55,394.28,177.88,394.28,171.87z"/>
|
||||
<path class="st0" style="fill:#ACCBFA;" d="M528.5,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||
c0-2.84,0.84-7.68,0.84-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C533.35,182.55,528.5,177.88,528.5,171.87z"/>
|
||||
<path class="st0" style="fill:#ACCBFA;" d="M576.92,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||
C565.23,68.36,570.57,63.18,576.92,63.18z M567.07,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||
s10.52,4.67,10.52,10.68c0,2.84-0.84,7.68-0.84,10.68v38.73c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||
s-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||
<path class="st0" style="fill:#ACCBFA;" d="M601.79,141.31c0-23.54,14.69-42.57,39.07-42.57c12.86,0,24.71,5.84,30.05,14.53c2,3.17,2.34,5.01,2.34,6.51
|
||||
c0,5.18-4.01,9.52-9.85,9.52c-3.84,0-7.34-2.17-8.85-6.01c-2.34-5.18-6.85-8.18-13.69-8.18c-12.86,0-20.03,11.52-20.03,26.04
|
||||
c0,14.69,7.51,26.04,20.53,26.04c7.01,0,12.02-2.5,14.36-7.68c1.67-3.51,4.84-6.51,9.18-6.51c6.01,0,9.68,4.17,9.68,9.35
|
||||
c0,2.5-1,5.51-3.17,8.35c-5.51,7.35-15.86,13.19-30.05,13.19C616.32,183.89,601.79,165.19,601.79,141.31z"/>
|
||||
<path class="st0" style="fill:#ACCBFA;" d="M737.69,171.87c0-2.84,0.67-7.68,0.67-10.68v-28.55c0-10.18-5.68-17.2-15.36-17.2
|
||||
c-6.68,0-12.35,3.17-16.03,8.35v37.4c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.67,10.68-10.52,10.68s-10.52-4.67-10.52-10.68
|
||||
c0-2.84,0.84-7.68,0.84-10.68v-80.8c0-3.01-0.84-7.85-0.84-10.68c0-6.01,4.84-10.68,10.52-10.68c5.84,0,10.52,4.67,10.52,10.68
|
||||
c0,2.84-0.67,7.68-0.67,10.68v27.21c5.01-5.51,12.19-8.85,21.37-8.85c17.2,0,29.55,12.86,29.55,31.22v31.22
|
||||
c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68C742.36,182.55,737.69,177.88,737.69,171.87z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" style="fill:#FA2921;" d="M114.82,96.21c11.92,10.55,21.52,21.86,27.7,32.52c10.62-18.99,17.71-41.55,17.8-55.92c0-0.1,0-0.19,0-0.28
|
||||
c0-21.26-21.21-29.54-39.48-29.54s-39.48,8.28-39.48,29.54c0,0.29,0,0.68,0,1.15C91.54,78.2,103.61,86.29,114.82,96.21z"/>
|
||||
<path class="st2" style="fill:#ED79B5;" d="M49.8,154.19c7.45-8.29,18.88-17.27,31.77-24.86c13.72-8.07,27.44-13.71,39.49-16.3
|
||||
c-14.78-15.96-34.04-29.68-47.68-34.21c-0.1-0.03-0.18-0.06-0.27-0.09c-20.22-6.57-34.65,11.05-40.3,28.42s-4.33,40.11,15.89,46.68
|
||||
C48.99,153.93,49.35,154.05,49.8,154.19z"/>
|
||||
<path class="st3" style="fill:#FFB400;" d="M209.07,106.86c-5.65-17.38-20.07-34.99-40.3-28.42c-0.28,0.09-0.65,0.21-1.09,0.35
|
||||
c-1.16,11.08-5.12,25.07-11.09,38.79c-6.35,14.6-14.14,27.23-22.36,36.39c21.34,4.23,44.99,4,58.68-0.35
|
||||
c0.1-0.03,0.19-0.06,0.27-0.09C213.4,146.97,214.71,124.24,209.07,106.86z"/>
|
||||
<path class="st4" style="fill:#1E83F7;" d="M102.8,171.18c-3.44-15.54-4.56-30.34-3.3-42.59c-19.75,9.12-38.75,23.2-47.27,34.78
|
||||
c-0.06,0.08-0.11,0.16-0.16,0.23c-12.5,17.2-0.2,36.37,14.58,47.11s36.81,16.51,49.31-0.69c0.17-0.24,0.4-0.55,0.68-0.93
|
||||
C111.05,199.44,106.04,185.79,102.8,171.18z"/>
|
||||
<path class="st5" style="fill:#18C249;" d="M189.48,162.49c-10.9,2.33-25.42,2.88-40.32,1.44c-15.84-1.53-30.26-5.03-41.52-10.02
|
||||
c2.57,21.6,10.09,44.02,18.47,55.7c0.06,0.08,0.11,0.16,0.16,0.23c12.5,17.2,34.52,11.43,49.31,0.69
|
||||
c14.78-10.74,27.08-29.9,14.58-47.11C189.99,163.18,189.76,162.86,189.48,162.49z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Router_Medium_x5F_White" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" viewBox="0 0 792 266.25" style="enable-background:new 0 0 792 266.25;" xml:space="preserve">
|
||||
<g>
|
||||
<path class="st0" style="fill:#4251B0;" d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||
c5.84,0,10.52,4.67,10.52,10.68c0,2.84-0.83,7.68-0.83,10.68v38.73c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||
c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||
<path class="st0" style="fill:#4251B0;" d="M394.28,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||
c0-2.84,0.83-7.68,0.83-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C399.12,182.55,394.28,177.88,394.28,171.87z"/>
|
||||
<path class="st0" style="fill:#4251B0;" d="M528.5,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||
c0-2.84,0.84-7.68,0.84-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C533.35,182.55,528.5,177.88,528.5,171.87z"/>
|
||||
<path class="st0" style="fill:#4251B0;" d="M576.92,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||
C565.23,68.36,570.57,63.18,576.92,63.18z M567.07,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||
s10.52,4.67,10.52,10.68c0,2.84-0.84,7.68-0.84,10.68v38.73c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||
s-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||
<path class="st0" style="fill:#4251B0;" d="M601.79,141.31c0-23.54,14.69-42.57,39.07-42.57c12.86,0,24.71,5.84,30.05,14.53c2,3.17,2.34,5.01,2.34,6.51
|
||||
c0,5.18-4.01,9.52-9.85,9.52c-3.84,0-7.34-2.17-8.85-6.01c-2.34-5.18-6.85-8.18-13.69-8.18c-12.86,0-20.03,11.52-20.03,26.04
|
||||
c0,14.69,7.51,26.04,20.53,26.04c7.01,0,12.02-2.5,14.36-7.68c1.67-3.51,4.84-6.51,9.18-6.51c6.01,0,9.68,4.17,9.68,9.35
|
||||
c0,2.5-1,5.51-3.17,8.35c-5.51,7.35-15.86,13.19-30.05,13.19C616.32,183.89,601.79,165.19,601.79,141.31z"/>
|
||||
<path class="st0" style="fill:#4251B0;" d="M737.69,171.87c0-2.84,0.67-7.68,0.67-10.68v-28.55c0-10.18-5.68-17.2-15.36-17.2
|
||||
c-6.68,0-12.35,3.17-16.03,8.35v37.4c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.67,10.68-10.52,10.68s-10.52-4.67-10.52-10.68
|
||||
c0-2.84,0.84-7.68,0.84-10.68v-80.8c0-3.01-0.84-7.85-0.84-10.68c0-6.01,4.84-10.68,10.52-10.68c5.84,0,10.52,4.67,10.52,10.68
|
||||
c0,2.84-0.67,7.68-0.67,10.68v27.21c5.01-5.51,12.19-8.85,21.37-8.85c17.2,0,29.55,12.86,29.55,31.22v31.22
|
||||
c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68C742.36,182.55,737.69,177.88,737.69,171.87z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" style="fill:#FA2921;" d="M114.82,96.21c11.92,10.55,21.52,21.86,27.7,32.52c10.62-18.99,17.71-41.55,17.8-55.92c0-0.1,0-0.19,0-0.28
|
||||
c0-21.26-21.21-29.54-39.48-29.54s-39.48,8.28-39.48,29.54c0,0.29,0,0.68,0,1.15C91.54,78.2,103.61,86.29,114.82,96.21z"/>
|
||||
<path class="st2" style="fill:#ED79B5;" d="M49.8,154.19c7.45-8.29,18.88-17.27,31.77-24.86c13.72-8.07,27.44-13.71,39.49-16.3
|
||||
c-14.78-15.96-34.04-29.68-47.68-34.21c-0.1-0.03-0.18-0.06-0.27-0.09c-20.22-6.57-34.65,11.05-40.3,28.42s-4.33,40.11,15.89,46.68
|
||||
C48.99,153.93,49.35,154.05,49.8,154.19z"/>
|
||||
<path class="st3" style="fill:#FFB400;" d="M209.07,106.86c-5.65-17.38-20.07-34.99-40.3-28.42c-0.28,0.09-0.65,0.21-1.09,0.35
|
||||
c-1.16,11.08-5.12,25.07-11.09,38.79c-6.35,14.6-14.14,27.23-22.36,36.39c21.34,4.23,44.99,4,58.68-0.35
|
||||
c0.1-0.03,0.19-0.06,0.27-0.09C213.4,146.97,214.71,124.24,209.07,106.86z"/>
|
||||
<path class="st4" style="fill:#1E83F7;" d="M102.8,171.18c-3.44-15.54-4.56-30.34-3.3-42.59c-19.75,9.12-38.75,23.2-47.27,34.78
|
||||
c-0.06,0.08-0.11,0.16-0.16,0.23c-12.5,17.2-0.2,36.37,14.58,47.11s36.81,16.51,49.31-0.69c0.17-0.24,0.4-0.55,0.68-0.93
|
||||
C111.05,199.44,106.04,185.79,102.8,171.18z"/>
|
||||
<path class="st5" style="fill:#18C249;" d="M189.48,162.49c-10.9,2.33-25.42,2.88-40.32,1.44c-15.84-1.53-30.26-5.03-41.52-10.02
|
||||
c2.57,21.6,10.09,44.02,18.47,55.7c0.06,0.08,0.11,0.16,0.16,0.23c12.5,17.2,34.52,11.43,49.31,0.69
|
||||
c14.78-10.74,27.08-29.9,14.58-47.11C189.99,163.18,189.76,162.86,189.48,162.49z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@@ -180,4 +180,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.12.1
|
||||
|
||||
@@ -383,7 +383,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 144;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -525,7 +525,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 144;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -553,7 +553,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 144;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -55,11 +55,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.98.2</string>
|
||||
<string>1.99.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>144</string>
|
||||
<string>145</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.98.2"
|
||||
version_number: "1.99.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000232">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000246">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.745911">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.175843">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.672711">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.871371">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.171883">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.189451">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="112.172167">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="142.078248">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="98.918418">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.774821">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ class ImageLoader {
|
||||
required ImageCacheManager cache,
|
||||
required ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent>? chunkEvents,
|
||||
int? height,
|
||||
int? width,
|
||||
}) async {
|
||||
final headers = {
|
||||
'x-immich-user-token': Store.get(StoreKey.accessToken),
|
||||
@@ -25,10 +23,8 @@ class ImageLoader {
|
||||
|
||||
final stream = cache.getImageFile(
|
||||
uri,
|
||||
withProgress: true,
|
||||
withProgress: chunkEvents != null,
|
||||
headers: headers,
|
||||
maxHeight: height,
|
||||
maxWidth: width,
|
||||
);
|
||||
|
||||
await for (final result in stream) {
|
||||
@@ -40,13 +36,9 @@ class ImageLoader {
|
||||
expectedTotalBytes: result.totalSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (result is FileInfo) {
|
||||
} else if (result is FileInfo) {
|
||||
// We have the file
|
||||
final file = result.file;
|
||||
final bytes = await file.readAsBytes();
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
|
||||
final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path);
|
||||
final decoded = await decode(buffer);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ class ExifPeople extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final peopleProvider =
|
||||
ref.watch(assetPeopleNotifierProvider(asset).notifier);
|
||||
final people = ref.watch(assetPeopleNotifierProvider(asset));
|
||||
final people = ref
|
||||
.watch(assetPeopleNotifierProvider(asset))
|
||||
.value
|
||||
?.where((p) => !p.isHidden);
|
||||
final double imageSize = math.min(context.width / 3, 150);
|
||||
|
||||
showPersonNameEditModel(
|
||||
@@ -40,15 +43,14 @@ class ExifPeople extends ConsumerWidget {
|
||||
});
|
||||
}
|
||||
|
||||
if (people.value?.isEmpty ?? true) {
|
||||
if (people?.isEmpty ?? true) {
|
||||
// Empty list or loading
|
||||
return Container();
|
||||
}
|
||||
|
||||
final curatedPeople = people.value
|
||||
?.map((p) => CuratedContent(id: p.id, label: p.name))
|
||||
.toList() ??
|
||||
[];
|
||||
final curatedPeople =
|
||||
people?.map((p) => CuratedContent(id: p.id, label: p.name)).toList() ??
|
||||
[];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
|
||||
@@ -5,11 +5,9 @@ import 'package:photo_manager/photo_manager.dart';
|
||||
class AvailableAlbum {
|
||||
final AssetPathEntity albumEntity;
|
||||
final DateTime? lastBackup;
|
||||
final Uint8List? thumbnailData;
|
||||
AvailableAlbum({
|
||||
required this.albumEntity,
|
||||
this.lastBackup,
|
||||
this.thumbnailData,
|
||||
});
|
||||
|
||||
AvailableAlbum copyWith({
|
||||
@@ -20,7 +18,6 @@ class AvailableAlbum {
|
||||
return AvailableAlbum(
|
||||
albumEntity: albumEntity ?? this.albumEntity,
|
||||
lastBackup: lastBackup ?? this.lastBackup,
|
||||
thumbnailData: thumbnailData ?? this.thumbnailData,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +31,7 @@ class AvailableAlbum {
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup, thumbnailData: $thumbnailData)';
|
||||
'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
|
||||
@@ -234,33 +234,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
for (AssetPathEntity album in albums) {
|
||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||
|
||||
final assetCountInAlbum = await album.assetCountAsync;
|
||||
if (assetCountInAlbum > 0) {
|
||||
final assetList = await album.getAssetListPaged(page: 0, size: 1);
|
||||
availableAlbums.add(availableAlbum);
|
||||
|
||||
// Even though we check assetCountInAlbum to make sure that there are assets in album
|
||||
// The `getAssetListPaged` method still return empty list and cause not assets get rendered
|
||||
if (assetList.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final thumbnailAsset = assetList.first;
|
||||
try {
|
||||
final thumbnailData = await thumbnailAsset
|
||||
.thumbnailDataWithSize(const ThumbnailSize(512, 512));
|
||||
availableAlbum =
|
||||
availableAlbum.copyWith(thumbnailData: thumbnailData);
|
||||
} catch (e, stack) {
|
||||
log.severe(
|
||||
"Failed to get thumbnail for album ${album.name}",
|
||||
e,
|
||||
stack,
|
||||
);
|
||||
}
|
||||
|
||||
availableAlbums.add(availableAlbum);
|
||||
|
||||
albumMap[album.id] = album;
|
||||
}
|
||||
albumMap[album.id] = album;
|
||||
}
|
||||
state = state.copyWith(availableAlbums: availableAlbums);
|
||||
|
||||
|
||||
@@ -11,17 +11,16 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
|
||||
class AlbumInfoCard extends HookConsumerWidget {
|
||||
final Uint8List? imageData;
|
||||
final AvailableAlbum albumInfo;
|
||||
final AvailableAlbum album;
|
||||
|
||||
const AlbumInfoCard({super.key, this.imageData, required this.albumInfo});
|
||||
const AlbumInfoCard({super.key, required this.album});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bool isSelected =
|
||||
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
||||
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
|
||||
final bool isExcluded =
|
||||
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
|
||||
ColorFilter selectedFilter = ColorFilter.mode(
|
||||
@@ -82,9 +81,9 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
if (isSelected) {
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo);
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
|
||||
} else {
|
||||
ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo);
|
||||
ref.read(backupProvider.notifier).addAlbumForBackup(album);
|
||||
}
|
||||
},
|
||||
onDoubleTap: () {
|
||||
@@ -92,13 +91,11 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
|
||||
if (isExcluded) {
|
||||
// Remove from exclude album list
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.removeExcludedAlbumForBackup(albumInfo);
|
||||
ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
|
||||
} else {
|
||||
// Add to exclude album list
|
||||
|
||||
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
|
||||
if (album.id == 'isAll' || album.name == 'Recents') {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'Cannot exclude album contains all assets',
|
||||
@@ -108,9 +105,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.addExcludedAlbumForBackup(albumInfo);
|
||||
ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
|
||||
}
|
||||
},
|
||||
child: Card(
|
||||
@@ -136,14 +131,12 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
children: [
|
||||
ColorFiltered(
|
||||
colorFilter: buildImageFilter(),
|
||||
child: Image(
|
||||
child: const Image(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
image: imageData != null
|
||||
? MemoryImage(imageData!)
|
||||
: const AssetImage(
|
||||
'assets/immich-logo.png',
|
||||
) as ImageProvider,
|
||||
image: AssetImage(
|
||||
'assets/immich-logo.png',
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
@@ -168,7 +161,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
albumInfo.name,
|
||||
album.name,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: context.primaryColor,
|
||||
@@ -182,7 +175,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
if (snapshot.hasData) {
|
||||
return Text(
|
||||
snapshot.data.toString() +
|
||||
(albumInfo.isAll
|
||||
(album.isAll
|
||||
? " (${'backup_all'.tr()})"
|
||||
: ""),
|
||||
style: TextStyle(
|
||||
@@ -193,7 +186,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
}
|
||||
return const Text("0");
|
||||
}),
|
||||
future: albumInfo.assetCount,
|
||||
future: album.assetCount,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -202,7 +195,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.pushRoute(
|
||||
AlbumPreviewRoute(album: albumInfo.albumEntity),
|
||||
AlbumPreviewRoute(album: album.albumEntity),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
|
||||
@@ -11,47 +11,26 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
|
||||
class AlbumInfoListTile extends HookConsumerWidget {
|
||||
final Uint8List? imageData;
|
||||
final AvailableAlbum albumInfo;
|
||||
final AvailableAlbum album;
|
||||
|
||||
const AlbumInfoListTile({super.key, this.imageData, required this.albumInfo});
|
||||
const AlbumInfoListTile({super.key, required this.album});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bool isSelected =
|
||||
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
||||
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
|
||||
final bool isExcluded =
|
||||
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||
|
||||
ColorFilter selectedFilter = ColorFilter.mode(
|
||||
context.primaryColor.withAlpha(100),
|
||||
BlendMode.darken,
|
||||
);
|
||||
ColorFilter excludedFilter =
|
||||
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
||||
ColorFilter unselectedFilter =
|
||||
const ColorFilter.mode(Colors.black, BlendMode.color);
|
||||
|
||||
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
|
||||
var assetCount = useState(0);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
albumInfo.assetCount.then((value) => assetCount.value = value);
|
||||
album.assetCount.then((value) => assetCount.value = value);
|
||||
return null;
|
||||
},
|
||||
[albumInfo],
|
||||
[album],
|
||||
);
|
||||
|
||||
buildImageFilter() {
|
||||
if (isSelected) {
|
||||
return selectedFilter;
|
||||
} else if (isExcluded) {
|
||||
return excludedFilter;
|
||||
} else {
|
||||
return unselectedFilter;
|
||||
}
|
||||
}
|
||||
|
||||
buildTileColor() {
|
||||
if (isSelected) {
|
||||
return context.isDarkTheme
|
||||
@@ -66,19 +45,38 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
buildIcon() {
|
||||
if (isSelected) {
|
||||
return const Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: Colors.green,
|
||||
);
|
||||
}
|
||||
|
||||
if (isExcluded) {
|
||||
return const Icon(
|
||||
Icons.remove_circle_rounded,
|
||||
color: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
return Icon(
|
||||
Icons.circle,
|
||||
color: context.isDarkTheme ? Colors.grey[400] : Colors.black45,
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onDoubleTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
if (isExcluded) {
|
||||
// Remove from exclude album list
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.removeExcludedAlbumForBackup(albumInfo);
|
||||
ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
|
||||
} else {
|
||||
// Add to exclude album list
|
||||
|
||||
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
|
||||
if (album.id == 'isAll' || album.name == 'Recents') {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'Cannot exclude album contains all assets',
|
||||
@@ -88,9 +86,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.addExcludedAlbumForBackup(albumInfo);
|
||||
ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
|
||||
}
|
||||
},
|
||||
child: ListTile(
|
||||
@@ -99,33 +95,14 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
if (isSelected) {
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo);
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
|
||||
} else {
|
||||
ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo);
|
||||
ref.read(backupProvider.notifier).addAlbumForBackup(album);
|
||||
}
|
||||
},
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
width: 80,
|
||||
child: ColorFiltered(
|
||||
colorFilter: buildImageFilter(),
|
||||
child: Image(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
image: imageData != null
|
||||
? MemoryImage(imageData!)
|
||||
: const AssetImage(
|
||||
'assets/immich-logo.png',
|
||||
) as ImageProvider,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
leading: buildIcon(),
|
||||
title: Text(
|
||||
albumInfo.name,
|
||||
album.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -135,7 +112,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
context.pushRoute(
|
||||
AlbumPreviewRoute(album: albumInfo.albumEntity),
|
||||
AlbumPreviewRoute(album: album.albumEntity),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
|
||||
@@ -43,10 +43,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
((context, index) {
|
||||
var thumbnailData = albums[index].thumbnailData;
|
||||
return AlbumInfoListTile(
|
||||
imageData: thumbnailData,
|
||||
albumInfo: albums[index],
|
||||
album: albums[index],
|
||||
);
|
||||
}),
|
||||
childCount: albums.length,
|
||||
@@ -74,10 +72,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||
),
|
||||
itemCount: albums.length,
|
||||
itemBuilder: ((context, index) {
|
||||
var thumbnailData = albums[index].thumbnailData;
|
||||
return AlbumInfoCard(
|
||||
imageData: thumbnailData,
|
||||
albumInfo: albums[index],
|
||||
album: albums[index],
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -26,7 +26,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
BackUpState backupState = ref.watch(backupProvider);
|
||||
final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty;
|
||||
|
||||
final didGetBackupInfo = useState(false);
|
||||
bool hasExclusiveAccess =
|
||||
backupState.backupProgress != BackUpProgressEnum.inBackground;
|
||||
bool shouldBackup = backupState.allUniqueAssets.length -
|
||||
@@ -38,11 +38,6 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (backupState.backupProgress != BackUpProgressEnum.inProgress &&
|
||||
backupState.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
ref.watch(backupProvider.notifier).getBackupInfo();
|
||||
}
|
||||
|
||||
// Update the background settings information just to make sure we
|
||||
// have the latest, since the platform channel will not update
|
||||
// automatically
|
||||
@@ -58,6 +53,18 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (backupState.backupProgress == BackUpProgressEnum.idle &&
|
||||
!didGetBackupInfo.value) {
|
||||
ref.watch(backupProvider.notifier).getBackupInfo();
|
||||
didGetBackupInfo.value = true;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[backupState.backupProgress],
|
||||
);
|
||||
|
||||
Widget buildSelectedAlbumName() {
|
||||
var text = "backup_controller_page_backup_selected".tr();
|
||||
var albums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||
@@ -235,6 +242,15 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
buildLoadingIndicator() {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(top: 42.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
@@ -297,7 +313,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
|
||||
buildBackupButton(),
|
||||
]
|
||||
: [buildFolderSelectionTile()],
|
||||
: [
|
||||
buildFolderSelectionTile(),
|
||||
if (!didGetBackupInfo.value) buildLoadingIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// Based on https://stackoverflow.com/a/52625182
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class AssetDragRegion extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
final void Function(AssetIndex valueKey)? onStart;
|
||||
final void Function(AssetIndex valueKey)? onAssetEnter;
|
||||
final void Function()? onEnd;
|
||||
final void Function()? onScrollStart;
|
||||
final void Function(ScrollDirection direction)? onScroll;
|
||||
|
||||
const AssetDragRegion({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onStart,
|
||||
this.onAssetEnter,
|
||||
this.onEnd,
|
||||
this.onScrollStart,
|
||||
this.onScroll,
|
||||
});
|
||||
@override
|
||||
State createState() => _AssetDragRegionState();
|
||||
}
|
||||
|
||||
class _AssetDragRegionState extends State<AssetDragRegion> {
|
||||
late AssetIndex? assetUnderPointer;
|
||||
late AssetIndex? anchorAsset;
|
||||
|
||||
// Scroll related state
|
||||
static const double scrollOffset = 0.10;
|
||||
double? topScrollOffset;
|
||||
double? bottomScrollOffset;
|
||||
Timer? scrollTimer;
|
||||
late bool scrollNotified;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
assetUnderPointer = null;
|
||||
anchorAsset = null;
|
||||
scrollNotified = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
topScrollOffset = null;
|
||||
bottomScrollOffset = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawGestureDetector(
|
||||
gestures: {
|
||||
_CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<
|
||||
_CustomLongPressGestureRecognizer>(
|
||||
() => _CustomLongPressGestureRecognizer(),
|
||||
_registerCallbacks,
|
||||
),
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) {
|
||||
recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details);
|
||||
recognizer.onLongPressStart = (details) => _onLongPressStart(details);
|
||||
recognizer.onLongPressUp = _onLongPressEnd;
|
||||
recognizer.onLongPressCancel = _onLongPressEnd;
|
||||
}
|
||||
|
||||
AssetIndex? _getValueKeyAtPositon(Offset position) {
|
||||
final box = context.findAncestorRenderObjectOfType<RenderBox>();
|
||||
if (box == null) return null;
|
||||
|
||||
final hitTestResult = BoxHitTestResult();
|
||||
final local = box.globalToLocal(position);
|
||||
if (!box.hitTest(hitTestResult, position: local)) return null;
|
||||
|
||||
return (hitTestResult.path
|
||||
.firstWhereOrNull((hit) => hit.target is _AssetIndexProxy)
|
||||
?.target as _AssetIndexProxy?)
|
||||
?.index;
|
||||
}
|
||||
|
||||
void _onLongPressStart(LongPressStartDetails event) {
|
||||
/// Calculate widget height and scroll offset when long press starting instead of in [initState]
|
||||
/// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size
|
||||
final height = context.size?.height;
|
||||
if (height != null &&
|
||||
(topScrollOffset == null || bottomScrollOffset == null)) {
|
||||
topScrollOffset = height * scrollOffset;
|
||||
bottomScrollOffset = height - topScrollOffset!;
|
||||
}
|
||||
|
||||
final initialHit = _getValueKeyAtPositon(event.globalPosition);
|
||||
anchorAsset = initialHit;
|
||||
if (initialHit == null) return;
|
||||
|
||||
if (anchorAsset != null) {
|
||||
widget.onStart?.call(anchorAsset!);
|
||||
}
|
||||
}
|
||||
|
||||
void _onLongPressEnd() {
|
||||
scrollNotified = false;
|
||||
scrollTimer?.cancel();
|
||||
widget.onEnd?.call();
|
||||
}
|
||||
|
||||
void _onLongPressMove(LongPressMoveUpdateDetails event) {
|
||||
if (anchorAsset == null) return;
|
||||
if (topScrollOffset == null || bottomScrollOffset == null) return;
|
||||
|
||||
final currentDy = event.localPosition.dy;
|
||||
|
||||
if (currentDy > bottomScrollOffset!) {
|
||||
scrollTimer ??= Timer.periodic(
|
||||
const Duration(milliseconds: 50),
|
||||
(_) => widget.onScroll?.call(ScrollDirection.forward),
|
||||
);
|
||||
} else if (currentDy < topScrollOffset!) {
|
||||
scrollTimer ??= Timer.periodic(
|
||||
const Duration(milliseconds: 50),
|
||||
(_) => widget.onScroll?.call(ScrollDirection.reverse),
|
||||
);
|
||||
} else {
|
||||
scrollTimer?.cancel();
|
||||
scrollTimer = null;
|
||||
}
|
||||
|
||||
final currentlyTouchingAsset = _getValueKeyAtPositon(event.globalPosition);
|
||||
if (currentlyTouchingAsset == null) return;
|
||||
|
||||
if (assetUnderPointer != currentlyTouchingAsset) {
|
||||
if (!scrollNotified) {
|
||||
scrollNotified = true;
|
||||
widget.onScrollStart?.call();
|
||||
}
|
||||
|
||||
widget.onAssetEnter?.call(currentlyTouchingAsset);
|
||||
assetUnderPointer = currentlyTouchingAsset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer {
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
acceptGesture(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: prefer-single-widget-per-file
|
||||
class AssetIndexWrapper extends SingleChildRenderObjectWidget {
|
||||
final int rowIndex;
|
||||
final int sectionIndex;
|
||||
|
||||
const AssetIndexWrapper({
|
||||
required Widget super.child,
|
||||
required this.rowIndex,
|
||||
required this.sectionIndex,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
_AssetIndexProxy createRenderObject(BuildContext context) {
|
||||
return _AssetIndexProxy(
|
||||
index: AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
_AssetIndexProxy renderObject,
|
||||
) {
|
||||
renderObject.index =
|
||||
AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetIndexProxy extends RenderProxyBox {
|
||||
AssetIndex index;
|
||||
|
||||
_AssetIndexProxy({
|
||||
required this.index,
|
||||
});
|
||||
}
|
||||
|
||||
class AssetIndex {
|
||||
final int rowIndex;
|
||||
final int sectionIndex;
|
||||
|
||||
const AssetIndex({
|
||||
required this.rowIndex,
|
||||
required this.sectionIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(covariant AssetIndex other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.rowIndex == rowIndex && other.sectionIndex == sectionIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => rowIndex.hashCode ^ sectionIndex.hashCode;
|
||||
}
|
||||
@@ -5,12 +5,15 @@ import 'dart:math';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_drag_region.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
@@ -73,6 +76,8 @@ class ImmichAssetGridView extends StatefulWidget {
|
||||
|
||||
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
final ScrollOffsetController _scrollOffsetController =
|
||||
ScrollOffsetController();
|
||||
final ItemPositionsListener _itemPositionsListener =
|
||||
ItemPositionsListener.create();
|
||||
|
||||
@@ -83,6 +88,12 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
final Set<Asset> _selectedAssets =
|
||||
LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
||||
|
||||
bool _dragging = false;
|
||||
int? _dragAnchorAssetIndex;
|
||||
int? _dragAnchorSectionIndex;
|
||||
final Set<Asset> _draggedAssets =
|
||||
HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
||||
|
||||
Set<Asset> _getSelectedAssets() {
|
||||
return Set.from(_selectedAssets);
|
||||
}
|
||||
@@ -93,20 +104,26 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
|
||||
void _selectAssets(List<Asset> assets) {
|
||||
setState(() {
|
||||
if (_dragging) {
|
||||
_draggedAssets.addAll(assets);
|
||||
}
|
||||
_selectedAssets.addAll(assets);
|
||||
_callSelectionListener(true);
|
||||
});
|
||||
}
|
||||
|
||||
void _deselectAssets(List<Asset> assets) {
|
||||
final assetsToDeselect = assets.where(
|
||||
(a) =>
|
||||
widget.canDeselect ||
|
||||
!(widget.preselectedAssets?.contains(a) ?? false),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_selectedAssets.removeAll(
|
||||
assets.where(
|
||||
(a) =>
|
||||
widget.canDeselect ||
|
||||
!(widget.preselectedAssets?.contains(a) ?? false),
|
||||
),
|
||||
);
|
||||
_selectedAssets.removeAll(assetsToDeselect);
|
||||
if (_dragging) {
|
||||
_draggedAssets.removeAll(assetsToDeselect);
|
||||
}
|
||||
_callSelectionListener(_selectedAssets.isNotEmpty);
|
||||
});
|
||||
}
|
||||
@@ -114,6 +131,10 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
void _deselectAll() {
|
||||
setState(() {
|
||||
_selectedAssets.clear();
|
||||
_dragAnchorAssetIndex = null;
|
||||
_dragAnchorSectionIndex = null;
|
||||
_draggedAssets.clear();
|
||||
_dragging = false;
|
||||
if (!widget.canDeselect &&
|
||||
widget.preselectedAssets != null &&
|
||||
widget.preselectedAssets!.isNotEmpty) {
|
||||
@@ -142,6 +163,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
showStorageIndicator: widget.showStorageIndicator,
|
||||
selectedAssets: _selectedAssets,
|
||||
selectionActive: widget.selectionActive,
|
||||
sectionIndex: index,
|
||||
section: section,
|
||||
margin: widget.margin,
|
||||
renderList: widget.renderList,
|
||||
@@ -199,6 +221,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
itemBuilder: _itemBuilder,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
itemScrollController: _itemScrollController,
|
||||
scrollOffsetController: _scrollOffsetController,
|
||||
itemCount: widget.renderList.elements.length +
|
||||
(widget.topWidget != null ? 1 : 0),
|
||||
addRepaintBoundaries: true,
|
||||
@@ -253,6 +276,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
if (widget.visibleItemsListener != null) {
|
||||
_itemPositionsListener.itemPositions.removeListener(_positionListener);
|
||||
}
|
||||
_itemPositionsListener.itemPositions.removeListener(_hapticsListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -308,6 +332,107 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
);
|
||||
}
|
||||
|
||||
void _setDragStartIndex(AssetIndex index) {
|
||||
setState(() {
|
||||
_dragAnchorAssetIndex = index.rowIndex;
|
||||
_dragAnchorSectionIndex = index.sectionIndex;
|
||||
_dragging = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _stopDrag() {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
_draggedAssets.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void _dragDragScroll(ScrollDirection direction) {
|
||||
_scrollOffsetController.animateScroll(
|
||||
offset: direction == ScrollDirection.forward ? 175 : -175,
|
||||
duration: const Duration(milliseconds: 125),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDragAssetEnter(AssetIndex index) {
|
||||
if (_dragAnchorSectionIndex == null || _dragAnchorAssetIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final dragAnchorSectionIndex = _dragAnchorSectionIndex!;
|
||||
final dragAnchorAssetIndex = _dragAnchorAssetIndex!;
|
||||
|
||||
late final int startSectionIndex;
|
||||
late final int startSectionAssetIndex;
|
||||
late final int endSectionIndex;
|
||||
late final int endSectionAssetIndex;
|
||||
|
||||
if (index.sectionIndex < dragAnchorSectionIndex) {
|
||||
startSectionIndex = index.sectionIndex;
|
||||
startSectionAssetIndex = index.rowIndex;
|
||||
endSectionIndex = dragAnchorSectionIndex;
|
||||
endSectionAssetIndex = dragAnchorAssetIndex;
|
||||
} else if (index.sectionIndex > dragAnchorSectionIndex) {
|
||||
startSectionIndex = dragAnchorSectionIndex;
|
||||
startSectionAssetIndex = dragAnchorAssetIndex;
|
||||
endSectionIndex = index.sectionIndex;
|
||||
endSectionAssetIndex = index.rowIndex;
|
||||
} else {
|
||||
startSectionIndex = dragAnchorSectionIndex;
|
||||
endSectionIndex = dragAnchorSectionIndex;
|
||||
|
||||
// If same section, assign proper start / end asset Index
|
||||
if (dragAnchorAssetIndex < index.rowIndex) {
|
||||
startSectionAssetIndex = dragAnchorAssetIndex;
|
||||
endSectionAssetIndex = index.rowIndex;
|
||||
} else {
|
||||
startSectionAssetIndex = index.rowIndex;
|
||||
endSectionAssetIndex = dragAnchorAssetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
final selectedAssets = <Asset>{};
|
||||
var currentSectionIndex = startSectionIndex;
|
||||
while (currentSectionIndex < endSectionIndex) {
|
||||
final section =
|
||||
widget.renderList.elements.elementAtOrNull(currentSectionIndex);
|
||||
if (section == null) continue;
|
||||
|
||||
final sectionAssets =
|
||||
widget.renderList.loadAssets(section.offset, section.count);
|
||||
|
||||
if (currentSectionIndex == startSectionIndex) {
|
||||
selectedAssets.addAll(
|
||||
sectionAssets.slice(startSectionAssetIndex, sectionAssets.length),
|
||||
);
|
||||
} else {
|
||||
selectedAssets.addAll(sectionAssets);
|
||||
}
|
||||
|
||||
currentSectionIndex += 1;
|
||||
}
|
||||
|
||||
final section = widget.renderList.elements.elementAtOrNull(endSectionIndex);
|
||||
if (section != null) {
|
||||
final sectionAssets =
|
||||
widget.renderList.loadAssets(section.offset, section.count);
|
||||
if (startSectionIndex == endSectionIndex) {
|
||||
selectedAssets.addAll(
|
||||
sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1),
|
||||
);
|
||||
} else {
|
||||
selectedAssets.addAll(
|
||||
sectionAssets.slice(0, endSectionAssetIndex + 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_deselectAssets(_draggedAssets.toList());
|
||||
_draggedAssets.clear();
|
||||
_draggedAssets.addAll(selectedAssets);
|
||||
_selectAssets(_draggedAssets.toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
@@ -315,7 +440,16 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
onPopInvoked: (didPop) => !didPop ? _deselectAll() : null,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildAssetGrid(),
|
||||
AssetDragRegion(
|
||||
onStart: _setDragStartIndex,
|
||||
onAssetEnter: _handleDragAssetEnter,
|
||||
onEnd: _stopDrag,
|
||||
onScroll: _dragDragScroll,
|
||||
onScrollStart: () => WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => controlBottomAppBarNotifier.minimize(),
|
||||
),
|
||||
child: _buildAssetGrid(),
|
||||
),
|
||||
if (widget.showMultiSelectIndicator && widget.selectionActive)
|
||||
_buildMultiSelectIndicator(),
|
||||
],
|
||||
@@ -361,6 +495,7 @@ class _PlaceholderRow extends StatelessWidget {
|
||||
/// A section for the render grid
|
||||
class _Section extends StatelessWidget {
|
||||
final RenderAssetGridElement section;
|
||||
final int sectionIndex;
|
||||
final Set<Asset> selectedAssets;
|
||||
final bool scrolling;
|
||||
final double margin;
|
||||
@@ -377,6 +512,7 @@ class _Section extends StatelessWidget {
|
||||
|
||||
const _Section({
|
||||
required this.section,
|
||||
required this.sectionIndex,
|
||||
required this.scrolling,
|
||||
required this.margin,
|
||||
required this.assetsPerRow,
|
||||
@@ -435,6 +571,8 @@ class _Section extends StatelessWidget {
|
||||
)
|
||||
: _AssetRow(
|
||||
key: ValueKey(i),
|
||||
rowStartIndex: i * assetsPerRow,
|
||||
sectionIndex: sectionIndex,
|
||||
assets: assetsToRender.nestedSlice(
|
||||
i * assetsPerRow,
|
||||
min((i + 1) * assetsPerRow, section.count),
|
||||
@@ -522,6 +660,8 @@ class _Title extends StatelessWidget {
|
||||
/// The row of assets
|
||||
class _AssetRow extends StatelessWidget {
|
||||
final List<Asset> assets;
|
||||
final int rowStartIndex;
|
||||
final int sectionIndex;
|
||||
final Set<Asset> selectedAssets;
|
||||
final int absoluteOffset;
|
||||
final double width;
|
||||
@@ -539,6 +679,8 @@ class _AssetRow extends StatelessWidget {
|
||||
|
||||
const _AssetRow({
|
||||
super.key,
|
||||
required this.rowStartIndex,
|
||||
required this.sectionIndex,
|
||||
required this.assets,
|
||||
required this.absoluteOffset,
|
||||
required this.width,
|
||||
@@ -594,18 +736,22 @@ class _AssetRow extends StatelessWidget {
|
||||
bottom: margin,
|
||||
right: last ? 0.0 : margin,
|
||||
),
|
||||
child: ThumbnailImage(
|
||||
asset: asset,
|
||||
index: absoluteOffset + index,
|
||||
loadAsset: renderList.loadAsset,
|
||||
totalAssets: renderList.totalAssets,
|
||||
multiselectEnabled: selectionActive,
|
||||
isSelected: isSelectionActive && selectedAssets.contains(asset),
|
||||
onSelect: () => onSelect?.call(asset),
|
||||
onDeselect: () => onDeselect?.call(asset),
|
||||
showStorageIndicator: showStorageIndicator,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
child: AssetIndexWrapper(
|
||||
rowIndex: rowStartIndex + index,
|
||||
sectionIndex: sectionIndex,
|
||||
child: ThumbnailImage(
|
||||
asset: asset,
|
||||
index: absoluteOffset + index,
|
||||
loadAsset: renderList.loadAsset,
|
||||
totalAssets: renderList.totalAssets,
|
||||
multiselectEnabled: selectionActive,
|
||||
isSelected: isSelectionActive && selectedAssets.contains(asset),
|
||||
onSelect: () => onSelect?.call(asset),
|
||||
onDeselect: () => onDeselect?.call(asset),
|
||||
showStorageIndicator: showStorageIndicator,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
@@ -11,8 +12,17 @@ import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
|
||||
|
||||
class ControlBottomAppBar extends ConsumerWidget {
|
||||
final controlBottomAppBarNotifier = ControlBottomAppBarNotifier();
|
||||
|
||||
class ControlBottomAppBarNotifier with ChangeNotifier {
|
||||
void minimize() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class ControlBottomAppBar extends HookConsumerWidget {
|
||||
final void Function(bool shareLocal) onShare;
|
||||
final void Function()? onFavorite;
|
||||
final void Function()? onArchive;
|
||||
@@ -64,6 +74,25 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||
const bottomPadding = 0.20;
|
||||
final scrollController = useDraggableScrollController();
|
||||
|
||||
void minimize() {
|
||||
scrollController.animateTo(
|
||||
bottomPadding,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
controlBottomAppBarNotifier.addListener(minimize);
|
||||
return () {
|
||||
controlBottomAppBarNotifier.removeListener(minimize);
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
void showForceDeleteDialog(
|
||||
Function(bool) deleteCb, {
|
||||
@@ -242,6 +271,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
}
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
controller: scrollController,
|
||||
initialChildSize: hasRemote ? 0.35 : bottomPadding,
|
||||
minChildSize: bottomPadding,
|
||||
maxChildSize: hasRemote ? 0.65 : bottomPadding,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
||||
@@ -23,7 +24,7 @@ class CuratedPeopleRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const imageSize = 85.0;
|
||||
const imageSize = 60.0;
|
||||
|
||||
// Guard empty [content]
|
||||
if (content.isEmpty) {
|
||||
@@ -82,11 +83,11 @@ class CuratedPeopleRow extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
"Add name",
|
||||
"exif_bottom_sheet_person_add_person",
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
)
|
||||
else
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'immich_logo_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<Uint8List> immichLogo(ImmichLogoRef ref) async {
|
||||
final json = await rootBundle.loadString('assets/immich-logo.json');
|
||||
final j = jsonDecode(json);
|
||||
return base64Decode(j['content']);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'immich_logo_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$immichLogoHash() => r'040cc44fae3339e0f40a091fb3b2f2abe9f83acd';
|
||||
|
||||
/// See also [immichLogo].
|
||||
@ProviderFor(immichLogo)
|
||||
final immichLogoProvider = AutoDisposeFutureProvider<Uint8List>.internal(
|
||||
immichLogo,
|
||||
name: r'immichLogoProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$immichLogoHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef ImmichLogoRef = AutoDisposeFutureProviderRef<Uint8List>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/immich_logo_provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_dialog.dart';
|
||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||
|
||||
@@ -26,6 +28,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
final bool isEnableAutoBackup =
|
||||
backupState.backgroundBackup || backupState.autoBackup;
|
||||
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
||||
final immichLogo = ref.watch(immichLogoProvider);
|
||||
final user = Store.tryGet(StoreKey.currentUser);
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
const widgetSize = 30.0;
|
||||
@@ -152,14 +155,29 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
builder: (BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 3),
|
||||
height: 30,
|
||||
child: Image.asset(
|
||||
context.isDarkTheme
|
||||
? 'assets/immich-logo-inline-dark.png'
|
||||
: 'assets/immich-logo-inline-light.png',
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final today = DateTime.now();
|
||||
if (today.month == 4 && today.day == 1) {
|
||||
if (immichLogo.value == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Image.memory(
|
||||
immichLogo.value!,
|
||||
fit: BoxFit.cover,
|
||||
height: 80,
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 3.0),
|
||||
child: SvgPicture.asset(
|
||||
context.isDarkTheme
|
||||
? 'assets/immich-logo-inline-dark.svg'
|
||||
: 'assets/immich-logo-inline-light.svg',
|
||||
height: 40,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ class ImmichLogo extends StatelessWidget {
|
||||
image: const AssetImage('assets/immich-logo.png'),
|
||||
width: size,
|
||||
filterQuality: FilterQuality.high,
|
||||
isAntiAlias: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.98.2
|
||||
- API version: 1.99.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
@@ -161,6 +161,7 @@ Class | Method | HTTP request | Description
|
||||
*PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
|
||||
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
|
||||
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
|
||||
*SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities |
|
||||
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
|
||||
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
|
||||
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
|
||||
|
||||
@@ -13,7 +13,7 @@ Name | Type | Description | Notes
|
||||
**isVisible** | **bool** | | [optional]
|
||||
**isWatched** | **bool** | | [optional]
|
||||
**name** | **String** | | [optional]
|
||||
**ownerId** | **String** | | [optional]
|
||||
**ownerId** | **String** | |
|
||||
**type** | [**LibraryType**](LibraryType.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)
|
||||
|
||||
@@ -9,6 +9,7 @@ All URIs are relative to */api*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**getAssetsByCity**](SearchApi.md#getassetsbycity) | **GET** /search/cities |
|
||||
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
|
||||
[**getSearchSuggestions**](SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
|
||||
[**search**](SearchApi.md#search) | **GET** /search |
|
||||
@@ -18,6 +19,57 @@ Method | HTTP request | Description
|
||||
[**searchSmart**](SearchApi.md#searchsmart) | **POST** /search/smart |
|
||||
|
||||
|
||||
# **getAssetsByCity**
|
||||
> List<AssetResponseDto> getAssetsByCity()
|
||||
|
||||
|
||||
|
||||
### 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 = SearchApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.getAssetsByCity();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling SearchApi->getAssetsByCity: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<AssetResponseDto>**](AssetResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **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)
|
||||
|
||||
# **getExploreData**
|
||||
> List<SearchExploreResponseDto> getExploreData()
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:openapi/api.dart';
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**importPath** | **String** | |
|
||||
**isValid** | **bool** | | [optional] [default to false]
|
||||
**isValid** | **bool** | | [default to false]
|
||||
**message** | **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)
|
||||
|
||||
@@ -16,6 +16,50 @@ class SearchApi {
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'GET /search/cities' operation and returns the [Response].
|
||||
Future<Response> getAssetsByCityWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/search/cities';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<AssetResponseDto>?> getAssetsByCity() async {
|
||||
final response = await getAssetsByCityWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
||||
.cast<AssetResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /search/explore' operation and returns the [Response].
|
||||
Future<Response> getExploreDataWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
|
||||
@@ -18,7 +18,7 @@ class CreateLibraryDto {
|
||||
this.isVisible,
|
||||
this.isWatched,
|
||||
this.name,
|
||||
this.ownerId,
|
||||
required this.ownerId,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
@@ -50,13 +50,7 @@ class CreateLibraryDto {
|
||||
///
|
||||
String? name;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? ownerId;
|
||||
String ownerId;
|
||||
|
||||
LibraryType type;
|
||||
|
||||
@@ -78,7 +72,7 @@ class CreateLibraryDto {
|
||||
(isVisible == null ? 0 : isVisible!.hashCode) +
|
||||
(isWatched == null ? 0 : isWatched!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(ownerId == null ? 0 : ownerId!.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(type.hashCode);
|
||||
|
||||
@override
|
||||
@@ -103,11 +97,7 @@ class CreateLibraryDto {
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
if (this.ownerId != null) {
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
} else {
|
||||
// json[r'ownerId'] = null;
|
||||
}
|
||||
json[r'type'] = this.type;
|
||||
return json;
|
||||
}
|
||||
@@ -129,7 +119,7 @@ class CreateLibraryDto {
|
||||
isVisible: mapValueOfType<bool>(json, r'isVisible'),
|
||||
isWatched: mapValueOfType<bool>(json, r'isWatched'),
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId'),
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
type: LibraryType.fromJson(json[r'type'])!,
|
||||
);
|
||||
}
|
||||
@@ -178,6 +168,7 @@ class CreateLibraryDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'ownerId',
|
||||
'type',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class ValidateLibraryImportPathResponseDto {
|
||||
|
||||
return ValidateLibraryImportPathResponseDto(
|
||||
importPath: mapValueOfType<String>(json, r'importPath')!,
|
||||
isValid: mapValueOfType<bool>(json, r'isValid') ?? false,
|
||||
isValid: mapValueOfType<bool>(json, r'isValid')!,
|
||||
message: mapValueOfType<String>(json, r'message'),
|
||||
);
|
||||
}
|
||||
@@ -117,6 +117,7 @@ class ValidateLibraryImportPathResponseDto {
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'importPath',
|
||||
'isValid',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ void main() {
|
||||
// final instance = SearchApi();
|
||||
|
||||
group('tests for SearchApi', () {
|
||||
//Future<List<AssetResponseDto>> getAssetsByCity() async
|
||||
test('test getAssetsByCity', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<SearchExploreResponseDto>> getExploreData() async
|
||||
test('test getExploreData', () async {
|
||||
// TODO
|
||||
|
||||
@@ -2,8 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 1.98.2+128
|
||||
isar_version: &isar_version 3.1.0+1
|
||||
version: 1.99.0+129
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@@ -34,12 +33,13 @@ dependencies:
|
||||
ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
|
||||
geolocator: ^11.0.0 # used to move to current location in map view
|
||||
flutter_udid: ^3.0.0
|
||||
flutter_svg: ^2.0.9
|
||||
package_info_plus: ^5.0.1
|
||||
url_launcher: ^6.2.4
|
||||
http: 0.13.5
|
||||
cancellation_token_http: ^1.1.0
|
||||
http: ^0.13.6
|
||||
cancellation_token_http: ^2.0.0
|
||||
easy_localization: ^3.0.3
|
||||
share_plus: ^7.2.1
|
||||
share_plus: ^7.2.2
|
||||
flutter_displaymode: ^0.6.0
|
||||
scrollable_positioned_list: ^0.3.8
|
||||
path: ^1.8.3
|
||||
@@ -48,8 +48,8 @@ dependencies:
|
||||
http_parser: ^4.0.2
|
||||
flutter_web_auth: ^0.5.0
|
||||
easy_image_viewer: ^1.4.0
|
||||
isar: *isar_version
|
||||
isar_flutter_libs: *isar_version # contains Isar Core
|
||||
isar: ^3.1.0+1
|
||||
isar_flutter_libs: ^3.1.0+1
|
||||
permission_handler: ^11.2.0
|
||||
device_info_plus: ^9.1.1
|
||||
connectivity_plus: ^5.0.2
|
||||
@@ -90,10 +90,10 @@ dev_dependencies:
|
||||
auto_route_generator: ^7.3.2
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
flutter_native_splash: ^2.3.9
|
||||
isar_generator: *isar_version
|
||||
isar_generator: ^3.1.0+1
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
custom_lint: ^0.5.8
|
||||
custom_lint: ^0.6.0
|
||||
riverpod_lint: ^2.3.7
|
||||
riverpod_generator: ^2.3.9
|
||||
mocktail: ^1.0.3
|
||||
|
||||
@@ -4597,6 +4597,41 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/search/cities": {
|
||||
"get": {
|
||||
"operationId": "getAssetsByCity",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Search"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/search/explore": {
|
||||
"get": {
|
||||
"operationId": "getExploreData",
|
||||
@@ -6503,7 +6538,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.98.2",
|
||||
"version": "1.99.0",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
@@ -7646,6 +7681,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ownerId",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -10689,7 +10725,8 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"importPath"
|
||||
"importPath",
|
||||
"isValid"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.2",
|
||||
"version": "1.99.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.2",
|
||||
"version": "1.99.0",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
@@ -22,9 +22,9 @@
|
||||
"integrity": "sha512-V33FjR6V+AkGRWYQW3XPm5BLn2loGl2ujSeja1TzdjjEn2zjGgl3ve0dcFf/jEwPZEOqQZl6YwIgIB/clXVqWw=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz",
|
||||
"integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==",
|
||||
"version": "20.11.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.28.tgz",
|
||||
"integrity": "sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.2",
|
||||
"version": "1.99.0",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 1.98.2
|
||||
* 1.99.0
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
@@ -466,7 +466,7 @@ export type CreateLibraryDto = {
|
||||
isVisible?: boolean;
|
||||
isWatched?: boolean;
|
||||
name?: string;
|
||||
ownerId?: string;
|
||||
ownerId: string;
|
||||
"type": LibraryType;
|
||||
};
|
||||
export type UpdateLibraryDto = {
|
||||
@@ -491,7 +491,7 @@ export type ValidateLibraryDto = {
|
||||
};
|
||||
export type ValidateLibraryImportPathResponseDto = {
|
||||
importPath: string;
|
||||
isValid?: boolean;
|
||||
isValid: boolean;
|
||||
message?: string;
|
||||
};
|
||||
export type ValidateLibraryResponseDto = {
|
||||
@@ -2204,6 +2204,14 @@ export function search({ clip, motion, page, q, query, recent, size, smart, $typ
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getAssetsByCity(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: AssetResponseDto[];
|
||||
}>("/search/cities", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getExploreData(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
@@ -22,7 +22,7 @@
|
||||
<br/>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
@@ -4,33 +4,28 @@
|
||||
"minimumReleaseAge": "5 days",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchFileNames": ["cli/**"],
|
||||
"groupName": "@immich/cli",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"schedule": "on tuesday"
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["docs/**"],
|
||||
"groupName": "docs",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"schedule": "on tuesday"
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["mobile/**"],
|
||||
"groupName": "mobile",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"schedule": "on tuesday"
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["server/**"],
|
||||
"groupName": "server",
|
||||
"matchFileNames": [
|
||||
"cli/**",
|
||||
"docs/**",
|
||||
"e2e/**",
|
||||
"open-api/**",
|
||||
"server/**",
|
||||
"web/**"
|
||||
],
|
||||
"groupName": "typescript-projects",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"excludePackagePrefixes": ["exiftool", "reflect-metadata"],
|
||||
"schedule": "on tuesday"
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["open-api/**"],
|
||||
"groupName": "open-api",
|
||||
"matchFileNames": ["machine-learning/**"],
|
||||
"groupName": "machine-learning",
|
||||
"rangeStrategy": "in-range-only",
|
||||
"schedule": "on tuesday"
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["mobile/**"],
|
||||
"groupName": "mobile",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"schedule": "on tuesday"
|
||||
},
|
||||
@@ -45,18 +40,6 @@
|
||||
"matchPackagePrefixes": ["@sveltejs"],
|
||||
"schedule": "on tuesday"
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["web/**"],
|
||||
"groupName": "web",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"schedule": "on tuesday"
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["machine-learning/**"],
|
||||
"groupName": "machine-learning",
|
||||
"rangeStrategy": "in-range-only",
|
||||
"schedule": "on tuesday"
|
||||
},
|
||||
{
|
||||
"matchFileNames": [".github/**"],
|
||||
"groupName": "github-actions",
|
||||
@@ -81,9 +64,6 @@
|
||||
}
|
||||
],
|
||||
"ignorePaths": ["mobile/openapi/pubspec.yaml"],
|
||||
"ignoreDeps": [
|
||||
"http",
|
||||
"intl"
|
||||
],
|
||||
"ignoreDeps": ["http", "intl"],
|
||||
"labels": ["dependencies", "renovate"]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
@@ -32,5 +33,6 @@ module.exports = {
|
||||
'@typescript-eslint/require-await': 'error',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'no-restricted-imports': ['error', { patterns: [{ group: ['.*'], message: 'Relative imports are not allowed.' }] }],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240312@sha256:3cb168dd87a2b412b25c512ec638a1e7f362e1d3eb8dd19a38d92d4a7c47999c as dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240319@sha256:9c9492d59b51a0c340ea3f61a1c9a483122b86b008c7fbe1759d7b858c5e6af5 as dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
@@ -10,13 +10,17 @@ RUN npm ci && \
|
||||
rm -rf node_modules/@img/sharp-libvips* && \
|
||||
rm -rf node_modules/@img/sharp-linuxmusl-x64
|
||||
COPY server .
|
||||
|
||||
WORKDIR /usr/src/app/server
|
||||
RUN npm run prisma:generate
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV PATH="${PATH}:/usr/src/app/bin" \
|
||||
NODE_ENV=development \
|
||||
NVIDIA_DRIVER_CAPABILITIES=all \
|
||||
NVIDIA_VISIBLE_DEVICES=all
|
||||
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
||||
|
||||
|
||||
FROM dev AS prod
|
||||
|
||||
RUN npm run build
|
||||
@@ -24,7 +28,7 @@ RUN npm prune --omit=dev --omit=optional
|
||||
COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
|
||||
|
||||
# web build
|
||||
FROM node:iron-alpine3.18@sha256:a02826c7340c37a29179152723190bcc3044f933c925f3c2d78abb20f794de3f as web
|
||||
FROM node:iron-alpine3.18@sha256:876514790dabd49fae7d9c4dfbba027954bd91d8e7d36da76334466533bc6b0c as web
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
@@ -40,7 +44,7 @@ RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240312@sha256:8359fb1acc56580f2b4835e273293fdaa99d273b210892e1485fc6f1e47cf2bb
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240319@sha256:6bfcb2f2b84d3070be95ab09ba614e2ff3e832e566a283f516a276a8ae6a147b
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssetResponseDto } from '@app/domain';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import request from 'supertest';
|
||||
|
||||
export const assetApi = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LoginResponseDto, UserResponseDto } from '@app/domain';
|
||||
import { adminSignupStub, loginResponseStub, loginStub } from '@test';
|
||||
import { LoginResponseDto } from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import request from 'supertest';
|
||||
import { adminSignupStub, loginResponseStub, loginStub } from 'test/fixtures/auth.stub';
|
||||
|
||||
export const authApi = {
|
||||
adminSignUp: async (server: any) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { assetApi } from './asset-api';
|
||||
import { authApi } from './auth-api';
|
||||
import { libraryApi } from './library-api';
|
||||
import { assetApi } from 'e2e/client/asset-api';
|
||||
import { authApi } from 'e2e/client/auth-api';
|
||||
import { libraryApi } from 'e2e/client/library-api';
|
||||
|
||||
export const api = {
|
||||
authApi,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from '@app/domain';
|
||||
import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from 'src/dtos/library.dto';
|
||||
import request from 'supertest';
|
||||
|
||||
export const libraryApi = {
|
||||
|
||||