Compare commits

...

70 Commits

Author SHA1 Message Date
Alex Tran
de0c59efe7 Added mobile change log 37 2022-09-06 10:03:55 -05:00
Alex Tran
c19d26f4f3 Update some mobile UI with Material 3 theme 2022-09-06 09:37:04 -05:00
Alex Tran
2edfc75c8a Fixed sliverappbar icon color to conform with theming 2022-09-06 08:18:07 -05:00
Matthias Rupp
4c977d2c1f fix(mobile): cache related crash (#593) 2022-09-06 08:10:52 -05:00
Alex
1425f2ec78 Up server version 2022-09-05 23:44:42 -05:00
Alex
b081eda76f fix(server): change the createdAt and modifiedAt to the correct type in database (#591)
* Added migration files

* Remove type casting in sql query
2022-09-05 20:51:01 -05:00
Thanh Pham
7f6837c751 hotfix(server): skip exif extraction on duplicate file (#590)
* fix(server): skip exif extraction on duplicate file

* fix(server): typo

* chore(server): remvoe un-use code
2022-09-05 20:02:50 -05:00
Thanh Pham
a467936e73 feat(server): de-duplication (#557)
* feat(server): remove un-used deviceAssetId cols.

* feat(server): return 409 if asset is duplicated

* feat(server): replace old unique constaint

* feat(server): strip deviceId in file path

* feat(server): skip duplicate asset

* chore(server): revert changes

* fix(server): asset test spec

* fix(server): checksum generation for uploaded assets

* fix(server): make sure generation queue run after migraion

* feat(server): remove temp file

* chore(server): remove dead code
2022-09-05 14:45:38 -05:00
Alex Tran
2677ddccaa Up version for release 2022-09-05 14:32:05 -05:00
bo0tzz
564ace3ddf Use runtime env var for login page message to lower web container startup time (#577)
* Use runtime env var for loginPageMessage

* Rename VITE_LOGIN_PAGE_MESSAGE to PUBLIC_LOGIN_PAGE_MESSAGE in .env.example

* Move docker image `npm run build` step into Dockerfile

* Remove comment from web Dockerfile
2022-09-05 09:51:45 -05:00
Thanh Pham
a81ef7497c feat(server): support 3gpp format (#582)
* feat(server): support 3gpp format

* feat(web): add 3gp ext

* Support 3gp video format.

video/3gpp mimetype added to supported video format.

* feat(mobile): add tif ext

Co-authored-by: Alexandre Bouijoux <alexandre@bouijoux.fr>
2022-09-05 08:53:13 -05:00
Matthias Rupp
caa7b07398 Show all albums an asset appears in on the asset viewer page (#575)
* Add route to query albums for a specific asset

* Update API and add to detail-panel

* Fix tests

* Refactor API endpoint

* Added alt attribute to img tag

Co-authored-by: Alex <alex.tran1502@gmail.com>
2022-09-05 08:50:20 -05:00
Alex
6976a7241e Fixed upload asset to album in asset selection (#579)
* Fixed error uploading a file from album

* Fixed album selection mode show viewing asset stage

* Navigate back after uploading asset to album
2022-09-05 00:18:53 -05:00
Alex Tran
172eda3ce5 Fixed readme 2022-09-04 21:08:13 -05:00
Alex
552340add7 Feature - Implemented virtual scroll on web (#573)
This PR implemented a virtual scroll on the web, as seen in this article.

[Building the Google Photos Web UI](https://medium.com/google-design/google-photos-45b714dfbed1)
2022-09-04 08:34:39 -05:00
SirBogner
bd92dde117 Update localizely.yml (#574) 2022-09-04 08:27:05 -05:00
Alex
617c54ab81 [Localizely] Translations update (#576) 2022-09-04 08:26:11 -05:00
Thanh Pham
c76f7804ab feat(server): generate checksum for previous uploaded assets (#558)
* feat(server): generate checksum for previous uploaded assets

* fix(server): typo
2022-09-02 08:32:21 -05:00
Damian Gomez
0799aa2c72 Italia language for Mobile App (#559)
Co-authored-by: Damian Gomez <damian.gomez@elitedivision.it>
2022-09-01 08:48:13 -05:00
Thanh Pham
b80dca74ef feat(server): calculate sha1 checksum (#525)
* feat(server): override multer storage

* feat(server): calc sha1 of uploaded file

* feat(server): add checksum into asset

* chore(server): add package-lock for mkdirp package

* fix(server): free hash stream

* chore(server): rollback this changes, not refactor here

* refactor(server): re-arrange import statement

* fix(server): make sure hash done before callback

* refactor(server): replace varchar to char for checksum, reserve pixelChecksum for future

* refactor(server): remove pixelChecksum

* refactor(server): convert checksum from string to bytea

* feat(server): add index to checksum

* refactor(): rollback package.json changes

* feat(server): remove uploaded file when progress fail

* feat(server): calculate hash in sequence
2022-08-31 09:27:17 -05:00
Thanh Pham
f5f00e0f6c fix(web): file uploading error in album page (#550)
* feat(web): show upload error notification

* fix(web): album upload issue
2022-08-31 08:12:31 -05:00
Fynn Petersen-Frey
75d2d82d05 ask user to disable battery optimizations when turning on background backup (#554)
* ask user to disable battery optimizations when turning on background backup

* remove obsolete texts/translations

* add button link to dontkillmyapp
2022-08-31 08:08:40 -05:00
Fynn Petersen-Frey
5172242f88 fix: persist WiFi + charging settings of background backup (#553) 2022-08-30 09:09:19 -05:00
Matthias Rupp
25e68cf826 Better caching for mobile (#521)
* Use custom caches in all modules

* Cache Settings

* Fix wrong key

* Create custom cache repository based on hive

* Show cache usage in settings

* Show cache sizes

* Change settings ranges and default value

* Handle cache clear by operating system

* Resolve review comments
2022-08-29 22:44:43 -05:00
be bright
e527685ebf Added korean translation for mobile app (#549)
* Added korean translation for mobile app

* Added locale to info.plist

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-08-29 08:54:40 -05:00
Thanh Pham
e745cb5e4b fix(server): parse all img formats and enrich metadata (#547)
* fix(server): use file path instead buffer to reduce memory usage

fix undefined exif data

* fix(server): parse all img formats

* feat(server): enrich metadata

* Format oneliner condition

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-08-28 15:43:31 -05:00
Thanh Pham
dfaa4969da Server - Fixed - Use file path instead buffer to reduce memory usage on EXIF extraction (#545)
fix undefined exif data
2022-08-28 11:09:24 -05:00
Alex
f980a2f27a Add asset repository and refactor asset service (#540)
* build endpoint to get asset count by month

* Added asset repository

* Added create asset

* get asset by device ID

* Added test for existing methods

* Refactor additional endpoint

* Refactor database api to get curated locations and curated objects

* Refactor get search properties

* Fixed cookies parsing for websocket

* Added API to get asset count by time group

* Remove unused code
2022-08-26 22:53:37 -07:00
Alex
6b7c97c02a Added release notes 2022-08-26 13:43:05 -07:00
Alex
fdd9f37abd Added error handling for layout.server.ts to avoid unaccessible to previous deploy instance due to changes in SvelteKit project 2022-08-26 11:30:45 -07:00
Alex
a09bba454c Pump version for release 2022-08-26 10:57:12 -07:00
Alex
4be9aa091b Added error handling notification (#536) 2022-08-26 10:36:41 -07:00
Alex
33b810de74 Removed upload button on sharing and album page 2022-08-26 10:05:15 -07:00
Alex
44ccb1eec1 Added timeout option for notification component 2022-08-26 10:01:47 -07:00
Alex
bef38c670c Reference CLI in limit upload message 2022-08-26 09:42:48 -07:00
Alex
025d7bf192 Merge branch 'main' of github.com:immich-app/immich 2022-08-26 09:42:17 -07:00
Alex
5ad2d62039 Added limit on total of file upload on web (#535) 2022-08-26 09:39:28 -07:00
Alex
a128833e68 Added limit on total of file upload on web 2022-08-26 09:36:54 -07:00
Alex
87f7b0849a Added migration down for change exif file type 2022-08-26 09:13:11 -07:00
Alex
4596a8ee01 Change fileSizeInByte to bigint from int to handle large size (#534) 2022-08-26 09:07:59 -07:00
Alex
f9b1b12b10 Implement notification box for web (#533)
* Added test button

* styling notification box

* Added auto dismission and animation to each notificaiont list

* Remove test button
2022-08-25 23:04:23 -07:00
Alex
68b1655e7f Show the first two letter of user first and last name when profile image not existed (#532)
* Added user first name and last name abbreviation to Circle Avatar:

* Remove unsued code
2022-08-25 15:52:11 -07:00
Alex
658b64df74 Added page navigation progress indicator 2022-08-25 13:02:36 -07:00
Alex
e344503834 Fixed navigating with keyboard skip assets (#531)
* Cleaned up event listner
2022-08-24 22:18:28 -07:00
Alex
bf2760ffef Fixed mobile timeline crash when date group cannot be parsed (#530)
* Handle error when datetime is incorrect

* Added better debug message
2022-08-24 21:31:20 -07:00
Alex
db2ed2d881 Migrate SvelteKit to the latest version 431 (#526) 2022-08-24 21:10:48 -07:00
Thanh Pham
fb0fa742f5 fix(web): buffering for video player (#520)
* fix(web): buffering for video player

* chore(): missing file -_-

* refactor(web): using URL builder

* chore(): add semicolon

* fix(web): video player

* remove deadcode

Co-authored-by: Alex <alex.tran1502@gmail.com>
2022-08-23 20:21:41 -07:00
Thanh Pham
3b55cdc0be refactor(server): move constant into common package (#522)
* refactor(server): move constant into common package

* refactor(server): re-arrange import statement in microservice module

* refactor(server): move app.config into common package

* fix(server): e2e testing
2022-08-23 07:34:21 -07:00
Alex
0efcc99f3e Added Dutch locale 2022-08-22 12:52:24 -07:00
Nick Pieper
7a85164a1e Added dutch translation for Immich (#519)
* Create nl-NL.json with dutch translation

* Add nl-NL to localizely.yml
2022-08-22 12:50:56 -07:00
Thanh Pham
ba2cda8955 feat(server): support tiff uploading (#513)
* feat(server): suport tiff uploading

* remove unused variable

Co-authored-by: Alex <alex.tran1502@gmail.com>
2022-08-22 12:49:17 -07:00
Alex
9048be4c8e Added Code of conduct 2022-08-21 12:43:56 -07:00
Alex
83716ae1bc Added changelog note 2022-08-21 12:30:15 -07:00
Alex
5cd4d2d158 Added condition to show notification setting on android only 2022-08-21 11:04:01 -07:00
Alex
13bb6d469b Pump version for release 2022-08-21 09:56:52 -07:00
Matthias Rupp
8e4c4c34e4 Use CachedNetworkImage and separate cache for thumbnails on library page (#509)
* Use CachedNetworkImage and separate cache for thumbnails on library page

* Use caching for shared albums as well

* Introduce cache service
2022-08-21 09:41:36 -07:00
Fynn Petersen-Frey
3125d04f32 show notifications on background backup errors (#496)
* show notifications on background backup errors

* settings page to configure (background backup error) notifications

* persist time since failed background backup

* fix darkmode slider color
2022-08-21 09:29:24 -07:00
Alex
c436c57cc9 Fixed immich-machine-learning container not starting correctly in production 2022-08-20 23:04:10 -07:00
Thanh Pham
7f9f825589 fix(server): correct media info (#508)
* fix(server): correct media info

* fix(server): video metadata
2022-08-20 22:58:47 -07:00
Alex
da9aed5c11 Fixed e2e container stage 2022-08-20 22:37:55 -07:00
Alex
10ef3509dd Fixed machine-learning container cannot start prod 2022-08-20 22:27:25 -07:00
Alex
3dc538f9e6 Fixed machine-learning container cannot start prod 2022-08-20 22:26:47 -07:00
Thanh Pham
1e29ff322d build(server): minimal container (#506)
* build(server): update Dockerfile

* build(server): fix dockerfile

* build(machine-learning): multiple build stages

* build(server): update Dockerfile
2022-08-20 21:19:02 -07:00
Thanh Pham
9c30d58b10 feat(server): preserve caption fields and extract mediainfo for video (#505)
* feat(server): preserve caption fields and extract mediainfo for video

* Fixed Geocoding missing info leads to fail EXIF extraction for the whole file

Co-authored-by: Alex <alex.tran1502@gmail.com>
2022-08-20 16:31:37 -07:00
Matthias Rupp
013a0f8324 Customization options for asset grid (#498)
* Add settings options for number of assets per row and storage indicator

* Add attributes to enum to avoid duplicate code

* Also apply customizations to albums

* Minor Refactorings

* Three stage loading i18n fix
2022-08-20 14:19:40 -07:00
Alex
07b58f46f9 Merge branch 'main' of github.com:alextran1501/immich 2022-08-20 08:01:59 -07:00
Alex
566e118a19 Added pt-BR translation locale to mobile app 2022-08-20 08:01:52 -07:00
Oton
0e18c88534 pt-BR Translation: Translation into Portuguese Brazil. (#500) 2022-08-20 08:01:25 -07:00
Alex
068d06b9ee Add x-adobe-dng to support file type (#504) 2022-08-20 07:50:58 -07:00
Thanh Pham
0cf7606ec9 fix(server): remove albumThumbnailAssetId when album is empty (#495) 2022-08-19 11:47:14 -07:00
236 changed files with 8474 additions and 2828 deletions

134
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,134 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation
in our community a harassment-free experience for everyone, regardless
of age, body size, visible or invisible disability, ethnicity, sex
characteristics, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open,
welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for
our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our
mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or
political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in
a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our
standards of acceptable behavior and will take appropriate and fair
corrective action in response to any behavior that they deem
inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit,
or reject comments, commits, code, wiki edits, issues, and other
contributions that are not aligned to this Code of Conduct, and will
communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also
applies when an individual is officially representing the community in
public spaces. Examples of representing our community include using an
official e-mail address, posting via an official social media account,
or acting as an appointed representative at an online or offline
event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported to the community leaders responsible for enforcement
at our Discord channel. All complaints
will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and
security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in
determining the consequences for any action they deem in violation of
this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior
deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders,
providing clarity around the nature of the violation and an
explanation of why the behavior was inappropriate. A public apology
may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued
behavior. No interaction with the people involved, including
unsolicited interaction with those enforcing the Code of Conduct, for
a specified period of time. This includes avoiding interactions in
community spaces as well as external channels like social
media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards,
including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or
public communication with the community for a specified period of
time. No public or private interaction with the people involved,
including unsolicited interaction with those enforcing the Code of
Conduct, is allowed during this period. Violating these terms may lead
to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of
community standards, including sustained inappropriate behavior,
harassment of an individual, or aggression toward or disparagement of
classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction
within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor
Covenant][homepage], version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of
conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the
FAQ at https://www.contributor-covenant.org/faq. Translations are
available at https://www.contributor-covenant.org/translations.

View File

@@ -36,20 +36,22 @@
> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development, there will be continuous functions, features and api changes.
| | Mobile | Web |
| Features | Mobile | Web |
| - | - | - |
| ☁️ Upload and view videos and photos | Yes | Yes
| 🔄 Auto backup when the app is opened | Yes | N/A
| ☑️ Selective album(s) for backup | Yes | N/A
| ⬇️ Download photos and videos to local device | Yes | Yes
| 👪 Multi-user support | Yes | Yes
| 🖼️ Album | Yes | Yes
| 🤝 Shared Albums | Yes | Yes
| 🚀 Quick navigation with draggable scrollbar | Yes | Yes
| 🗃️ Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
| 🧭 Metadata view (EXIF, map) | Yes | Yes
| 🔎 Search by metadata, objects and image tags | Yes | No
| ⚙️ Administrative functions (user management) | N/A | Yes
| Upload and view videos and photos | Yes | Yes
| Auto backup when the app is opened | Yes | N/A
| Selective album(s) for backup | Yes | N/A
| Download photos and videos to local device | Yes | Yes
| Multi-user support | Yes | Yes
| Album | Yes | Yes
| Shared Albums | Yes | Yes
| Quick navigation with draggable scrollbar | Yes | Yes
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
| Metadata view (EXIF, map) | Yes | Yes
| Search by metadata, objects and image tags | Yes | No
| Administrative functions (user management) | N/A | Yes
| Background backup | Android | N/A
| Virtual scroll | N/A | Yes
<br/>

View File

@@ -61,6 +61,6 @@ MAPBOX_KEY=
####################################################################################
# Custom message on the login page, should be written in HTML form.
# For example VITE_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
VITE_LOGIN_PAGE_MESSAGE=
PUBLIC_LOGIN_PAGE_MESSAGE=

View File

@@ -6,6 +6,7 @@ services:
build:
context: ../server
dockerfile: Dockerfile
target: builder
command: npm run start:dev immich
volumes:
- ../server:/usr/src/app
@@ -24,6 +25,7 @@ services:
build:
context: ../machine-learning
dockerfile: Dockerfile
target: builder
command: npm run start:dev
volumes:
- ../machine-learning:/usr/src/app
@@ -41,6 +43,7 @@ services:
build:
context: ../server
dockerfile: Dockerfile
target: builder
command: npm run start:dev microservices
volumes:
- ../server:/usr/src/app

View File

@@ -6,6 +6,7 @@ services:
build:
context: ../server
dockerfile: Dockerfile
target: builder
command: npm run test:e2e
expose:
- "3000"

View File

@@ -9,6 +9,14 @@ upload:
locale_code: de-DE
- file: mobile/assets/i18n/fr-FR.json
locale_code: fr-FR
- file: mobile/assets/i18n/it-IT.json
locale_code: it-IT
- file: mobile/assets/i18n/nl-NL.json
locale_code: nl-NL
- file: mobile/assets/i18n/ko-KR.json
locale_code: ko-KR
- file: mobile/assets/i18n/da-DK.json
locale_code: da-DK
download:
files:
- file: mobile/assets/i18n/en-US.json
@@ -17,3 +25,11 @@ download:
locale_code: de-DE
- file: mobile/assets/i18n/fr-FR.json
locale_code: fr-FR
- file: mobile/assets/i18n/it-IT.json
locale_code: it-IT
- file: mobile/assets/i18n/nl-NL.json
locale_code: nl-NL
- file: mobile/assets/i18n/ko-KR.json
locale_code: ko-KR
- file: mobile/assets/i18n/da-DK.json
locale_code: da-DK

View File

@@ -1,4 +1,5 @@
FROM node:16-bullseye-slim
# Build stage
FROM node:16-bullseye-slim as builder
ARG DEBIAN_FRONTEND=noninteractive
@@ -15,3 +16,27 @@ RUN npm rebuild @tensorflow/tfjs-node --build-from-source
COPY . .
RUN npm run build
# Prod stage
FROM node:16-bullseye-slim
ARG DEBIAN_FRONTEND=noninteractive
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
COPY entrypoint.sh ./
RUN mkdir -p /usr/src/app/dist \
&& mkdir -p /usr/src/app/node_modules \
&& apt-get update \
&& apt-get install -y ffmpeg \
&& rm -rf /var/cache/apt/lists
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/dist ./dist
RUN npm prune --production
# CMD [ "node", "dist/main" ]

View File

@@ -1,2 +1,3 @@
# npm run typeorm migration:run
npm run build && npm run start:prod
# npm run start:prod
node dist/main.js

View File

@@ -11,3 +11,6 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
# Fastlane
/fastlane/report.xml

View File

@@ -69,26 +69,8 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
"isEnabled" -> {
result.success(BackupWorker.isEnabled(ctx))
}
"disableBatteryOptimizations" -> {
if(!BackupWorker.isIgnoringBatteryOptimizations(ctx)) {
val args = call.arguments<ArrayList<*>>()!!
val text = args.get(0) as String
Toast.makeText(ctx, text, Toast.LENGTH_LONG).show()
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.setData(Uri.parse("package:" + ctx.getPackageName()))
try {
ctx.startActivity(intent)
} catch(e: Exception) {
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
try {
ctx.startActivity(intent)
} catch (e2: Exception) {
return result.success(false)
}
}
}
result.success(true)
"isIgnoringBatteryOptimizations" -> {
result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
}
else -> result.notImplemented()
}

View File

@@ -138,6 +138,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
immediate = true,
requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
initialDelayInMs = ONE_MINUTE,
retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
}
engine?.destroy()
@@ -169,6 +170,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
immediate = true,
requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
initialDelayInMs = ONE_MINUTE,
retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
}
}
@@ -186,22 +188,27 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
val args = call.arguments<ArrayList<*>>()!!
val title = args.get(0) as String
val content = args.get(1) as String
showError(title, content)
val individualTag = args.get(2) as String?
showError(title, content, individualTag)
}
"clearErrorNotifications" -> clearErrorNotifications()
else -> r.notImplemented()
}
}
private fun showError(title: String, content: String) {
private fun showError(title: String, content: String, individualTag: String?) {
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
.setContentTitle(title)
.setTicker(title)
.setContentText(content)
.setSmallIcon(R.mipmap.ic_launcher)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.build()
val notificationId = SystemClock.uptimeMillis() as Int
notificationManager.notify(notificationId, notification)
notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
}
private fun clearErrorNotifications() {
notificationManager.cancel(NOTIFICATION_ERROR_ID)
}
private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
@@ -212,14 +219,14 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build()
return ForegroundInfo(1, notification)
return ForegroundInfo(NOTIFICATION_ID, notification)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannel() {
val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(foreground)
val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_HIGH)
val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(error)
}
@@ -236,6 +243,9 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_ERROR_ID = 2
private const val ONE_MINUTE: Long = 60000
/**
* Enqueues the `BackupWorker` to run when all constraints are met.
@@ -262,6 +272,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
keepExisting: Boolean = false,
requireUnmeteredNetwork: Boolean = false,
requireCharging: Boolean = false,
initialDelayInMs: Long = 0,
retries: Int = 0) {
if (!isEnabled(context)) {
return
@@ -287,9 +298,10 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
val photoCheck = OneTimeWorkRequest.Builder(BackupWorker::class.java)
.setConstraints(constraints.build())
.setInputData(inputData)
.setInitialDelay(initialDelayInMs, TimeUnit.MILLISECONDS)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
ONE_MINUTE,
TimeUnit.MILLISECONDS)
.build()
val policy = if (immediate) ExistingWorkPolicy.REPLACE else (if (keepExisting) ExistingWorkPolicy.KEEP else ExistingWorkPolicy.APPEND_OR_REPLACE)

View File

@@ -30,8 +30,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 34,
"android.injected.version.name" => "1.24.0",
"android.injected.version.code" => 37,
"android.injected.version.name" => "1.27.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -0,0 +1,4 @@
* Feature - Customization options for asset grid
* Added pt-BR Translation: Translation into Portuguese Brazil
* Feature - Show notifications on background backup errors
* Optimization - Use CachedNetworkImage and separate cache for thumbnails on library page

View File

@@ -0,0 +1 @@
* Fixed rendering blank when failed to parse datetime on main timeline

View File

@@ -0,0 +1,2 @@
* Add cache setting and improve caching mechanism
* Persist WiFi + charging settings of background backup

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000221">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000222">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="55.750133">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="55.311329">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="35.558064">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.070842">
</testcase>

View File

@@ -1,6 +1,9 @@
{
"album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
"album_info_card_backup_album_included": "EINGESCHLOSSEN",
"album_thumbnail_card_item": "1 Element",
"album_thumbnail_card_items": "{} Elemente",
"album_thumbnail_card_shared": " · Geteilt",
"album_viewer_appbar_share_delete": "Album löschen",
"album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden",
"album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden",
@@ -9,6 +12,8 @@
"album_viewer_appbar_share_leave": "Album verlassen",
"album_viewer_appbar_share_remove": "Entferne vom Album",
"album_viewer_page_share_add_users": "Nutzer hinzufügen",
"asset_list_settings_subtitle": "",
"asset_list_settings_title": "",
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
"backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
@@ -16,7 +21,26 @@
"backup_album_selection_page_selection_info": "Auswahl",
"backup_album_selection_page_total_assets": "Elemente",
"backup_all": "Alle",
"backup_background_service_backup_failed_message": "",
"backup_background_service_connection_failed_message": "",
"backup_background_service_current_upload_notification": "",
"backup_background_service_default_notification": "",
"backup_background_service_error_title": "",
"backup_background_service_in_progress_notification": "",
"backup_background_service_upload_failure_notification": "",
"backup_controller_page_albums": "Gesicherte Alben",
"backup_controller_page_background_battery_info_link": "",
"backup_controller_page_background_battery_info_message": "",
"backup_controller_page_background_battery_info_ok": "",
"backup_controller_page_background_battery_info_title": "",
"backup_controller_page_background_charging": "",
"backup_controller_page_background_configure_error": "",
"backup_controller_page_background_description": "",
"backup_controller_page_background_is_off": "",
"backup_controller_page_background_is_on": "",
"backup_controller_page_background_turn_off": "",
"backup_controller_page_background_turn_on": "",
"backup_controller_page_background_wifi": "",
"backup_controller_page_backup": "Sicherung",
"backup_controller_page_backup_selected": "Ausgewählt: ",
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
@@ -45,10 +69,25 @@
"backup_controller_page_uploading_file_info": "Informationen",
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
"backup_info_card_assets": "Elemente",
"cache_settings_album_thumbnails": "",
"cache_settings_clear_cache_button": "",
"cache_settings_clear_cache_button_title": "",
"cache_settings_image_cache_size": "",
"cache_settings_statistics_album": "",
"cache_settings_statistics_assets": "",
"cache_settings_statistics_full": "",
"cache_settings_statistics_shared": "",
"cache_settings_statistics_thumbnail": "",
"cache_settings_statistics_title": "",
"cache_settings_subtitle": "",
"cache_settings_thumbnail_size": "",
"cache_settings_title": "",
"control_bottom_app_bar_delete": "Löschen",
"create_shared_album_page_share": "Teilen",
"control_bottom_app_bar_share": "Teilen",
"create_album_page_untitled": "Unbenannt",
"create_shared_album_page_create": "Erstellen",
"create_shared_album_page_share_add_assets": "FOTOS HINZUFÜGEN",
"create_shared_album_page_share": "Teilen",
"create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
"create_shared_album_page_share_select_photos": "Fotos auswählen",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
@@ -60,6 +99,8 @@
"exif_bottom_sheet_description": "Beschreibung hinzufügen...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "STANDORT",
"library_page_albums": "Alben",
"library_page_new_album": "Neues Album",
"login_form_button_text": "Anmelden",
"login_form_email_hint": "deine@email.de",
"login_form_endpoint_hint": "http://deine-server-ip:port/api",
@@ -68,15 +109,15 @@
"login_form_err_invalid_email": "Ungültige E-Mail",
"login_form_err_leading_whitespace": "Führendes Leerzichen",
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
"login_form_failed_login": "Fehler bei der Anmeldung, überprüfen Sie Server URL, E-Mail und Passwort",
"login_form_failed_login": "Error logging you in, check server url, email and password",
"login_form_label_email": "E-Mail",
"login_form_label_password": "Passwort",
"login_form_password_hint": "Passwort",
"login_form_password_hint": "password",
"login_form_save_login": "Angemeldet bleiben",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
"profile_drawer_sign_out": "Abmelden",
"profile_drawer_settings": "Einstellungen",
"profile_drawer_sign_out": "Abmelden",
"search_bar_hint": "Durchsuche deine Fotos",
"search_page_no_objects": "Keine Objektinformationen verfügbar",
"search_page_no_places": "Keine Informationen über Orte verfügbar",
@@ -85,42 +126,44 @@
"search_result_page_new_search_hint": "Neue Suche",
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
"select_user_for_sharing_page_share_suggestions": "Vorschläge",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"setting_notifications_notify_failures_grace_period": "",
"setting_notifications_notify_hours": "",
"setting_notifications_notify_immediately": "",
"setting_notifications_notify_minutes": "",
"setting_notifications_notify_never": "",
"setting_notifications_subtitle": "",
"setting_notifications_title": "",
"setting_pages_app_bar_settings": "Einstellungen",
"share_add": "Hinzufügen",
"share_add_photos": "Fotos hinzufügen",
"share_add_title": "Titel hinzufügen",
"share_create_album": "Album erstellen",
"share_dialog_preparing": "Vorbereiten...",
"share_invite": "Zum Album einladen",
"sharing_page_album": "Geteilte Alben",
"sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.",
"sharing_page_empty_list": "LEERE LISTE",
"sharing_silver_appbar_create_shared_album": "Neues geteiltes Album",
"sharing_silver_appbar_share_partner": "Teile mit Partner",
"tab_controller_nav_library": "Bibliothek",
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Suche",
"tab_controller_nav_sharing": "Teilen",
"tab_controller_nav_library": "Bibliothek",
"theme_setting_asset_list_storage_indicator_title": "",
"theme_setting_asset_list_tiles_per_row_title": "",
"theme_setting_dark_mode_switch": "Dunkler Modus",
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
"theme_setting_system_theme_switch": "Automatisch (Systemeinstellung folgen)",
"theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App",
"theme_setting_theme_title": "Theme",
"theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhöht allerdings den Datenverbrauch deutlich",
"theme_setting_three_stage_loading_title": "Dreistufiges Laden aktivieren",
"version_announcement_overlay_ack": "Ich habe verstanden",
"version_announcement_overlay_release_notes": "Änderungsprotokoll",
"version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
"version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ",
"version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89",
"album_thumbnail_card_item": "1 Element",
"album_thumbnail_card_items": "{} Elemente",
"album_thumbnail_card_shared": " · Geteilt",
"library_page_albums": "Alben",
"library_page_new_album": "Neues Album",
"create_album_page_untitled": "Unbenannt",
"share_dialog_preparing": "Vorbereiten...",
"control_bottom_app_bar_share": "Teilen",
"setting_pages_app_bar_settings": "Einstellungen",
"theme_setting_theme_title": "Theme",
"theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App",
"theme_setting_system_theme_switch": "Automatisch (Systemeinstellung folgen)",
"theme_setting_dark_mode_switch": "Dunkler Modus",
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
"theme_setting_three_stage_loading_title": "Dreistufiges Laden aktivieren",
"theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren liefert die beste Bildqualität, ist dafür aber langsamer beim Laden."
}
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89"
}

View File

@@ -1,6 +1,9 @@
{
"album_info_card_backup_album_excluded": "EXCLUDED",
"album_info_card_backup_album_included": "INCLUDED",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Shared",
"album_viewer_appbar_share_delete": "Delete album",
"album_viewer_appbar_share_err_delete": "Failed to delete album",
"album_viewer_appbar_share_err_leave": "Failed to leave album",
@@ -9,6 +12,8 @@
"album_viewer_appbar_share_leave": "Leave album",
"album_viewer_appbar_share_remove": "Remove from album",
"album_viewer_page_share_add_users": "Add users",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"backup_album_selection_page_albums_device": "Albums on device ({})",
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
@@ -16,23 +21,29 @@
"backup_album_selection_page_selection_info": "Selection Info",
"backup_album_selection_page_total_assets": "Total unique assets",
"backup_all": "All",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_disable_battery_optimizations": "Please disable battery optimization for Immich to enable background backup",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_background_service_in_progress_notification": "Backing up your assets…",
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
"backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_error_title": "Backup error",
"backup_background_service_in_progress_notification": "Backing up your assets…",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_controller_page_albums": "Backup Albums",
"backup_controller_page_background_battery_info_link": "Show me how",
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Battery optimizations",
"backup_controller_page_background_charging": "Only while charging",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
"backup_controller_page_background_is_off": "Automatic background backup is off",
"backup_controller_page_background_is_on": "Automatic background backup is on",
"backup_controller_page_background_turn_off": "Turn off background service",
"backup_controller_page_background_turn_on": "Turn on background service",
"backup_controller_page_background_wifi": "Only on WiFi",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selected: ",
"backup_controller_page_backup_sub": "Backed up photos and videos",
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
"backup_controller_page_background_wifi": "Only on WiFi",
"backup_controller_page_background_charging": "Only while charging",
"backup_controller_page_background_is_on": "Automatic background backup is on",
"backup_controller_page_background_is_off": "Automatic background backup is off",
"backup_controller_page_background_turn_on": "Turn on background service",
"backup_controller_page_background_turn_off": "Turn off background service",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_cancel": "Cancel",
"backup_controller_page_created": "Created on: {}",
"backup_controller_page_desc_backup": "Turn on backup to automatically upload new assets to the server.",
@@ -58,10 +69,25 @@
"backup_controller_page_uploading_file_info": "Uploading file info",
"backup_err_only_album": "Cannot remove the only album",
"backup_info_card_assets": "assets",
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
"cache_settings_clear_cache_button": "Clear cache",
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
"cache_settings_image_cache_size": "Image cache size ({} assets)",
"cache_settings_statistics_album": "Library thumbnails",
"cache_settings_statistics_assets": "{} assets ({})",
"cache_settings_statistics_full": "Full images",
"cache_settings_statistics_shared": "Shared album thumbnails",
"cache_settings_statistics_thumbnail": "Thumbnails",
"cache_settings_statistics_title": "Cache usage",
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
"cache_settings_title": "Caching Settings",
"control_bottom_app_bar_delete": "Delete",
"create_shared_album_page_share": "Share",
"control_bottom_app_bar_share": "Share",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share_add_assets": "ADD PHOTOS",
"create_shared_album_page_share": "Share",
"create_shared_album_page_share_add_assets": "ADD ASSETS",
"create_shared_album_page_share_select_photos": "Select Photos",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
@@ -73,6 +99,8 @@
"exif_bottom_sheet_description": "Add Description...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"login_form_button_text": "Login",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
@@ -88,8 +116,8 @@
"login_form_save_login": "Stay logged in",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_sign_out": "Sign out",
"profile_drawer_settings": "Settings",
"profile_drawer_sign_out": "Sign Out",
"search_bar_hint": "Search your photos",
"search_page_no_objects": "No Objects Info Available",
"search_page_no_places": "No Places Info Available",
@@ -99,41 +127,43 @@
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
"select_user_for_sharing_page_err_album": "Failed to create album",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"setting_notifications_notify_hours": "{} hours",
"setting_notifications_notify_immediately": "immediately",
"setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_never": "never",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_title": "Notifications",
"setting_pages_app_bar_settings": "Settings",
"share_add": "Add",
"share_add_photos": "Add photos",
"share_add_title": "Add a title",
"share_create_album": "Create album",
"share_dialog_preparing": "Preparing...",
"share_invite": "Invite to album",
"sharing_page_album": "Shared albums",
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
"sharing_page_empty_list": "EMPTY LIST",
"sharing_silver_appbar_create_shared_album": "Create shared album",
"sharing_silver_appbar_share_partner": "Share with partner",
"tab_controller_nav_library": "Library",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Search",
"tab_controller_nav_sharing": "Sharing",
"tab_controller_nav_library": "Library",
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_theme_title": "Theme",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"version_announcement_overlay_ack": "Acknowledge",
"version_announcement_overlay_release_notes": "release notes",
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Shared",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"create_album_page_untitled": "Untitled",
"share_dialog_preparing": "Preparing...",
"control_bottom_app_bar_share": "Share",
"setting_pages_app_bar_settings": "Settings",
"theme_setting_theme_title": "Theme",
"theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"theme_setting_three_stage_loading_subtitle": "The three-stage loading delivers the best quality image in exchange for a slower loading speed"
}
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
}

View File

@@ -1,6 +1,9 @@
{
"album_info_card_backup_album_excluded": "EXCLU",
"album_info_card_backup_album_included": "INCLUS",
"album_thumbnail_card_item": "1 élément",
"album_thumbnail_card_items": "{} éléments",
"album_thumbnail_card_shared": " · Partagé",
"album_viewer_appbar_share_delete": "Supprimer l'album",
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
@@ -9,6 +12,8 @@
"album_viewer_appbar_share_leave": "Quitter l'album",
"album_viewer_appbar_share_remove": "Retirer de l'album",
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
"asset_list_settings_subtitle": "",
"asset_list_settings_title": "",
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
@@ -16,7 +21,26 @@
"backup_album_selection_page_selection_info": "Informations sur la sélection",
"backup_album_selection_page_total_assets": "Total des éléments uniques",
"backup_all": "Tout",
"backup_background_service_backup_failed_message": "",
"backup_background_service_connection_failed_message": "",
"backup_background_service_current_upload_notification": "",
"backup_background_service_default_notification": "",
"backup_background_service_error_title": "",
"backup_background_service_in_progress_notification": "",
"backup_background_service_upload_failure_notification": "",
"backup_controller_page_albums": "Sauvegarder les albums",
"backup_controller_page_background_battery_info_link": "",
"backup_controller_page_background_battery_info_message": "",
"backup_controller_page_background_battery_info_ok": "",
"backup_controller_page_background_battery_info_title": "",
"backup_controller_page_background_charging": "",
"backup_controller_page_background_configure_error": "",
"backup_controller_page_background_description": "",
"backup_controller_page_background_is_off": "",
"backup_controller_page_background_is_on": "",
"backup_controller_page_background_turn_off": "",
"backup_controller_page_background_turn_on": "",
"backup_controller_page_background_wifi": "",
"backup_controller_page_backup": "Sauvegardé",
"backup_controller_page_backup_selected": "Sélectionné : ",
"backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
@@ -45,7 +69,23 @@
"backup_controller_page_uploading_file_info": "Envoi d'informations sur le fichier",
"backup_err_only_album": "Impossible de retirer le seul album",
"backup_info_card_assets": "éléments",
"cache_settings_album_thumbnails": "",
"cache_settings_clear_cache_button": "",
"cache_settings_clear_cache_button_title": "",
"cache_settings_image_cache_size": "",
"cache_settings_statistics_album": "",
"cache_settings_statistics_assets": "",
"cache_settings_statistics_full": "",
"cache_settings_statistics_shared": "",
"cache_settings_statistics_thumbnail": "",
"cache_settings_statistics_title": "",
"cache_settings_subtitle": "",
"cache_settings_thumbnail_size": "",
"cache_settings_title": "",
"control_bottom_app_bar_delete": "Supprimer",
"control_bottom_app_bar_share": "Partager",
"create_album_page_untitled": "Sans titre",
"create_shared_album_page_create": "Créer",
"create_shared_album_page_share": "Partager",
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
@@ -59,6 +99,8 @@
"exif_bottom_sheet_description": "Ajouter une description...",
"exif_bottom_sheet_details": "DÉTAILS",
"exif_bottom_sheet_location": "LOCALISATION",
"library_page_albums": "Albums",
"library_page_new_album": "Nouvel album",
"login_form_button_text": "Connexion",
"login_form_email_hint": "votreemail@email.com",
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
@@ -74,6 +116,7 @@
"login_form_save_login": "Rester connecté",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
"profile_drawer_settings": "Paramètres",
"profile_drawer_sign_out": "Se déconnecter",
"search_bar_hint": "Rechercher vos photos",
"search_page_no_objects": "Aucune information disponible sur les objets",
@@ -84,19 +127,39 @@
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"setting_notifications_notify_failures_grace_period": "",
"setting_notifications_notify_hours": "",
"setting_notifications_notify_immediately": "",
"setting_notifications_notify_minutes": "",
"setting_notifications_notify_never": "",
"setting_notifications_subtitle": "",
"setting_notifications_title": "",
"setting_pages_app_bar_settings": "",
"share_add": "Ajouter",
"share_add_photos": "Ajouter des photos",
"share_add_title": "Ajouter un titre",
"share_create_album": "Créer un album",
"share_dialog_preparing": "Préparation...",
"share_invite": "Inviter à l'album",
"sharing_page_album": "Albums partagés",
"sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
"sharing_page_empty_list": "LISTE VIDE",
"sharing_silver_appbar_create_shared_album": "Créer un album partagé",
"sharing_silver_appbar_share_partner": "Partager avec un partenaire",
"tab_controller_nav_library": "Bibliothèque",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Recherche",
"tab_controller_nav_sharing": "Partage",
"theme_setting_asset_list_storage_indicator_title": "",
"theme_setting_asset_list_tiles_per_row_title": "",
"theme_setting_dark_mode_switch": "",
"theme_setting_image_viewer_quality_subtitle": "",
"theme_setting_image_viewer_quality_title": "",
"theme_setting_system_theme_switch": "",
"theme_setting_theme_subtitle": "",
"theme_setting_theme_title": "",
"theme_setting_three_stage_loading_subtitle": "",
"theme_setting_three_stage_loading_title": "",
"version_announcement_overlay_ack": "Confirmer",
"version_announcement_overlay_release_notes": "notes de mise à jour",
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",

View File

@@ -1,28 +1,52 @@
{
"album_info_card_backup_album_excluded": "ESCLUSI",
"album_info_card_backup_album_included": "INCLUSI",
"album_thumbnail_card_item": "1 elemento ",
"album_thumbnail_card_items": "{} elementi",
"album_thumbnail_card_shared": "Condiviso",
"album_viewer_appbar_share_delete": "Elimina album ",
"album_viewer_appbar_share_err_delete": "Fallito nel cancellare l'album ",
"album_viewer_appbar_share_err_leave": "Fallito nel lasciare l'album ",
"album_viewer_appbar_share_err_delete": "Errore nel cancellare l'album ",
"album_viewer_appbar_share_err_leave": "Errore nel lasciare l'album ",
"album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album ",
"album_viewer_appbar_share_err_title": "Fallito nel cambiare titolo dell'album ",
"album_viewer_appbar_share_leave": "Lascia l'album",
"album_viewer_appbar_share_err_title": "Errore nel cambiare il titolo dell'album ",
"album_viewer_appbar_share_leave": "Lascia album",
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
"album_viewer_page_share_add_users": "Aggiungi utenti",
"backup_album_selection_page_albums_device": "Albums nel device ({})",
"asset_list_settings_subtitle": "Impostazion del layout della griglia delle foto",
"asset_list_settings_title": "Griglia foto",
"backup_album_selection_page_albums_device": "Albums sul device ({})",
"backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
"backup_album_selection_page_assets_scatter": "Stesse immagini e video possono trovarsi tra più album, così gli album possono essere inclusi o esclusi dal backup.",
"backup_album_selection_page_select_albums": "Seleziona gli album",
"backup_album_selection_page_selection_info": "Informazioni sulla selezione ",
"backup_album_selection_page_total_assets": "Numero totale di oggetti unici",
"backup_all": "Tutti",
"backup_controller_page_albums": "Backup album",
"backup_background_service_backup_failed_message": "Impossibile caricare contenuti. Nuovo tentativo…",
"backup_background_service_connection_failed_message": "Impossibile connettersi al server. Nuovo tentativo…",
"backup_background_service_current_upload_notification": "Caricamento {}",
"backup_background_service_default_notification": "Verifica di nuovi contenuti…",
"backup_background_service_error_title": "Errore di Backup",
"backup_background_service_in_progress_notification": "Backing dei tuoi contenuti…",
"backup_background_service_upload_failure_notification": "Impossibile caricare {}",
"backup_controller_page_albums": "Backup Album",
"backup_controller_page_background_battery_info_link": "Mostrami come",
"backup_controller_page_background_battery_info_message": "Per una migliore esperienza di backup, disabilita le ottimisazioni della batteria per l'app Immich.\n\nDal momento che è una funzionalità specifica del dispositivo, per favore consulta il manuale del produttore.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Ottimizzazioni batteria",
"backup_controller_page_background_charging": "Solo durante la ricarica",
"backup_controller_page_background_configure_error": "Impossibile configurare i servizi in background",
"backup_controller_page_background_description": "Abilita i servizi in background per sincronizzare tutti i nuovi contenuti senza la necessità di aprire l'app",
"backup_controller_page_background_is_off": "Backup automatico spento",
"backup_controller_page_background_is_on": "Backup automatico attivo",
"backup_controller_page_background_turn_off": "Disabilita servizi in background",
"backup_controller_page_background_turn_on": "Abilita servizi in background",
"backup_controller_page_background_wifi": "Solo su WiFi",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selezionati:",
"backup_controller_page_backup_sub": "Photo e video salvati",
"backup_controller_page_backup_sub": "Foto e video caricati",
"backup_controller_page_cancel": "Cancella ",
"backup_controller_page_created": "Creato il: {}",
"backup_controller_page_desc_backup": "Attiva il backup automatico per eseguire upload sul server",
"backup_controller_page_desc_backup": "Attiva il backup per eseguire il caricamento automatico sul server",
"backup_controller_page_excluded": "Esclusi:",
"backup_controller_page_failed": "Falliti: ({})",
"backup_controller_page_filename": "Nome del file: {} [{}]",
@@ -30,39 +54,57 @@
"backup_controller_page_info": "Informazioni sul backup",
"backup_controller_page_none_selected": "Nessuna selezione",
"backup_controller_page_remainder": "Promemoria ",
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da salvare",
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da caricare",
"backup_controller_page_select": "Seleziona ",
"backup_controller_page_server_storage": "Spazio nel server",
"backup_controller_page_server_storage": "Spazio sul server",
"backup_controller_page_start_backup": "Inizia backup ",
"backup_controller_page_status_off": "Backup è disattivato ",
"backup_controller_page_status_on": "Backup è attivato",
"backup_controller_page_storage_format": "{} di {} usati",
"backup_controller_page_to_backup": "Album da salvare",
"backup_controller_page_to_backup": "Album da caricare",
"backup_controller_page_total": "Totale",
"backup_controller_page_total_sub": "Tutte le foto e i video unici salvati dagli album selezionati ",
"backup_controller_page_total_sub": "Tutte le foto e i video unici caricati dagli album selezionati ",
"backup_controller_page_turn_off": "Disattiva backup",
"backup_controller_page_turn_on": "Attiva backup ",
"backup_controller_page_uploading_file_info": "Info sul file caricato",
"backup_err_only_album": "Non è possibile rimuovere l'unico album",
"backup_info_card_assets": "Oggetti ",
"backup_info_card_assets": "oggetti ",
"cache_settings_album_thumbnails": "Anteprime pagine librerie ({} assets)",
"cache_settings_clear_cache_button": "Cancella cache",
"cache_settings_clear_cache_button_title": "Cancella cache app. Questo impatterà sulle prestazioni applicative fino a quando la cache non sarà rigenerata.",
"cache_settings_image_cache_size": "Dimensione cache foto ({} assets)",
"cache_settings_statistics_album": "Anteprime librerie",
"cache_settings_statistics_assets": "{} contenuti ({})",
"cache_settings_statistics_full": "Immagini complete",
"cache_settings_statistics_shared": "Anteprime album condivisi",
"cache_settings_statistics_thumbnail": "Anteprime",
"cache_settings_statistics_title": "Uso della cache",
"cache_settings_subtitle": "Controlla il comportamento della cache",
"cache_settings_thumbnail_size": "Dimensione cache anteprime ({} assets)",
"cache_settings_title": "Impostazioni della Cache",
"control_bottom_app_bar_delete": "Elimina",
"control_bottom_app_bar_share": "Condividi",
"create_album_page_untitled": "Senza titolo",
"create_shared_album_page_create": "Crea",
"create_shared_album_page_share": "Condividi",
"create_shared_album_page_share_add_assets": "AGGIUNGI OGGETTI",
"create_shared_album_page_share_select_photos": "Seleziona foto",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E, d LLL, y • hh:mm",
"delete_dialog_alert": "Questi oggetti saranno cancellati permanentemente da Immich e dal tuo device",
"delete_dialog_alert": "Questi oggetti saranno cancellati definitivamente da Immich e dal tuo device",
"delete_dialog_cancel": "Annulla",
"delete_dialog_ok": "Elimina",
"delete_dialog_title": "Cancella in modo permanente ",
"delete_dialog_title": "Cancella definitivamente",
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
"exif_bottom_sheet_details": "DETTAGLI",
"exif_bottom_sheet_location": "POSIZIONE",
"login_form_button_text": "Accedi",
"library_page_albums": "Album",
"library_page_new_album": "Nuovo Album",
"login_form_button_text": "Login",
"login_form_email_hint": "tuaemail@email.com",
"login_form_endpoint_hint": "http://tuo-ip-del-server:port/api",
"login_form_endpoint_url": "URL del Server Endpoint",
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Per favore specificare http:// o https://",
"login_form_err_invalid_email": "Email non valida",
"login_form_err_leading_whitespace": "Spazio bianco all'inizio ",
@@ -74,33 +116,54 @@
"login_form_save_login": "Rimani connesso ",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
"profile_drawer_sign_out": "Esci",
"profile_drawer_settings": "Impostazioni ",
"profile_drawer_sign_out": "Logout",
"search_bar_hint": "Cerca le tue foto",
"search_page_no_objects": "Nessuna Informazione relativa all'Oggetto Disponibile",
"search_page_no_places": "Nessun informazione sulla posizione ",
"search_page_no_objects": "Nessuna informazione relativa all'oggetto disponibile",
"search_page_no_places": "Nessun informazione sul luogo disponibile",
"search_page_places": "Luoghi",
"search_page_things": "Oggetti",
"search_result_page_new_search_hint": "Nuova ricerca ",
"select_additional_user_for_sharing_page_suggestions": "Suggerimenti ",
"select_user_for_sharing_page_err_album": "Fallito nel creare l'album ",
"select_user_for_sharing_page_err_album": "Errore nel creare l'album ",
"select_user_for_sharing_page_share_suggestions": "Suggerimenti",
"setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {}",
"setting_notifications_notify_hours": "{} Ore",
"setting_notifications_notify_immediately": "Immediatamente",
"setting_notifications_notify_minutes": "{} Minuti",
"setting_notifications_notify_never": "Mai",
"setting_notifications_subtitle": "Cambia le impostazioni di notifica",
"setting_notifications_title": "Notifiche",
"setting_pages_app_bar_settings": "Impostazioni",
"share_add": "Aggiungi",
"share_add_photos": "Aggiungi foto",
"share_add_title": "Aggiungi un titolo ",
"share_create_album": "Crea album",
"share_invite": "Invitare all'album ",
"share_dialog_preparing": "Preparo…",
"share_invite": "Invitare nell'album ",
"sharing_page_album": "Album condivisi",
"sharing_page_description": "Crea un album condiviso per condividere foto e video con gente nel tuo network",
"sharing_page_description": "Crea un album condiviso per condividere foto e video con persone nel tuo network",
"sharing_page_empty_list": "LISTA VUOTA",
"sharing_silver_appbar_create_shared_album": "Crea album condiviso",
"sharing_silver_appbar_share_partner": "Condividi con il partner",
"tab_controller_nav_library": "Libreria",
"tab_controller_nav_photos": "Foto",
"tab_controller_nav_search": "Cerca",
"tab_controller_nav_sharing": "Condividi",
"theme_setting_asset_list_storage_indicator_title": "Mostra indicatore dello storage nei titoli dei contenuti",
"theme_setting_asset_list_tiles_per_row_title": "Numero di contenuti per riga ({})",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_subtitle": "Cambia la qualità del dettaglio dell'immagine",
"theme_setting_image_viewer_quality_title": "Qualità immagine",
"theme_setting_system_theme_switch": "Automatico (Vai alle impostazioni di sistema)",
"theme_setting_theme_subtitle": "Scegli un'impostazione per il tema",
"theme_setting_theme_title": "Tema",
"theme_setting_three_stage_loading_subtitle": "Il caricamento in 3 stage aumenterà le performance di caricamento ma anche il consumo di banda",
"theme_setting_three_stage_loading_title": "Abilita il caricamento a tre stage",
"version_announcement_overlay_ack": "Riconosci ",
"version_announcement_overlay_release_notes": "le note di rilascio ",
"version_announcement_overlay_release_notes": "note di rilascio ",
"version_announcement_overlay_text_1": "Ciao amico, c'è una nuova versione di",
"version_announcement_overlay_text_2": "prova a controllare ",
"version_announcement_overlay_text_2": "per favore prenditi il tuo tempo per controllare il",
"version_announcement_overlay_text_3": "e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore nella configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico delle immagini docker.",
"version_announcement_overlay_title": "Nuova versione di server disponibile! \uD83C\uDF89"
"version_announcement_overlay_title": "Nuova versione del server disponibile! \uD83C\uDF89"
}

View File

@@ -0,0 +1,152 @@
{
"album_info_card_backup_album_excluded": "제외됨",
"album_info_card_backup_album_included": "포함됨",
"album_viewer_appbar_share_delete": "앨범 삭제",
"album_viewer_appbar_share_err_delete": "앨범 삭제 실패",
"album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다",
"album_viewer_appbar_share_err_remove": "앨범에서 미디어를 제거하는 데 문제가 있습니다",
"album_viewer_appbar_share_err_title": "앨범 제목 변경 실패",
"album_viewer_appbar_share_leave": "앨범 나가기",
"album_viewer_appbar_share_remove": "앨범에서 제거",
"album_viewer_page_share_add_users": "사용자 추가",
"backup_album_selection_page_albums_device": "기기의 앨범({})",
"backup_album_selection_page_albums_tap": "포함하려면 탭하고 제외하려면 두 번 탭하세요",
"backup_album_selection_page_assets_scatter": "미디어파일은 여러 앨범에 분산될 수 있습니다. 따라서 백업 프로세스 중에 앨범에서 포함하거나 제외할 수 있습니다.",
"backup_album_selection_page_select_albums": "앨범 선택",
"backup_album_selection_page_selection_info": "선택 정보",
"backup_album_selection_page_total_assets": "총 미디어파일 수",
"backup_all": "모두",
"backup_background_service_default_notification": "새 미디어파일 확인중...",
"backup_background_service_upload_failure_notification": "{} 업로드 실패",
"backup_background_service_in_progress_notification": "미디어파일 백업 중...",
"backup_background_service_current_upload_notification": "{} 업로드 중",
"backup_background_service_error_title": "백업 오류",
"backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중...",
"backup_background_service_backup_failed_message": "미디어파일을 백업하지 못했습니다. 다시 시도하는 중...",
"backup_controller_page_albums": "백업대상",
"backup_controller_page_backup": "백업",
"backup_controller_page_backup_selected": "선택됨: ",
"backup_controller_page_backup_sub": "백업된 사진 및 비디오",
"backup_controller_page_background_description": "백그라운드 서비스를 켜서 앱을 열지 않고도 새 미디어파일을 자동으로 백업합니다.",
"backup_controller_page_background_wifi": "WiFi에서만",
"backup_controller_page_background_charging": "충전 중일 때만",
"backup_controller_page_background_is_on": "자동 백그라운드 백업이 켜져 있습니다",
"backup_controller_page_background_is_off": "자동 백그라운드 백업이 꺼져 있습니다",
"backup_controller_page_background_turn_on": "백그라운드 서비스 켜기",
"backup_controller_page_background_turn_off": "백그라운드 서비스 끄기",
"backup_controller_page_background_configure_error": "백그라운드 서비스를 구성하지 못했습니다",
"backup_controller_page_cancel": "취소",
"backup_controller_page_created": "생성일: {}",
"backup_controller_page_desc_backup": "새 미디어파일을 서버에 자동으로 업로드하려면 백업을 켜주세요.",
"backup_controller_page_excluded": "제외됨: ",
"backup_controller_page_failed": "실패함 ({})",
"backup_controller_page_filename": "파일 이름: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "정보",
"backup_controller_page_none_selected": "선택되지 않음",
"backup_controller_page_remainder": "남은 백업파일",
"backup_controller_page_remainder_sub": "백업 대기중인 남은 사진 및 비디오",
"backup_controller_page_select": "선택",
"backup_controller_page_server_storage": "서버 저장소",
"backup_controller_page_start_backup": "백업 시작",
"backup_controller_page_status_off": "백업이 꺼져 있습니다",
"backup_controller_page_status_on": "백업이 켜져 있습니다",
"backup_controller_page_storage_format": "{}/{} 사용",
"backup_controller_page_to_backup": "백업할 앨범",
"backup_controller_page_total": "전체 백업대상",
"backup_controller_page_total_sub": "선택한 앨범의 모든 사진 및 비디오",
"backup_controller_page_turn_off": "백업 끄기",
"backup_controller_page_turn_on": "백업 켜기",
"backup_controller_page_uploading_file_info": "파일 정보 업로드 중",
"backup_err_only_album": "유일한 앨범은 제거할 수 없습니다",
"backup_info_card_assets": "미디어",
"control_bottom_app_bar_delete": "삭제",
"create_shared_album_page_share": "공유",
"create_shared_album_page_create": "만들기",
"create_shared_album_page_share_add_assets": "사진 추가",
"create_shared_album_page_share_select_photos": "사진 선택",
"daily_title_text_date": "E, M월 d일",
"daily_title_text_date_year": "E, M월 d일, yyyy",
"date_format": "yyyy년 M월 d일, EEEE • a h:mm",
"delete_dialog_alert": "이 항목은 Immich 및 휴대폰에서 영구적으로 삭제됩니다",
"delete_dialog_cancel": "취소",
"delete_dialog_ok": "삭제",
"delete_dialog_title": "영구적으로 삭제",
"exif_bottom_sheet_description": "설명 추가...",
"exif_bottom_sheet_details": "상세정보",
"exif_bottom_sheet_location": "위치",
"login_form_button_text": "로그인",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "https://your-server-ip:port/api",
"login_form_endpoint_url": "서버 엔드포인트 URL",
"login_form_err_http": "엔드포인트는 http:// 또는 https://로 시작해야 합니다",
"login_form_err_invalid_email": "잘못된 이메일 형식입니다",
"login_form_err_leading_whitespace": "이메일 앞에 공백문자가 포함되어 있습니다",
"login_form_err_trailing_whitespace": "이메일 뒤에 공백문자가 포함되어 있습니다",
"login_form_failed_login": "로그인 오류, 서버 URL, 이메일 및 비밀번호를 확인하세요",
"login_form_label_email": "이메일",
"login_form_label_password": "비밀번호",
"login_form_password_hint": "비밀번호",
"login_form_save_login": "로그인상태 유지",
"monthly_title_text_date_format": "y년 M월",
"profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신 상태입니다",
"profile_drawer_sign_out": "로그아웃",
"profile_drawer_settings": "설정",
"search_bar_hint": "사진 검색",
"search_page_no_objects": "발견된 사물이\n없습니다",
"search_page_no_places": "발견된 장소가\n없습니다",
"search_page_places": "장소",
"search_page_things": "사물",
"search_result_page_new_search_hint": "새 검색",
"select_additional_user_for_sharing_page_suggestions": "초대 가능한 사용자 제안",
"select_user_for_sharing_page_err_album": "앨범 생성 실패",
"select_user_for_sharing_page_share_suggestions": "초대 가능한 사용자 제안",
"share_add": "추가",
"share_add_photos": "사진 추가",
"share_add_title": "새 앨범제목",
"share_create_album": "앨범 만들기",
"share_invite": "앨범에 초대",
"sharing_page_album": "공유앨범",
"sharing_page_description": "공유앨범을 만들어 다른 사용자들과 사진 및 비디오를 공유합니다.",
"sharing_page_empty_list": "공유앨범 없음",
"sharing_silver_appbar_create_shared_album": "공유앨범 만들기",
"sharing_silver_appbar_share_partner": "파트너와 공유",
"tab_controller_nav_photos": "사진",
"tab_controller_nav_search": "검색",
"tab_controller_nav_sharing": "공유",
"tab_controller_nav_library": "라이브러리",
"version_announcement_overlay_ack": "승인",
"version_announcement_overlay_release_notes": "릴리스 정보",
"version_announcement_overlay_text_1": "안녕하세요!",
"version_announcement_overlay_text_2": "앱에 새로운 업데이트가 있습니다!",
"version_announcement_overlay_text_3": "특히 WatchTower 또는 서버 응용 프로그램 자동 업데이트를 처리하는 메커니즘을 사용하는 경우 잘못된 구성을 방지하기 위해 docker-compose 및 .env 설정이 최신 상태인지 확인하세요.",
"version_announcement_overlay_title": "새 서버 버전 사용 가능 \uD83C\uDF89",
"album_thumbnail_card_item": "1개 항목",
"album_thumbnail_card_items": "{}개 항목",
"album_thumbnail_card_shared": " · 공유",
"library_page_albums": "앨범",
"library_page_new_album": "새 앨범",
"create_album_page_untitled": "제목없음",
"share_dialog_preparing": "준비중...",
"control_bottom_app_bar_share": "공유",
"setting_pages_app_bar_settings": "설정",
"theme_setting_theme_title": "테마",
"theme_setting_theme_subtitle": "앱테마 선택",
"theme_setting_system_theme_switch": "자동(시스템 설정에 따름)",
"theme_setting_dark_mode_switch": "다크모드",
"theme_setting_image_viewer_quality_title": "이미지 뷰어 품질",
"theme_setting_image_viewer_quality_subtitle": "디테일 이미지 뷰어 품질 조정",
"theme_setting_three_stage_loading_title": "3단계 로딩 활성화",
"theme_setting_three_stage_loading_subtitle": "이 기능은 로딩 성능을 향상시킬 수 있지만 훨씬 더 많은 데이터를 사용합니다.",
"asset_list_settings_title": "사진 배열",
"asset_list_settings_subtitle": "사진 배열 레이아웃 설정",
"theme_setting_asset_list_storage_indicator_title": "미디어 타일에 스토리지 싱크여부 표시",
"theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 미디어 수 ({})",
"setting_notifications_title": "알림",
"setting_notifications_subtitle": "알림 기본 설정 조정",
"setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {}",
"setting_notifications_notify_immediately": "즉시",
"setting_notifications_notify_minutes": "{}분 뒤",
"setting_notifications_notify_hours": "{}시간 뒤",
"setting_notifications_notify_never": "알리지 않음"
}

View File

@@ -0,0 +1,152 @@
{
"album_info_card_backup_album_excluded": "UITGESLOTEN",
"album_info_card_backup_album_included": "INGESLOTEN",
"album_viewer_appbar_share_delete": "Verwijder album",
"album_viewer_appbar_share_err_delete": "Fout bij verwijderen album",
"album_viewer_appbar_share_err_leave": "Fout bij verlaten album",
"album_viewer_appbar_share_err_remove": "Er gaat iets mis bij het verwijderen van items uit het album",
"album_viewer_appbar_share_err_title": "Fout bij wijzigen album titel",
"album_viewer_appbar_share_leave": "Verlaat album",
"album_viewer_appbar_share_remove": "Verwijder uit album",
"album_viewer_page_share_add_users": "Voeg gebruiker toe",
"backup_album_selection_page_albums_device": "Albums op apparaat ({})",
"backup_album_selection_page_albums_tap": "Tik om in te voegen, dubbel tik om uit te sluiten",
"backup_album_selection_page_assets_scatter": "Items kunnen over verschillende albums verdeeld zijn, dus albums kunnen ingesloten of uitgesloten zijn van het backup proces.",
"backup_album_selection_page_select_albums": "Selecteer albums",
"backup_album_selection_page_selection_info": "Selectie info",
"backup_album_selection_page_total_assets": "Totaal unieke items",
"backup_all": "Alle",
"backup_background_service_default_notification": "Controleren op nieuw items…",
"backup_background_service_upload_failure_notification": "Fout bij upload {}",
"backup_background_service_in_progress_notification": "Backuppen van items…",
"backup_background_service_current_upload_notification": "Uploaden {}",
"backup_background_service_error_title": "Backup fout",
"backup_background_service_connection_failed_message": "Fout bij verbinden server. Opnieuw proberen…",
"backup_background_service_backup_failed_message": "Fout bij backuppen items. Opnieuw proberen…",
"backup_controller_page_albums": "Backup Albums",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Geselecteerd: ",
"backup_controller_page_backup_sub": "Foto's en video's gebackupped",
"backup_controller_page_background_description": "Gebruik achtergrondservice om automatisch nieuwe items te uploaden naar server zonder de app te openen",
"backup_controller_page_background_wifi": "Alleen op WiFi",
"backup_controller_page_background_charging": "Alleen tijdens opladen",
"backup_controller_page_background_is_on": "Automatische achtergrond backup staat aan",
"backup_controller_page_background_is_off": "Automatische achtergrond backup staat uit",
"backup_controller_page_background_turn_on": "Zet achtergrondservice aan",
"backup_controller_page_background_turn_off": "Zet achtergrondservice uit",
"backup_controller_page_background_configure_error": "Achtergrondservice configuratie mislukt",
"backup_controller_page_cancel": "Annuleren",
"backup_controller_page_created": "Gemaakt op: {}",
"backup_controller_page_desc_backup": "Configureer backup om automatisch nieuwe items te uploaden naar server.",
"backup_controller_page_excluded": "Uitgezonderd: ",
"backup_controller_page_failed": "Mislukt ({})",
"backup_controller_page_filename": "Bestandsnaam: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Backup informatie",
"backup_controller_page_none_selected": "Geen geselecteerd",
"backup_controller_page_remainder": "Rest",
"backup_controller_page_remainder_sub": "Overgebleven foto's en video's om te backuppen uit selectie",
"backup_controller_page_select": "Selecteer",
"backup_controller_page_server_storage": "Server Opslag",
"backup_controller_page_start_backup": "Start Backup",
"backup_controller_page_status_off": "Backup staat uit",
"backup_controller_page_status_on": "Backup staat aan",
"backup_controller_page_storage_format": "{} van {} gebruikt",
"backup_controller_page_to_backup": "Albums om te backuppen",
"backup_controller_page_total": "Totaal",
"backup_controller_page_total_sub": "Alle unieke foto's en video's uit geselecteerde albums",
"backup_controller_page_turn_off": "Backup uitzetten",
"backup_controller_page_turn_on": "Backup aanzetten",
"backup_controller_page_uploading_file_info": "Bestandsgegevens uploaden",
"backup_err_only_album": "Kan niet alleen het album verwijderen",
"backup_info_card_assets": "items",
"control_bottom_app_bar_delete": "Verwijderen",
"create_shared_album_page_share": "Delen",
"create_shared_album_page_create": "Aanmaken",
"create_shared_album_page_share_add_assets": "VOEG FOTO'S TOE",
"create_shared_album_page_share_select_photos": "Selecteer Foto's",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat",
"delete_dialog_cancel": "Annuleren",
"delete_dialog_ok": "Verwijderen",
"delete_dialog_title": "Verwijder permanent",
"exif_bottom_sheet_description": "Voeg beschrijving toe...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATIE",
"login_form_button_text": "Login",
"login_form_email_hint": "jouwemail@email.com",
"login_form_endpoint_hint": "http://jouw-server-ip:port/api",
"login_form_endpoint_url": "Server URL",
"login_form_err_http": "Voer http:// of https:// in",
"login_form_err_invalid_email": "Ongeldige Email",
"login_form_err_leading_whitespace": "Spatie aan het begin",
"login_form_err_trailing_whitespace": "Spatie aan het eind",
"login_form_failed_login": "Fout bij inloggen, controleer server url, email en wachtwoord",
"login_form_label_email": "Email",
"login_form_label_password": "Wachtwoord",
"login_form_password_hint": "wachtwoord",
"login_form_save_login": "Ingelogd blijven",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client en Server zijn up-to-date",
"profile_drawer_sign_out": "Uitloggen",
"profile_drawer_settings": "Instellingen",
"search_bar_hint": "Zoek je foto's",
"search_page_no_objects": "Geen object gegevens beschikbaar",
"search_page_no_places": "Geen locatie gegevens beschikbaar",
"search_page_places": "Plaatsen",
"search_page_things": "Dingen",
"search_result_page_new_search_hint": "Nieuw resultaat",
"select_additional_user_for_sharing_page_suggestions": "Suggesties",
"select_user_for_sharing_page_err_album": "Album aanmaken mislukt",
"select_user_for_sharing_page_share_suggestions": "Suggesties",
"share_add": "Toevoegen",
"share_add_photos": "Foto's toevoegen",
"share_add_title": "Titel toevoegen",
"share_create_album": "Album aanmaken",
"share_invite": "Uitnodigen voor album",
"sharing_page_album": "Gedeelde albums",
"sharing_page_description": "Maak gedeelde albums om foto's en video's te delen met mensen in je netwerk.",
"sharing_page_empty_list": "LEGE LIJST",
"sharing_silver_appbar_create_shared_album": "Maak gedeeld album",
"sharing_silver_appbar_share_partner": "Delen met partner",
"tab_controller_nav_photos": "Foto's",
"tab_controller_nav_search": "Zoeken",
"tab_controller_nav_sharing": "Delen",
"tab_controller_nav_library": "Bibliotheek",
"version_announcement_overlay_ack": "Bevestig",
"version_announcement_overlay_release_notes": "release opmerkingen",
"version_announcement_overlay_text_1": "Er is een nieuwe versie beschikbaar van",
"version_announcement_overlay_text_2": "neem je tijd en bezoek de ",
"version_announcement_overlay_text_3": " controleer of je docker-compose en .env up-to-date zijn om te voorkomen dat er misconfiguraties zijn, in het bijzonder als je gebruik maakt van WatchTower of een ander mechanisme dat je server automatisch configureert.",
"version_announcement_overlay_title": "Nieuwe server versie beschikbaar \uD83C\uDF89",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Gedeeld",
"library_page_albums": "Albums",
"library_page_new_album": "Nieuw album",
"create_album_page_untitled": "Naamloos",
"share_dialog_preparing": "Voorbereiden...",
"control_bottom_app_bar_share": "Delen",
"setting_pages_app_bar_settings": "Instellingen",
"theme_setting_theme_title": "Thema",
"theme_setting_theme_subtitle": "Kies de thema instelling van de app",
"theme_setting_system_theme_switch": "Automatisch (volg systeeminstelling)",
"theme_setting_dark_mode_switch": "Donkere modus",
"theme_setting_image_viewer_quality_title": "Foto weergave kwaliteit",
"theme_setting_image_viewer_quality_subtitle": "Pas de kwaliteit aan van de gedetailleerde foto weergave",
"theme_setting_three_stage_loading_title": "Drie-laags laden inschakelen",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
"asset_list_settings_title": "Foto Grid",
"asset_list_settings_subtitle": "Foto grid layout instellingen",
"theme_setting_asset_list_storage_indicator_title": "Laat ruimte indicator zien bij item tegels",
"theme_setting_asset_list_tiles_per_row_title": "Aantal items per rij ({})",
"setting_notifications_title": "Notificaties",
"setting_notifications_subtitle": "Werk je notificatievoorkeuren bij",
"setting_notifications_notify_failures_grace_period": "Melding achtergrond backup fouten: {}",
"setting_notifications_notify_immediately": "meteen",
"setting_notifications_notify_minutes": "{} minuten",
"setting_notifications_notify_hours": "{} uur",
"setting_notifications_notify_never": "nooit"
}

View File

@@ -0,0 +1,139 @@
{
"album_info_card_backup_album_excluded": "EXCLUÍDO",
"album_info_card_backup_album_included": "INCLUÍDO",
"album_viewer_appbar_share_delete": "Excluir álbum",
"album_viewer_appbar_share_err_delete": "Falha ao excluir álbum",
"album_viewer_appbar_share_err_leave": "Falha ao sair do álbum",
"album_viewer_appbar_share_err_remove": "Há problemas ao remover recursos do álbum",
"album_viewer_appbar_share_err_title": "Falha ao alterar o título do álbum",
"album_viewer_appbar_share_leave": "Sair do álbum",
"album_viewer_appbar_share_remove": "Remover do álbum",
"album_viewer_page_share_add_users": "Adicionar usuários",
"backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})",
"backup_album_selection_page_albums_tap": "Toque para incluir, toque duas vezes para excluir",
"backup_album_selection_page_assets_scatter": "Os recursos podem se espalhar por vários álbuns. Assim, os álbuns podem ser incluídos ou excluídos durante o processo de backup.",
"backup_album_selection_page_select_albums": "Selecionar álbuns",
"backup_album_selection_page_selection_info": "Informações da Seleção",
"backup_album_selection_page_total_assets": "Total de recursos exclusivos",
"backup_all": "Todos",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_upload_failure_notification": "Falha ao carregar {}",
"backup_background_service_in_progress_notification": "Fazendo backup de seus ativos…",
"backup_background_service_current_upload_notification": "Enviando {}",
"backup_controller_page_albums": "Álbuns de backup",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selecionado: ",
"backup_controller_page_backup_sub": "Backup de fotos e vídeos",
"backup_controller_page_background_description": "Ative o serviço em segundo plano para fazer backup automático de novos ativos sem precisar abrir o aplicativo",
"backup_controller_page_background_wifi": "Apenas em Wi-Fi",
"backup_controller_page_background_charging": "Apenas durante o carregamento",
"backup_controller_page_background_is_on": "O backup automático em segundo plano está ativado",
"backup_controller_page_background_is_off": "O backup automático em segundo plano está desativado",
"backup_controller_page_background_turn_on": "Ativar o serviço em segundo plano",
"backup_controller_page_background_turn_off": "Desativar o serviço em segundo plano",
"backup_controller_page_background_configure_error": "Falha ao configurar o serviço em segundo plano",
"backup_controller_page_cancel": "Cancelar",
"backup_controller_page_created": "Criado em: {}",
"backup_controller_page_desc_backup": "Ative o backup para carregar automaticamente novos ativos no servidor.",
"backup_controller_page_excluded": "Excluído: ",
"backup_controller_page_failed": "Falhou ({})",
"backup_controller_page_filename": "Nome do arquivo: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informações de backup",
"backup_controller_page_none_selected": "Nenhum selecionado",
"backup_controller_page_remainder": "Restante",
"backup_controller_page_remainder_sub": "Fotos e vídeos restantes para fazer backup da seleção",
"backup_controller_page_select": "Selecionar",
"backup_controller_page_server_storage": "Armazenamento do servidor",
"backup_controller_page_start_backup": "Iniciar backup",
"backup_controller_page_status_off": "O backup está desativado",
"backup_controller_page_status_on": "O backup está ativado",
"backup_controller_page_storage_format": "{} de {} usado",
"backup_controller_page_to_backup": "Álbuns para backup",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "Todas as fotos e vídeos únicos dos álbuns selecionados",
"backup_controller_page_turn_off": "Desativar o backup",
"backup_controller_page_turn_on": "Ativar Backup",
"backup_controller_page_uploading_file_info": "Carregando informações do arquivo",
"backup_err_only_album": "Não é possível remover o único álbum",
"backup_info_card_assets": "ativos",
"control_bottom_app_bar_delete": "Excluir",
"create_shared_album_page_share": "Compartilhar",
"create_shared_album_page_create": "Criar",
"create_shared_album_page_share_add_assets": "ADICIONAR FOTOS",
"create_shared_album_page_share_select_photos": "Selecionar fotos",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Esses itens serão excluídos permanentemente do Immich e do seu dispositivo",
"delete_dialog_cancel": "Cancelar",
"delete_dialog_ok": "Excluir",
"delete_dialog_title": "Excluir permanentemente",
"exif_bottom_sheet_description": "Adicionar descrição...",
"exif_bottom_sheet_details": "DETALHES",
"exif_bottom_sheet_location": "LOCALIZAÇÃO",
"login_form_button_text": "Login",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Please specify http:// or https://",
"login_form_err_invalid_email": "E-mail inválido",
"login_form_err_leading_whitespace": "Leading whitespace",
"login_form_err_trailing_whitespace": "Trailing whitespace",
"login_form_failed_login": "Erro ao fazer login, verifique a url do servidor, e-mail e senha",
"login_form_label_email": "Email",
"login_form_label_password": "Password",
"login_form_password_hint": "password",
"login_form_save_login": "Permaneçer conectado",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Cliente e Servidor estão atualizados",
"profile_drawer_sign_out": "Sair",
"profile_drawer_settings": "Configurações",
"search_bar_hint": "Procurar fotos",
"search_page_no_objects": "Nenhuma informação de objeto disponível",
"search_page_no_places": "Nenhuma informação de lugares disponível",
"search_page_places": "Lugares",
"search_page_things": "Coisas",
"search_result_page_new_search_hint": "Nova pesquisa",
"select_additional_user_for_sharing_page_suggestions": "Sugestões",
"select_user_for_sharing_page_err_album": "Falha ao criar álbum",
"select_user_for_sharing_page_share_suggestions": "Sugestões",
"share_add": "Adicionar",
"share_add_photos": "Adicionar fotos",
"share_add_title": "Adicione um título",
"share_create_album": "Criar álbum",
"share_invite": "Convidar para o álbum",
"sharing_page_album": "Álbuns compartilhados",
"sharing_page_description": "Crie álbuns compartilhados para compartilhar fotos e vídeos com pessoas em sua rede.",
"sharing_page_empty_list": "LISTA VAZIA",
"sharing_silver_appbar_create_shared_album": "Criar álbum compartilhado",
"sharing_silver_appbar_share_partner": "Compartilhe com o parceiro",
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Procurar",
"tab_controller_nav_sharing": "Compartilhamento",
"tab_controller_nav_library": "Biblioteca",
"version_announcement_overlay_ack": "Confirmar",
"version_announcement_overlay_release_notes": "notas de lançamento",
"version_announcement_overlay_text_1": "Oi amigo, há um novo lançamento de",
"version_announcement_overlay_text_2": "reserve um tempo para visitar o ",
"version_announcement_overlay_text_3": " e verifique se a configuração do docker-compose e do .env está atualizada para evitar configurações incorretas, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização automática do aplicativo do servidor.",
"version_announcement_overlay_title": "Nova versão do servidor disponível \uD83C\uDF89",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Compartilhado",
"library_page_albums": "Albums",
"library_page_new_album": "Novo album",
"create_album_page_untitled": "Sem título",
"share_dialog_preparing": "Preparando...",
"control_bottom_app_bar_share": "Compartilhar",
"setting_pages_app_bar_settings": "Configurações",
"theme_setting_theme_title": "Tema",
"theme_setting_theme_subtitle": "Escolha a configuração de tema do app",
"theme_setting_system_theme_switch": "Automático (seguir a configuração do sistema)",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_title": "Qualidade das imagens do visualizador",
"theme_setting_image_viewer_quality_subtitle": "Ajuste a qualidade de imagens detalhadas do visualizador",
"theme_setting_three_stage_loading_title": "Ative o carregamento em três estágios",
"theme_setting_three_stage_loading_subtitle": "O carregamento em três estágios oferece a imagem de melhor qualidade em troca de uma velocidade de carregamento mais lenta"
}

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>

View File

@@ -84,7 +84,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock/ios"
SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a

View File

@@ -360,7 +360,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 52;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -495,7 +495,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 52;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -522,7 +522,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 52;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.21.0</string>
<string>1.27.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>40</string>
<string>52</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
@@ -92,7 +92,10 @@
<string>it</string>
<string>fi</string>
<string>ja</string>
<string>ko</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>
</array>
</dict>
</plist>

View File

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

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000205">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.360401">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.499192">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.012696">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="30.057077">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.378836">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.438506">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="80.023705">
<testcase classname="fastlane.lanes" name="4: build_app" time="91.259106">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="98.18403">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="102.092139">
</testcase>

View File

@@ -19,3 +19,9 @@ const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
// User Setting Info
const String userSettingInfoBox = "immichUserSettingInfoBox";
// Background backup Info
const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
const String backupFailedSince = "immichBackupFailedSince"; // Key 1
const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2
const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3

View File

@@ -11,7 +11,10 @@ const List<Locale> locales = [
Locale('fr', 'FR'),
Locale('it', 'IT'),
Locale('ja', 'JP'),
Locale('pl', 'PL')
Locale('nl', 'NL'),
Locale('pl', 'PL'),
Locale('pt', 'PR'),
Locale('ko', 'KR'),
];
const String translationsPath = 'assets/i18n';

View File

@@ -1,16 +1,25 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
import 'package:transparent_image/transparent_image.dart';
class AlbumThumbnailCard extends StatelessWidget {
const AlbumThumbnailCard({Key? key, required this.album}) : super(key: key);
const AlbumThumbnailCard({
Key? key,
required this.album,
required this.cacheService,
}) : super(key: key);
final AlbumResponseDto album;
final CacheService cacheService;
@override
Widget build(BuildContext context) {
@@ -29,19 +38,19 @@ class AlbumThumbnailCard extends StatelessWidget {
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: FadeInImage(
child: CachedNetworkImage(
cacheManager: cacheService.getCache(CacheType.albumThumbnail),
memCacheHeight: max(400, cardSize.toInt() * 3),
width: cardSize,
height: cardSize,
fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage),
image: NetworkImage(
'${box.get(serverEndpointKey)}/asset/thumbnail/${album.albumThumbnailAssetId}?format=JPEG',
headers: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
),
fadeInDuration: const Duration(milliseconds: 200),
fadeOutDuration: const Duration(milliseconds: 200),
imageUrl:
getAlbumThumbnailUrl(album, type: ThumbnailFormat.JPEG),
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
cacheKey: "${album.albumThumbnailAssetId}",
),
),
Padding(

View File

@@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,22 +9,26 @@ import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class AlbumViewerThumbnail extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
final bool showStorageIndicator;
final BaseCacheManager? cacheManager;
const AlbumViewerThumbnail({
Key? key,
required this.asset,
required this.assetList,
this.cacheManager,
this.showStorageIndicator = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = getThumbnailUrl(asset);
var deviceId = ref.watch(authenticationProvider).deviceId;
@@ -121,7 +126,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
return Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
cacheManager: cacheManager,
cacheKey: asset.id,
width: 300,
height: 300,
memCacheHeight: 200,
@@ -166,7 +172,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
child: Stack(
children: [
_buildThumbnailImage(),
_buildAssetStoreLocationIcon(),
if (showStorageIndicator) _buildAssetStoreLocationIcon(),
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
],

View File

@@ -5,6 +5,8 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class SelectionThumbnailImage extends HookConsumerWidget {
@@ -15,15 +17,14 @@ class SelectionThumbnailImage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
var thumbnailRequestUrl = getThumbnailUrl(asset);
var selectedAsset =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
var newAssetsForAlbum =
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
final cacheService = ref.watch(cacheServiceProvider);
Widget _buildSelectionIcon(AssetResponseDto asset) {
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
@@ -113,7 +114,8 @@ class SelectionThumbnailImage extends HookConsumerWidget {
Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
cacheManager: cacheService.getCache(CacheType.thumbnail),
cacheKey: asset.id,
width: 150,
height: 150,
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,

View File

@@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -15,8 +16,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
final cacheService = ref.watch(cacheServiceProvider);
var box = Hive.box(userInfoBox);
return GestureDetector(
@@ -26,7 +26,8 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
child: Stack(
children: [
CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
cacheManager: cacheService.getCache(CacheType.thumbnail),
cacheKey: asset.id,
width: 500,
height: 500,
memCacheHeight: 500,

View File

@@ -13,7 +13,10 @@ import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
@@ -186,20 +189,28 @@ class AlbumViewerPage extends HookConsumerWidget {
}
Widget _buildImageGrid(AlbumResponseDto albumInfo) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final bool showStorageIndicator =
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
final cacheService = ref.watch(cacheServiceProvider);
if (albumInfo.assets.isNotEmpty) {
return SliverPadding(
padding: const EdgeInsets.only(top: 10.0),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return AlbumViewerThumbnail(
cacheManager: cacheService.getCache(CacheType.thumbnail),
asset: albumInfo.assets[index],
assetList: albumInfo.assets,
showStorageIndicator: showStorageIndicator,
);
},
childCount: albumInfo.assetCount,

View File

@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
class LibraryPage extends HookConsumerWidget {
const LibraryPage({Key? key}) : super(key: key);
@@ -13,6 +14,7 @@ class LibraryPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider);
final cacheService = ref.watch(cacheServiceProvider);
useEffect(
() {
@@ -102,6 +104,7 @@ class LibraryPage extends HookConsumerWidget {
_buildCreateAlbumButton(),
for (var album in albums)
AlbumThumbnailCard(
cacheService: cacheService,
album: album,
),
],

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -8,8 +9,9 @@ import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
import 'package:transparent_image/transparent_image.dart';
class SharingPage extends HookConsumerWidget {
const SharingPage({Key? key}) : super(key: key);
@@ -19,6 +21,7 @@ class SharingPage extends HookConsumerWidget {
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
final CacheService cacheService = ref.watch(cacheServiceProvider);
useEffect(
() {
@@ -32,29 +35,26 @@ class SharingPage extends HookConsumerWidget {
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
String thumbnailUrl = sharedAlbums[index].albumThumbnailAssetId !=
null
? "$thumbnailRequestUrl/${sharedAlbums[index].albumThumbnailAssetId}"
: "https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60";
final album = sharedAlbums[index];
return ListTile(
contentPadding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: FadeInImage(
child: CachedNetworkImage(
width: 60,
height: 60,
memCacheHeight: 200,
fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage),
image: NetworkImage(
thumbnailUrl,
headers: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
),
cacheManager:
cacheService.getCache(CacheType.sharedAlbumThumbnail),
imageUrl: getAlbumThumbnailUrl(album),
cacheKey: album.albumThumbnailAssetId,
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
fadeInDuration: const Duration(milliseconds: 200),
fadeOutDuration: const Duration(milliseconds: 200),
),
),
title: Text(
@@ -95,12 +95,12 @@ class SharingPage extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 5.0, bottom: 5),
Padding(
padding: const EdgeInsets.only(left: 5.0, bottom: 5),
child: Icon(
Icons.offline_share_outlined,
size: 50,
// color: Theme.of(context).primaryColor,
color: Theme.of(context).primaryColor,
),
),
Padding(

View File

@@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:photo_view/photo_view.dart';
enum _RemoteImageStatus { empty, thumbnail, preview, full }
@@ -63,11 +64,13 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
widget.onLoadingCompleted();
}
CachedNetworkImageProvider _authorizedImageProvider(String url) {
CachedNetworkImageProvider _authorizedImageProvider(
String url, String cacheKey, BaseCacheManager? cacheManager) {
return CachedNetworkImageProvider(
url,
headers: {"Authorization": widget.authToken},
cacheKey: url,
cacheKey: cacheKey,
cacheManager: cacheManager,
);
}
@@ -101,8 +104,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}
void _loadImages() {
CachedNetworkImageProvider thumbnailProvider =
_authorizedImageProvider(widget.thumbnailUrl);
CachedNetworkImageProvider thumbnailProvider = _authorizedImageProvider(
widget.thumbnailUrl,
widget.cacheKey,
widget.thumbnailCacheManager,
);
_imageProvider = thumbnailProvider;
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
@@ -115,8 +121,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
);
if (widget.previewUrl != null) {
CachedNetworkImageProvider previewProvider =
_authorizedImageProvider(widget.previewUrl!);
CachedNetworkImageProvider previewProvider = _authorizedImageProvider(
widget.previewUrl!,
"${widget.cacheKey}_previewStage",
widget.previewCacheManager,
);
previewProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(_RemoteImageStatus.preview, previewProvider);
@@ -124,8 +133,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
);
}
CachedNetworkImageProvider fullProvider =
_authorizedImageProvider(widget.imageUrl);
CachedNetworkImageProvider fullProvider = _authorizedImageProvider(
widget.imageUrl,
"${widget.cacheKey}_fullStage",
widget.fullCacheManager,
);
fullProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(_RemoteImageStatus.full, fullProvider);
@@ -153,6 +165,10 @@ class RemotePhotoView extends StatefulWidget {
this.previewUrl,
required this.onLoadingCompleted,
required this.onLoadingStart,
this.thumbnailCacheManager,
this.previewCacheManager,
this.fullCacheManager,
required this.cacheKey,
}) : super(key: key);
final String thumbnailUrl;
@@ -161,6 +177,10 @@ class RemotePhotoView extends StatefulWidget {
final String? previewUrl;
final Function onLoadingCompleted;
final Function onLoadingStart;
final BaseCacheManager? thumbnailCacheManager;
final BaseCacheManager? previewCacheManager;
final BaseCacheManager? fullCacheManager;
final String cacheKey;
final void Function() onSwipeDown;
final void Function() onSwipeUp;

View File

@@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -12,7 +10,7 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
required this.onMoreInfoPressed,
required this.onDownloadPressed,
required this.onSharePressed,
this.loading = false
this.loading = false,
}) : super(key: key);
final AssetResponseDto asset;
@@ -26,6 +24,8 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
double iconSize = 18.0;
return AppBar(
// iconTheme: IconThemeData(color: Colors.grey[100]),
// actionsIconTheme: IconThemeData(color: Colors.grey[100]),
foregroundColor: Colors.grey[100],
toolbarHeight: 60,
backgroundColor: Colors.black,
@@ -33,37 +33,32 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
onPressed: () {
AutoRouter.of(context).pop();
},
icon: const Icon(
icon: Icon(
Icons.arrow_back_ios_new_rounded,
size: 20.0,
color: Colors.grey[200],
),
),
actions: [
if (loading) Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 15.0),
width: iconSize,
height: iconSize,
child: const CircularProgressIndicator(strokeWidth: 2.0),
if (loading)
Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 15.0),
width: iconSize,
height: iconSize,
child: const CircularProgressIndicator(strokeWidth: 2.0),
),
),
) ,
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onDownloadPressed();
},
icon: const Icon(Icons.cloud_download_rounded),
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
log("favorite");
},
icon: asset.isFavorite
? const Icon(Icons.favorite_rounded)
: const Icon(Icons.favorite_border_rounded),
icon: Icon(
Icons.cloud_download_rounded,
color: Colors.grey[200],
),
),
IconButton(
iconSize: iconSize,
@@ -71,7 +66,10 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
onPressed: () {
onSharePressed();
},
icon: const Icon(Icons.share),
icon: Icon(
Icons.share,
color: Colors.grey[200],
),
),
IconButton(
iconSize: iconSize,
@@ -79,7 +77,10 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
onPressed: () {
onMoreInfoPressed();
},
icon: const Icon(Icons.more_horiz_rounded),
icon: Icon(
Icons.more_horiz_rounded,
color: Colors.grey[200],
),
)
],
);

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -40,6 +41,7 @@ class ImageViewerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus;
final cacheService = ref.watch(cacheServiceProvider);
getAssetExif() async {
assetDetail =
@@ -73,6 +75,7 @@ class ImageViewerPage extends HookConsumerWidget {
tag: heroTag,
child: RemotePhotoView(
thumbnailUrl: getThumbnailUrl(asset),
cacheKey: asset.id,
imageUrl: getImageUrl(asset),
previewUrl: threeStageLoading
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
@@ -84,6 +87,12 @@ class ImageViewerPage extends HookConsumerWidget {
onSwipeUp: () => showInfo(),
onLoadingCompleted: onLoadingCompleted,
onLoadingStart: onLoadingStart,
thumbnailCacheManager:
cacheService.getCache(CacheType.thumbnail),
previewCacheManager:
cacheService.getCache(CacheType.imageViewerPreview),
fullCacheManager:
cacheService.getCache(CacheType.imageViewerFull),
),
),
),

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:isolate';
import 'dart:ui' show IsolateNameServer, PluginUtilities;
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@@ -16,6 +17,7 @@ import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dar
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:photo_manager/photo_manager.dart';
@@ -39,6 +41,7 @@ class BackgroundService {
bool _hasLock = false;
SendPort? _waitingIsolate;
ReceivePort? _rp;
bool _errorGracePeriodExceeded = true;
bool get isForegroundInitialized {
return _isForegroundInitialized;
@@ -119,8 +122,8 @@ class BackgroundService {
}
}
/// Opens an activity to let the user disable battery optimizations for Immich
Future<bool> disableBatteryOptimizations() async {
/// Returns `true` if battery optimizations are disabled
Future<bool> isIgnoringBatteryOptimizations() async {
if (!Platform.isAndroid) {
return true;
}
@@ -128,20 +131,16 @@ class BackgroundService {
if (!_isForegroundInitialized) {
await _initialize();
}
final String message =
"backup_background_service_disable_battery_optimizations".tr();
return await _foregroundChannel.invokeMethod(
'disableBatteryOptimizations',
message,
);
return await _foregroundChannel
.invokeMethod('isIgnoringBatteryOptimizations');
} catch (error) {
return false;
}
}
/// Updates the notification shown by the background service
Future<bool> updateNotification({
String title = "Immich",
Future<bool> _updateNotification({
required String title,
String? content,
}) async {
if (!Platform.isAndroid) {
@@ -153,28 +152,44 @@ class BackgroundService {
.invokeMethod('updateNotification', [title, content]);
}
} catch (error) {
debugPrint("[updateNotification] failed to communicate with plugin");
debugPrint("[_updateNotification] failed to communicate with plugin");
}
return Future.value(false);
}
/// Shows a new priority notification
Future<bool> showErrorNotification(
String title,
String content,
) async {
Future<bool> _showErrorNotification({
required String title,
String? content,
String? individualTag,
}) async {
if (!Platform.isAndroid) {
return true;
}
try {
if (_isBackgroundInitialized && _errorGracePeriodExceeded) {
return await _backgroundChannel
.invokeMethod('showError', [title, content, individualTag]);
}
} catch (error) {
debugPrint("[_showErrorNotification] failed to communicate with plugin");
}
return false;
}
Future<bool> _clearErrorNotifications() async {
if (!Platform.isAndroid) {
return true;
}
try {
if (_isBackgroundInitialized) {
return await _backgroundChannel
.invokeMethod('showError', [title, content]);
return await _backgroundChannel.invokeMethod('clearErrorNotifications');
}
} catch (error) {
debugPrint("[showErrorNotification] failed to communicate with plugin");
debugPrint(
"[_clearErrorNotifications] failed to communicate with plugin");
}
return Future.value(false);
return false;
}
/// await to ensure this thread (foreground or background) has exclusive access
@@ -278,7 +293,15 @@ class BackgroundService {
return false;
}
await translationsLoaded;
return await _onAssetsChanged();
final bool ok = await _onAssetsChanged();
if (ok) {
Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
} else if (Hive.box(backgroundBackupInfoBox).get(backupFailedSince) ==
null) {
Hive.box(backgroundBackupInfoBox)
.put(backupFailedSince, DateTime.now());
}
return ok;
} catch (error) {
debugPrint(error.toString());
return false;
@@ -303,6 +326,8 @@ class BackgroundService {
Hive.registerAdapter(HiveBackupAlbumsAdapter());
await Hive.openBox(userInfoBox);
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox(userSettingInfoBox);
await Hive.openBox(backgroundBackupInfoBox);
ApiService apiService = ApiService();
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
@@ -313,23 +338,36 @@ class BackgroundService {
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
if (backupAlbumInfo == null) {
_clearErrorNotifications();
return true;
}
await PhotoManager.setIgnorePermissionCheck(true);
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
if (_canceledBySystem) {
return false;
}
final List<AssetEntity> toUpload =
await backupService.getAssetsToBackup(backupAlbumInfo);
List<AssetEntity> toUpload =
await backupService.buildUploadCandidates(backupAlbumInfo);
try {
toUpload = await backupService.removeAlreadyUploadedAssets(toUpload);
} catch (e) {
_showErrorNotification(
title: "backup_background_service_error_title".tr(),
content: "backup_background_service_connection_failed_message".tr(),
);
return false;
}
if (_canceledBySystem) {
return false;
}
if (toUpload.isEmpty) {
_clearErrorNotifications();
return true;
}
@@ -343,10 +381,16 @@ class BackgroundService {
_onBackupError,
);
if (ok) {
_clearErrorNotifications();
await box.put(
backupInfoKey,
backupAlbumInfo,
);
} else {
_showErrorNotification(
title: "backup_background_service_error_title".tr(),
content: "backup_background_service_backup_failed_message".tr(),
);
}
return ok;
}
@@ -358,20 +402,48 @@ class BackgroundService {
void _onProgress(int sent, int total) {}
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
showErrorNotification(
"backup_background_service_upload_failure_notification"
_showErrorNotification(
title: "Upload failed",
content: "backup_background_service_upload_failure_notification"
.tr(args: [errorAssetInfo.fileName]),
errorAssetInfo.errorMessage,
individualTag: errorAssetInfo.id,
);
}
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
updateNotification(
_updateNotification(
title: "backup_background_service_in_progress_notification".tr(),
content: "backup_background_service_current_upload_notification"
.tr(args: [currentUploadAsset.fileName]),
);
}
bool _isErrorGracePeriodExceeded() {
final int value = AppSettingsService()
.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod);
if (value == 0) {
return true;
} else if (value == 5) {
return false;
}
final DateTime? failedSince =
Hive.box(backgroundBackupInfoBox).get(backupFailedSince);
if (failedSince == null) {
return false;
}
final Duration duration = DateTime.now().difference(failedSince);
if (value == 1) {
return duration > const Duration(minutes: 30);
} else if (value == 2) {
return duration > const Duration(hours: 2);
} else if (value == 3) {
return duration > const Duration(hours: 8);
} else if (value == 4) {
return duration > const Duration(hours: 24);
}
assert(false, "Invalid value");
return true;
}
}
/// entry point called by Kotlin/Java code; needs to be a top-level function

View File

@@ -117,6 +117,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
bool? requireWifi,
bool? requireCharging,
required void Function(String msg) onError,
required void Function() onBatteryInfo,
}) async {
assert(enabled != null || requireWifi != null || requireCharging != null);
if (Platform.isAndroid) {
@@ -131,14 +132,21 @@ class BackupNotifier extends StateNotifier<BackUpState> {
if (state.backgroundBackup) {
if (!wasEnabled) {
await _backgroundService.disableBatteryOptimizations();
if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
onBatteryInfo();
}
}
final bool success = await _backgroundService.stopService() &&
await _backgroundService.startService(
requireUnmetered: state.backupRequireWifi,
requireCharging: state.backupRequireCharging,
);
if (!success) {
if (success) {
await Hive.box(backgroundBackupInfoBox)
.put(backupRequireWifi, state.backupRequireWifi);
await Hive.box(backgroundBackupInfoBox)
.put(backupRequireCharging, state.backupRequireCharging);
} else {
state = state.copyWith(
backgroundBackup: wasEnabled,
backupRequireWifi: wasWifi,
@@ -549,10 +557,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
albums.lastExcludedBackupTime,
);
}
final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
state = state.copyWith(
backupProgress: previous,
selectedBackupAlbums: selectedAlbums,
excludedBackupAlbums: excludedAlbums,
backupRequireWifi: backgroundBox.get(backupRequireWifi),
backupRequireCharging: backgroundBox.get(backupRequireCharging),
);
}
return _resumeBackup();
@@ -590,6 +601,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
} catch (error) {
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
}
try {
if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
await Hive.box(backgroundBackupInfoBox).close();
}
} catch (error) {
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
}
_backgroundService.releaseLock();
}
}

View File

@@ -41,21 +41,8 @@ class BackupService {
}
}
/// Returns all assets to backup from the backup info taking into account the
/// time of the last successfull backup per album
Future<List<AssetEntity>> getAssetsToBackup(
HiveBackupAlbums backupAlbumInfo,
) async {
final List<AssetEntity> candidates =
await _buildUploadCandidates(backupAlbumInfo);
final List<AssetEntity> toUpload = candidates.isEmpty
? []
: await _removeAlreadyUploadedAssets(candidates);
return toUpload;
}
Future<List<AssetEntity>> _buildUploadCandidates(
/// Returns all assets newer than the last successful backup per album
Future<List<AssetEntity>> buildUploadCandidates(
HiveBackupAlbums backupAlbums,
) async {
final filter = FilterOptionGroup(
@@ -147,7 +134,8 @@ class BackupService {
return result;
}
Future<List<AssetEntity>> _removeAlreadyUploadedAssets(
/// Returns a new list of assets not yet uploaded
Future<List<AssetEntity>> removeAlreadyUploadedAssets(
List<AssetEntity> candidates,
) async {
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);

View File

@@ -14,6 +14,7 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:url_launcher/url_launcher.dart';
class BackupControllerPage extends HookConsumerWidget {
const BackupControllerPage({Key? key}) : super(key: key);
@@ -156,6 +157,46 @@ class BackupControllerPage extends HookConsumerWidget {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
void _showBatteryOptimizationInfoToUser() {
final buttonTextColor = Theme.of(context).primaryColor;
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
'backup_controller_page_background_battery_info_title',
).tr(),
content: SingleChildScrollView(
child: const Text(
'backup_controller_page_background_battery_info_message',
).tr(),
),
actions: [
TextButton(
onPressed: () => launchUrl(
Uri.parse('https://dontkillmyapp.com'),
mode: LaunchMode.externalApplication),
child: Text(
"backup_controller_page_background_battery_info_link",
style: TextStyle(color: buttonTextColor),
).tr(),
),
TextButton(
child: Text(
'backup_controller_page_background_battery_info_ok',
style: TextStyle(color: buttonTextColor),
).tr(),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
ListTile _buildBackgroundBackupController() {
final bool isBackgroundEnabled = backupState.backgroundBackup;
final bool isWifiRequired = backupState.backupRequireWifi;
@@ -197,6 +238,7 @@ class BackupControllerPage extends HookConsumerWidget {
.configureBackgroundBackup(
requireWifi: isChecked,
onError: _showErrorToUser,
onBatteryInfo: _showBatteryOptimizationInfoToUser,
)
: null,
),
@@ -217,6 +259,7 @@ class BackupControllerPage extends HookConsumerWidget {
.configureBackgroundBackup(
requireCharging: isChecked,
onError: _showErrorToUser,
onBatteryInfo: _showBatteryOptimizationInfoToUser,
)
: null,
),
@@ -225,6 +268,7 @@ class BackupControllerPage extends HookConsumerWidget {
ref.read(backupProvider.notifier).configureBackgroundBackup(
enabled: !isBackgroundEnabled,
onError: _showErrorToUser,
onBatteryInfo: _showBatteryOptimizationInfoToUser,
),
child: Text(
isBackgroundEnabled

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
import 'package:openapi/api.dart';
@@ -7,11 +8,17 @@ import 'package:openapi/api.dart';
class ImageGrid extends ConsumerWidget {
final List<AssetResponseDto> assetGroup;
final List<AssetResponseDto> sortedAssetGroup;
final int tilesPerRow;
final bool showStorageIndicator;
final BaseCacheManager? cacheManager;
ImageGrid({
Key? key,
required this.assetGroup,
required this.sortedAssetGroup,
this.cacheManager,
this.tilesPerRow = 4,
this.showStorageIndicator = true,
}) : super(key: key);
List<AssetResponseDto> imageSortedList = [];
@@ -19,8 +26,8 @@ class ImageGrid extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: tilesPerRow,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
@@ -32,8 +39,10 @@ class ImageGrid extends ConsumerWidget {
child: Stack(
children: [
ThumbnailImage(
cacheManager: cacheManager,
asset: assetGroup[index],
assetList: sortedAssetGroup,
showStorageIndicator: showStorageIndicator,
),
if (assetType != AssetTypeEnum.IMAGE)
Positioned(

View File

@@ -42,9 +42,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
top: 5,
child: IconButton(
splashRadius: 25,
icon: const Icon(
icon: Icon(
Icons.face_outlined,
size: 30,
color: Theme.of(context).primaryColor,
),
onPressed: () {
Scaffold.of(context).openDrawer();
@@ -109,7 +110,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
splashRadius: 25,
iconSize: 30,
icon: isEnableAutoBackup
? const Icon(Icons.backup_rounded)
? Icon(
Icons.backup_rounded,
color: Theme.of(context).primaryColor,
)
: Badge(
padding: const EdgeInsets.all(4),
elevation: 3,
@@ -120,7 +124,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
size: 8,
color: Colors.indigo,
),
child: const Icon(Icons.backup_rounded),
child: Icon(
Icons.backup_rounded,
color: Theme.of(context).primaryColor,
),
),
onPressed: () async {
var onPop = await AutoRouter.of(context)

View File

@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -15,14 +16,19 @@ import 'package:openapi/api.dart';
class ThumbnailImage extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
final bool showStorageIndicator;
final BaseCacheManager? cacheManager;
const ThumbnailImage({Key? key, required this.asset, required this.assetList})
: super(key: key);
const ThumbnailImage({
Key? key,
required this.asset,
required this.assetList,
this.cacheManager,
this.showStorageIndicator = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = getThumbnailUrl(asset);
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
@@ -89,7 +95,8 @@ class ThumbnailImage extends HookConsumerWidget {
: const Border(),
),
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
cacheKey: asset.id,
cacheManager: cacheManager,
width: 300,
height: 300,
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400,
@@ -123,17 +130,18 @@ class ThumbnailImage extends HookConsumerWidget {
child: _buildSelectionIcon(asset),
),
),
Positioned(
right: 10,
bottom: 5,
child: Icon(
(deviceId != asset.deviceId)
? Icons.cloud_done_outlined
: Icons.photo_library_rounded,
color: Colors.white,
size: 18,
),
)
if (showStorageIndicator)
Positioned(
right: 10,
bottom: 5,
child: Icon(
(deviceId != asset.deviceId)
? Icons.cloud_done_outlined
: Icons.photo_library_rounded,
color: Colors.white,
size: 18,
),
)
],
),
),

View File

@@ -10,10 +10,13 @@ import 'package:immich_mobile/modules/home/ui/image_grid.dart';
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:openapi/api.dart';
class HomePage extends HookConsumerWidget {
@@ -21,6 +24,9 @@ class HomePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final cacheService = ref.watch(cacheServiceProvider);
ScrollController scrollController = useScrollController();
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
List<Widget> imageGridGroup = [];
@@ -61,35 +67,46 @@ class HomePage extends HookConsumerWidget {
int? lastMonth;
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
DateTime parseDateGroup = DateTime.parse(dateGroup);
int currentMonth = parseDateGroup.month;
try {
DateTime parseDateGroup = DateTime.parse(dateGroup);
int currentMonth = parseDateGroup.month;
if (lastMonth != null) {
if (currentMonth - lastMonth! != 0) {
imageGridGroup.add(
MonthlyTitleText(
isoDate: dateGroup,
),
);
if (lastMonth != null) {
if (currentMonth - lastMonth! != 0) {
imageGridGroup.add(
MonthlyTitleText(
isoDate: dateGroup,
),
);
}
}
imageGridGroup.add(
DailyTitleText(
key: Key('${dateGroup.toString()}title'),
isoDate: dateGroup,
assetGroup: immichAssetList,
),
);
imageGridGroup.add(
ImageGrid(
cacheManager: cacheService.getCache(CacheType.thumbnail),
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
tilesPerRow:
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
showStorageIndicator: appSettingService
.getSetting(AppSettingsEnum.storageIndicator),
),
);
lastMonth = currentMonth;
} catch (e) {
debugPrint(
"[ERROR] Cannot parse $dateGroup - Wrong create date format : ${immichAssetList.map((asset) => asset.createdAt).toList()}",
);
}
imageGridGroup.add(
DailyTitleText(
key: Key('${dateGroup.toString()}title'),
isoDate: dateGroup,
assetGroup: immichAssetList,
),
);
imageGridGroup.add(
ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
),
);
lastMonth = currentMonth;
});
}

View File

@@ -1,11 +1,21 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
enum AppSettingsEnum {
threeStageLoading, // true, false,
themeMode, // "light","dark","system"
enum AppSettingsEnum<T> {
threeStageLoading<bool>("threeStageLoading", false),
themeMode<String>("themeMode", "system"), // "light","dark","system"
tilesPerRow<int>("tilesPerRow", 4),
uploadErrorNotificationGracePeriod<int>(
"uploadErrorNotificationGracePeriod", 2),
storageIndicator<bool>("storageIndicator", true),
thumbnailCacheSize<int>("thumbnailCacheSize", 10000),
imageCacheSize<int>("imageCacheSize", 350),
albumThumbnailCacheSize<int>("albumThumbnailCacheSize", 200);
const AppSettingsEnum(this.hiveKey, this.defaultValue);
final String hiveKey;
final T defaultValue;
}
class AppSettingsService {
@@ -15,63 +25,26 @@ class AppSettingsService {
hiveBox = Hive.box(userSettingInfoBox);
}
T getSetting<T>(AppSettingsEnum settingType) {
var settingKey = _settingHiveBoxKeyLookup(settingType);
if (!hiveBox.containsKey(settingKey)) {
T defaultSetting = _setDefaultSetting(settingType);
return defaultSetting;
T getSetting<T>(AppSettingsEnum<T> settingType) {
if (!hiveBox.containsKey(settingType.hiveKey)) {
return _setDefault(settingType);
}
var result = hiveBox.get(settingKey);
var result = hiveBox.get(settingType.hiveKey);
if (result is T) {
return result;
} else {
debugPrint("Incorrect setting type");
throw TypeError();
if (result is! T) {
return _setDefault(settingType);
}
return result;
}
setSetting<T>(AppSettingsEnum settingType, T value) {
var settingKey = _settingHiveBoxKeyLookup(settingType);
if (hiveBox.containsKey(settingKey)) {
var result = hiveBox.get(settingKey);
if (result is! T) {
debugPrint("Incorrect setting type");
throw TypeError();
}
hiveBox.put(settingKey, value);
} else {
hiveBox.put(settingKey, value);
}
setSetting<T>(AppSettingsEnum<T> settingType, T value) {
hiveBox.put(settingType.hiveKey, value);
}
_setDefaultSetting(AppSettingsEnum settingType) {
var settingKey = _settingHiveBoxKeyLookup(settingType);
// Default value of threeStageLoading is false
if (settingType == AppSettingsEnum.threeStageLoading) {
hiveBox.put(settingKey, false);
return false;
}
// Default value of themeMode is "light"
if (settingType == AppSettingsEnum.themeMode) {
hiveBox.put(settingKey, "system");
return "system";
}
}
String _settingHiveBoxKeyLookup(AppSettingsEnum settingType) {
switch (settingType) {
case AppSettingsEnum.threeStageLoading:
return 'threeStageLoading';
case AppSettingsEnum.themeMode:
return 'themeMode';
}
T _setDefault<T>(AppSettingsEnum<T> settingType) {
hiveBox.put(settingType.hiveKey, settingType.defaultValue);
return settingType.defaultValue;
}
}

View File

@@ -0,0 +1,33 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart';
import 'asset_list_tiles_per_row.dart';
class AssetListSettings extends StatelessWidget {
const AssetListSettings({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ExpansionTile(
textColor: Theme.of(context).primaryColor,
title: const Text(
'asset_list_settings_title',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
'asset_list_settings_subtitle',
style: TextStyle(
fontSize: 13,
),
).tr(),
children: const [
TilesPerRow(),
StorageIndicator(),
],
);
}
}

View File

@@ -0,0 +1,48 @@
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/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
class StorageIndicator extends HookConsumerWidget {
const StorageIndicator({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final showStorageIndicator = useState(true);
void switchChanged(bool value) {
appSettingService.setSetting(AppSettingsEnum.storageIndicator, value);
showStorageIndicator.value = value;
ref.invalidate(assetGroupByDateTimeProvider);
}
useEffect(
() {
showStorageIndicator.value = appSettingService.getSetting<bool>(AppSettingsEnum.storageIndicator);
return null;
},
[],
);
return SwitchListTile.adaptive(
activeColor: Theme.of(context).primaryColor,
title: const Text(
"theme_setting_asset_list_storage_indicator_title",
style: TextStyle(
fontSize: 12,
),
).tr(),
onChanged: switchChanged,
value: showStorageIndicator.value,
);
}
}

View File

@@ -0,0 +1,64 @@
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/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
class TilesPerRow extends HookConsumerWidget {
const TilesPerRow({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final itemsValue = useState(4.0);
void sliderChanged(double value) {
appSettingService.setSetting(AppSettingsEnum.tilesPerRow, value.toInt());
itemsValue.value = value;
}
void sliderChangedEnd(double _) {
ref.invalidate(assetGroupByDateTimeProvider);
}
useEffect(
() {
int tilesPerRow =
appSettingService.getSetting(AppSettingsEnum.tilesPerRow);
itemsValue.value = tilesPerRow.toDouble();
return null;
},
[],
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: const Text(
"theme_setting_asset_list_tiles_per_row_title",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(args: ["${itemsValue.value.toInt()}"]),
),
Slider(
onChangeEnd: sliderChangedEnd,
onChanged: sliderChanged,
value: itemsValue.value,
min: 2,
max: 6,
divisions: 4,
label: "${itemsValue.value.toInt()}",
activeColor: Theme.of(context).primaryColor,
),
],
);
}
}

View File

@@ -0,0 +1,151 @@
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/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/modules/settings/ui/cache_settings/cache_settings_slider_pref.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
class CacheSettings extends HookConsumerWidget {
const CacheSettings({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final CacheService cacheService = ref.watch(cacheServiceProvider);
final clearCacheState = useState(false);
Future<void> clearCache() async {
await cacheService.emptyAllCaches();
clearCacheState.value = true;
}
Widget cacheStatisticsRow(String name, CacheType type) {
final cacheSize = useState(0);
final cacheAssets = useState(0);
if (!clearCacheState.value) {
final repo = cacheService.getCacheRepo(type);
repo.open().then((_) {
cacheSize.value = repo.getCacheSize();
cacheAssets.value = repo.getNumberOfCachedObjects();
});
} else {
cacheSize.value = 0;
cacheAssets.value = 0;
}
return Container(
margin: const EdgeInsets.only(left: 20, bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const Text(
"cache_settings_statistics_assets",
style: TextStyle(color: Colors.grey),
).tr(
args: ["${cacheAssets.value}", formatBytes(cacheSize.value)],
),
],
),
);
}
return ExpansionTile(
expandedCrossAxisAlignment: CrossAxisAlignment.start,
textColor: Theme.of(context).primaryColor,
title: const Text(
'cache_settings_title',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
'cache_settings_subtitle',
style: TextStyle(
fontSize: 13,
),
).tr(),
children: [
const CacheSettingsSliderPref(
setting: AppSettingsEnum.thumbnailCacheSize,
translationKey: "cache_settings_thumbnail_size",
min: 1000,
max: 20000,
divisions: 19,
),
const CacheSettingsSliderPref(
setting: AppSettingsEnum.imageCacheSize,
translationKey: "cache_settings_image_cache_size",
min: 0,
max: 1000,
divisions: 20,
),
const CacheSettingsSliderPref(
setting: AppSettingsEnum.albumThumbnailCacheSize,
translationKey: "cache_settings_album_thumbnails",
min: 0,
max: 1000,
divisions: 20,
),
ListTile(
title: const Text(
"cache_settings_statistics_title",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(),
),
cacheStatisticsRow(
"cache_settings_statistics_thumbnail".tr(),
CacheType.thumbnail,
),
cacheStatisticsRow(
"cache_settings_statistics_album".tr(),
CacheType.albumThumbnail,
),
cacheStatisticsRow(
"cache_settings_statistics_shared".tr(),
CacheType.sharedAlbumThumbnail,
),
cacheStatisticsRow(
"cache_settings_statistics_full".tr(),
CacheType.imageViewerFull,
),
ListTile(
title: const Text(
"cache_settings_clear_cache_button_title",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(),
),
Container(
alignment: Alignment.center,
child: ElevatedButton(
onPressed: clearCache,
child: const Text(
"cache_settings_clear_cache_button",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
).tr(),
),
)
],
);
}
}

View File

@@ -0,0 +1,63 @@
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/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
class CacheSettingsSliderPref extends HookConsumerWidget {
final AppSettingsEnum<int> setting;
final String translationKey;
final int min;
final int max;
final int divisions;
const CacheSettingsSliderPref({
Key? key,
required this.setting,
required this.translationKey,
required this.min,
required this.max,
required this.divisions,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final itemsValue = useState(appSettingService.getSetting<int>(setting));
void sliderChanged(double value) {
itemsValue.value = value.toInt();
}
void sliderChangedEnd(double value) {
appSettingService.setSetting(setting, value.toInt());
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(
translationKey,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(args: ["${itemsValue.value.toInt()}"]),
),
Slider(
onChangeEnd: sliderChangedEnd,
onChanged: sliderChanged,
value: itemsValue.value.toDouble(),
min: min.toDouble(),
max: max.toDouble(),
divisions: divisions,
label: "${itemsValue.value.toInt()}",
activeColor: Theme.of(context).primaryColor,
),
],
);
}
}

View File

@@ -36,6 +36,7 @@ class ThreeStageLoading extends HookConsumerWidget {
}
return SwitchListTile.adaptive(
activeColor: Theme.of(context).primaryColor,
title: const Text(
"theme_setting_three_stage_loading_title",
style: TextStyle(

View File

@@ -0,0 +1,82 @@
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/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
class NotificationSetting extends HookConsumerWidget {
const NotificationSetting({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final sliderValue = useState(0.0);
useEffect(
() {
sliderValue.value = appSettingService
.getSetting<int>(AppSettingsEnum.uploadErrorNotificationGracePeriod)
.toDouble();
return null;
},
[],
);
final String formattedValue = _formatSliderValue(sliderValue.value);
return ExpansionTile(
textColor: Theme.of(context).primaryColor,
title: const Text(
'setting_notifications_title',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
'setting_notifications_subtitle',
style: TextStyle(
fontSize: 13,
),
).tr(),
children: [
ListTile(
isThreeLine: false,
dense: true,
title: const Text(
'setting_notifications_notify_failures_grace_period',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(args: [formattedValue]),
subtitle: Slider(
value: sliderValue.value,
onChanged: (double v) => sliderValue.value = v,
onChangeEnd: (double v) => appSettingService.setSetting(
AppSettingsEnum.uploadErrorNotificationGracePeriod, v.toInt()),
max: 5.0,
divisions: 5,
label: formattedValue,
activeColor: Theme.of(context).primaryColor,
),
),
],
);
}
}
String _formatSliderValue(double v) {
if (v == 0.0) {
return 'setting_notifications_notify_immediately'.tr();
} else if (v == 1.0) {
return 'setting_notifications_notify_minutes'.tr(args: const ['30']);
} else if (v == 2.0) {
return 'setting_notifications_notify_hours'.tr(args: const ['2']);
} else if (v == 3.0) {
return 'setting_notifications_notify_hours'.tr(args: const ['8']);
} else if (v == 4.0) {
return 'setting_notifications_notify_hours'.tr(args: const ['24']);
} else {
return 'setting_notifications_notify_never'.tr();
}
}

View File

@@ -1,7 +1,12 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/modules/settings/ui/cache_settings/cache_settings.dart';
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
class SettingsPage extends HookConsumerWidget {
@@ -36,6 +41,9 @@ class SettingsPage extends HookConsumerWidget {
tiles: [
const ImageViewerQualitySetting(),
const ThemeSetting(),
const AssetListSettings(),
const CacheSettings(),
if (Platform.isAndroid) const NotificationSetting(),
],
).toList(),
],

View File

@@ -0,0 +1,79 @@
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/utils/immich_cache_info_repository.dart';
enum CacheType {
// Shared cache for asset thumbnails in various modules
thumbnail,
imageViewerPreview,
imageViewerFull,
albumThumbnail,
sharedAlbumThumbnail;
}
final cacheServiceProvider = Provider(
(ref) => CacheService(ref.watch(appSettingsServiceProvider)),
);
class CacheService {
final AppSettingsService _settingsService;
final _cacheRepositoryInstances = <CacheType, ImmichCacheRepository>{};
CacheService(this._settingsService);
BaseCacheManager getCache(CacheType type) {
return _getDefaultCache(
type.name,
_getCacheSize(type) + 1,
getCacheRepo(type),
);
}
ImmichCacheRepository getCacheRepo(CacheType type) {
if (!_cacheRepositoryInstances.containsKey(type)) {
final repo = ImmichCacheInfoRepository(
"cache_${type.name}",
"cacheKeys_${type.name}",
);
_cacheRepositoryInstances[type] = repo;
}
return _cacheRepositoryInstances[type]!;
}
Future<void> emptyAllCaches() async {
for (var type in CacheType.values) {
await getCache(type).emptyCache();
}
}
int _getCacheSize(CacheType type) {
switch (type) {
case CacheType.thumbnail:
return _settingsService.getSetting(AppSettingsEnum.thumbnailCacheSize);
case CacheType.imageViewerPreview:
case CacheType.imageViewerFull:
return _settingsService.getSetting(AppSettingsEnum.imageCacheSize);
case CacheType.sharedAlbumThumbnail:
case CacheType.albumThumbnail:
return _settingsService
.getSetting(AppSettingsEnum.albumThumbnailCacheSize);
default:
return 200;
}
}
BaseCacheManager _getDefaultCache(
String cacheName, int size, CacheInfoRepository repo) {
return CacheManager(
Config(
cacheName,
maxNrOfCacheObjects: size,
repo: repo,
),
);
}
}

View File

@@ -0,0 +1,15 @@
String formatBytes(int bytes) {
if (bytes < 1000) {
return "$bytes B";
} else if (bytes < 1000000) {
final kb = (bytes / 1000).toStringAsFixed(1);
return "$kb kB";
} else if (bytes < 1000000000) {
final mb = (bytes / 1000000).toStringAsFixed(1);
return "$mb MB";
} else {
final gb = (bytes / 1000000000).toStringAsFixed(1);
return "$gb GB";
}
}

View File

@@ -17,6 +17,9 @@ class FileHelper {
case 'png':
return {"type": "image", "subType": "png"};
case 'tif':
return {"type": "image", "subType": "tiff"};
case 'mov':
return {"type": "video", "subType": "quicktime"};
@@ -38,6 +41,9 @@ class FileHelper {
case 'webp':
return {"type": "image", "subType": "webp"};
case '3gp':
return {"type": "video", "subType": "3gpp"};
default:
return {"type": "unsupport", "subType": "unsupport"};
}

View File

@@ -3,14 +3,31 @@ import 'package:openapi/api.dart';
import '../constants/hive_box.dart';
String getThumbnailUrl(final AssetResponseDto asset,
{ThumbnailFormat type = ThumbnailFormat.WEBP}) {
final box = Hive.box(userInfoBox);
String getThumbnailUrl(
final AssetResponseDto asset, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {
return _getThumbnailUrl(asset.id, type: type);
}
return '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}?format=${type.value}';
String getAlbumThumbnailUrl(
final AlbumResponseDto album, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {
if (album.albumThumbnailAssetId == null) {
return '';
}
return _getThumbnailUrl(album.albumThumbnailAssetId!, type: type);
}
String getImageUrl(final AssetResponseDto asset) {
final box = Hive.box(userInfoBox);
return '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false';
}
String _getThumbnailUrl(final String id,
{ThumbnailFormat type = ThumbnailFormat.WEBP}) {
final box = Hive.box(userInfoBox);
return '${box.get(serverEndpointKey)}/asset/thumbnail/${id}?format=${type.value}';
}

View File

@@ -0,0 +1,204 @@
import 'dart:io';
import 'dart:math';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_cache_manager/src/storage/cache_object.dart';
import 'package:hive/hive.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
// Implementation of a CacheInfoRepository based on Hive
abstract class ImmichCacheRepository extends CacheInfoRepository {
int getNumberOfCachedObjects();
int getCacheSize();
}
class ImmichCacheInfoRepository extends ImmichCacheRepository {
final String hiveBoxName;
final String keyLookupHiveBoxName;
// To circumvent some of the limitations of a non-relational key-value database,
// we use two hive boxes per cache.
// [cacheObjectLookupBox] maps ids to cache objects.
// [keyLookupHiveBox] maps keys to ids.
// The lookup of a cache object by key therefore involves two steps:
// id = keyLookupHiveBox[key]
// object = cacheObjectLookupBox[id]
late Box<Map<dynamic, dynamic>> cacheObjectLookupBox;
late Box<int> keyLookupHiveBox;
ImmichCacheInfoRepository(this.hiveBoxName, this.keyLookupHiveBoxName);
@override
Future<bool> close() async {
await cacheObjectLookupBox.close();
return true;
}
@override
Future<int> delete(int id) async {
if (cacheObjectLookupBox.containsKey(id)) {
await cacheObjectLookupBox.delete(id);
return 1;
}
return 0;
}
@override
Future<int> deleteAll(Iterable<int> ids) async {
int deleted = 0;
for (var id in ids) {
if (cacheObjectLookupBox.containsKey(id)) {
deleted++;
await cacheObjectLookupBox.delete(id);
}
}
return deleted;
}
@override
Future<void> deleteDataFile() async {
await cacheObjectLookupBox.clear();
await keyLookupHiveBox.clear();
}
@override
Future<bool> exists() async {
return cacheObjectLookupBox.isNotEmpty && keyLookupHiveBox.isNotEmpty;
}
@override
Future<CacheObject?> get(String key) async {
if (!keyLookupHiveBox.containsKey(key)) {
return null;
}
int id = keyLookupHiveBox.get(key)!;
if (!cacheObjectLookupBox.containsKey(id)) {
keyLookupHiveBox.delete(key);
return null;
}
return _deserialize(cacheObjectLookupBox.get(id)!);
}
@override
Future<List<CacheObject>> getAllObjects() async {
return cacheObjectLookupBox.values.map(_deserialize).toList();
}
@override
Future<List<CacheObject>> getObjectsOverCapacity(int capacity) async {
if (cacheObjectLookupBox.length <= capacity) {
return List.empty();
}
var values = cacheObjectLookupBox.values.map(_deserialize).toList();
values.sort((CacheObject a, CacheObject b) {
final aTouched = a.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
final bTouched = b.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
return aTouched.compareTo(bTouched);
});
return values.skip(capacity).take(10).toList();
}
@override
Future<List<CacheObject>> getOldObjects(Duration maxAge) async {
return cacheObjectLookupBox.values
.map(_deserialize)
.where((CacheObject element) {
DateTime touched =
element.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
return touched.isBefore(DateTime.now().subtract(maxAge));
}).toList();
}
@override
Future<CacheObject> insert(CacheObject cacheObject,
{bool setTouchedToNow = true}) async {
int newId = keyLookupHiveBox.length == 0
? 0
: keyLookupHiveBox.values.reduce(max) + 1;
cacheObject = cacheObject.copyWith(id: newId);
keyLookupHiveBox.put(cacheObject.key, newId);
cacheObjectLookupBox.put(newId, cacheObject.toMap());
return cacheObject;
}
@override
Future<bool> open() async {
cacheObjectLookupBox = await Hive.openBox(hiveBoxName);
keyLookupHiveBox = await Hive.openBox(keyLookupHiveBoxName);
// The cache might have cleared by the operating system.
// This could create inconsistencies between the file system cache and database.
// To check whether the cache was cleared, a file within the cache directory
// is created for each database. If the file is absent, the cache was cleared and therefore
// the database has to be cleared as well.
if (!await _checkAndCreateAnchorFile()) {
await cacheObjectLookupBox.clear();
await keyLookupHiveBox.clear();
}
return cacheObjectLookupBox.isOpen;
}
@override
Future<int> update(CacheObject cacheObject,
{bool setTouchedToNow = true}) async {
if (cacheObject.id != null) {
cacheObjectLookupBox.put(cacheObject.id, cacheObject.toMap());
return 1;
}
return 0;
}
@override
Future updateOrInsert(CacheObject cacheObject) {
if (cacheObject.id == null) {
return insert(cacheObject);
} else {
return update(cacheObject);
}
}
@override
int getNumberOfCachedObjects() {
return cacheObjectLookupBox.length;
}
@override
int getCacheSize() {
final cacheElementsWithSize =
cacheObjectLookupBox.values.map(_deserialize).map((e) => e.length ?? 0);
if (cacheElementsWithSize.isEmpty) {
return 0;
}
return cacheElementsWithSize.reduce((value, element) => value + element);
}
CacheObject _deserialize(Map serData) {
Map<String, dynamic> converted = {};
serData.forEach((key, value) {
converted[key.toString()] = value;
});
return CacheObject.fromMap(converted);
}
Future<bool> _checkAndCreateAnchorFile() async {
final tmpDir = await getTemporaryDirectory();
final cacheFile = File(p.join(tmpDir.path, "$hiveBoxName.tmp"));
if (await cacheFile.exists()) {
return true;
}
await cacheFile.create();
return false;
}
}

View File

@@ -8,6 +8,8 @@ doc/AdminSignupResponseDto.md
doc/AlbumApi.md
doc/AlbumResponseDto.md
doc/AssetApi.md
doc/AssetCountByTimeBucket.md
doc/AssetCountByTimeBucketResponseDto.md
doc/AssetFileUploadResponseDto.md
doc/AssetResponseDto.md
doc/AssetTypeEnum.md
@@ -27,6 +29,8 @@ doc/DeviceInfoApi.md
doc/DeviceInfoResponseDto.md
doc/DeviceTypeEnum.md
doc/ExifResponseDto.md
doc/GetAssetByTimeBucketDto.md
doc/GetAssetCountByTimeBucketDto.md
doc/LoginCredentialDto.md
doc/LoginResponseDto.md
doc/LogoutResponseDto.md
@@ -39,6 +43,7 @@ doc/ServerVersionReponseDto.md
doc/SignUpDto.md
doc/SmartInfoResponseDto.md
doc/ThumbnailFormat.md
doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md
doc/UpdateDeviceInfoDto.md
doc/UpdateUserDto.md
@@ -66,6 +71,8 @@ lib/model/add_assets_dto.dart
lib/model/add_users_dto.dart
lib/model/admin_signup_response_dto.dart
lib/model/album_response_dto.dart
lib/model/asset_count_by_time_bucket.dart
lib/model/asset_count_by_time_bucket_response_dto.dart
lib/model/asset_file_upload_response_dto.dart
lib/model/asset_response_dto.dart
lib/model/asset_type_enum.dart
@@ -83,6 +90,8 @@ lib/model/delete_asset_status.dart
lib/model/device_info_response_dto.dart
lib/model/device_type_enum.dart
lib/model/exif_response_dto.dart
lib/model/get_asset_by_time_bucket_dto.dart
lib/model/get_asset_count_by_time_bucket_dto.dart
lib/model/login_credential_dto.dart
lib/model/login_response_dto.dart
lib/model/logout_response_dto.dart
@@ -94,6 +103,7 @@ lib/model/server_version_reponse_dto.dart
lib/model/sign_up_dto.dart
lib/model/smart_info_response_dto.dart
lib/model/thumbnail_format.dart
lib/model/time_group_enum.dart
lib/model/update_album_dto.dart
lib/model/update_device_info_dto.dart
lib/model/update_user_dto.dart

View File

@@ -79,10 +79,12 @@ Class | Method | HTTP request | Description
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download |
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/searchTerm |
*AssetApi* | [**getAssetByTimeBucket**](doc//AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket |
*AssetApi* | [**getAssetCountByTimeBucket**](doc//AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket |
*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/allLocation |
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/allObjects |
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file |
@@ -112,6 +114,8 @@ Class | Method | HTTP request | Description
- [AddUsersDto](doc//AddUsersDto.md)
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
- [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
- [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
- [AssetResponseDto](doc//AssetResponseDto.md)
- [AssetTypeEnum](doc//AssetTypeEnum.md)
@@ -129,6 +133,8 @@ Class | Method | HTTP request | Description
- [DeviceInfoResponseDto](doc//DeviceInfoResponseDto.md)
- [DeviceTypeEnum](doc//DeviceTypeEnum.md)
- [ExifResponseDto](doc//ExifResponseDto.md)
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
- [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
- [LoginCredentialDto](doc//LoginCredentialDto.md)
- [LoginResponseDto](doc//LoginResponseDto.md)
- [LogoutResponseDto](doc//LogoutResponseDto.md)
@@ -140,6 +146,7 @@ Class | Method | HTTP request | Description
- [SignUpDto](doc//SignUpDto.md)
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
- [ThumbnailFormat](doc//ThumbnailFormat.md)
- [TimeGroupEnum](doc//TimeGroupEnum.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md)

View File

@@ -259,7 +259,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAllAlbums**
> List<AlbumResponseDto> getAllAlbums(shared)
> List<AlbumResponseDto> getAllAlbums(shared, assetId)
@@ -275,9 +275,10 @@ import 'package:openapi/api.dart';
final api_instance = AlbumApi();
final shared = true; // bool |
final assetId = assetId_example; // String | Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
try {
final result = api_instance.getAllAlbums(shared);
final result = api_instance.getAllAlbums(shared, assetId);
print(result);
} catch (e) {
print('Exception when calling AlbumApi->getAllAlbums: $e\n');
@@ -289,6 +290,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**shared** | **bool**| | [optional]
**assetId** | **String**| Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums | [optional]
### Return type

View File

@@ -14,10 +14,12 @@ Method | HTTP request | Description
[**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download |
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/searchTerm |
[**getAssetByTimeBucket**](AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket |
[**getAssetCountByTimeBucket**](AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket |
[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
[**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/allLocation |
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/allObjects |
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file |
@@ -267,6 +269,100 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAssetByTimeBucket**
> List<AssetResponseDto> getAssetByTimeBucket(getAssetByTimeBucketDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final getAssetByTimeBucketDto = GetAssetByTimeBucketDto(); // GetAssetByTimeBucketDto |
try {
final result = api_instance.getAssetByTimeBucket(getAssetByTimeBucketDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getAssetByTimeBucket: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**getAssetByTimeBucketDto** | [**GetAssetByTimeBucketDto**](GetAssetByTimeBucketDto.md)| |
### Return type
[**List<AssetResponseDto>**](AssetResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAssetCountByTimeBucket**
> AssetCountByTimeBucketResponseDto getAssetCountByTimeBucket(getAssetCountByTimeBucketDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final getAssetCountByTimeBucketDto = GetAssetCountByTimeBucketDto(); // GetAssetCountByTimeBucketDto |
try {
final result = api_instance.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getAssetCountByTimeBucket: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**getAssetCountByTimeBucketDto** | [**GetAssetCountByTimeBucketDto**](GetAssetCountByTimeBucketDto.md)| |
### Return type
[**AssetCountByTimeBucketResponseDto**](AssetCountByTimeBucketResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAssetSearchTerms**
> List<String> getAssetSearchTerms()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,8 @@ part 'model/add_assets_dto.dart';
part 'model/add_users_dto.dart';
part 'model/admin_signup_response_dto.dart';
part 'model/album_response_dto.dart';
part 'model/asset_count_by_time_bucket.dart';
part 'model/asset_count_by_time_bucket_response_dto.dart';
part 'model/asset_file_upload_response_dto.dart';
part 'model/asset_response_dto.dart';
part 'model/asset_type_enum.dart';
@@ -55,6 +57,8 @@ part 'model/delete_asset_status.dart';
part 'model/device_info_response_dto.dart';
part 'model/device_type_enum.dart';
part 'model/exif_response_dto.dart';
part 'model/get_asset_by_time_bucket_dto.dart';
part 'model/get_asset_count_by_time_bucket_dto.dart';
part 'model/login_credential_dto.dart';
part 'model/login_response_dto.dart';
part 'model/logout_response_dto.dart';
@@ -66,6 +70,7 @@ part 'model/server_version_reponse_dto.dart';
part 'model/sign_up_dto.dart';
part 'model/smart_info_response_dto.dart';
part 'model/thumbnail_format.dart';
part 'model/time_group_enum.dart';
part 'model/update_album_dto.dart';
part 'model/update_device_info_dto.dart';
part 'model/update_user_dto.dart';

View File

@@ -259,7 +259,10 @@ class AlbumApi {
/// Parameters:
///
/// * [bool] shared:
Future<Response> getAllAlbumsWithHttpInfo({ bool? shared, }) async {
///
/// * [String] assetId:
/// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
Future<Response> getAllAlbumsWithHttpInfo({ bool? shared, String? assetId, }) async {
// ignore: prefer_const_declarations
final path = r'/album';
@@ -273,6 +276,9 @@ class AlbumApi {
if (shared != null) {
queryParams.addAll(_queryParams('', 'shared', shared));
}
if (assetId != null) {
queryParams.addAll(_queryParams('', 'assetId', assetId));
}
const contentTypes = <String>[];
@@ -291,8 +297,11 @@ class AlbumApi {
/// Parameters:
///
/// * [bool] shared:
Future<List<AlbumResponseDto>?> getAllAlbums({ bool? shared, }) async {
final response = await getAllAlbumsWithHttpInfo( shared: shared, );
///
/// * [String] assetId:
/// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
Future<List<AlbumResponseDto>?> getAllAlbums({ bool? shared, String? assetId, }) async {
final response = await getAllAlbumsWithHttpInfo( shared: shared, assetId: assetId, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -298,10 +298,107 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'GET /asset/searchTerm' operation and returns the [Response].
/// Performs an HTTP 'POST /asset/time-bucket' operation and returns the [Response].
/// Parameters:
///
/// * [GetAssetByTimeBucketDto] getAssetByTimeBucketDto (required):
Future<Response> getAssetByTimeBucketWithHttpInfo(GetAssetByTimeBucketDto getAssetByTimeBucketDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/time-bucket';
// ignore: prefer_final_locals
Object? postBody = getAssetByTimeBucketDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [GetAssetByTimeBucketDto] getAssetByTimeBucketDto (required):
Future<List<AssetResponseDto>?> getAssetByTimeBucket(GetAssetByTimeBucketDto getAssetByTimeBucketDto,) async {
final response = await getAssetByTimeBucketWithHttpInfo(getAssetByTimeBucketDto,);
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();
}
return null;
}
/// Performs an HTTP 'POST /asset/count-by-time-bucket' operation and returns the [Response].
/// Parameters:
///
/// * [GetAssetCountByTimeBucketDto] getAssetCountByTimeBucketDto (required):
Future<Response> getAssetCountByTimeBucketWithHttpInfo(GetAssetCountByTimeBucketDto getAssetCountByTimeBucketDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/count-by-time-bucket';
// ignore: prefer_final_locals
Object? postBody = getAssetCountByTimeBucketDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [GetAssetCountByTimeBucketDto] getAssetCountByTimeBucketDto (required):
Future<AssetCountByTimeBucketResponseDto?> getAssetCountByTimeBucket(GetAssetCountByTimeBucketDto getAssetCountByTimeBucketDto,) async {
final response = await getAssetCountByTimeBucketWithHttpInfo(getAssetCountByTimeBucketDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetCountByTimeBucketResponseDto',) as AssetCountByTimeBucketResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response].
Future<Response> getAssetSearchTermsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/searchTerm';
final path = r'/asset/search-terms';
// ignore: prefer_final_locals
Object? postBody;
@@ -398,10 +495,10 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'GET /asset/allLocation' operation and returns the [Response].
/// Performs an HTTP 'GET /asset/curated-locations' operation and returns the [Response].
Future<Response> getCuratedLocationsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/allLocation';
final path = r'/asset/curated-locations';
// ignore: prefer_final_locals
Object? postBody;
@@ -442,10 +539,10 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'GET /asset/allObjects' operation and returns the [Response].
/// Performs an HTTP 'GET /asset/curated-objects' operation and returns the [Response].
Future<Response> getCuratedObjectsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/allObjects';
final path = r'/asset/curated-objects';
// ignore: prefer_final_locals
Object? postBody;

View File

@@ -200,6 +200,10 @@ class ApiClient {
return AdminSignupResponseDto.fromJson(value);
case 'AlbumResponseDto':
return AlbumResponseDto.fromJson(value);
case 'AssetCountByTimeBucket':
return AssetCountByTimeBucket.fromJson(value);
case 'AssetCountByTimeBucketResponseDto':
return AssetCountByTimeBucketResponseDto.fromJson(value);
case 'AssetFileUploadResponseDto':
return AssetFileUploadResponseDto.fromJson(value);
case 'AssetResponseDto':
@@ -234,6 +238,10 @@ class ApiClient {
return DeviceTypeEnumTypeTransformer().decode(value);
case 'ExifResponseDto':
return ExifResponseDto.fromJson(value);
case 'GetAssetByTimeBucketDto':
return GetAssetByTimeBucketDto.fromJson(value);
case 'GetAssetCountByTimeBucketDto':
return GetAssetCountByTimeBucketDto.fromJson(value);
case 'LoginCredentialDto':
return LoginCredentialDto.fromJson(value);
case 'LoginResponseDto':
@@ -256,6 +264,8 @@ class ApiClient {
return SmartInfoResponseDto.fromJson(value);
case 'ThumbnailFormat':
return ThumbnailFormatTypeTransformer().decode(value);
case 'TimeGroupEnum':
return TimeGroupEnumTypeTransformer().decode(value);
case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value);
case 'UpdateDeviceInfoDto':

View File

@@ -67,6 +67,9 @@ String parameterToString(dynamic value) {
if (value is ThumbnailFormat) {
return ThumbnailFormatTypeTransformer().encode(value).toString();
}
if (value is TimeGroupEnum) {
return TimeGroupEnumTypeTransformer().encode(value).toString();
}
return value.toString();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class TimeBucketEnum {
/// Instantiate a new enum with the provided [value].
const TimeBucketEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const day = TimeBucketEnum._(r'day');
static const month = TimeBucketEnum._(r'month');
/// List of all possible values in this [enum][TimeBucketEnum].
static const values = <TimeBucketEnum>[
day,
month,
];
static TimeBucketEnum? fromJson(dynamic value) => TimeBucketEnumTypeTransformer().decode(value);
static List<TimeBucketEnum>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimeBucketEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimeBucketEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [TimeBucketEnum] to String,
/// and [decode] dynamic data back to [TimeBucketEnum].
class TimeBucketEnumTypeTransformer {
factory TimeBucketEnumTypeTransformer() => _instance ??= const TimeBucketEnumTypeTransformer._();
const TimeBucketEnumTypeTransformer._();
String encode(TimeBucketEnum data) => data.value;
/// Decodes a [dynamic value][data] to a TimeBucketEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
TimeBucketEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data.toString()) {
case r'day': return TimeBucketEnum.day;
case r'month': return TimeBucketEnum.month;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [TimeBucketEnumTypeTransformer] instance.
static TimeBucketEnumTypeTransformer? _instance;
}

View File

@@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class TimeGroupEnum {
/// Instantiate a new enum with the provided [value].
const TimeGroupEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const day = TimeGroupEnum._(r'day');
static const month = TimeGroupEnum._(r'month');
/// List of all possible values in this [enum][TimeGroupEnum].
static const values = <TimeGroupEnum>[
day,
month,
];
static TimeGroupEnum? fromJson(dynamic value) => TimeGroupEnumTypeTransformer().decode(value);
static List<TimeGroupEnum>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimeGroupEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimeGroupEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [TimeGroupEnum] to String,
/// and [decode] dynamic data back to [TimeGroupEnum].
class TimeGroupEnumTypeTransformer {
factory TimeGroupEnumTypeTransformer() => _instance ??= const TimeGroupEnumTypeTransformer._();
const TimeGroupEnumTypeTransformer._();
String encode(TimeGroupEnum data) => data.value;
/// Decodes a [dynamic value][data] to a TimeGroupEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
TimeGroupEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data.toString()) {
case r'day': return TimeGroupEnum.day;
case r'month': return TimeGroupEnum.month;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [TimeGroupEnumTypeTransformer] instance.
static TimeGroupEnumTypeTransformer? _instance;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for TimeBucketEnum
void main() {
group('test TimeBucketEnum', () {
});
}

View File

@@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for TimeGroupEnum
void main() {
group('test TimeGroupEnum', () {
});
}

View File

@@ -35,7 +35,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.2"
version: "2.9.0"
auto_route:
dependency: "direct main"
description:
@@ -161,7 +161,7 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.2.1"
charcode:
dependency: transitive
description:
@@ -189,7 +189,7 @@ packages:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
code_builder:
dependency: transitive
description:
@@ -287,7 +287,7 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.3.1"
ffi:
dependency: transitive
description:
@@ -322,7 +322,7 @@ packages:
source: hosted
version: "0.6.8"
flutter_cache_manager:
dependency: transitive
dependency: "direct main"
description:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
@@ -601,21 +601,21 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.11"
version: "0.12.12"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
version: "0.1.5"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0"
mgrs_dart:
dependency: transitive
description:
@@ -706,7 +706,7 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
version: "1.8.2"
path_provider:
dependency: "direct main"
description:
@@ -1026,7 +1026,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
version: "1.9.0"
sprintf:
dependency: transitive
description:
@@ -1082,7 +1082,7 @@ packages:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
synchronized:
dependency: transitive
description:
@@ -1096,14 +1096,14 @@ packages:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.9"
version: "0.4.12"
timing:
dependency: transitive
description:

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