Compare commits

...

45 Commits

Author SHA1 Message Date
Immich Release Bot
4dab50c10a Version v1.48.0 2023-02-21 17:18:38 +00:00
Alex
c416dd30e2 [Localizely] Translations update (#1817) 2023-02-21 09:54:26 -06:00
Alex
4ebc8870c2 fix(web): modal overlay hidden behind nav bar (#1816) 2023-02-21 09:33:16 -06:00
Alex
bf3f4e560d chore(mobile): Improve reliability of asset loading and indexing (#1813)
* chore(mobile): Improve reliability of asset loading and indexing

* chore: add comments

* chore: remove log

* fix: put back box open sequence
2023-02-21 09:25:31 -06:00
Michel Heusschen
4be55428d2 feat(web): syling sidebar buttons (#1812)
* feat(web): syling sidebar buttons

* style: some small changes

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-02-21 06:42:22 -06:00
martyfuhry
e9c9b7a3e2 feat(mobile): iOS background sync notifications (#1811)
* adds notification handling logic

* notification on background updates for iOS

* fixed regression where i accidentally removed load translations from the background sync

* fixed ios translations

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-02-21 06:28:52 -06:00
martyfuhry
2d2cfb0349 fix(mobiile): Fixed load translations in background sync (#1810)
* fixed regression where i accidentally removed load translations from the background sync

* only for android

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>
2023-02-20 21:44:14 -06:00
Alex
98998cccbc fix(mobile): user get logged out upon clicking on any thing after logging in (#1808)
* fix(mobile): user get logged out upon clicking on any thing after logging in

* wip: fixing still

* fix: the actual issue

* Fix: avaialble album not updating UI
2023-02-20 21:43:39 -06:00
Alex
03d484aba2 chore(mobile): styling and linter (#1807)
* chore(mobile): styling and linter

* style: adaptive SwitchListTile
2023-02-20 21:40:19 -06:00
Michel Heusschen
88a2966666 feat(mobile): improve date formatting (#1804) 2023-02-20 11:23:00 -06:00
Michel Heusschen
e408e8ca4a feat(mobile): added more translations (#1805) 2023-02-20 11:22:35 -06:00
Michel Heusschen
9bfb4dfd06 fix(web): loading profile image (#1803) 2023-02-20 09:59:00 -06:00
Michel Heusschen
7dc7281e69 fix(server): asset search query (#1806) 2023-02-20 09:58:46 -06:00
martyfuhry
87fea29e32 feat(mobile): iOS background sync (#1758)
* first run of getting background sync working in iOS

* got background sync calling into flutter

* added background task

* added necessary sync files

* fixed some names and added more implementations

* got as far as Hive.initFlutter

* brute force got to await Hive.initFlutter

* lots of print statements to figure out where execution is failing, and its failing at the root asset bundle in the localization.dart service

* first time working, got plugins registered

* removed broken cleanup code

* refactored

* linters

* now can pass user settings

* background service plugin uses app background processing instead of fetch

* renamed backgroundFetch to backgroundProcessing to make it clearer

* don't use max delay

* adds fetch back in

* fixes require charging default values and backup controller page

* fixes background fetch

* fixes ios not importing photos

* guarded path provider ios

* lint

* adds max tries for heartbeat to work in iOS

* fail after seconds

* timeout instead of fail after seconds

* removes release lock from system stop

* restores checkLockReleasedWithHeartbeat to Future<void>

* removes max tries from acquire lock

* fixes lock timeout with iOS

* restored for loop

* adds comments, made the AppRefresh task only run while not requiring network or charge

* fixed compile issue

* now both are registered and added better comments. also added ability for task to cancel itself

* added the podfile and pubspec

* added backup diagnostics to IOS and removed iOS ignored backup options and fixed network connectivity always required

* Added Alex's dev team

* styled debug list item, fixed refresh task not set bug, fixed enable / disable background service on platform channel

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-02-20 05:59:50 +00:00
Alex
2cf42e867c feat(web): add some material design 3 styling to forms and ui elements (#1798)
* stlye: forms

* style: navigation bar

* style: user profile popup

* style: context menu

* fix: prettier

* style: manage account dark theme color

* style: user profile image border; fix: profile panel z-index

* style: border for profile image on hover and scrolling in administration page

* style: font size

* style: gap between day in a row
2023-02-19 23:29:06 -06:00
Jason Rasmussen
83a2669ff5 fix(web): user profile 404 (#1800) 2023-02-19 22:56:02 -06:00
Zack Pollard
824409351e fix: use fileCreatedAt for asset sorting after recent refactor (#1799) 2023-02-20 01:50:48 +00:00
Zack Pollard
d1ea6a897e chore(server): typeorm definitions fix part 3 (#1796)
* chore(server): tidy up exif typeorm entity definition

* chore(server): tidy up shared link typeorm entity definition

* chore(server): tidy up smart info typeorm entity definition

* chore(server): tidy up tag typeorm entity definition

* ci: add job that checks typeorm migrations are correct and up-to-date
2023-02-20 01:50:27 +00:00
Alex
5d3e8f17d1 fix(server): use updated AssetEntity property in getting server info query (#1797) 2023-02-19 16:57:39 -06:00
Zack Pollard
78a5fe2d37 test(app): fix integration test and improve reliability and speed (#1792) 2023-02-19 17:50:36 +00:00
Zack Pollard
5ad4e5b614 infra(server)!: fix typeorm asset entity relations (#1782)
* fix: add correct relations to asset typeorm entity

* fix: add missing createdAt column to asset entity

* ci: run check to make sure generated API is up-to-date

* ci: cancel workflows that aren't for the latest commit in a branch

* chore: add fvm config for flutter
2023-02-19 16:44:53 +00:00
Zack Pollard
000d0a08f4 infra(server): fix Album TypeORM relations and change ids to uuids (#1582)
* infra: make api-key primary key column a UUID

* infra: move ManyToMany relations in album entity, make ownerId ManyToOne

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-02-18 14:58:55 -06:00
bo0tzz
917f1dea9f fix(ci): Repair broken docker build matrix (#1793)
* fix(ci): Typo in matrix.platforms ref

* fix(ci): Add platforms key to every entry in matrix
2023-02-18 19:15:23 +00:00
Jason Rasmussen
e309647f1b refactor(server): remove checksum job (#1786) 2023-02-18 09:18:07 -06:00
Alex
57136e48fb feat(machine-learning)!: move machine learning to Python based image (#1774)
BREAKING CHANGES
* Users have to update the docker-compose file, machine-learning portion.
* Temporary dropping machine-learning support for Arm64 and Armv7
2023-02-18 09:13:37 -06:00
martyfuhry
8c315dfeb1 fix(mobile): Delete goes to next page instead of popping back to the main timeline (#1781)
* delete goes to next page instead of popping

* moves pagecontroller to constructor so we dont rebuilt each time
2023-02-17 20:47:28 -06:00
martyfuhry
6e9749d6c4 feat(mobile): Scroll to top when tapping photos while already on photo page (#1784)
* adds scroll to top when tapping photos while already on photo page

* unused import
2023-02-17 20:46:25 -06:00
Michel Heusschen
bf6f94f69f feat(web): only show copy image when supported (#1776) 2023-02-17 10:41:52 -06:00
Michel Heusschen
575154fdea refactor(web): use cookies interface (#1777) 2023-02-17 10:37:26 -06:00
bo0tzz
857bbe3c3b fix(docs): Tiny tweaks in unraid guide (#1772) 2023-02-16 10:01:48 -06:00
Alex
0a0b255505 [Localizely] Translations update (#1773) 2023-02-16 10:01:04 -06:00
Jason Rasmussen
73b4b032b1 fix(release): pump script (#1768) 2023-02-16 09:13:42 -06:00
Jakob Lindskog
8234e44921 Add Swedish i18n (#1769) 2023-02-16 09:11:33 -06:00
Jason Rasmussen
36197cca98 feat(server): auto-link live photos (#1761)
* feat(server): auto-link live photos

* fix: video extraction and linking
2023-02-16 01:41:51 -06:00
Immich Release Bot
7a25d359b7 Version v1.47.3 2023-02-16 03:38:44 +00:00
Michel Heusschen
7cfb257c00 feat(nginx): refactor + ipv6 (#1763)
* feat(nginx): refactor + ipv6 + increased buffer

* Revert changes to proxy buffering

* remove commented lines
2023-02-15 15:21:52 -06:00
Alex
b660240059 fix(web/server) uploaded asset in shared link not loaded (#1766)
* fix(web/server): Uploaded asset to shared link does not get added to the shared link/album

* remove unused code

* Add endpoints for each remove and add assets to shared link

* Update api

* Added deletion logic

* Convert callback to async/await

* Fix linter

* Fix test

* Fix server test

* added test

* Test coverage

* modify DTO

* Add notification

* fix test
2023-02-15 15:21:22 -06:00
Alex
125ec1e85f fix(web): using serverApi on the client request lead to uncaught error (#1767) 2023-02-15 13:09:28 -06:00
Michel Heusschen
d31b35873f feat(web): improve login screen (#1754) 2023-02-15 11:56:54 -06:00
Michel Heusschen
e1c520b9e7 feat(web): remove duplicate asset calls (#1764)
* feat(web): remove duplicate asset calls

* use source element instead of video.src
2023-02-15 11:56:19 -06:00
Michel Heusschen
1361f18964 feat(web): redirect to login from getting started (#1755) 2023-02-15 06:42:41 -06:00
Michel Heusschen
0f00f22212 fix(web): remove link header causing 502 errors (#1765) 2023-02-15 06:40:52 -06:00
Skyler Mäntysaari
0d543bbb0a feat(server/web): Initial support for RAF and SRW RAW formats (#1414)
* feat(server/web): Initial support for RAF and SRW RAW formats.

* It should return the promise.

* Better comment

* feat(server/web): file-uploader needed changes.

* Remove un-used imports

* The failing test.. is no longer failing.

* Run prettier

* Original implementation with just a catch block added.

* feat(server): Some tests and specific handling for the two raw formats

* feat(web): Helper for raw image type.

* Handling of mimetype on server

* Handling of mimetypes on web with a map

* Bring back the acceptedfile filter

* Fix the asset-upload tests after changes

* acceptedFile is not usable due to type being empty from browser.

* Switch needs to use lowercase variants.

* Address Discord comments

* feat(mobile): Library page rework (album sorting, favorites) (#1501)

* Add album sorting

* Change AppBar to match photos page behaviour

* Add buttons

* First crude implementation of the favorites page

* Clean up

* Add favorite button

* i18n

* Add star indicator to thumbnail

* Add favorite logic to separate provider and fix favorite behavior in album

* Review feedback (Add isFavorite variable)

* dev: style buttons

* dev: styled drop down button

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>

* feat(mobile): Tap to enter immersive mode on gallery viewer (#1546)

* feat(mobile): Removed stay logged in checkbox and made it enabled by default (#1550)

* removed stay logged in checkbox and made it enabled by default

* adds padding to login button

* removed all isSaveLogin

* fix: logout would re-login with previous credential upon app restart

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>

* chore(server): remove token when logged out (#1560)

* chore(mobile): invoke logout() on mobile app

* feat: add mechanism to delete token from logging out endpoint

* fix: set state after login sequence success

* fix: not removing token when logging out from OAuth

* fix: prettier

* refactor: using accessTokenId to delete

* chore: pr comments

* fix: test

* fix: test threshold

* feat(deployment): support docker secrets (#1254)

* Support secrets

* Rewrite to support sh

* Remove JWT_SECRET

* fix(mobile): Added flutter native splash and splash screens (#1520)

* rebasing

* added launch background image to repository

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>

* refactor(mobile): introduce Album & User classes (#1561)

replace usages of AlbumResponseDto with Album
replace usages of UserResponseDto with User

* feat(mobile): Multiselect add to favorite from the timeline (#1558)

* multiselect add to favorites

* feat(server): add updatedAt to Asset, Album and User (#1566)

* feat: add updatedAt info to DTO and generate api

* chore: remove unsued file

* chore: Add update statement to add/remove asset/user to album

* fix: test

* chore(server): update package-lock.json to match package.json (#1573)

* chore(server) Add user FK to album entity (#1569)

* chore(deps): bump docker/setup-buildx-action from 2.4.0 to 2.4.1 (#1575)

Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.4.0...v2.4.1)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(server): make owner as required response for AlbumResponseDto (#1579)

* feat(GitHub): update bug and feature request template (#1584)

* dev: Reusing template from Home Assistant

* dev: add bug report template

* fix: template

* dev: change type

* dev:

* dev: add default labels

* dev: Add default title

* dev: add feature request template

* remove feature request from markdown

* dev: frontmatter

* fix(GitHub): feature request template

* fix(GitHub): feature request form has wrong type for textarea

* feat(mobile): Responsive layout improvements with a navigation rail and album grid (#1583)

* feat(proxy): Initial IPv6 support (#1577)

* fix(server): Create album response doesn't have owner property as required (#1704)

* feat(web): allow uploading more file types (#1570)

* feat(web): allow uploading more file types

* fix(web): make filename extension lowercase

* refactor(mobile): add Isar DB & Store class (#1574)

* refactor(mobile): add Isar DB & Store class

new Store: globally accessible key-value store like Hive (but based on Isar)

replace first few places of Hive usage with the new Store

* reduce max. DB size to prevent errors on older iOS devices

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>

* feat(mobile): Home screen customization options (#1563)

* Try staggered layout for home page

* Introduce setting for dynamic layout

* Fix some provider related bugs

* Make asset grouping configurable

* Add translation keys, refactor group title

* Rename enum values

* Fix enum names

* Reformat long if statement

* Fix timezone related bug

* Minor clean up

* Fix unit test

* Add second assets check back to home screen

* [Localizely] Translations update (#1707)

* fix(server): get shared link album info doesn't contain owner property (#1708)

* Version v1.46.0

* feat(server/web): file-uploader needed changes.

* Add raf and srw to the file names.

* Remember to add the extensions to fileSelector.

* Removed the getMimeType function on server as shouldn't be needed anymore.

* Revert "Removed the getMimeType function on server as shouldn't be needed anymore."

It is required still.

This reverts commit fc766dd0be.

* Should use proper mimetypes.

* fix linter

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Matthias Rupp <matthias.rupp@posteo.de>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: martyfuhry <martyfuhry@gmail.com>
Co-authored-by: James <jdm12989@gmail.com>
Co-authored-by: Marty Fuhry <marty@fuhry.farm>
Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Immich Release Bot <bot@immich.app>
2023-02-14 13:54:28 -06:00
Alex Tran
86b3bdb90b chore(mobile): bump pubspec version 2023-02-13 21:21:44 -06:00
Alex
d47cdfb647 chore(post-release): add release note 2023-02-13 17:56:03 -06:00
246 changed files with 4523 additions and 19625 deletions

View File

@@ -11,6 +11,10 @@ on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-sign-android:
name: Build and sign Android
@@ -24,7 +28,7 @@ jobs:
github_ref="${{ github.sha }}"
ref="${input_ref:-$github_ref}"
echo "ref=$ref" >> $GITHUB_OUTPUT
- uses: actions/checkout@v3
with:
ref: ${{ steps.get-ref.outputs.ref }}

View File

@@ -4,24 +4,28 @@ on:
types:
- closed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
BRANCH=${{ github.ref }}
echo "Fetching list of cache keys"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR

View File

@@ -20,6 +20,10 @@ on:
schedule:
- cron: '20 13 * * 1'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze
@@ -48,11 +52,11 @@ jobs:
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
@@ -61,7 +65,7 @@ jobs:
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |

View File

@@ -5,6 +5,10 @@ on:
push:
branches: ["main"]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
update-sdk-repos:
runs-on: ubuntu-latest

View File

@@ -9,6 +9,10 @@ on:
release:
types: [published]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_and_push:
runs-on: ubuntu-latest
@@ -19,13 +23,17 @@ jobs:
include:
- context: "server"
image: "immich-server"
platforms: "linux/arm/v7,linux/amd64,linux/arm64"
- context: "web"
image: "immich-web"
platforms: "linux/arm/v7,linux/amd64,linux/arm64"
- context: "machine-learning"
image: "immich-machine-learning"
platforms: "linux/amd64"
- context: "nginx"
image: "immich-proxy"
platforms: "linux/arm/v7,linux/amd64,linux/arm64"
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -49,7 +57,7 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
# Skip when PR from a fork
@@ -92,7 +100,7 @@ jobs:
uses: docker/build-push-action@v4.0.0
with:
context: ${{ matrix.context }}
platforms: linux/arm/v7,linux/amd64,linux/arm64
platforms: ${{ matrix.platforms }}
# Skip pushing when PR from a fork
push: ${{ !github.event.pull_request.head.repo.fork }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}

View File

@@ -7,6 +7,10 @@ on:
- cron: "0 23 * * *"
workflow_dispatch: # Allow for running this manually.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
j1:
name: github-repo-stats

View File

@@ -17,13 +17,17 @@ on:
required: false
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
bump_version:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.push-tag.outputs.commit_long_sha }}
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -42,7 +46,7 @@ jobs:
message: "Version ${{ env.IMMICH_VERSION }}"
tag: ${{ env.IMMICH_VERSION }}
push: true
build_mobile:
uses: ./.github/workflows/build-mobile.yml
needs: bump_version
@@ -59,7 +63,7 @@ jobs:
uses: actions/checkout@v3
with:
token: ${{ secrets.ORG_RELEASE_TOKEN }}
- name: Download APK
uses: actions/download-artifact@v3
with:

View File

@@ -5,6 +5,10 @@ on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
mobile-dart-analyze:
name: Run Dart Code Analysis

View File

@@ -5,6 +5,10 @@ on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e-tests:
name: Run end-to-end test suites
@@ -54,6 +58,72 @@ jobs:
working-directory: ./mobile
run: flutter test
generated-api-up-to-date:
name: Check generated files are up-to-date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run API generation
run: cd server && npm ci && npm run api:generate
- name: Find file changes
uses: tj-actions/verify-changed-files@v13.1
id: verify-changed-files
with:
files: |
mobile/openapi
web/src/api/open-api
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
run: |
echo "ERROR: Generated files not up to date!"
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
exit 1
generated-typeorm-migrations-up-to-date:
name: Check generated TypeORM migrations are up-to-date
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: immich
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Install server dependencies
run: |
cd server
npm ci
- name: Run existing migrations
run: |
cd server
npm run typeorm:migrations:run
- name: Generate new migrations
continue-on-error: true
run: |
cd server
npm run typeorm:migrations:generate ./libs/infra/src/db/migrations/TestMigration
- name: Find file changes
uses: tj-actions/verify-changed-files@v13.1
id: verify-changed-files
with:
files: |
server/libs/infra/src/db/migrations/
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
run: |
echo "ERROR: Generated files not up to date!"
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
exit 1
mobile-integration-tests:
name: Run mobile end-to-end integration tests
runs-on: macos-latest
@@ -61,8 +131,9 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
distribution: 'zulu'
java-version: '12.x'
cache: 'gradle'
- name: Cache android SDK
uses: actions/cache@v3
id: android-sdk
@@ -71,24 +142,59 @@ jobs:
path: |
/usr/local/lib/android/
~/.android
- name: Cache Gradle
uses: actions/cache@v3
with:
path: |
./mobile/build/
./mobile/android/.gradle/
key: ${{ runner.os }}-flutter-${{ hashFiles('**/*.gradle*', 'pubspec.lock') }}
- name: Setup Android SDK
if: steps.android-sdk.outputs.cache-hit != 'true'
uses: android-actions/setup-android@v2
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-29
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2.27.0
with:
working-directory: ./mobile
cores: 2
api-level: 29
arch: x86_64
profile: pixel
target: default
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.7.3'
cache: true
- name: Run integration tests
uses: reactivecircus/android-emulator-runner@v2.27.0
uses: Wandalen/wretry.action@master
with:
working-directory: ./mobile
api-level: 29
arch: x86_64
profile: pixel
target: default
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
disable-linux-hw-accel: false
script: |
flutter pub get
flutter test integration_test
action: reactivecircus/android-emulator-runner@v2.27.0
with: |
working-directory: ./mobile
cores: 2
api-level: 29
arch: x86_64
profile: pixel
target: default
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: |
flutter pub get
flutter test integration_test
attempt_limit: 3

View File

@@ -30,18 +30,20 @@ services:
build:
context: ../machine-learning
dockerfile: Dockerfile
target: builder
command: npm run start:dev
command: python main.py
ports:
- 3003:3003
volumes:
- ../machine-learning:/usr/src/app
- ../machine-learning/src:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
- model-cache:/cache
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
restart: always
immich-microservices:
container_name: immich_microservices
@@ -126,3 +128,4 @@ services:
volumes:
pgdata:
model-cache:

View File

@@ -1,95 +0,0 @@
version: "3.8"
services:
immich-server:
container_name: immich_server
image: altran1502/immich-server:staging
entrypoint: ["/bin/sh", "./start-server.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- redis
- database
restart: always
immich-microservices:
container_name: immich_microservices
image: altran1502/immich-server:staging
entrypoint: ["/bin/sh", "./start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- redis
- database
restart: always
immich-machine-learning:
container_name: immich_machine_learning
image: altran1502/immich-machine-learning:staging
entrypoint: ["/bin/sh", "./entrypoint.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- database
restart: always
immich-web:
container_name: immich_web
image: altran1502/immich-web:staging
entrypoint: ["/bin/sh", "./entrypoint.sh"]
env_file:
- .env
environment:
# Rename these values for svelte public interface
- PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL}
- PUBLIC_IMMICH_API_URL_EXTERNAL=${IMMICH_API_URL_EXTERNAL}
restart: always
redis:
container_name: immich_redis
image: redis:6.2
restart: always
database:
container_name: immich_postgres
image: postgres:14
env_file:
- .env
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
PG_DATA: /var/lib/postgresql/data
volumes:
- pgdata:/var/lib/postgresql/data
restart: always
immich-proxy:
container_name: immich_proxy
image: altran1502/immich-proxy:staging
environment:
# Make sure these values get passed through from the env file
- IMMICH_SERVER_URL
- IMMICH_WEB_URL
ports:
- 2283:8080
logging:
driver: none
depends_on:
- immich-server
restart: always
volumes:
pgdata:

View File

@@ -4,7 +4,7 @@ services:
immich-server:
container_name: immich_server
image: altran1502/immich-server:release
entrypoint: ["/bin/sh", "./start-server.sh"]
entrypoint: [ "/bin/sh", "./start-server.sh" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
@@ -19,7 +19,7 @@ services:
immich-microservices:
container_name: immich_microservices
image: altran1502/immich-server:release
entrypoint: ["/bin/sh", "./start-microservices.sh"]
entrypoint: [ "/bin/sh", "./start-microservices.sh" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
@@ -34,9 +34,10 @@ services:
immich-machine-learning:
container_name: immich_machine_learning
image: altran1502/immich-machine-learning:release
entrypoint: ["/bin/sh", "./entrypoint.sh"]
command: [ "python", "src/main.py" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- model-cache:/cache
env_file:
- .env
environment:
@@ -48,7 +49,7 @@ services:
immich-web:
container_name: immich_web
image: altran1502/immich-web:release
entrypoint: ["/bin/sh", "./entrypoint.sh"]
entrypoint: [ "/bin/sh", "./entrypoint.sh" ]
env_file:
- .env
restart: always
@@ -89,3 +90,4 @@ services:
volumes:
pgdata:
model-cache:

View File

@@ -18,7 +18,7 @@ Immich can easily be installed and updated on Unraid via:
In order to install Immich from the Unraid CA, you will need an existing Redis and PostgreSQL 14 container, If you do not already have Redis or PostgreSQL you can install them from the Unraid CA, just make sure you choose PostgreSQL **14**.
Once you have Redis and PostgreSQL running, search for Immich on the Unraid CA, Choose either of the templates listed and fill out the example variables.
Once you have Redis and PostgreSQL running, search for Immich on the Unraid CA, choose either of the templates listed and fill out the example variables.
For more information about setting up the community image see [here](https://github.com/imagegenius/docker-immich#application-setup)
@@ -71,7 +71,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
</details>
5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**"
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
7. Past the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
@@ -143,8 +143,8 @@ alt="Immich update notification"
/>
1. Go to the "**Docker**" tab and scroll to the Compose section
2. Next to Immich click the "**Update Stack**" button and Unraid will begin to update all Immmich related containers
> Note: **Do not** select Compose Down first, it is unecessary.
2. Next to Immich click the "**Update Stack**" button and Unraid will begin to update all Immich related containers
> Note: **Do not** select Compose Down first, it is unnecessary.
3. Once complete you will see a "_Connection Closed_" message, select "**Done**".
<img
src={require('./img/unraid11.png').default}

View File

@@ -34,6 +34,8 @@ download:
locale_code: pt-BR
- file: mobile/assets/i18n/pl-PL.json
locale_code: pl-PL
- file: mobile/assets/i18n/sv-SE.json
locale_code: sv-SE
- file: mobile/assets/i18n/sk-SK.json
locale_code: sk-SK
- file: mobile/assets/i18n/zh-CN.json

View File

@@ -1,4 +1 @@
node_modules/
upload/
dist/
venv/

View File

@@ -1,24 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

View File

@@ -1,37 +1,4 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
upload/
upload/
venv/
__pycache__/
model-cache/

View File

@@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -1,34 +1,11 @@
FROM node:16-bullseye-slim as builder
FROM python:3.10
ARG DEBIAN_FRONTEND=noninteractive
ENV TRANSFORMERS_CACHE=/cache
WORKDIR /usr/src/app
RUN apt-get update
RUN apt-get install gcc g++ make cmake python3 python3-pip -y
COPY package.json package-lock.json ./
RUN npm ci
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
RUN pip install --user --no-cache-dir torch==1.13.1+cpu -f https://download.pytorch.org/whl/torch_stable.html
RUN pip install --user transformers tqdm numpy scikit-learn scipy nltk sentencepiece flask Pillow
RUN pip install --user --no-deps sentence-transformers
COPY . .
FROM builder as prod
RUN npm run build
RUN npm prune --omit=dev
FROM node:16-bullseye-slim
ARG DEBIAN_FRONTEND=noninteractive
WORKDIR /usr/src/app
COPY --from=prod /usr/src/app/node_modules ./node_modules
COPY --from=prod /usr/src/app/dist ./dist
COPY package.json package-lock.json ./
COPY entrypoint.sh ./
# CMD [ "node", "dist/main" ]

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Hau Tran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,4 +1,5 @@
# Microservices for Immich
# Immich Machine Learning
## Image Classifier
- Object Detection
- Image Classification

View File

@@ -1,4 +0,0 @@
#! /bin/sh
# npm run typeorm migration:run
# npm run start:prod
exec node dist/main.js

View File

@@ -1,4 +0,0 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,71 +0,0 @@
{
"name": "nest_microservices",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0",
"@tensorflow-models/coco-ssd": "^2.2.2",
"@tensorflow-models/mobilenet": "^2.1.0",
"@tensorflow/tfjs-node": "^3.19.0"
},
"devDependencies": {
"@nestjs/cli": "^8.2.4",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
"@types/express": "^4.17.13",
"@types/jest": "27.4.1",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.2.5",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typescript": "^4.3.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { ImageClassifierModule } from './image-classifier/image-classifier.module';
import { ObjectDetectionModule } from './object-detection/object-detection.module';
@Module({
imports: [ImageClassifierModule, ObjectDetectionModule],
controllers: [],
providers: [],
})
export class AppModule {}

View File

@@ -1,14 +0,0 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ImageClassifierService } from './image-classifier.service';
@Controller('image-classifier')
export class ImageClassifierController {
constructor(
private readonly imageClassifierService: ImageClassifierService,
) { }
@Post('/tag-image')
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
return await this.imageClassifierService.tagImage(thumbnailPath);
}
}

View File

@@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { ImageClassifierService } from './image-classifier.service';
import { ImageClassifierController } from './image-classifier.controller';
@Module({
controllers: [ImageClassifierController],
providers: [ImageClassifierService],
})
export class ImageClassifierModule {}

View File

@@ -1,49 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import * as mobilenet from '@tensorflow-models/mobilenet';
import * as cocoSsd from '@tensorflow-models/coco-ssd';
import * as tf from '@tensorflow/tfjs-node';
import * as fs from 'fs';
@Injectable()
export class ImageClassifierService {
private readonly MOBILENET_VERSION = 2;
private readonly MOBILENET_ALPHA = 1.0;
private mobileNetModel: mobilenet.MobileNet;
constructor() {
Logger.log(
`Running Node TensorFlow Version : ${tf.version['tfjs']}`,
'ImageClassifier',
);
mobilenet
.load({
version: this.MOBILENET_VERSION,
alpha: this.MOBILENET_ALPHA,
})
.then((mobilenetModel) => (this.mobileNetModel = mobilenetModel));
}
async tagImage(thumbnailPath: string) {
try {
const isExist = fs.existsSync(thumbnailPath);
if (isExist) {
const tags = [];
const image = fs.readFileSync(thumbnailPath);
const decodedImage = tf.node.decodeImage(image, 3) as tf.Tensor3D;
const predictions = await this.mobileNetModel.classify(decodedImage);
for (const prediction of predictions) {
if (prediction.probability >= 0.1) {
tags.push(...prediction.className.split(',').map((e) => e.trim()));
}
}
tf.dispose(decodedImage);
return tags;
}
} catch (e) {
console.log('Error reading file ', e);
}
}
}

View File

@@ -0,0 +1,61 @@
import os
from flask import Flask, request
from transformers import pipeline
server = Flask(__name__)
classifier = pipeline(
task="image-classification",
model="microsoft/resnet-50"
)
detector = pipeline(
task="object-detection",
model="hustvl/yolos-tiny"
)
# Environment resolver
is_dev = os.getenv('NODE_ENV') == 'development'
server_port = os.getenv('MACHINE_LEARNING_PORT') or 3003
@server.route("/ping")
def ping():
return "pong"
@server.route("/object-detection/detect-object", methods=['POST'])
def object_detection():
assetPath = request.json['thumbnailPath']
return run_engine(detector, assetPath), 201
@server.route("/image-classifier/tag-image", methods=['POST'])
def image_classification():
assetPath = request.json['thumbnailPath']
return run_engine(classifier, assetPath), 201
def run_engine(engine, path):
result = []
predictions = engine(path)
for index, pred in enumerate(predictions):
tags = pred['label'].split(', ')
if (index == 0):
result = tags
else:
if (pred['score'] > 0.5):
result = [*result, *tags]
if (len(result) > 1):
result = list(set(result))
return result
if __name__ == "__main__":
server.run(debug=is_dev, host='0.0.0.0', port=server_port)

View File

@@ -1,27 +0,0 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = Number(process.env.MACHINE_LEARNING_PORT) || 3003;
await app.listen(port, () => {
if (process.env.NODE_ENV == 'development') {
Logger.log(
'Running Immich Machine Learning in DEVELOPMENT environment',
'IMMICH MICROSERVICES',
);
}
if (process.env.NODE_ENV == 'production') {
Logger.log(
'Running Immich Machine Learning in PRODUCTION environment',
'IMMICH MICROSERVICES',
);
}
});
}
bootstrap();

View File

@@ -1,15 +0,0 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ObjectDetectionService } from './object-detection.service';
import { Logger } from '@nestjs/common';
@Controller('object-detection')
export class ObjectDetectionController {
constructor(
private readonly objectDetectionService: ObjectDetectionService,
) { }
@Post('/detect-object')
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
return await this.objectDetectionService.detectObject(thumbnailPath);
}
}

View File

@@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { ObjectDetectionService } from './object-detection.service';
import { ObjectDetectionController } from './object-detection.controller';
@Module({
controllers: [ObjectDetectionController],
providers: [ObjectDetectionService],
})
export class ObjectDetectionModule {}

View File

@@ -1,39 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import * as cocoSsd from '@tensorflow-models/coco-ssd';
import * as tf from '@tensorflow/tfjs-node';
import * as fs from 'fs';
@Injectable()
export class ObjectDetectionService {
private cocoSsdModel: cocoSsd.ObjectDetection;
constructor() {
Logger.log(
`Running Node TensorFlow Version : ${tf.version['tfjs']}`,
'ObjectDetection',
);
cocoSsd.load().then((model) => (this.cocoSsdModel = model));
}
async detectObject(thumbnailPath: string) {
try {
const isExist = fs.existsSync(thumbnailPath);
if (isExist) {
const tags = new Set();
const image = fs.readFileSync(thumbnailPath);
const decodedImage = tf.node.decodeImage(image, 3) as tf.Tensor3D;
const predictions = await this.cocoSsdModel.detect(decodedImage);
for (const result of predictions) {
if (result.score > 0.5) {
tags.add(result.class);
}
}
tf.dispose(decodedImage);
return [...tags];
}
} catch (e) {
console.log('Error reading file ', e);
}
}
}

View File

@@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@@ -1,21 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

View File

@@ -60,22 +60,19 @@ else
fi
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package.json
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package-lock.json
sed -i "s/\"version\": \"$CURRENT_SERVER\",$/\"version\": \"$NEXT_SERVER\",/" server/immich-openapi-specs.json
sed -i "s/\"android\.injected\.version\.name\" => \"$CURRENT_SERVER\",/\"android\.injected\.version\.name\" => \"$NEXT_SERVER\",/" mobile/android/fastlane/Fastfile
sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/" mobile/ios/fastlane/Fastfile
fi
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then
echo "Pumping Mobile: $CURRENT_MOBILE => $NEXT_MOBILE"
sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
fi
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package.json
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package-lock.json
sed -i "s/\"version\": \"$CURRENT_SERVER\",$/\"version\": \"$NEXT_SERVER\",/" server/immich-openapi-specs.json
sed -i "s/\"android\.injected\.version\.name\" => \"$CURRENT_SERVER\",/\"android\.injected\.version\.name\" => \"$NEXT_SERVER\",/" mobile/android/fastlane/Fastfile
sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/" mobile/ios/fastlane/Fastfile
sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
echo "IMMICH_VERSION=v$NEXT_SERVER" >>$GITHUB_ENV

View File

@@ -0,0 +1,4 @@
{
"flutterSdkVersion": "3.7.0",
"flavors": {}
}

4
mobile/.gitignore vendored
View File

@@ -24,7 +24,6 @@
# Flutter/Dart/Pub related
**/doc/api/
**/ios/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
@@ -32,6 +31,7 @@
.pub-cache/
.pub/
/build/
.fvm/flutter_sdk
# Web related
lib/generated_plugin_registrant.dart
@@ -48,4 +48,4 @@ app.*.map.json
/android/app/release
# Fastlane
ios/fastlane/report.xml
ios/fastlane/report.xml

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 70,
"android.injected.version.name" => "1.47.2",
"android.injected.version.code" => 71,
"android.injected.version.name" => "1.48.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,11 @@
* Adds over scroll at end of timeline to select images on the bottom
* Fixes back navigation with tab controller
* Shows a toast after adding to favorites
* Fixed download button style
* Cleaned up action bar, changed horizontal icon more to info icon
* Responsive display of exif data in bottom sheet
* Upgrade to Flutter 3.7
* Fix freeze bug on app start
* Uses profile photo for user avatar drawer
* Spinning flower
* Remove unsplash placeholder image and style empty places

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000281">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000209">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="142.850758">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="79.840593">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="39.589103">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="21.361905">
</testcase>

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "VYLOUČENO",
"album_info_card_backup_album_included": "ZAHRNUTO",
"album_thumbnail_card_item": "1 položka",
@@ -87,11 +89,21 @@
"cache_settings_subtitle": "Ovládání chování mobilní aplikace Immich v mezipaměti",
"cache_settings_thumbnail_size": "Velikost vyrovnávací paměti náhledů (položek {})",
"cache_settings_title": "Nastavení vyrovnávací paměti",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Přidat do alba",
"control_bottom_app_bar_album_info": "{} položky",
"control_bottom_app_bar_album_info_shared": "{} položky - sdílené",
"control_bottom_app_bar_create_new_album": "Vytvořit nové album",
"control_bottom_app_bar_delete": "Vymazat",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Sdílet",
"create_album_page_untitled": "Bez názvu",
"create_shared_album_page_create": "Vytvořit",
@@ -114,9 +126,13 @@
"experimental_settings_title": "Experimentální",
"favorites_page_title": "Oblíbené",
"home_page_add_to_album_conflicts": "Přidány {added} položky do alba {album}. {failed} položky jsou již v albu.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Přidány položky {added} do alba {album}.",
"home_page_building_timeline": "Vytváraní časové osy",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných albech.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Alba",
"library_page_favorites": "Oblíbené",
"library_page_new_album": "Nové album",
@@ -140,6 +156,12 @@
"login_form_password_hint": "heslo",
"login_form_save_login": "Zůstat přihlášen",
"monthly_title_text_date_format": "LLLL y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Logy",
"profile_drawer_client_server_up_to_date": "Klient a server jsou aktuální",
"profile_drawer_settings": "Nastavení",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "Návrhy",
"select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album",
"select_user_for_sharing_page_share_suggestions": "Návrhy",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "V prohlížeči detailů se nejprve načte malá miniatura, poté se načte náhled střední velikosti (je-li povolen) a nakonec se načte originál (je-li povolen).",
"setting_image_viewer_original_subtitle": "Umožňuje načíst původní obrázek v plném rozlišení (velký!). Zakázat pro snížení používání dat (v síti iv mezipaměti zařízení).",
"setting_image_viewer_original_title": "Načíst původní obrázek",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "EKSKLUDERET",
"album_info_card_backup_album_included": "INKLUDERET",
"album_thumbnail_card_item": "1 genstand",
@@ -12,10 +14,10 @@
"album_viewer_appbar_share_leave": "Forlad album",
"album_viewer_appbar_share_remove": "Fjern fra album",
"album_viewer_page_share_add_users": "Tilføj brugere",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout",
"asset_list_layout_settings_group_by": "Gruppér elementer pr. ",
"asset_list_layout_settings_group_by_month": "Måned",
"asset_list_layout_settings_group_by_month_day": "Måned + dag",
"asset_list_settings_subtitle": "Indstillinger for billedgitterlayout",
"asset_list_settings_title": "Billedgitter",
"backup_album_selection_page_albums_device": "Albummer på enhed ({})",
@@ -25,12 +27,12 @@
"backup_album_selection_page_selection_info": "Oplysninger om valgte",
"backup_album_selection_page_total_assets": "Samlede unikke elementer",
"backup_all": "Alt",
"backup_background_service_backup_failed_message": "Backup af billeder og videoer fejlede. Forsøger igen...",
"backup_background_service_backup_failed_message": "Backup af elementer fejlede. Forsøger igen...",
"backup_background_service_connection_failed_message": "Forbindelsen til serveren blev tabt. Forsøger igen...",
"backup_background_service_current_upload_notification": "Uploader {}",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_default_notification": "Søger efter nye elementer...",
"backup_background_service_error_title": "Fejl med backup",
"backup_background_service_in_progress_notification": "Tager backup af dine billeder og videoer...",
"backup_background_service_in_progress_notification": "Tager backup af dine elementer...",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_controller_page_albums": "Sikkerhedskopiér albummer",
"backup_controller_page_background_battery_info_link": "Vis mig hvordan",
@@ -39,8 +41,8 @@
"backup_controller_page_background_battery_info_title": "Batterioptimering",
"backup_controller_page_background_charging": "Kun under opladning",
"backup_controller_page_background_configure_error": "Fejlede konfigureringen af baggrundsbackup",
"backup_controller_page_background_delay": "Udskyd backup af nye billeder og videoer: {}",
"backup_controller_page_background_description": "Slå baggrundsbackup til, for automatisk at tage backup af nye billeder og videoer, uden at skulle åbne appen",
"backup_controller_page_background_delay": "Udskyd backup af nye elementer: {}",
"backup_controller_page_background_description": "Slå baggrundsbackup til, for automatisk at tage backup af nye elementer, uden at skulle åbne appen",
"backup_controller_page_background_is_off": "Automatisk baggrundsbackup er slået fra",
"backup_controller_page_background_is_on": "Automatisk baggrundsbackup er slået til",
"backup_controller_page_background_turn_off": "Slå baggrundsbackup fra",
@@ -74,24 +76,34 @@
"backup_controller_page_uploading_file_info": "Uploader filinformation",
"backup_err_only_album": "Kan ikke slette det eneste album",
"backup_info_card_assets": "elementer",
"cache_settings_album_thumbnails": "Biblioteksminiaturebilleder ({} billeder og videoer)",
"cache_settings_album_thumbnails": "Biblioteksminiaturebilleder ({} elementer)",
"cache_settings_clear_cache_button": "Fjern cache",
"cache_settings_clear_cache_button_title": "Fjern appens cache. Dette vil i stor grad påvirke appens ydeevne indtil cachen er genopbygget.",
"cache_settings_image_cache_size": "Størrelse af billedecache ({} billeder og videoer)",
"cache_settings_image_cache_size": "Størrelse af billedecache ({} elementer)",
"cache_settings_statistics_album": "Biblioteksminiaturer",
"cache_settings_statistics_assets": "{} billeder og videoer ({})",
"cache_settings_statistics_assets": "{} elementer ({})",
"cache_settings_statistics_full": "Fulde billeder",
"cache_settings_statistics_shared": "Miniaturebilleder til delte albummer",
"cache_settings_statistics_thumbnail": "Miniaturebilleder",
"cache_settings_statistics_title": "Cacheforbrug",
"cache_settings_subtitle": "Håndter cache-adfærden for Immich-appen.",
"cache_settings_thumbnail_size": "Størrelse af miniaturebillede cache ({} billeder og videoer)",
"cache_settings_thumbnail_size": "Størrelse af miniaturebillede cache ({} elementer)",
"cache_settings_title": "Cache-indstillinger",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Tilføj til album",
"control_bottom_app_bar_album_info": "{} genstande",
"control_bottom_app_bar_album_info_shared": "{} genstande • Delt",
"control_bottom_app_bar_create_new_album": "Opret nyt album",
"control_bottom_app_bar_delete": "Slet",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Del",
"create_album_page_untitled": "Uden titel",
"create_shared_album_page_create": "Opret",
@@ -113,10 +125,14 @@
"experimental_settings_subtitle": "Brug på eget ansvar!",
"experimental_settings_title": "Eksperimentelle",
"favorites_page_title": "Favoritter",
"home_page_add_to_album_conflicts": "Tilføjede {added} billeder og videoer til album {album}. {failed} billeder og videoer er allerede i albummet.",
"home_page_add_to_album_success": "Tilføjede {added} billeder og videoer til album {album}.",
"home_page_add_to_album_conflicts": "Tilføjede {added} elementer til album {album}. {failed} elementer er allerede i albummet.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Tilføjede {added} elementer til album {album}.",
"home_page_building_timeline": "Bygger tidslinjen",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Hvis dette er din første gang i appen, bedes du vælge en backup af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albummer",
"library_page_favorites": "Favoritter",
"library_page_new_album": "Nyt album",
@@ -140,6 +156,12 @@
"login_form_password_hint": "kodeord",
"login_form_save_login": "Forbliv logget ind",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Log",
"profile_drawer_client_server_up_to_date": "Klient og server er ajour",
"profile_drawer_settings": "Indstillinger",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "Anbefalinger",
"select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album",
"select_user_for_sharing_page_share_suggestions": "Anbefalinger",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "Detaljeret visning indlæser miniaturebilleder først. Herefter indlæses mediumstørrelse forhåndsvisning af billedet (hvis dette er slået til), for til sidst at vise originalen (hvis dette er slået til).",
"setting_image_viewer_original_subtitle": "Slå indlæsning af originalbillede i fuld størrelse til (stort!). Deaktiver for at reducere dataforbruget (både på netværket og for enhedscache).",
"setting_image_viewer_original_title": "Indlæs originalbillede",
@@ -164,11 +188,11 @@
"setting_notifications_notify_minutes": "{} minutter",
"setting_notifications_notify_never": "aldrig",
"setting_notifications_notify_seconds": "{} sekunder",
"setting_notifications_single_progress_subtitle": "Detaljeret uploadstatus pr. billed og video",
"setting_notifications_single_progress_subtitle": "Detaljeret uploadstatus pr. element",
"setting_notifications_single_progress_title": "Vis detaljeret baggrundsuploadstatus",
"setting_notifications_subtitle": "Tilpas dine notifikationspræferencer",
"setting_notifications_title": "Notifikationer",
"setting_notifications_total_progress_subtitle": "Samlet uploadstatus (færdige/samlede billeder og videoer)",
"setting_notifications_total_progress_subtitle": "Samlet uploadstatus (færdige/samlet antal elementer)",
"setting_notifications_total_progress_title": "Vis samlet baggrundsuploadstatus",
"setting_pages_app_bar_settings": "Indstillinger",
"settings_require_restart": "Genstart venligst Immich for at anvende denne ændring",
@@ -188,7 +212,7 @@
"tab_controller_nav_search": "Søg",
"tab_controller_nav_sharing": "Deling",
"theme_setting_asset_list_storage_indicator_title": "Vis opbevaringsindikator på filer",
"theme_setting_asset_list_tiles_per_row_title": "Antal billeder og videoer per række ({})",
"theme_setting_asset_list_tiles_per_row_title": "Antal elementer per række ({})",
"theme_setting_dark_mode_switch": "Mørk tilstand",
"theme_setting_image_viewer_quality_subtitle": "Juster kvaliteten i billedfremviseren",
"theme_setting_image_viewer_quality_title": "Billedfremviserkvalitet",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
"album_info_card_backup_album_included": "EINGESCHLOSSEN",
"album_thumbnail_card_item": "1 Element",
@@ -87,11 +89,21 @@
"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",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_create_new_album": "Neues Album erstellen",
"control_bottom_app_bar_delete": "Löschen",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Teilen",
"create_album_page_untitled": "Unbenannt",
"create_shared_album_page_create": "Erstellen",
@@ -114,9 +126,13 @@
"experimental_settings_title": "Experimentell",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Alben",
"library_page_favorites": "Favorites",
"library_page_new_album": "Neues Album",
@@ -140,6 +156,12 @@
"login_form_password_hint": "password",
"login_form_save_login": "Angemeldet bleiben",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
"profile_drawer_settings": "Einstellungen",
@@ -153,6 +175,8 @@
"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": "Suggestions",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
"setting_image_viewer_original_title": "Load original image",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "EXCLUDED",
"album_info_card_backup_album_included": "INCLUDED",
"album_thumbnail_card_item": "1 item",
@@ -87,11 +89,21 @@
"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",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_delete": "Delete",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Share",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
@@ -114,9 +126,13 @@
"experimental_settings_title": "Experimental",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albums",
"library_page_favorites": "Favorites",
"library_page_new_album": "New album",
@@ -140,6 +156,12 @@
"login_form_password_hint": "password",
"login_form_save_login": "Stay logged in",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_settings": "Settings",
@@ -153,6 +175,8 @@
"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",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
"setting_image_viewer_original_title": "Load original image",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "EXCLUIDOS",
"album_info_card_backup_album_included": "INCLUIDOS",
"album_thumbnail_card_item": "1 item",
@@ -12,10 +14,10 @@
"album_viewer_appbar_share_leave": "Abandonar álbum ",
"album_viewer_appbar_share_remove": "Eliminar del álbum ",
"album_viewer_page_share_add_users": "Añadir usuarios",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_layout_settings_dynamic_layout_title": "Disposición dinámica",
"asset_list_layout_settings_group_by": "Agrupar recursos por",
"asset_list_layout_settings_group_by_month": "Mes",
"asset_list_layout_settings_group_by_month_day": "Mes + día",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
@@ -87,11 +89,21 @@
"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",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Añadir al álbum",
"control_bottom_app_bar_album_info": "{} elementos",
"control_bottom_app_bar_album_info_shared": "{} elementos · Compartido",
"control_bottom_app_bar_create_new_album": "Crear nuevo álbum",
"control_bottom_app_bar_delete": "Eliminar",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Share",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
@@ -112,24 +124,28 @@
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_title": "Experimental",
"favorites_page_title": "Favorites",
"favorites_page_title": "Favoritos",
"home_page_add_to_album_conflicts": "Añadidos {added} elementos al álbum {album}. {failed} elementos ya están añadidos.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Añadidos {added} elementos al álbum {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_building_timeline": "Construyendo la línea de tiempo",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albums",
"library_page_favorites": "Favorites",
"library_page_favorites": "Favoritos",
"library_page_new_album": "New album",
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"library_page_sharing": "Compartiendo",
"library_page_sort_created": "Creado más recientemente",
"library_page_sort_title": "Título del álbum",
"login_form_button_text": "Iniciar Sesión",
"login_form_email_hint": "tucorreo@correo.com",
"login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto/api",
"login_form_endpoint_url": "URL del servidor",
"login_form_err_http": "Por favor, especifique http:// o https://",
"login_form_err_invalid_email": "Correo electrónico no válido",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_invalid_url": "URL no válida",
"login_form_err_leading_whitespace": "Espacio en blanco inicial",
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
@@ -140,6 +156,12 @@
"login_form_password_hint": "contraseña",
"login_form_save_login": "Mantener la sesión iniciada",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
"profile_drawer_settings": "Settings",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "Sugerencias",
"select_user_for_sharing_page_err_album": "Fallo al crear el álbum",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Habilitar para cargar la imagen en resolución original (¡muy grande!). Deshabilitar para reducir el consumo de datos (de red y caché).",
"setting_image_viewer_original_title": "Cargar imagen original",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "JÄTETTY POIS",
"album_info_card_backup_album_included": "SISÄLLYTETTY",
"album_thumbnail_card_item": "1 kohde",
@@ -12,10 +14,10 @@
"album_viewer_appbar_share_leave": "Poistu albumista",
"album_viewer_appbar_share_remove": "Poista albumista",
"album_viewer_page_share_add_users": "Lisää käyttäjiä",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_layout_settings_dynamic_layout_title": "Dynaaminen asetelma",
"asset_list_layout_settings_group_by": "Ryhmittele",
"asset_list_layout_settings_group_by_month": "Kuukauden mukaan",
"asset_list_layout_settings_group_by_month_day": "Kuukauden ja päivän mukaan",
"asset_list_settings_subtitle": "Kuvaruudukon asettelu",
"asset_list_settings_title": "Kuvaruudukko",
"backup_album_selection_page_albums_device": "Laitteen albumit ({})",
@@ -87,11 +89,21 @@
"cache_settings_subtitle": "Hallitse Immich-mobiilisovelluksen välimuistin käyttöä",
"cache_settings_thumbnail_size": "Esikatselukuvien välimuistin koko ({} kohdetta)",
"cache_settings_title": "Välimuistin asetukset",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Lisää albumiin",
"control_bottom_app_bar_album_info": "{} kohdetta",
"control_bottom_app_bar_album_info_shared": "{} kohdetta · Jaettu",
"control_bottom_app_bar_create_new_album": "Luo uusi albumi",
"control_bottom_app_bar_delete": "Poista",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Jaa",
"create_album_page_untitled": "Nimetön",
"create_shared_album_page_create": "Luo",
@@ -112,24 +124,28 @@
"experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko",
"experimental_settings_subtitle": "Käyttö omalla vastuulla!",
"experimental_settings_title": "Kokeellinen",
"favorites_page_title": "Favorites",
"favorites_page_title": "Suosikit",
"home_page_add_to_album_conflicts": "Lisätty {added} kohdetta albumiin {album}. {failed} kohdetta on jo albumissa.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Lisätty {added} kohdetta albumiin {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_building_timeline": "Rakennetaan aikajanaa",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Jos käytät sovellusta ensimmäistä kertaa, muista valita varmuuskopioitavat albumi(t), jotta aikajanalla voi olla kuvia ja videoita.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albumit",
"library_page_favorites": "Favorites",
"library_page_favorites": "Suosikit",
"library_page_new_album": "Uusi albumi",
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"library_page_sharing": "Jakaminen",
"library_page_sort_created": "Viimeisin luotu",
"library_page_sort_title": "Albumin otsikko",
"login_form_button_text": "Kirjaudu",
"login_form_email_hint": "sahkopostisi@esimerkki.fi",
"login_form_endpoint_hint": "http://palvelimesi-osoite:portti/api",
"login_form_endpoint_url": "Palvelimen URL",
"login_form_err_http": "Lisää http:// tai https://",
"login_form_err_invalid_email": "Virheellinen sähköpostiosoite",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_invalid_url": "Virheellinen URL",
"login_form_err_leading_whitespace": "Alussa välilyönti",
"login_form_err_trailing_whitespace": "Lopussa välilyönti",
"login_form_failed_get_oauth_server_config": "Virhe kirjauduttaessa OAuth:lla, tarkista palvelimen URL",
@@ -140,6 +156,12 @@
"login_form_password_hint": "salasana",
"login_form_save_login": "Pysy kirjautuneena",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Lokit",
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
"profile_drawer_settings": "Asetukset",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "Ehdotukset",
"select_user_for_sharing_page_err_album": "Albumin luonti epäonnistui",
"select_user_for_sharing_page_share_suggestions": "Ehdotukset",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "Sovellus lataa ensin pienen esikatselukuvan, toisena keskitarkkuuksisen kuvan (jos käytössä) ja kolmantena alkuperäisen täysitarkkuuksisen kuvan (jos käytössä)",
"setting_image_viewer_original_subtitle": "Ota käyttöön ladataksesi alkuperäinen täysitarkkuuksinen kuva (suuri!). Poista käytöstä vähentääksesi datan käyttöä (sekä verkossa että laitteen välimuistissa).",
"setting_image_viewer_original_title": "Lataa alkuperäinen kuva",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "EXCLU",
"album_info_card_backup_album_included": "INCLUS",
"album_thumbnail_card_item": "1 élément",
@@ -12,10 +14,10 @@
"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_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_layout_settings_dynamic_layout_title": "Affichage dynamique",
"asset_list_layout_settings_group_by": "Grouper les éléments par",
"asset_list_layout_settings_group_by_month": "Mois",
"asset_list_layout_settings_group_by_month_day": "Mois + jour",
"asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos",
"asset_list_settings_title": "Grille de photos",
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
@@ -87,11 +89,21 @@
"cache_settings_subtitle": "Contrôler le comportement de mise en cache de l'application mobile Immich",
"cache_settings_thumbnail_size": "Taille du cache des miniatures ({} éléments)",
"cache_settings_title": "Paramètres de mise en cache",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Ajouter à l'album",
"control_bottom_app_bar_album_info": "{} éléments",
"control_bottom_app_bar_album_info_shared": "{} éléments - Partagés",
"control_bottom_app_bar_create_new_album": "Créer un nouvel album",
"control_bottom_app_bar_delete": "Supprimer",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Partager",
"create_album_page_untitled": "Sans titre",
"create_shared_album_page_create": "Créer",
@@ -112,17 +124,21 @@
"experimental_settings_new_asset_list_title": "Activez la grille de photos expérimentale",
"experimental_settings_subtitle": "Utilisez à vos dépends !",
"experimental_settings_title": "Expérimental",
"favorites_page_title": "Favorites",
"favorites_page_title": "Favoris",
"home_page_add_to_album_conflicts": "{added} éléments ajoutés à l'album {album}. Les éléments {failed} sont déjà dans l'album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "{added} éléments ajoutés à l'album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_building_timeline": "Construction de la chronologie",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albums",
"library_page_favorites": "Favorites",
"library_page_favorites": "Favoris",
"library_page_new_album": "Nouvel album",
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"library_page_sharing": "Partage",
"library_page_sort_created": "Créations les plus récentes",
"library_page_sort_title": "Titre de l'album",
"login_form_button_text": "Connexion",
"login_form_email_hint": "votreemail@email.com",
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
@@ -140,6 +156,12 @@
"login_form_password_hint": "mot de passe",
"login_form_save_login": "Rester connecté",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Journaux",
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
"profile_drawer_settings": "Paramètres",
@@ -153,8 +175,10 @@
"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",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "Le visualiseur de détails charge d'abord la petite miniature, puis l'aperçu de taille moyenne (s'il est activé), enfin l'original (s'il est activé).",
"setting_image_viewer_original_subtitle": "Activez cette option pour charger l'image originale en résolution originale (volumineux !). Désactiver pour réduire l'utilisation des données (réseau et cache de l'appareil).",
"setting_image_viewer_original_subtitle": "Activez cette option pour charger l'image en résolution originale (volumineux !). Désactiver pour réduire l'utilisation des données (réseau et cache de l'appareil).",
"setting_image_viewer_original_title": "Charger l'image originale",
"setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la miniature.",
"setting_image_viewer_preview_title": "Charger l'image d'aperçu",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "ESCLUSI",
"album_info_card_backup_album_included": "INCLUSI",
"album_thumbnail_card_item": "1 elemento ",
@@ -87,11 +89,21 @@
"cache_settings_subtitle": "Controlla il comportamento della cache dell'applicazione mobile immich",
"cache_settings_thumbnail_size": "Dimensione cache anteprime ({} assets)",
"cache_settings_title": "Impostazioni della Cache",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Aggiungi all'album",
"control_bottom_app_bar_album_info": "{} elementi",
"control_bottom_app_bar_album_info_shared": "{} elementi · Condivisi",
"control_bottom_app_bar_create_new_album": "Crea nuovo album",
"control_bottom_app_bar_delete": "Elimina",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Condividi",
"create_album_page_untitled": "Senza titolo",
"create_shared_album_page_create": "Crea",
@@ -114,9 +126,13 @@
"experimental_settings_title": "Sperimentale",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "Aggiunti {added} elementi all'album {album}. {failed} elementi erano già presenti nell'album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Aggiunti {added} elementi all'album {album}",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Album",
"library_page_favorites": "Favorites",
"library_page_new_album": "Nuovo Album",
@@ -140,6 +156,12 @@
"login_form_password_hint": "password ",
"login_form_save_login": "Rimani connesso ",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
"profile_drawer_settings": "Impostazioni ",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "Suggerimenti ",
"select_user_for_sharing_page_err_album": "Impossibile nel creare l'album ",
"select_user_for_sharing_page_share_suggestions": "Suggerimenti",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "Il visualizzatore dettagliato carica una piccola thumbnail per prima, per poi caricare un immagine di media grandezza (se abilitato). Ed infine carica l'originale (se abilitato).",
"setting_image_viewer_original_subtitle": "Abilita per caricare l'immagine originale a risoluzione massima (grande!). Disabilita per ridurre l'utilizzo di banda (sia sul network che nella cache del dispositivo).",
"setting_image_viewer_original_title": "Carica l'immagine originale",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "除外中",
"album_info_card_backup_album_included": "選択中",
"album_thumbnail_card_item": "項目数: 1",
@@ -87,11 +89,21 @@
"cache_settings_subtitle": "キャッシュの動作を変更できるよ",
"cache_settings_thumbnail_size": "サムネイルのキャッシュのサイズ ({}枚)",
"cache_settings_title": "キャッシュの設定",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_delete": "削除",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "共有",
"create_album_page_untitled": "タイトル無し",
"create_shared_album_page_create": "作成",
@@ -114,9 +126,13 @@
"experimental_settings_title": "試験的",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "アルバム",
"library_page_favorites": "Favorites",
"library_page_new_album": "新しいアルバム",
@@ -140,6 +156,12 @@
"login_form_password_hint": "パスワード",
"login_form_save_login": "ログインしたままにする",
"monthly_title_text_date_format": "yyyy年 MM月",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "サーバーとクライアント、両方最新バージョンだよ",
"profile_drawer_settings": "設定",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "ユーザーリスト",
"select_user_for_sharing_page_err_album": "アルバム作成に失敗...",
"select_user_for_sharing_page_share_suggestions": "ユーザーの一覧",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
"setting_image_viewer_original_title": "Load original image",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "UITGESLOTEN",
"album_info_card_backup_album_included": "INGESLOTEN",
"album_thumbnail_card_item": "1 item",
@@ -12,10 +14,10 @@
"album_viewer_appbar_share_leave": "Verlaat album",
"album_viewer_appbar_share_remove": "Verwijder uit album",
"album_viewer_page_share_add_users": "Gebruikers toevoegen",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_layout_settings_dynamic_layout_title": "Dynamische layout",
"asset_list_layout_settings_group_by": "Groupeer items per",
"asset_list_layout_settings_group_by_month": "Maand",
"asset_list_layout_settings_group_by_month_day": "Maand + dag",
"asset_list_settings_subtitle": "Foto grid layout instellingen",
"asset_list_settings_title": "Foto Grid",
"backup_album_selection_page_albums_device": "Albums op apparaat ({})",
@@ -87,11 +89,21 @@
"cache_settings_subtitle": "Beheer het cachegedrag van de Immich app",
"cache_settings_thumbnail_size": "Thumbnail cachegrootte ({} items)",
"cache_settings_title": "Cache instellingen",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Toevoegen aan album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Gedeeld",
"control_bottom_app_bar_create_new_album": "Maak nieuw album",
"control_bottom_app_bar_delete": "Verwijderen",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Delen",
"create_album_page_untitled": "Naamloos",
"create_shared_album_page_create": "Aanmaken",
@@ -114,9 +126,13 @@
"experimental_settings_title": "Experimenteel",
"favorites_page_title": "Favorieten",
"home_page_add_to_album_conflicts": "{added} items toegevoegd aan album {album}. {failed} items staan al in het album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "{added} items toegevoegd aan album {album}.",
"home_page_building_timeline": "Tijdlijn opbouwen",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albums",
"library_page_favorites": "Favorieten",
"library_page_new_album": "Nieuw album",
@@ -140,8 +156,14 @@
"login_form_password_hint": "wachtwoord",
"login_form_save_login": "Ingelogd blijven",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Logboek",
"profile_drawer_client_server_up_to_date": "Client en server zijn up-to-date",
"profile_drawer_client_server_up_to_date": "App en server zijn up-to-date",
"profile_drawer_settings": "Instellingen",
"profile_drawer_sign_out": "Uitloggen",
"search_bar_hint": "Zoeken naar foto's",
@@ -153,6 +175,8 @@
"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",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "De gedetailleerde weergave laadt eerst de kleine thumbnail, vervolgens het middelgrote voorbeeld (indien ingeschakeld) en ten slotte het origineel (indien ingeschakeld).",
"setting_image_viewer_original_subtitle": "Inschakelen om de originele afbeelding met volledige resolutie (groot!) te laden. Uitschakelen om datagebruik te verminderen (zowel netwerk- als apparaatcache).",
"setting_image_viewer_original_title": "Originele afbeelding laden",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "WYKLUCZONE",
"album_info_card_backup_album_included": "WŁĄCZONE",
"album_thumbnail_card_item": "1 pozycja",
@@ -87,11 +89,21 @@
"cache_settings_subtitle": "Kontrolowanie zachowania buforowania aplikacji mobilnej Immich",
"cache_settings_thumbnail_size": "Rozmiar pamięci podręcznej miniatur ({} zasobów)",
"cache_settings_title": "Ustawienia Buforowania",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_delete": "Usuń",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Udostępnij",
"create_album_page_untitled": "Bez tytułu",
"create_shared_album_page_create": "Utwórz",
@@ -114,9 +126,13 @@
"experimental_settings_title": "Experimental",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albumy",
"library_page_favorites": "Favorites",
"library_page_new_album": "Nowy album",
@@ -140,6 +156,12 @@
"login_form_password_hint": "hasło",
"login_form_save_login": "Pozostań zalogowany",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne",
"profile_drawer_settings": "Ustawienia",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "Propozycje",
"select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu",
"select_user_for_sharing_page_share_suggestions": "Propozycje",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
"setting_image_viewer_original_title": "Load original image",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "ИСКЛЮЧЕН",
"album_info_card_backup_album_included": "ВКЛЮЧЕН",
"album_thumbnail_card_item": "1 объект",
@@ -12,10 +14,10 @@
"album_viewer_appbar_share_leave": "Покинуть альбом",
"album_viewer_appbar_share_remove": "Удалить из альбома",
"album_viewer_page_share_add_users": "Добавить пользователей",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_layout_settings_dynamic_layout_title": "Динамическое расположение",
"asset_list_layout_settings_group_by": "Группировать объекты по",
"asset_list_layout_settings_group_by_month": "месяцу",
"asset_list_layout_settings_group_by_month_day": "месяцу и дню",
"asset_list_settings_subtitle": "Настройки макета сетки фотографий",
"asset_list_settings_title": "Сетка фотографий",
"backup_album_selection_page_albums_device": "Альбомов на устройстве ({})",
@@ -87,20 +89,30 @@
"cache_settings_subtitle": "Управление кэшированием мобильного приложения Immich",
"cache_settings_thumbnail_size": "Размер кэша эскизов ({} объектов)",
"cache_settings_title": "Настройки кэширования",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Добавить в альбом",
"control_bottom_app_bar_album_info": "{} файлов",
"control_bottom_app_bar_album_info_shared": "{} файлов · Общий",
"control_bottom_app_bar_create_new_album": "\nСоздать новый альбом",
"control_bottom_app_bar_delete": "Удалить",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Поделиться",
"create_album_page_untitled": "Без названия",
"create_shared_album_page_create": "Создать",
"create_shared_album_page_share": "Поделиться",
"create_shared_album_page_share_add_assets": "ДОБАВИТЬ ОБЪЕКТЫ",
"create_shared_album_page_share_select_photos": "Выберите фотографии",
"daily_title_text_date": "ДН, МММ дд",
"daily_title_text_date_year": "ДН, МММ дд, гггг",
"date_format": "ДН, LLL д, гч:мм a",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, yh:mm a",
"delete_dialog_alert": "Эти объекты будут безвозвратно удалены из приложения, а также с вашего устройства",
"delete_dialog_cancel": "Отменить",
"delete_dialog_ok": "Удалить",
@@ -114,9 +126,13 @@
"experimental_settings_title": "Экспериментальные функции",
"favorites_page_title": "Избранное",
"home_page_add_to_album_conflicts": "Добавлено {added} объектов в альбом {album}. Объекты {failed} уже есть в альбоме.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Добавлено {added} объектов в альбом {album}.",
"home_page_building_timeline": "Построение временной шкалы",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Если вы используете приложение впервые, убедитесь, что вы выбрали резервный(е) альбом(ы), чтобы временная шкала могла заполнить фотографии и видео в альбоме(ах).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Альбомы",
"library_page_favorites": "Избранное",
"library_page_new_album": "Новый альбом",
@@ -139,7 +155,13 @@
"login_form_label_password": "Пароль",
"login_form_password_hint": "пароль",
"login_form_save_login": "Оставаться в системе",
"monthly_title_text_date_format": "ММММ г",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Журналы",
"profile_drawer_client_server_up_to_date": "Клиент и сервер обновлены",
"profile_drawer_settings": "Настройки",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "Предложения",
"select_user_for_sharing_page_err_album": "\nНе удалось создать альбом",
"select_user_for_sharing_page_share_suggestions": "Предложения",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "Средство просмотра деталей сначала загружает маленькую миниатюру, затем загружает предварительный просмотр среднего размера (если включено) и, наконец, загружает оригинал (если включено).",
"setting_image_viewer_original_subtitle": "Включите загрузку оригинального изображения в полном разрешении (большое!). Отключите, чтобы уменьшить объем данных (как в сети, так и в кеше устройства).",
"setting_image_viewer_original_title": "Загрузить исходное изображение",

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "VYLÚČENÉ",
"album_info_card_backup_album_included": "ZAHRNUTÉ",
"album_thumbnail_card_item": "1 položka",
@@ -87,11 +89,21 @@
"cache_settings_subtitle": "Ovládanie správania mobilnej aplikácie Immich v medzipamäti",
"cache_settings_thumbnail_size": "Veľkosť vyrovnávacej pamäte náhľadov (položiek {})",
"cache_settings_title": "Nastavenia vyrovnávacej pamäte",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Pridať do albumu",
"control_bottom_app_bar_album_info": "{} položky",
"control_bottom_app_bar_album_info_shared": "{} položky - zdieľané",
"control_bottom_app_bar_create_new_album": "Vytvoriť nový album",
"control_bottom_app_bar_delete": "Vymazať",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Zdieľať",
"create_album_page_untitled": "Bez názvu",
"create_shared_album_page_create": "Vytvoriť",
@@ -114,9 +126,13 @@
"experimental_settings_title": "Experimentálne",
"favorites_page_title": "Obľúbené",
"home_page_add_to_album_conflicts": "Pridané {added} položky do albumu {album}. {failed} položky sú už v albume.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Pridané {added} položky do albumu {album}.",
"home_page_building_timeline": "Vytváranie časovej osi",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Ak aplikáciu používate prvýkrát, nezabudnite si vybrať zálohované albumy, aby sa na časovej osi mohli nachádzať fotografie a videá z vybraných albumoch.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albumy",
"library_page_favorites": "Obľúbené",
"library_page_new_album": "Nový album",
@@ -140,6 +156,12 @@
"login_form_password_hint": "heslo",
"login_form_save_login": "Zostať prihlásený",
"monthly_title_text_date_format": "LLLL y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Logy",
"profile_drawer_client_server_up_to_date": "Klient a server sú aktuálne",
"profile_drawer_settings": "Nastavenia",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "Návrhy",
"select_user_for_sharing_page_err_album": "Nepodarilo sa vytvoriť album",
"select_user_for_sharing_page_share_suggestions": "Návrhy",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "V prehliadači detailov sa najprv načíta malá miniatúra, potom sa načíta náhľad strednej veľkosti (ak je povolený) a nakoniec sa načíta originál (ak je povolený).",
"setting_image_viewer_original_subtitle": "Umožňuje načítať pôvodný obrázok v plnom rozlíšení (veľký!). Zakázať pre zníženie používania dát (v sieti aj v medzipamäti zariadenia).",
"setting_image_viewer_original_title": "Načítať pôvodný obrázok",

View File

@@ -0,0 +1,230 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "EXKLUDERAD",
"album_info_card_backup_album_included": "INKLUDERAD",
"album_thumbnail_card_item": "1 objekt",
"album_thumbnail_card_items": "{} objekt",
"album_thumbnail_card_shared": ". Delad",
"album_viewer_appbar_share_delete": "Radera album",
"album_viewer_appbar_share_err_delete": "Kunde inte radera album",
"album_viewer_appbar_share_err_leave": "Kunde inte lämna album",
"album_viewer_appbar_share_err_remove": "Kunde inte ta bort objekt från album",
"album_viewer_appbar_share_err_title": "Kunde inte ändra albumtitel",
"album_viewer_appbar_share_leave": "Lämna album",
"album_viewer_appbar_share_remove": "Ta bort från album",
"album_viewer_page_share_add_users": "Lägg till användare",
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout",
"asset_list_layout_settings_group_by": "Gruppera bilder efter",
"asset_list_layout_settings_group_by_month": "Månad",
"asset_list_layout_settings_group_by_month_day": "Månad + dag",
"asset_list_settings_subtitle": "Layoutinställningar för bildrutnät",
"asset_list_settings_title": "Bildrutnät",
"backup_album_selection_page_albums_device": "Album på enhet ({})",
"backup_album_selection_page_albums_tap": "Tryck en gång för att inkludera, tryck två gånger för att exkludera",
"backup_album_selection_page_assets_scatter": "Objekt kan vara utspridda över flera album. Därför kan album inkluderas eller exkluderas under säkerhetskopieringsprocessen",
"backup_album_selection_page_select_albums": "Välj album",
"backup_album_selection_page_selection_info": "Info om valda objekt",
"backup_album_selection_page_total_assets": "Antal unika objekt",
"backup_all": "Allt",
"backup_background_service_backup_failed_message": "Säkerhetskopiering av foton och videor misslyckades. Försöker igen...",
"backup_background_service_connection_failed_message": "Anslutningen till servern förlorades. Försöker igen...",
"backup_background_service_current_upload_notification": "Laddar upp {}",
"backup_background_service_default_notification": "Söker efter nya objekt...",
"backup_background_service_error_title": "Fel i säkerhetskopiering",
"backup_background_service_in_progress_notification": "Säkerhetskopierar dina foton och videor...",
"backup_background_service_upload_failure_notification": "Kunde inte ladda upp {}",
"backup_controller_page_albums": "Säkerhetskopiera album",
"backup_controller_page_background_battery_info_link": "Visa mig hur",
"backup_controller_page_background_battery_info_message": "För optimal säkerhetskopiering i bakgrunden bör du stänga av batterioptimering som begränsar bakgrundsaktivitet för Immich.\n\nEftersom detta är enhetsspecifikt så bör du söka instruktioner från din enhetstillverkare.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Batterioptimering",
"backup_controller_page_background_charging": "Endast vid laddning",
"backup_controller_page_background_configure_error": "Kunde inte konfigurera bakgrundstjänsten",
"backup_controller_page_background_delay": "Skjut upp säkerhetskopiering av nya foton och videor: {}",
"backup_controller_page_background_description": "Aktivera säkerhetskopiering i bakgrunden för att automatiskt säkerhetskopiera nya foton och videor utan att öppna appen",
"backup_controller_page_background_is_off": "Automatisk säkerhetskopiering i bakgrunden är avstängd",
"backup_controller_page_background_is_on": "Automatisk säkerhetskopiering i bakgrunden är aktiverad",
"backup_controller_page_background_turn_off": "Stäng av säkerhetskopiering i bakgrunden",
"backup_controller_page_background_turn_on": "Aktivera säkerhetskopiering i bakgrunden",
"backup_controller_page_background_wifi": "Endast med WiFi",
"backup_controller_page_backup": "Säkerhetskopiera",
"backup_controller_page_backup_selected": "Valt: ",
"backup_controller_page_backup_sub": "Säkerhetskopierade foton och videor",
"backup_controller_page_cancel": "Avbryt",
"backup_controller_page_created": "Skapad den: {}",
"backup_controller_page_desc_backup": "Aktivera förgrunds-säkerhetskopiering för att automatiskt ladda upp nya foton och videor när du öppnar appen.",
"backup_controller_page_excluded": "Exkluderat: ",
"backup_controller_page_failed": "Misslyckades ({})",
"backup_controller_page_filename": "Filnamn: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Säkerhetskopieringsinformation",
"backup_controller_page_none_selected": "Ingenting valt",
"backup_controller_page_remainder": "Resterande",
"backup_controller_page_remainder_sub": "Återstående foton och album att säkerhetskopiera från valda",
"backup_controller_page_select": "Välj",
"backup_controller_page_server_storage": "Serverlagring",
"backup_controller_page_start_backup": "Starta säkerhetskopiering",
"backup_controller_page_status_off": "Automatisk säkerhetskopiering är avstängd",
"backup_controller_page_status_on": "Automatisk säkerhetskopiering är aktiverad",
"backup_controller_page_storage_format": "{} av {} brukat",
"backup_controller_page_to_backup": "Album att säkerhetskopiera",
"backup_controller_page_total": "Sammanlagt",
"backup_controller_page_total_sub": "Alla unika foton och videor från valda album",
"backup_controller_page_turn_off": "Stäng av automatisk säkerhetskopiering",
"backup_controller_page_turn_on": "Aktivera automatisk säkerhetskopiering",
"backup_controller_page_uploading_file_info": "Laddar upp filinformation",
"backup_err_only_album": "Kan inte ta bort det enda albumet",
"backup_info_card_assets": "objekt",
"cache_settings_album_thumbnails": "Miniatyrbilder för bibliotek ({} bilder och videor)",
"cache_settings_clear_cache_button": "Rensa cacheminnet",
"cache_settings_clear_cache_button_title": "Rensar appens cacheminne. Detta kommer att avsevärt påverka appens prestanda tills cachen har byggts om.",
"cache_settings_image_cache_size": "Cacheminnets storlek ({} bilder och videor)",
"cache_settings_statistics_album": "Miniatyrbilder för bibliotek",
"cache_settings_statistics_assets": "{} bilder och videor ({})",
"cache_settings_statistics_full": "Hela bilder",
"cache_settings_statistics_shared": "Miniatyrbilder till delat album",
"cache_settings_statistics_thumbnail": "Miniatyrbilder",
"cache_settings_statistics_title": "Cacheförbrukning",
"cache_settings_subtitle": "Hantera cachebeteendet för Immich-appen.",
"cache_settings_thumbnail_size": "Storlek på cacheminnet ({} bilder och videor)",
"cache_settings_title": "Cache Inställningar",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Lägg till i album",
"control_bottom_app_bar_album_info": "{} objekt",
"control_bottom_app_bar_album_info_shared": "{} objekt • Delat",
"control_bottom_app_bar_create_new_album": "Skapa nytt album",
"control_bottom_app_bar_delete": "Radera",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Dela",
"create_album_page_untitled": "Namnlös",
"create_shared_album_page_create": "Skapa",
"create_shared_album_page_share": "Dela",
"create_shared_album_page_share_add_assets": "LÄGG TILL OBJEKT",
"create_shared_album_page_share_select_photos": "Välj bilder",
"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": "Dessa objekt kommer att raderas permanent från Immich och din enhet",
"delete_dialog_cancel": "Avbryt",
"delete_dialog_ok": "Radera",
"delete_dialog_title": "Radera permanent",
"exif_bottom_sheet_description": "Lägg till beskrivning...",
"exif_bottom_sheet_details": "DETALJER",
"exif_bottom_sheet_location": "PLATS",
"experimental_settings_new_asset_list_subtitle": "Under uppbyggnad",
"experimental_settings_new_asset_list_title": "Aktivera experimentellt fotorutnät",
"experimental_settings_subtitle": "Använd på egen risk!",
"experimental_settings_title": "Experimentellt",
"favorites_page_title": "Favoriter",
"home_page_add_to_album_conflicts": "Lade till {added} foton och videor i albumet {album}. {failed} foton och videor finns redan i albumet.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Lade till {added} foton och videor i albumet {album}.",
"home_page_building_timeline": "Bygger tidslinjen",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Om det här är första gången du använder appen, välj ett eller flera backup-album så att tidslinjen kan fyllas med foton och videor från albumen.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Album",
"library_page_favorites": "Favoriter",
"library_page_new_album": "Nytt album",
"library_page_sharing": "Delas",
"library_page_sort_created": "Senast skapad",
"library_page_sort_title": "Albumtitel",
"login_form_button_text": "Logga in",
"login_form_email_hint": "din.email@email.com",
"login_form_endpoint_hint": "http://din-server-ip:port/api",
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Var god ange http:// eller https://",
"login_form_err_invalid_email": "Ogiltig email",
"login_form_err_invalid_url": "Ogiltig webbadress",
"login_form_err_leading_whitespace": "Mellanrum före",
"login_form_err_trailing_whitespace": "Mellanrum efter",
"login_form_failed_get_oauth_server_config": "Kunde inte logga in med OAuth. Kontrollera serverns webbadress",
"login_form_failed_get_oauth_server_disable": "OAuth är inte tillgänglig på den här servern",
"login_form_failed_login": "Kunde inte logga in. Kontrollera serverns webbadress, email och lösenord.",
"login_form_label_email": "Email",
"login_form_label_password": "Lösenord",
"login_form_password_hint": "lösenord",
"login_form_save_login": "Håll mig inloggad",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "Loggar",
"profile_drawer_client_server_up_to_date": "Klient och server är uppdaterade",
"profile_drawer_settings": "Inställningar",
"profile_drawer_sign_out": "Logga ut",
"search_bar_hint": "Sök bland dina foton",
"search_page_no_objects": "Inga objekt är tillgängliga",
"search_page_no_places": "Ingen platsinformation finns tillgänglig",
"search_page_places": "Platser",
"search_page_things": "Saker",
"search_result_page_new_search_hint": "Ny sökning",
"select_additional_user_for_sharing_page_suggestions": "Förslag",
"select_user_for_sharing_page_err_album": "Kunde inte skapa nytt album",
"select_user_for_sharing_page_share_suggestions": "Förslag",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "Detaljerad vy laddar miniatyrer först. Efter detta laddas den medelstora förhandsgranskningen av bilden (om detta är aktiverat), och visar slutligen originalet (om detta är aktiverat).",
"setting_image_viewer_original_subtitle": "Aktivera för att ladda originalbilden i full storlek (stor!). Inaktivera för att minska dataanvändningen (både i nätverket och för enhetscache).",
"setting_image_viewer_original_title": "Ladda originalbilden",
"setting_image_viewer_preview_subtitle": "Aktivera för att ladda en mellanstor bild. Stäng av för att antingen ladda originalet direkt eller bara använda miniatyrbilden.",
"setting_image_viewer_preview_title": "Ladda förhandsgranskning av bild",
"setting_notifications_notify_failures_grace_period": "Rapportera säkerhetskopieringsfel i bakgrunden: {}",
"setting_notifications_notify_hours": "{} timmar",
"setting_notifications_notify_immediately": "omedelbart",
"setting_notifications_notify_minutes": "{} minuter",
"setting_notifications_notify_never": "aldrig",
"setting_notifications_notify_seconds": "{} sekunder",
"setting_notifications_single_progress_subtitle": "Detaljerad uppladdningsstatus per bild och video",
"setting_notifications_single_progress_title": "Visa detaljerat uppladdningsförlopp",
"setting_notifications_subtitle": "Anpassa dina notifikations-inställningar",
"setting_notifications_title": "Notifikationer",
"setting_notifications_total_progress_subtitle": "Övergripande uppladdningsförlopp (klar/totala tillgångar)",
"setting_notifications_total_progress_title": "Visa totalt uppladdningsförlopp",
"setting_pages_app_bar_settings": "Inställningar",
"settings_require_restart": "Starta om Immich för att tillämpa den här inställningen",
"share_add": "Lägg till",
"share_add_photos": "Lägg till foton",
"share_add_title": "Lägg till en titel",
"share_create_album": "Skapa album",
"share_dialog_preparing": "Förbereder...",
"share_invite": "Bjuder in till album",
"sharing_page_album": "Delade album",
"sharing_page_description": "Skapa delade album för att dela foton och video med personer i ditt nätverk.",
"sharing_page_empty_list": "TOM LISTA",
"sharing_silver_appbar_create_shared_album": "Skapa delat album",
"sharing_silver_appbar_share_partner": "Dela med partner",
"tab_controller_nav_library": "Bibliotek",
"tab_controller_nav_photos": "Bilder",
"tab_controller_nav_search": "Sök",
"tab_controller_nav_sharing": "Delning",
"theme_setting_asset_list_storage_indicator_title": "Visa lagringsindikator på filer",
"theme_setting_asset_list_tiles_per_row_title": "Antal bilder och videor per rad ({})",
"theme_setting_dark_mode_switch": "Mörkt läge",
"theme_setting_image_viewer_quality_subtitle": "Justera kvaliteten i bildvisaren",
"theme_setting_image_viewer_quality_title": "Bildvisarens kvalitet",
"theme_setting_system_theme_switch": "Automatisk (Följ systeminställningar)",
"theme_setting_theme_subtitle": "Välj inställning för appens tema",
"theme_setting_theme_title": "Tema",
"theme_setting_three_stage_loading_subtitle": "Trestegsladdning kan öka prestandan, men kan också leda till signifikant högre nätverksbelastning",
"theme_setting_three_stage_loading_title": "Aktivera trestegsladdning",
"version_announcement_overlay_ack": "Bekräfta",
"version_announcement_overlay_release_notes": "versionsinformation",
"version_announcement_overlay_text_1": "Hej vännen, det finns en ny version av",
"version_announcement_overlay_text_2": ". Ta gärna din tid att besöka ",
"version_announcement_overlay_text_3": " för att se till att din docker-compose och .env-fil är uppdaterad för att undvika felkonfiguration, speciellt om du använder WatchTower eller liknande mekanism som automatiskt uppdaterar din container",
"version_announcement_overlay_title": "Ny serverversion är tillgänglig \uD83C\uDF89"
}

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"album_info_card_backup_album_excluded": "排除",
"album_info_card_backup_album_included": "已选",
"album_thumbnail_card_item": "1张",
@@ -87,11 +89,21 @@
"cache_settings_subtitle": "控制 Immich的缓存表现",
"cache_settings_thumbnail_size": "缩略图缓存大小({}张)",
"cache_settings_title": "缓存设置",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "添加到相册",
"control_bottom_app_bar_album_info": "{}张",
"control_bottom_app_bar_album_info_shared": "{} 张已分享",
"control_bottom_app_bar_create_new_album": "新建相册",
"control_bottom_app_bar_delete": "删除",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "分享",
"create_album_page_untitled": "未命名",
"create_shared_album_page_create": "新建",
@@ -114,9 +126,13 @@
"experimental_settings_title": "实验功能",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "添加{added}张到相册{album}。{failed} 项已经处于该相册中。",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "添加了{added}张到相册{album}。",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "相册",
"library_page_favorites": "Favorites",
"library_page_new_album": "新建相册",
@@ -140,6 +156,12 @@
"login_form_password_hint": "密码",
"login_form_save_login": "保持登录",
"monthly_title_text_date_format": "MMMM y",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"profile_drawer_app_logs": "日志",
"profile_drawer_client_server_up_to_date": "客户端和服务端都是最新的",
"profile_drawer_settings": "设置",
@@ -153,6 +175,8 @@
"select_additional_user_for_sharing_page_suggestions": "建议",
"select_user_for_sharing_page_err_album": "创建相册失败",
"select_user_for_sharing_page_share_suggestions": "建议",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "查看大图时会首先加载缩略图,然后加载中等质量的图片(如果启用),最后加载原始质量的图片(如果启用)。",
"setting_image_viewer_original_subtitle": "开启将会加载原图。关闭将会减少内存和网络占用。",
"setting_image_viewer_original_title": "加载原图",

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
@@ -43,7 +45,6 @@ class ImmichTestHelper {
// Load main Widget
await tester.pumpWidget(app.getMainWidget(db));
// Post run tasks
await tester.pumpAndSettle();
await EasyLocalization.ensureInitialized();
}
}
@@ -62,3 +63,17 @@ void immichWidgetTest(
semanticsEnabled: false,
);
}
Future<void> pumpUntilFound(
WidgetTester tester,
Finder finder, {
Duration timeout = const Duration(seconds: 120),
}) async {
bool found = false;
final timer = Timer(timeout, () => throw TimeoutException("Pump until has timed out"));
while (found != true) {
await tester.pump();
found = tester.any(finder);
}
timer.cancel();
}

View File

@@ -2,33 +2,20 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'general_helper.dart';
class ImmichTestLoginHelper {
final WidgetTester tester;
ImmichTestLoginHelper(this.tester);
Future<void> waitForLoginScreen({int timeoutSeconds = 20}) async {
for (var i = 0; i < timeoutSeconds; i++) {
// Search for "IMMICH" test in the app bar
final result = find.text("IMMICH");
if (tester.any(result)) {
// Wait 5s until everything settled
await tester.pump(const Duration(seconds: 5));
return;
}
// Wait 1s before trying again
await Future.delayed(const Duration(seconds: 1));
}
fail("Timeout while waiting for login screen");
Future<void> waitForLoginScreen() async {
await pumpUntilFound(tester, find.text("Login"));
}
Future<bool> acknowledgeNewServerVersion() async {
await pumpUntilFound(tester, find.text("Acknowledge"));
final result = find.text("Acknowledge");
if (!tester.any(result)) {
return false;
}
await tester.tap(result);
await tester.pump();
@@ -43,17 +30,17 @@ class ImmichTestLoginHelper {
}) async {
final loginForms = find.byType(TextFormField);
await tester.pump(const Duration(milliseconds: 500));
await tester.enterText(loginForms.at(0), email);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.enterText(loginForms.at(1), password);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.enterText(loginForms.at(2), server);
await tester.pump();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
await tester.pump();
}
Future<void> enterCredentialsOf(LoginCredentials credentials) async {
@@ -65,32 +52,17 @@ class ImmichTestLoginHelper {
}
Future<void> pressLoginButton() async {
await pumpUntilFound(tester, find.textContaining("login_form_button_text".tr()));
final button = find.textContaining("login_form_button_text".tr());
await tester.tap(button);
}
Future<void> assertLoginSuccess({int timeoutSeconds = 15}) async {
for (var i = 0; i < timeoutSeconds * 2; i++) {
if (tester.any(find.text("home_page_building_timeline".tr()))) {
return;
}
await tester.pump(const Duration(milliseconds: 500));
}
fail("Login failed.");
await pumpUntilFound(tester, find.text("home_page_building_timeline".tr()));
}
Future<void> assertLoginFailed({int timeoutSeconds = 15}) async {
for (var i = 0; i < timeoutSeconds * 2; i++) {
if (tester.any(find.text("login_form_failed_login".tr()))) {
return;
}
await tester.pump(const Duration(milliseconds: 500));
}
fail("Timeout.");
await pumpUntilFound(tester, find.text("login_form_failed_login".tr()));
}
}

View File

@@ -37,5 +37,66 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
# Permission macros for the permission_handler (https://pub.dev/packages/permission_handler)
# Start of the permission_handler configuration
# Remove the # character in front of the permission you do want to use.
target.build_configurations.each do |config|
# You can enable the permissions needed here. For example to enable camera
# permission, just remove the `#` character in front so it looks like this:
#
# ## dart: PermissionGroup.camera
# 'PERMISSION_CAMERA=1'
#
# Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.calendar
# 'PERMISSION_EVENTS=1',
## dart: PermissionGroup.reminders
# 'PERMISSION_REMINDERS=1',
## dart: PermissionGroup.contacts
# 'PERMISSION_CONTACTS=1',
## dart: PermissionGroup.camera
# 'PERMISSION_CAMERA=1',
## dart: PermissionGroup.microphone
# 'PERMISSION_MICROPHONE=1',
## dart: PermissionGroup.speech
# 'PERMISSION_SPEECH_RECOGNIZER=1',
## dart: PermissionGroup.photos
# 'PERMISSION_PHOTOS=1',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
# 'PERMISSION_LOCATION=1',
## dart: PermissionGroup.notification
'PERMISSION_NOTIFICATIONS=1',
## dart: PermissionGroup.mediaLibrary
# 'PERMISSION_MEDIA_LIBRARY=1',
## dart: PermissionGroup.sensors
# 'PERMISSION_SENSORS=1',
## dart: PermissionGroup.bluetooth
# 'PERMISSION_BLUETOOTH=1',
## dart: PermissionGroup.appTrackingTransparency
# 'PERMISSION_APP_TRACKING_TRANSPARENCY=1',
## dart: PermissionGroup.criticalAlerts
# 'PERMISSION_CRITICAL_ALERTS=1'
]
end
# End of the permission_handler configuration
end
end
end

View File

@@ -24,6 +24,10 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- path_provider_ios (0.0.1):
- Flutter
- permission_handler_apple (9.0.4):
- Flutter
- photo_manager (2.0.0):
- Flutter
- FlutterMacOS
@@ -55,6 +59,8 @@ DEPENDENCIES:
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
@@ -90,6 +96,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
share_plus:
@@ -117,16 +127,18 @@ SPEC CHECKSUMS:
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
PODFILE CHECKSUM: c798208781ca5116c4a3d5927d689946791f0189
PODFILE CHECKSUM: 4a7e0475ea85ab7bf89955bc4c7ea9d18b54dfd8
COCOAPODS: 1.11.3

View File

@@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; };
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
@@ -34,6 +36,8 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = "<group>"; };
65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -80,6 +84,15 @@
name = Frameworks;
sourceTree = "<group>";
};
65DD438629917FAD0047FFA8 /* BackgroundSync */ = {
isa = PBXGroup;
children = (
65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */,
65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */,
);
path = BackgroundSync;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -113,6 +126,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
65DD438629917FAD0047FFA8 /* BackgroundSync */,
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
@@ -275,8 +289,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -362,8 +378,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
CURRENT_PROJECT_VERSION = 86;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -372,6 +387,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -498,8 +514,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
CURRENT_PROJECT_VERSION = 86;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -508,6 +523,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -526,8 +542,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
CURRENT_PROJECT_VERSION = 86;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -536,6 +551,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -43,11 +43,13 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
disableMainThreadChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@@ -1,13 +1,38 @@
import UIKit
import shared_preferences_foundation
import Flutter
import BackgroundTasks
import path_provider_ios
import photo_manager
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
GeneratedPluginRegistrant.register(with: self)
BackgroundServicePlugin.registerBackgroundProcessing()
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
if !registry.hasPlugin("org.cocoapods.path-provider-ios") {
FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!)
}
if !registry.hasPlugin("org.cocoapods.photo-manager") {
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
}
if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,305 @@
//
// BackgroundServicePlugin.swift
// Runner
//
// Created by Marty Fuhry on 2/14/23.
//
import Flutter
import BackgroundTasks
import path_provider_foundation
class BackgroundServicePlugin: NSObject, FlutterPlugin {
public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?
public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) {
flutterPluginRegistrantCallback = callback
}
// Pause the application in XCode, then enter
// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"]
// or
// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"]
// Then resume the application see the background code run
// Tested on a physical device, not a simulator
// This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing.
// In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command
// This is the task ID in Info.plist to register as our background task ID
public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch"
public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing"
// Establish communication with the main isolate and set up the channel call
// to this BackgroundServicePlugion()
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "immich/foregroundChannel",
binaryMessenger: registrar.messenger()
)
let instance = BackgroundServicePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addApplicationDelegate(instance)
}
// Registers the Flutter engine with the plugins, used by the other Background Flutter engine
public static func register(engine: FlutterEngine) {
GeneratedPluginRegistrant.register(with: engine)
}
// Registers the task IDs from the system so that we can process them here in this class
public static func registerBackgroundProcessing() {
let processingRegisterd = BGTaskScheduler.shared.register(
forTaskWithIdentifier: backgroundProcessingTaskID,
using: nil) { task in
if task is BGProcessingTask {
handleBackgroundProcessing(task: task as! BGProcessingTask)
}
}
let fetchRegisterd = BGTaskScheduler.shared.register(
forTaskWithIdentifier: backgroundFetchTaskID,
using: nil) { task in
if task is BGAppRefreshTask {
handleBackgroundFetch(task: task as! BGAppRefreshTask)
}
}
}
// Handles the channel methods from Flutter
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "enable":
handleBackgroundEnable(call: call, result: result)
break
case "configure":
handleConfigure(call: call, result: result)
break
case "disable":
handleDisable(call: call, result: result)
break
case "isEnabled":
handleIsEnabled(call: call, result: result)
break
case "isIgnoringBatteryOptimizations":
result(FlutterMethodNotImplemented)
break
case "lastBackgroundFetchTime":
let defaults = UserDefaults.standard
let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time")
result(lastRunTime)
case "lastBackgroundProcessingTime":
let defaults = UserDefaults.standard
let lastRunTime = defaults.value(forKey: "last_background_processing_run_time")
result(lastRunTime)
case "numberOfBackgroundProcesses":
handleNumberOfProcesses(call: call, result: result)
default:
result(FlutterMethodNotImplemented)
break
}
}
// Called by the flutter code when enabled so that we can turn on the backround services
// and save the callback information to communicate on this method channel
public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) {
// Needs to parse the arguments from the method call
guard let args = call.arguments as? Array<Any> else {
print("Cannot parse args as array: \(call.arguments)")
result(FlutterMethodNotImplemented)
return
}
// Requires 3 arguments in the array
guard args.count == 3 else {
print("Requires 3 arguments and received \(args.count)")
result(FlutterMethodNotImplemented)
return
}
// Parses the arguments
let callbackHandle = args[0] as? Int64
let notificationTitle = args[1] as? String
let instant = args[2] as? Bool
// Write enabled to settings
let defaults = UserDefaults.standard
// We are now enabled, so store this
defaults.set(true, forKey: "background_service_enabled")
// The callback handle is an int64 address to communicate with the main isolate's
// entry function
defaults.set(callbackHandle, forKey: "callback_handle")
// This is not used yet and will need to be implemented
defaults.set(notificationTitle, forKey: "notification_title")
// Schedule the background services if instant
if (instant ?? true) {
BackgroundServicePlugin.scheduleBackgroundSync()
BackgroundServicePlugin.scheduleBackgroundFetch()
}
result(true)
}
// Called by the flutter code at launch to see if the background service is enabled or not
func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) {
let defaults = UserDefaults.standard
let enabled = defaults.value(forKey: "background_service_enabled") as? Bool
// False by default
result(enabled ?? false)
}
// Called by the Flutter code whenever a change in configuration is set
func handleConfigure(call: FlutterMethodCall, result: FlutterResult) {
// Needs to be able to parse the arguments or else fail
guard let args = call.arguments as? Array<Any> else {
print("Cannot parse args as array: \(call.arguments)")
result(FlutterError())
return
}
// Needs to have 4 arguments in the call or else fail
guard args.count == 4 else {
print("Not enough arguments, 4 required: \(args.count) given")
result(FlutterError())
return
}
// Parse the arguments from the method call
let requireUnmeteredNetwork = args[0] as? Bool
let requireCharging = args[1] as? Bool
let triggerUpdateDelay = args[2] as? Int
let triggerMaxDelay = args[3] as? Int
// Store the values from the call in the defaults
let defaults = UserDefaults.standard
defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network")
defaults.set(requireCharging, forKey: "require_charging")
defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay")
defaults.set(triggerMaxDelay, forKey: "trigger_max_delay")
// Cancel the background services and reschedule them
BGTaskScheduler.shared.cancelAllTaskRequests()
BackgroundServicePlugin.scheduleBackgroundSync()
BackgroundServicePlugin.scheduleBackgroundFetch()
result(true)
}
// Returns the number of currently scheduled background processes to Flutter, striclty
// for debugging
func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) {
BGTaskScheduler.shared.getPendingTaskRequests { requests in
result(requests.count)
}
}
// Disables the service, cancels all the task requests
func handleDisable(call: FlutterMethodCall, result: FlutterResult) {
let defaults = UserDefaults.standard
defaults.set(false, forKey: "background_service_enabled")
BGTaskScheduler.shared.cancelAllTaskRequests()
result(true)
}
// Schedules a short-running background sync to sync only a few photos
static func scheduleBackgroundFetch() {
// We will only schedule this task to run if the user has explicitely allowed us to backup while
// not connected to power
let defaults = UserDefaults.standard
if defaults.value(forKey: "require_charging") as? Bool == true {
return
}
let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID)
// Use 5 minutes from now as earliest begin date
backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60)
do {
try BGTaskScheduler.shared.submit(backgroundFetch)
} catch {
print("Could not schedule the background task \(error.localizedDescription)")
}
}
// Schedules a long-running background sync for syncing all of the photos
static func scheduleBackgroundSync() {
let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID)
// We need the values for requiring charging
let defaults = UserDefaults.standard
let requireCharging = defaults.value(forKey: "require_charging") as? Bool
// Always require network connectivity, and set the require charging from the above
backgroundProcessing.requiresNetworkConnectivity = true
backgroundProcessing.requiresExternalPower = requireCharging ?? true
// Use 15 minutes from now as earliest begin date
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
// Submit the task to the scheduler
try BGTaskScheduler.shared.submit(backgroundProcessing)
} catch {
print("Could not schedule the background task \(error.localizedDescription)")
}
}
// This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler
static func handleBackgroundFetch(task: BGAppRefreshTask) {
// Log the time of last background processing to now
let defaults = UserDefaults.standard
defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time")
// Schedule the next sync task so we can run this again later
scheduleBackgroundFetch()
// The background sync task should only run for 20 seconds at most
BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20)
}
// This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler
static func handleBackgroundProcessing(task: BGProcessingTask) {
// Log the time of last background processing to now
let defaults = UserDefaults.standard
defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time")
// Schedule the next sync task so we run this again later
scheduleBackgroundSync()
// We won't specify a max time for the background sync service, so this can run for longer
BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil)
}
// This is a synchronous function which uses a semaphore to run the background sync worker's run
// function, which will create a background Isolate and communicate with the Flutter code to back
// up the assets. When it completes, we signal the semaphore and complete the execution allowing the
// control to pass back to the caller synchronously
static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) {
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.main.async {
let backgroundWorker = BackgroundSyncWorker { _ in
semaphore.signal()
}
task.expirationHandler = {
backgroundWorker.cancel()
task.setTaskCompleted(success: true)
}
backgroundWorker.run(maxSeconds: maxSeconds)
task.setTaskCompleted(success: true)
}
semaphore.wait()
}
}

View File

@@ -0,0 +1,271 @@
//
// BackgroundSyncProcessing.swift
// Runner
//
// Created by Marty Fuhry on 2/6/23.
//
// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift
import Foundation
import Flutter
import BackgroundTasks
// The background worker which creates a new Flutter VM, communicates with it
// to run the backup job, and then finishes execution and calls back to its callback
// handler
class BackgroundSyncWorker {
// The Flutter engine we create for background execution.
// This is not the main Flutter engine which shows the UI,
// this is a brand new isolate created and managed in this code
// here. It does not share memory with the main
// Flutter engine which shows the UI.
// It needs to be started up, registered, and torn down here
let engine: FlutterEngine? = FlutterEngine(
name: "BackgroundImmich"
)
let notificationId = "com.alextran.immich/backgroundNotifications"
// The background message passing channel
var channel: FlutterMethodChannel?
var completionHandler: (UIBackgroundFetchResult) -> Void
let taskSessionStart = Date()
// We need the completion handler to tell the system when we are done running
init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// This is the background message passing channel to be used with the background engine
// created here in this platform code
self.channel = FlutterMethodChannel(
name: "immich/backgroundChannel",
binaryMessenger: engine!.binaryMessenger
)
self.completionHandler = completionHandler
}
// Handles all of the messages from the Flutter VM called into this platform code
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "initialized":
// Initialize tells us that we can now call into the Flutter VM to tell it to begin the update
self.channel?.invokeMethod(
"backgroundProcessing",
arguments: nil,
result: { flutterResult in
// This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or
// if this execution failed
let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed
// Show the task duration
let taskSessionCompleter = Date()
let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart)
print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)")
// Complete the execution
self.complete(result)
})
break
case "updateNotification":
let handled = self.handleNotification(call)
result(handled)
break
case "showError":
let handled = self.handleError(call)
result(handled)
break
case "clearErrorNotifications":
self.handleClearErrorNotifications()
result(true)
break
case "hasContentChanged":
// This is only called for Android, but we provide an implementation here
// telling Flutter that we don't have any information about whether the gallery
// contents have changed or not, so we can just say "no, they've not changed"
result(false)
break
default:
result(FlutterError())
self.complete(UIBackgroundFetchResult.failed)
}
}
// Runs the background sync by starting up a new isolate and handling the calls
// until it completes
public func run(maxSeconds: Int?) {
// We need the callback handle to start up the Flutter VM from the entry point
let defaults = UserDefaults.standard
guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else {
// Can't find the callback handle, this is fatal
complete(UIBackgroundFetchResult.failed)
return
}
// Use the provided callbackHandle to get the callback function
guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else {
// We need this callback or else this is fatal
complete(UIBackgroundFetchResult.failed)
return
}
// Sanity check for the engine existing
if engine == nil {
complete(UIBackgroundFetchResult.failed)
return
}
// Run the engine
let isRunning = engine!.run(
withEntrypoint: callback.callbackName,
libraryURI: callback.callbackLibraryPath
)
// If this engine isn't running, this is fatal
if !isRunning {
complete(UIBackgroundFetchResult.failed)
return
}
// If we have a timer, we need to start the timer to cancel ourselves
// so that we don't run longer than the provided maxSeconds
// After maxSeconds has elapsed, we will invoke "systemStop"
if maxSeconds != nil {
// Schedule a non-repeating timer to run after maxSeconds
let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!),
repeats: false) { timer in
// The callback invalidates the timer and stops execution
timer.invalidate()
// If the channel is already deallocated, we don't need to do anything
if self.channel == nil {
return
}
// Tell the Flutter VM to stop backing up now
self.channel?.invokeMethod(
"systemStop",
arguments: nil,
result: nil)
// Complete the execution
self.complete(UIBackgroundFetchResult.newData)
}
}
// Set the handle function to the channel message handler
self.channel?.setMethodCallHandler(handle)
// Register this to get access to the plugins on the platform channel
BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!)
}
// Cancels execution of this task, used by the system's task expiration handler
// which is called shortly before execution is about to expire
public func cancel() {
// If the channel is already deallocated, we don't need to do anything
if self.channel == nil {
return
}
// Tell the Flutter VM to stop backing up now
self.channel?.invokeMethod(
"systemStop",
arguments: nil,
result: nil)
// Complete the execution
self.complete(UIBackgroundFetchResult.newData)
}
// Completes the execution, destroys the engine, and sends a completion to our callback completionHandler
private func complete(_ fetchResult: UIBackgroundFetchResult) {
engine?.destroyContext()
channel = nil
completionHandler(fetchResult)
}
private func handleNotification(_ call: FlutterMethodCall) -> Bool {
// Parse the arguments as an array list
guard let args = call.arguments as? Array<Any> else {
print("Failed to parse \(call.arguments) as array")
return false;
}
// Requires 7 arguments passed or else fail
guard args.count == 7 else {
print("Needs 7 arguments, but was only passed \(args.count)")
return false
}
// Parse the arguments to send the notification update
let title = args[0] as? String
let content = args[1] as? String
let progress = args[2] as? Int
let maximum = args[3] as? Int
let indeterminate = args[4] as? Bool
let isDetail = args[5] as? Bool
let onlyIfForeground = args[6] as? Bool
// Build the notification
let notificationContent = UNMutableNotificationContent()
notificationContent.body = content ?? "Uploading..."
notificationContent.title = title ?? "Immich"
// Add it to the Notification center
let notification = UNNotificationRequest(
identifier: notificationId,
content: notificationContent,
trigger: nil
)
let center = UNUserNotificationCenter.current()
center.add(notification) { (error: Error?) in
if let theError = error {
print("Error showing notifications: \(theError)")
}
}
return true
}
private func handleError(_ call: FlutterMethodCall) -> Bool {
// Parse the arguments as an array list
guard let args = call.arguments as? Array<Any> else {
return false;
}
// Requires 7 arguments passed or else fail
guard args.count == 3 else {
return false
}
let title = args[0] as? String
let content = args[1] as? String
let individualTag = args[2] as? String
// Build the notification
let notificationContent = UNMutableNotificationContent()
notificationContent.body = content ?? "Error running the backup job."
notificationContent.title = title ?? "Immich"
// Add it to the Notification center
let notification = UNNotificationRequest(
identifier: notificationId,
content: notificationContent,
trigger: nil
)
let center = UNUserNotificationCenter.current()
center.add(notification)
return true
}
private func handleClearErrorNotifications() {
let center = UNUserNotificationCenter.current()
center.removeDeliveredNotifications(withIdentifiers: [notificationId])
center.removePendingNotificationRequests(withIdentifiers: [notificationId])
}
}

View File

@@ -2,6 +2,13 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>app.alextran.immich.backgroundFetch</string>
<string>app.alextran.immich.backgroundProcessing</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -12,16 +19,44 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>cs</string>
<string>da</string>
<string>de</string>
<string>en</string>
<string>es</string>
<string>fi</string>
<string>fr</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>
<string>ru</string>
<string>se</string>
<string>sk</string>
<string>zh</string>
</array>
<key>CFBundleName</key>
<string>immich_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<string>1.47.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<string>86</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
@@ -31,22 +66,31 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access the microphone to let you take beautiful video using this app</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@@ -64,38 +108,5 @@
<true/>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>CFBundleLocalizations</key>
<array>
<string>cs</string>
<string>da</string>
<string>de</string>
<string>en</string>
<string>es</string>
<string>fi</string>
<string>fr</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>
<string>ru</string>
<string>sk</string>
<string>zh</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>FLTEnableImpeller</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.47.2"
version_number: "1.48.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.000283">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.755864">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="3.705535">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="7.319767">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.23144">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.376562">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.423549">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="105.396514">
<testcase classname="fastlane.lanes" name="4: build_app" time="98.940158">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="86.092896">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="64.950609">
</testcase>

View File

@@ -17,6 +17,7 @@ const List<Locale> locales = [
Locale('pl', 'PL'),
Locale('pt', 'PR'),
Locale('ru', 'RU'),
Locale('sv', 'SE'),
Locale('sk', 'SK'),
Locale('zh', 'CN'),
];

View File

@@ -14,6 +14,7 @@ import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/settings/providers/permission.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
@@ -47,10 +48,6 @@ Future<void> openBoxes() async {
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
Hive.openBox(hiveGithubReleaseInfoBox),
Hive.openBox(userSettingInfoBox),
if (!Platform.isAndroid) Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
if (!Platform.isAndroid)
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox),
EasyLocalization.ensureInitialized(),
]);
}
@@ -130,6 +127,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
ref.watch(notificationPermissionProvider.notifier)
.getNotificationPermission();
break;
case AppLifecycleState.inactive:

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -49,12 +50,16 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
if (result.alreadyInAlbum.isNotEmpty) {
ImmichToast.show(
context: context,
msg: 'Already in ${album.name}',
msg: 'add_to_album_bottom_sheet_already_exists'.tr(
namedArgs: { "album": album.name },
),
);
} else {
ImmichToast.show(
context: context,
msg: 'Added to ${album.name}',
msg: 'add_to_album_bottom_sheet_added'.tr(
namedArgs: { "album": album.name },
),
);
}
}
@@ -90,12 +95,12 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Add to album',
'common_add_to_album'.tr(),
style: Theme.of(context).textTheme.displayMedium,
),
TextButton.icon(
icon: const Icon(Icons.add),
label: const Text('Create new album'),
label: Text('common_create_new_album'.tr()),
onPressed: () {
ref
.watch(assetSelectionProvider.notifier)

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_listtile.dart';
@@ -27,7 +28,7 @@ class AddToAlbumSliverList extends HookConsumerWidget {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ExpansionTile(
title: const Text('Shared'),
title: Text('common_shared'.tr()),
tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
leading: const Icon(Icons.group),
children: sharedAlbums

View File

@@ -120,12 +120,12 @@ class AlbumViewerPage extends HookConsumerWidget {
}
Widget buildAlbumDateRange(Album album) {
final DateTime startDate = album.assets.first.createdAt;
final DateTime endDate = album.assets.last.createdAt; //Need default.
final DateTime startDate = album.assets.first.fileCreatedAt;
final DateTime endDate = album.assets.last.fileCreatedAt; //Need default.
final String startDateText =
DateFormat(startDate.year == endDate.year ? 'LLL d' : 'LLL d, y')
(startDate.year == endDate.year ? DateFormat.MMMd() : DateFormat.yMMMd())
.format(startDate);
final String endDateText = DateFormat('LLL d, y').format(endDate);
final String endDateText = DateFormat.yMMMd().format(endDate);
return Padding(
padding: EdgeInsets.only(
@@ -134,7 +134,7 @@ class AlbumViewerPage extends HookConsumerWidget {
bottom: album.shared ? 0.0 : 8.0,
),
child: Text(
"$startDateText-$endDateText",
"$startDateText - $endDateText",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -29,7 +30,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
ImmichToast.show(
context: context,
msg: "Download Success",
msg: 'image_viewer_page_state_provider_download_success'.tr(),
toastType: ToastType.success,
gravity: ToastGravity.BOTTOM,
);
@@ -37,7 +38,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
ImmichToast.show(
context: context,
msg: "Download Error",
msg: 'image_viewer_page_state_provider_download_error'.tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);

View File

@@ -0,0 +1,9 @@
import 'package:flutter/material.dart';
final scrollToTopNotifierProvider = ScrollNotifier();
class ScrollNotifier with ChangeNotifier {
void scrollToTop() {
notifyListeners();
}
}

View File

@@ -144,10 +144,12 @@ class ExifBottomSheet extends HookConsumerWidget {
}
buildDate() {
final fileCreatedAt = assetDetail.fileCreatedAt.toLocal();
final date = DateFormat.yMMMEd().format(fileCreatedAt);
final time = DateFormat.jm().format(fileCreatedAt);
return Text(
DateFormat('date_format'.tr()).format(
assetDetail.createdAt.toLocal(),
),
'$date$time',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,

View File

@@ -35,13 +35,16 @@ class GalleryViewerPage extends HookConsumerWidget {
final Asset asset;
GalleryViewerPage({
Key? key,
super.key,
required this.assetList,
required this.asset,
}) : super(key: key);
}) : controller =
PageController(initialPage: assetList.indexOf(asset));
Asset? assetDetail;
late PageController controller;
@override
Widget build(BuildContext context, WidgetRef ref) {
final Box<dynamic> box = Hive.box(userInfoBox);
@@ -56,9 +59,6 @@ class GalleryViewerPage extends HookConsumerWidget {
late Offset localPosition;
final authToken = 'Bearer ${box.get(accessTokenKey)}';
PageController controller =
PageController(initialPage: assetList.indexOf(asset));
useEffect(
() {
isLoadPreview.value =
@@ -187,8 +187,17 @@ class GalleryViewerPage extends HookConsumerWidget {
builder: (BuildContext _) {
return DeleteDialog(
onDelete: () {
if (assetList.length == 1) {
// Handle only one asset
AutoRouter.of(context).pop();
} else {
// Go to next page otherwise
controller.nextPage(
duration: const Duration(milliseconds: 100),
curve: Curves.fastLinearToSlowEaseIn,
);
}
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
AutoRouter.of(context).pop(null);
},
);
},

View File

@@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:isolate';
import 'dart:ui' show IsolateNameServer, PluginUtilities;
import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
import 'package:cancellation_token_http/http.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
@@ -19,6 +19,7 @@ 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:path_provider_ios/path_provider_ios.dart';
import 'package:photo_manager/photo_manager.dart';
final backgroundServiceProvider = Provider(
@@ -51,8 +52,7 @@ class BackgroundService {
late final _Throttle _throttledDetailNotify =
_Throttle(_updateDetailProgress, notifyInterval);
Completer<bool> _hasAccessCompleter = Completer();
late Future<bool> _hasAccess =
Platform.isAndroid ? _hasAccessCompleter.future : Future.value(true);
late Future<bool> _hasAccess = _hasAccessCompleter.future;
Future<bool> get hasAccess => _hasAccess;
@@ -67,9 +67,6 @@ class BackgroundService {
/// Enqueues the background service
Future<bool> enableService({bool immediate = false}) async {
if (!Platform.isAndroid) {
return true;
}
try {
final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
final String title =
@@ -89,9 +86,6 @@ class BackgroundService {
int triggerUpdateDelay = 5000,
int triggerMaxDelay = 50000,
}) async {
if (!Platform.isAndroid) {
return true;
}
try {
final bool ok = await _foregroundChannel.invokeMethod(
'configure',
@@ -110,9 +104,6 @@ class BackgroundService {
/// Cancels the background service (if currently running) and removes it from work queue
Future<bool> disableService() async {
if (!Platform.isAndroid) {
return true;
}
try {
final ok = await _foregroundChannel.invokeMethod('disable');
return ok;
@@ -123,9 +114,6 @@ class BackgroundService {
/// Returns `true` if the background service is enabled
Future<bool> isBackgroundBackupEnabled() async {
if (!Platform.isAndroid) {
return false;
}
try {
return await _foregroundChannel.invokeMethod("isEnabled");
} catch (error) {
@@ -135,7 +123,8 @@ class BackgroundService {
/// Returns `true` if battery optimizations are disabled
Future<bool> isIgnoringBatteryOptimizations() async {
if (!Platform.isAndroid) {
// iOS does not need battery optimizations enabled
if (Platform.isIOS) {
return true;
}
try {
@@ -156,9 +145,6 @@ class BackgroundService {
bool isDetail = false,
bool onlyIfFG = false,
}) async {
if (!Platform.isAndroid) {
return true;
}
try {
if (_isBackgroundInitialized) {
return _backgroundChannel.invokeMethod<bool>(
@@ -178,9 +164,6 @@ class BackgroundService {
String? content,
String? individualTag,
}) async {
if (!Platform.isAndroid) {
return true;
}
try {
if (_isBackgroundInitialized && _errorGracePeriodExceeded) {
return await _backgroundChannel
@@ -193,9 +176,6 @@ class BackgroundService {
}
Future<bool> _clearErrorNotifications() async {
if (!Platform.isAndroid) {
return true;
}
try {
if (_isBackgroundInitialized) {
return await _backgroundChannel.invokeMethod('clearErrorNotifications');
@@ -210,9 +190,6 @@ class BackgroundService {
/// await to ensure this thread (foreground or background) has exclusive access
Future<bool> acquireLock() async {
if (!Platform.isAndroid) {
return true;
}
if (_hasLock) {
debugPrint("WARNING: [acquireLock] called more than once");
return true;
@@ -253,7 +230,7 @@ class BackgroundService {
while (_wantsLockTime == lockTime) {
other.send(tempSp);
final dynamic answer = await bs.first
.timeout(const Duration(seconds: 5), onTimeout: () => null);
.timeout(const Duration(seconds: 3), onTimeout: () => null);
if (_wantsLockTime != lockTime) {
break;
}
@@ -270,7 +247,7 @@ class BackgroundService {
// other isolate is still active
}
final dynamic isFinished = await bs.first
.timeout(const Duration(seconds: 5), onTimeout: () => false);
.timeout(const Duration(seconds: 3), onTimeout: () => false);
if (isFinished == true) {
break;
}
@@ -288,9 +265,6 @@ class BackgroundService {
/// releases the exclusive access lock
void releaseLock() {
if (!Platform.isAndroid) {
return;
}
_wantsLockTime = 0;
if (_hasLock) {
_hasAccessCompleter = Completer();
@@ -311,17 +285,40 @@ class BackgroundService {
}
Future<bool> _callHandler(MethodCall call) async {
DartPluginRegistrant.ensureInitialized();
if (Platform.isIOS) {
// NOTE: I'm not sure this is strictly necessary anymore, but
// out of an abundance of caution, we will keep it in until someone
// can say for sure
PathProviderIOS.registerWith();
}
switch (call.method) {
case "backgroundProcessing":
case "onAssetsChanged":
final Future<bool> translationsLoaded = loadTranslations();
try {
_clearErrorNotifications();
final bool hasAccess = await acquireLock();
// iOS should time out after some threshhold so it doesn't wait
// indefinitely and can run later
// Android is fine to wait here until the lock releases
final waitForLock = Platform.isIOS
? acquireLock().timeout(
const Duration(seconds: 5),
onTimeout: () => false,
)
: acquireLock();
final bool hasAccess = await waitForLock;
if (!hasAccess) {
debugPrint("[_callHandler] could not acquire lock, exiting");
return false;
}
await translationsLoaded;
final translationsOk = await loadTranslations();
if (!translationsOk) {
debugPrint("[_callHandler] could not load translations");
}
final bool ok = await _onAssetsChanged();
return ok;
} catch (error) {
@@ -388,9 +385,10 @@ class BackgroundService {
.put(backupFailedSince, DateTime.now());
return false;
}
// check for new assets added while performing backup
} while (true ==
await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
// Android should check for new assets added while performing backup
} while (Platform.isAndroid &&
true ==
await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
return true;
}
@@ -560,8 +558,26 @@ class BackgroundService {
final String total = numberFormat.format(bytesTotal / 1024.0);
return "$percent% ($done/$total$unit)";
}
Future<DateTime?> getIOSBackupLastRun(IosBackgroundTask task) async {
// Seconds since last run
final double? lastRun = task == IosBackgroundTask.fetch
? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime')
: await _foregroundChannel.invokeMethod('lastBackgroundProcessingTime');
if (lastRun == null) {
return null;
}
final time = DateTime.fromMillisecondsSinceEpoch(lastRun.toInt() * 1000);
return time;
}
Future<int> getIOSBackupNumberOfProcesses() async {
return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses');
}
}
enum IosBackgroundTask { fetch, processing }
class _Throttle {
_Throttle(this._fun, Duration interval) : _interval = interval.inMicroseconds;
final void Function(String?, int, int) _fun;
@@ -603,6 +619,7 @@ class _Throttle {
@pragma('vm:entry-point')
void _nativeEntry() {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
BackgroundService backgroundService = BackgroundService();
backgroundService._setupBackgroundCallHandler();
}

View File

@@ -2,26 +2,26 @@ import 'dart:convert';
class CurrentUploadAsset {
final String id;
final DateTime createdAt;
final DateTime fileCreatedAt;
final String fileName;
final String fileType;
CurrentUploadAsset({
required this.id,
required this.createdAt,
required this.fileCreatedAt,
required this.fileName,
required this.fileType,
});
CurrentUploadAsset copyWith({
String? id,
DateTime? createdAt,
DateTime? fileCreatedAt,
String? fileName,
String? fileType,
}) {
return CurrentUploadAsset(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
fileName: fileName ?? this.fileName,
fileType: fileType ?? this.fileType,
);
@@ -31,7 +31,7 @@ class CurrentUploadAsset {
final result = <String, dynamic>{};
result.addAll({'id': id});
result.addAll({'createdAt': createdAt.millisecondsSinceEpoch});
result.addAll({'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch});
result.addAll({'fileName': fileName});
result.addAll({'fileType': fileType});
@@ -41,7 +41,7 @@ class CurrentUploadAsset {
factory CurrentUploadAsset.fromMap(Map<String, dynamic> map) {
return CurrentUploadAsset(
id: map['id'] ?? '',
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']),
fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt']),
fileName: map['fileName'] ?? '',
fileType: map['fileType'] ?? '',
);
@@ -54,7 +54,7 @@ class CurrentUploadAsset {
@override
String toString() {
return 'CurrentUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType)';
return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType)';
}
@override
@@ -63,7 +63,7 @@ class CurrentUploadAsset {
return other is CurrentUploadAsset &&
other.id == id &&
other.createdAt == createdAt &&
other.fileCreatedAt == fileCreatedAt &&
other.fileName == fileName &&
other.fileType == fileType;
}
@@ -71,7 +71,7 @@ class CurrentUploadAsset {
@override
int get hashCode {
return id.hashCode ^
createdAt.hashCode ^
fileCreatedAt.hashCode ^
fileName.hashCode ^
fileType.hashCode;
}

View File

@@ -2,7 +2,7 @@ import 'package:photo_manager/photo_manager.dart';
class ErrorUploadAsset {
final String id;
final DateTime createdAt;
final DateTime fileCreatedAt;
final String fileName;
final String fileType;
final AssetEntity asset;
@@ -10,7 +10,7 @@ class ErrorUploadAsset {
const ErrorUploadAsset({
required this.id,
required this.createdAt,
required this.fileCreatedAt,
required this.fileName,
required this.fileType,
required this.asset,
@@ -19,7 +19,7 @@ class ErrorUploadAsset {
ErrorUploadAsset copyWith({
String? id,
DateTime? createdAt,
DateTime? fileCreatedAt,
String? fileName,
String? fileType,
AssetEntity? asset,
@@ -27,7 +27,7 @@ class ErrorUploadAsset {
}) {
return ErrorUploadAsset(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
fileName: fileName ?? this.fileName,
fileType: fileType ?? this.fileType,
asset: asset ?? this.asset,
@@ -37,7 +37,7 @@ class ErrorUploadAsset {
@override
String toString() {
return 'ErrorUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)';
return 'ErrorUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)';
}
@override
@@ -46,7 +46,7 @@ class ErrorUploadAsset {
return other is ErrorUploadAsset &&
other.id == id &&
other.createdAt == createdAt &&
other.fileCreatedAt == fileCreatedAt &&
other.fileName == fileName &&
other.fileType == fileType &&
other.asset == asset &&
@@ -56,7 +56,7 @@ class ErrorUploadAsset {
@override
int get hashCode {
return id.hashCode ^
createdAt.hashCode ^
fileCreatedAt.hashCode ^
fileName.hashCode ^
fileType.hashCode ^
asset.hashCode ^

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:cancellation_token_http/http.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
@@ -55,14 +53,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
selectedAlbumsBackupAssetsIds: const {},
currentUploadAsset: CurrentUploadAsset(
id: '...',
createdAt: DateTime.parse('2020-10-04'),
fileCreatedAt: DateTime.parse('2020-10-04'),
fileName: '...',
fileType: '...',
),
),
) {
getBackupInfo();
}
);
final log = Logger('BackupNotifier');
final BackupService _backupService;
@@ -130,55 +126,53 @@ class BackupNotifier extends StateNotifier<BackUpState> {
requireCharging != null ||
triggerDelay != null,
);
if (Platform.isAndroid) {
final bool wasEnabled = state.backgroundBackup;
final bool wasWifi = state.backupRequireWifi;
final bool wasCharging = state.backupRequireCharging;
final int oldTriggerDelay = state.backupTriggerDelay;
state = state.copyWith(
backgroundBackup: enabled,
backupRequireWifi: requireWifi,
backupRequireCharging: requireCharging,
backupTriggerDelay: triggerDelay,
);
final bool wasEnabled = state.backgroundBackup;
final bool wasWifi = state.backupRequireWifi;
final bool wasCharging = state.backupRequireCharging;
final int oldTriggerDelay = state.backupTriggerDelay;
state = state.copyWith(
backgroundBackup: enabled,
backupRequireWifi: requireWifi,
backupRequireCharging: requireCharging,
backupTriggerDelay: triggerDelay,
);
if (state.backgroundBackup) {
bool success = true;
if (!wasEnabled) {
if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
onBatteryInfo();
}
success &= await _backgroundService.enableService(immediate: true);
if (state.backgroundBackup) {
bool success = true;
if (!wasEnabled) {
if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
onBatteryInfo();
}
success &= success &&
await _backgroundService.configureService(
requireUnmetered: state.backupRequireWifi,
requireCharging: state.backupRequireCharging,
triggerUpdateDelay: state.backupTriggerDelay,
triggerMaxDelay: state.backupTriggerDelay * 10,
);
if (success) {
final box = Hive.box(backgroundBackupInfoBox);
await Future.wait([
box.put(backupRequireWifi, state.backupRequireWifi),
box.put(backupRequireCharging, state.backupRequireCharging),
box.put(backupTriggerDelay, state.backupTriggerDelay),
]);
} else {
state = state.copyWith(
backgroundBackup: wasEnabled,
backupRequireWifi: wasWifi,
backupRequireCharging: wasCharging,
backupTriggerDelay: oldTriggerDelay,
success &= await _backgroundService.enableService(immediate: true);
}
success &= success &&
await _backgroundService.configureService(
requireUnmetered: state.backupRequireWifi,
requireCharging: state.backupRequireCharging,
triggerUpdateDelay: state.backupTriggerDelay,
triggerMaxDelay: state.backupTriggerDelay * 10,
);
onError("backup_controller_page_background_configure_error");
}
if (success) {
final box = Hive.box(backgroundBackupInfoBox);
await Future.wait([
box.put(backupRequireWifi, state.backupRequireWifi),
box.put(backupRequireCharging, state.backupRequireCharging),
box.put(backupTriggerDelay, state.backupTriggerDelay),
]);
} else {
final bool success = await _backgroundService.disableService();
if (!success) {
state = state.copyWith(backgroundBackup: wasEnabled);
onError("backup_controller_page_background_configure_error");
}
state = state.copyWith(
backgroundBackup: wasEnabled,
backupRequireWifi: wasWifi,
backupRequireCharging: wasCharging,
backupTriggerDelay: oldTriggerDelay,
);
onError("backup_controller_page_background_configure_error");
}
} else {
final bool success = await _backgroundService.disableService();
if (!success) {
state = state.copyWith(backgroundBackup: wasEnabled);
onError("backup_controller_page_background_configure_error");
}
}
}
@@ -464,6 +458,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
}
}
void setAvailableAlbums(availableAlbums) {
state = state.copyWith(
availableAlbums: availableAlbums,
);
}
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
}
@@ -577,48 +577,52 @@ class BackupNotifier extends StateNotifier<BackUpState> {
}
Future<void> resumeBackup() async {
if (Platform.isAndroid) {
// assumes the background service is currently running
// if true, waits until it has stopped to update the app state from HiveDB
// before actually resuming backup by calling the internal `_resumeBackup`
final BackUpProgressEnum previous = state.backupProgress;
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
final bool hasLock = await _backgroundService.acquireLock();
if (!hasLock) {
log.warning("WARNING [resumeBackup] failed to acquireLock");
return;
}
await Future.wait([
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
Hive.openBox(backgroundBackupInfoBox),
]);
final HiveBackupAlbums? albums =
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
if (albums != null) {
// assumes the background service is currently running
// if true, waits until it has stopped to update the app state from HiveDB
// before actually resuming backup by calling the internal `_resumeBackup`
final BackUpProgressEnum previous = state.backupProgress;
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
final bool hasLock = await _backgroundService.acquireLock();
if (!hasLock) {
log.warning("WARNING [resumeBackup] failed to acquireLock");
return;
}
await Future.wait([
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
Hive.openBox(backgroundBackupInfoBox),
]);
final HiveBackupAlbums? albums =
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
if (albums != null) {
if (selectedAlbums.isNotEmpty) {
selectedAlbums = _updateAlbumsBackupTime(
selectedAlbums,
albums.selectedAlbumIds,
albums.lastSelectedBackupTime,
);
}
if (excludedAlbums.isNotEmpty) {
excludedAlbums = _updateAlbumsBackupTime(
excludedAlbums,
albums.excludedAlbumsIds,
albums.lastExcludedBackupTime,
);
}
final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
state = state.copyWith(
backupProgress: previous,
selectedBackupAlbums: selectedAlbums,
excludedBackupAlbums: excludedAlbums,
backupRequireWifi: backgroundBox.get(backupRequireWifi),
backupRequireCharging: backgroundBox.get(backupRequireCharging),
backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
);
}
final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
state = state.copyWith(
backupProgress: previous,
selectedBackupAlbums: selectedAlbums,
excludedBackupAlbums: excludedAlbums,
backupRequireWifi: backgroundBox.get(backupRequireWifi),
backupRequireCharging: backgroundBox.get(backupRequireCharging),
backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
);
return _resumeBackup();
}
@@ -649,8 +653,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
AppStateEnum.paused,
AppStateEnum.detached,
];
if (Platform.isAndroid &&
allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
try {
if (Hive.isBoxOpen(hiveBackupInfoBox)) {
await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();

View File

@@ -260,8 +260,8 @@ class BackupService {
req.fields['deviceAssetId'] = entity.id;
req.fields['deviceId'] = deviceId;
req.fields['assetType'] = _getAssetType(entity.type);
req.fields['createdAt'] = entity.createDateTime.toIso8601String();
req.fields['modifiedAt'] = entity.modifiedDateTime.toIso8601String();
req.fields['fileCreatedAt'] = entity.createDateTime.toIso8601String();
req.fields['fileModifiedAt'] = entity.modifiedDateTime.toIso8601String();
req.fields['isFavorite'] = entity.isFavorite.toString();
req.fields['fileExtension'] = fileExtension;
req.fields['duration'] = entity.videoDuration.toString();
@@ -278,7 +278,7 @@ class BackupService {
setCurrentUploadAssetCb(
CurrentUploadAsset(
id: entity.id,
createdAt: entity.createDateTime.year == 1970
fileCreatedAt: entity.createDateTime.year == 1970
? entity.modifiedDateTime
: entity.createDateTime,
fileName: originalFileName,
@@ -308,7 +308,7 @@ class BackupService {
ErrorUploadAsset(
asset: entity,
id: entity.id,
createdAt: entity.createDateTime,
fileCreatedAt: entity.createDateTime,
fileName: originalFileName,
fileType: _getAssetType(entity.type),
errorMessage: error['error'],

View File

@@ -21,7 +21,7 @@ class BackupInfoCard extends StatelessWidget {
borderRadius: BorderRadius.circular(20), // if you need this
side: BorderSide(
color: isDarkMode
? const Color.fromARGB(255, 101, 101, 101)
? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12,
width: 1,
),

View File

@@ -18,9 +18,9 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
final isShowThumbnail = useState(false);
String getAssetCreationDate() {
return DateFormat.yMMMMd('en_US').format(
return DateFormat.yMMMMd().format(
DateTime.parse(
asset.createdAt.toString(),
asset.fileCreatedAt.toString(),
).toLocal(),
);
}

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:intl/intl.dart';
/// This is a simple debug widget which should be removed later on when we are
/// more confident about background sync
class IosDebugInfoTile extends HookConsumerWidget {
const IosDebugInfoTile({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final futures = [
ref
.read(backgroundServiceProvider)
.getIOSBackupLastRun(IosBackgroundTask.fetch),
ref
.read(backgroundServiceProvider)
.getIOSBackupLastRun(IosBackgroundTask.processing),
ref.read(backgroundServiceProvider).getIOSBackupNumberOfProcesses(),
];
return FutureBuilder<List<dynamic>>(
future: Future.wait(futures),
builder: (context, snapshot) {
String? title;
String? subtitle;
if (snapshot.hasData) {
final results = snapshot.data as List<dynamic>;
final fetch = results[0] as DateTime?;
final processing = results[1] as DateTime?;
final processes = results[2] as int;
final processOrProcesses = processes == 1 ? 'process' : 'processes';
final numberOrZero = processes == 0 ? 'No' : processes.toString();
title = '$numberOrZero background $processOrProcesses queued';
final df = DateFormat.yMd().add_jm();
if (fetch == null && processing == null) {
subtitle = 'No background sync job has run yet';
} else if (fetch != null && processing == null) {
subtitle = 'Fetch ran ${df.format(fetch)}';
} else if (processing != null && fetch == null) {
subtitle = 'Processing ran ${df.format(processing)}';
} else {
final fetchOrProcessing =
fetch!.isAfter(processing!) ? fetch : processing;
subtitle = 'Last sync ${df.format(fetchOrProcessing)}';
}
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: ListTile(
key: ValueKey(title),
title: Text(
title ?? '',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Theme.of(context).primaryColor,
),
),
subtitle: Text(
subtitle ?? '',
style: const TextStyle(
fontSize: 14,
),
),
leading: Icon(
Icons.bug_report,
color: Theme.of(context).primaryColor,
),
),
);
},
);
}
}

View File

@@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
@@ -19,20 +18,18 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final albums = useState<List<AvailableAlbum>>(
ref.watch(backupProvider).availableAlbums,
);
final albums = ref.watch(backupProvider).availableAlbums;
useEffect(
() {
ref.read(backupProvider.notifier).getBackupInfo();
ref.watch(backupProvider.notifier).getBackupInfo();
return null;
},
[],
);
buildAlbumSelectionList() {
if (albums.value.isEmpty) {
if (albums.isEmpty) {
return const Center(
child: ImmichLoadingIndicator(),
);
@@ -42,17 +39,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
height: 265,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: albums.value.length,
itemCount: albums.length,
physics: const BouncingScrollPhysics(),
itemBuilder: ((context, index) {
var thumbnailData = albums.value[index].thumbnailData;
var thumbnailData = albums[index].thumbnailData;
return Padding(
padding: index == 0
? const EdgeInsets.only(left: 16.00)
: const EdgeInsets.all(0),
child: AlbumInfoCard(
imageData: thumbnailData,
albumInfo: albums.value[index],
albumInfo: albums[index],
),
);
}),
@@ -142,7 +139,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
child: TextFormField(
onChanged: (searchValue) {
albums.value = ref
var avaialbleAlbums = ref
.watch(backupProvider)
.availableAlbums
.where(
@@ -151,6 +148,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
.contains(searchValue.toLowerCase()),
)
.toList();
ref
.read(backupProvider.notifier)
.setAvailableAlbums(avaialbleAlbums);
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(

View File

@@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -195,7 +196,7 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
ListTile buildBackgroundBackupController() {
Widget buildBackgroundBackupController() {
final bool isBackgroundEnabled = backupState.backgroundBackup;
final bool isWifiRequired = backupState.backupRequireWifi;
final bool isChargingRequired = backupState.backupRequireCharging;
@@ -240,118 +241,132 @@ class BackupControllerPage extends HookConsumerWidget {
final triggerDelay =
useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
return ListTile(
isThreeLine: true,
leading: isBackgroundEnabled
? Icon(
Icons.cloud_sync_rounded,
color: activeColor,
)
: const Icon(Icons.cloud_sync_rounded),
title: Text(
isBackgroundEnabled
? "backup_controller_page_background_is_on"
: "backup_controller_page_background_is_off",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isBackgroundEnabled)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child:
const Text("backup_controller_page_background_description")
.tr(),
),
if (isBackgroundEnabled)
SwitchListTile(
title:
const Text("backup_controller_page_background_wifi").tr(),
secondary: Icon(
Icons.wifi,
color: isWifiRequired ? activeColor : null,
),
dense: true,
activeColor: activeColor,
value: isWifiRequired,
onChanged: hasExclusiveAccess
? (isChecked) => ref
.read(backupProvider.notifier)
.configureBackgroundBackup(
requireWifi: isChecked,
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
)
: null,
),
if (isBackgroundEnabled)
SwitchListTile(
title: const Text("backup_controller_page_background_charging")
.tr(),
secondary: Icon(
Icons.charging_station,
color: isChargingRequired ? activeColor : null,
),
dense: true,
activeColor: activeColor,
value: isChargingRequired,
onChanged: hasExclusiveAccess
? (isChecked) => ref
.read(backupProvider.notifier)
.configureBackgroundBackup(
requireCharging: isChecked,
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
)
: null,
),
if (isBackgroundEnabled)
ListTile(
isThreeLine: false,
dense: true,
enabled: hasExclusiveAccess,
title: const Text(
'backup_controller_page_background_delay',
style: TextStyle(
fontWeight: FontWeight.bold,
return Column(
children: [
ListTile(
isThreeLine: true,
leading: isBackgroundEnabled
? Icon(
Icons.cloud_sync_rounded,
color: activeColor,
)
: const Icon(Icons.cloud_sync_rounded),
title: Text(
isBackgroundEnabled
? "backup_controller_page_background_is_on"
: "backup_controller_page_background_is_off",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isBackgroundEnabled)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: const Text(
"backup_controller_page_background_description",
).tr(),
),
).tr(args: [formatBackupDelaySliderValue(triggerDelay.value)]),
subtitle: Slider(
value: triggerDelay.value,
onChanged: hasExclusiveAccess
? (double v) => triggerDelay.value = v
: null,
onChangeEnd: (double v) => ref
if (isBackgroundEnabled && Platform.isAndroid)
SwitchListTile.adaptive(
title: const Text("backup_controller_page_background_wifi")
.tr(),
secondary: Icon(
Icons.wifi,
color: isWifiRequired ? activeColor : null,
),
dense: true,
activeColor: activeColor,
value: isWifiRequired,
onChanged: hasExclusiveAccess
? (isChecked) => ref
.read(backupProvider.notifier)
.configureBackgroundBackup(
requireWifi: isChecked,
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
)
: null,
),
if (isBackgroundEnabled)
SwitchListTile.adaptive(
title:
const Text("backup_controller_page_background_charging")
.tr(),
secondary: Icon(
Icons.charging_station,
color: isChargingRequired ? activeColor : null,
),
dense: true,
activeColor: activeColor,
value: isChargingRequired,
onChanged: hasExclusiveAccess
? (isChecked) => ref
.read(backupProvider.notifier)
.configureBackgroundBackup(
requireCharging: isChecked,
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
)
: null,
),
if (isBackgroundEnabled && Platform.isAndroid)
ListTile(
isThreeLine: false,
dense: true,
enabled: hasExclusiveAccess,
title: const Text(
'backup_controller_page_background_delay',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(
args: [formatBackupDelaySliderValue(triggerDelay.value)],
),
subtitle: Slider(
value: triggerDelay.value,
onChanged: hasExclusiveAccess
? (double v) => triggerDelay.value = v
: null,
onChangeEnd: (double v) => ref
.read(backupProvider.notifier)
.configureBackgroundBackup(
triggerDelay: backupDelayToMilliseconds(v),
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
),
max: 3.0,
divisions: 3,
label: formatBackupDelaySliderValue(triggerDelay.value),
activeColor: Theme.of(context).primaryColor,
),
),
ElevatedButton(
onPressed: () => ref
.read(backupProvider.notifier)
.configureBackgroundBackup(
triggerDelay: backupDelayToMilliseconds(v),
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
),
max: 3.0,
divisions: 3,
label: formatBackupDelaySliderValue(triggerDelay.value),
activeColor: Theme.of(context).primaryColor,
),
),
ElevatedButton(
onPressed: () =>
ref.read(backupProvider.notifier).configureBackgroundBackup(
enabled: !isBackgroundEnabled,
onError: showErrorToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser,
),
child: Text(
isBackgroundEnabled
? "backup_controller_page_background_turn_off"
: "backup_controller_page_background_turn_on",
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
).tr(),
child: Text(
isBackgroundEnabled
? "backup_controller_page_background_turn_off"
: "backup_controller_page_background_turn_on",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
).tr(),
),
],
),
],
),
),
if (isBackgroundEnabled)
IosDebugInfoTile(
key: ValueKey(isChargingRequired),
),
],
);
}
@@ -425,7 +440,7 @@ class BackupControllerPage extends HookConsumerWidget {
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: isDarkMode
? const Color.fromARGB(255, 101, 101, 101)
? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12,
width: 1,
),
@@ -525,7 +540,7 @@ class BackupControllerPage extends HookConsumerWidget {
borderRadius: BorderRadius.circular(20), // if you need this
side: BorderSide(
color: isDarkMode
? const Color.fromARGB(255, 101, 101, 101)
? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12,
width: 1,
),
@@ -577,23 +592,28 @@ class BackupControllerPage extends HookConsumerWidget {
BackupInfoCard(
title: "backup_controller_page_total".tr(),
subtitle: "backup_controller_page_total_sub".tr(),
info: "${backupState.allUniqueAssets.length}",
info: ref.watch(backupProvider).availableAlbums.isEmpty
? "..."
: "${backupState.allUniqueAssets.length}",
),
BackupInfoCard(
title: "backup_controller_page_backup".tr(),
subtitle: "backup_controller_page_backup_sub".tr(),
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
info: ref.watch(backupProvider).availableAlbums.isEmpty
? "..."
: "${backupState.selectedAlbumsBackupAssetsIds.length}",
),
BackupInfoCard(
title: "backup_controller_page_remainder".tr(),
subtitle: "backup_controller_page_remainder_sub".tr(),
info:
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
info: ref.watch(backupProvider).availableAlbums.isEmpty
? "..."
: "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
),
const Divider(),
buildAutoBackupController(),
if (Platform.isAndroid) const Divider(),
if (Platform.isAndroid) buildBackgroundBackupController(),
const Divider(),
buildBackgroundBackupController(),
const Divider(),
buildStorageInformation(),
const Divider(),

View File

@@ -87,9 +87,9 @@ class FailedBackupStatusPage extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
DateFormat.yMMMMd('en_US').format(
DateFormat.yMMMMd().format(
DateTime.parse(
errorAsset.createdAt.toString(),
errorAsset.fileCreatedAt.toString(),
).toLocal(),
),
style: TextStyle(

View File

@@ -55,16 +55,10 @@ class AssetGridLayoutParameters {
}
class _AssetGroupsToRenderListComputeParameters {
final String monthFormat;
final String dayFormat;
final String dayFormatYear;
final List<Asset> assets;
final AssetGridLayoutParameters layout;
_AssetGroupsToRenderListComputeParameters(
this.monthFormat,
this.dayFormat,
this.dayFormatYear,
this.assets,
this.layout,
);
@@ -82,14 +76,14 @@ class RenderList {
if (groupBy == GroupAssetsBy.day) {
return assets.groupListsBy(
(element) {
final date = element.createdAt.toLocal();
final date = element.fileCreatedAt.toLocal();
return DateTime(date.year, date.month, date.day);
},
);
} else if (groupBy == GroupAssetsBy.month) {
return assets.groupListsBy(
(element) {
final date = element.createdAt.toLocal();
final date = element.fileCreatedAt.toLocal();
return DateTime(date.year, date.month);
},
);
@@ -101,9 +95,10 @@ class RenderList {
static Future<RenderList> _processAssetGroupData(
_AssetGroupsToRenderListComputeParameters data,
) async {
final monthFormat = DateFormat(data.monthFormat);
final dayFormatSameYear = DateFormat(data.dayFormat);
final dayFormatOtherYear = DateFormat(data.dayFormatYear);
// TODO: Make DateFormat use the configured locale.
final monthFormat = DateFormat.yMMM();
final dayFormatSameYear = DateFormat.MMMEd();
final dayFormatOtherYear = DateFormat.yMMMEd();
final allAssets = data.assets;
final perRow = data.layout.perRow;
final dynamicLayout = data.layout.dynamicLayout;
@@ -210,9 +205,6 @@ class RenderList {
return compute(
_processAssetGroupData,
_AssetGroupsToRenderListComputeParameters(
"monthly_title_text_date_format".tr(),
"daily_title_text_date".tr(),
"daily_title_text_date_year".tr(),
assets,
layout,
),

View File

@@ -3,6 +3,7 @@ import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@@ -234,6 +235,30 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
return true;
}
@override
void initState() {
super.initState();
scrollToTopNotifierProvider.addListener(_scrollToTop);
}
@override
void dispose() {
scrollToTopNotifierProvider.removeListener(_scrollToTop);
super.dispose();
}
void _scrollToTop() {
// for some reason, this is necessary as well in order
// to correctly reposition the drag thumb scroll bar
_itemScrollController.jumpTo(
index: 0,
);
_itemScrollController.scrollTo(
index: 0,
duration: const Duration(milliseconds: 200),
);
}
@override
Widget build(BuildContext context) {
return WillPopScope(

View File

@@ -1,31 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class MonthlyTitleText extends StatelessWidget {
const MonthlyTitleText({
Key? key,
required this.isoDate,
}) : super(key: key);
final String isoDate;
@override
Widget build(BuildContext context) {
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
.format(DateTime.parse(isoDate).toLocal());
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 12.0, top: 32),
child: Text(
monthTitleText,
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.displayLarge?.color,
),
),
),
);
}
}

View File

@@ -43,7 +43,7 @@ class ControlBottomAppBar extends ConsumerWidget {
),
ControlBoxButton(
iconData: Icons.star_rounded,
label: "Favorite",
label: "control_bottom_app_bar_favorite".tr(),
onPressed: () {
onFavorite();
},
@@ -140,7 +140,7 @@ class AddToAlbumTitleRow extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"control_bottom_app_bar_add_to_album",
"common_add_to_album",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@@ -150,7 +150,7 @@ class AddToAlbumTitleRow extends StatelessWidget {
onPressed: onCreateNewAlbum,
icon: const Icon(Icons.add),
label: Text(
"control_bottom_app_bar_create_new_album",
"common_create_new_album",
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,

View File

@@ -73,7 +73,7 @@ class ServerInfoBox extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"App Version",
"server_info_box_app_version".tr(),
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
@@ -98,7 +98,7 @@ class ServerInfoBox extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Server Version",
"server_info_box_server_version".tr(),
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],

View File

@@ -46,10 +46,10 @@ class HomePage extends HookConsumerWidget {
useEffect(
() {
ref.read(websocketProvider.notifier).connect();
ref.read(assetProvider.notifier).getAllAsset();
ref.read(albumProvider.notifier).getAllAlbums();
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.watch(websocketProvider.notifier).connect();
ref.watch(assetProvider.notifier).getAllAsset();
ref.watch(albumProvider.notifier).getAllAlbums();
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.watch(serverInfoProvider.notifier).getServerVersion();
selectionEnabledHook.addListener(() {
@@ -67,7 +67,7 @@ class HomePage extends HookConsumerWidget {
);
void reloadAllAsset() {
ref.read(assetProvider.notifier).getAllAsset();
ref.watch(assetProvider.notifier).getAllAsset();
}
Widget buildBody() {
@@ -102,7 +102,7 @@ class HomePage extends HookConsumerWidget {
void onFavoriteAssets() {
final remoteAssets = remoteOnlySelection(
localErrorMessage: 'Can not favorite local assets yet, skipping',
localErrorMessage: 'home_page_favorite_err_local'.tr(),
);
if (remoteAssets.isNotEmpty) {
ref.watch(favoriteProvider.notifier).addToFavorites(remoteAssets);
@@ -125,7 +125,7 @@ class HomePage extends HookConsumerWidget {
void onAddToAlbum(Album album) async {
final Iterable<Asset> assets = remoteOnlySelection(
localErrorMessage: "Can not add local assets to albums yet, skipping",
localErrorMessage: "home_page_add_to_album_err_local".tr(),
);
if (assets.isEmpty) {
return;
@@ -166,7 +166,7 @@ class HomePage extends HookConsumerWidget {
void onCreateNewAlbum() async {
final Iterable<Asset> assets = remoteOnlySelection(
localErrorMessage: "Can not add local assets to albums yet, skipping",
localErrorMessage: "home_page_add_to_album_err_local".tr(),
);
if (assets.isEmpty) {
return;
@@ -229,8 +229,8 @@ class HomePage extends HookConsumerWidget {
top: true,
child: Stack(
children: [
ref.watch(assetProvider).renderList == null
|| ref.watch(assetProvider).allAssets.isEmpty
ref.watch(assetProvider).renderList == null ||
ref.watch(assetProvider).allAssets.isEmpty
? buildLoadingIndicator()
: ImmichAssetGrid(
renderList: ref.watch(assetProvider).renderList!,

View File

@@ -92,20 +92,25 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
}
Future<bool> logout() async {
await Future.wait([
_apiService.authenticationApi.logout(),
Hive.box(userInfoBox).delete(accessTokenKey),
Store.delete(StoreKey.assetETag),
Store.delete(StoreKey.userRemoteId),
_assetCacheService.invalidate(),
_albumCacheService.invalidate(),
_sharedAlbumCacheService.invalidate(),
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey)
]);
try {
await Future.wait([
_apiService.authenticationApi.logout(),
Hive.box(userInfoBox).delete(accessTokenKey),
Store.delete(StoreKey.assetETag),
Store.delete(StoreKey.userRemoteId),
_assetCacheService.invalidate(),
_albumCacheService.invalidate(),
_sharedAlbumCacheService.invalidate(),
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey)
]);
state = state.copyWith(isAuthenticated: false);
state = state.copyWith(isAuthenticated: false);
return true;
return true;
} catch (e) {
debugPrint("Error logging out $e");
return false;
}
}
setAutoBackup(bool backupState) async {

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -30,7 +31,7 @@ class ChangePasswordForm extends HookConsumerWidget {
alignment: WrapAlignment.start,
children: [
Text(
'Change Password',
'common_change_password'.tr(),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@@ -40,7 +41,12 @@ class ChangePasswordForm extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text(
'Hi ${authState.firstName} ${authState.lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.',
'change_password_form_description'.tr(
namedArgs: {
'firstName': authState.firstName,
'lastName': authState.lastName,
},
),
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
@@ -62,7 +68,28 @@ class ChangePasswordForm extends HookConsumerWidget {
),
ChangePasswordButton(
passwordController: passwordController,
formKey: formKey,
onPressed: () async {
if (formKey.currentState!.validate()) {
var isSuccess = await ref
.read(authenticationProvider.notifier)
.changePassword(passwordController.value.text);
if (isSuccess) {
bool res = await ref
.read(authenticationProvider.notifier)
.logout();
if (res) {
ref.read(backupProvider.notifier).cancelBackup();
ref.read(assetProvider.notifier).clearAllAsset();
ref.read(websocketProvider.notifier).disconnect();
AutoRouter.of(context)
.replace(const LoginRoute());
}
}
}
},
),
],
),
@@ -85,10 +112,10 @@ class PasswordInput extends StatelessWidget {
return TextFormField(
obscureText: true,
controller: controller,
decoration: const InputDecoration(
labelText: 'New Password',
border: OutlineInputBorder(),
hintText: 'New Password',
decoration: InputDecoration(
labelText: 'change_password_form_new_password'.tr(),
border: const OutlineInputBorder(),
hintText: 'change_password_form_new_password'.tr(),
),
);
}
@@ -106,7 +133,7 @@ class ConfirmPasswordInput extends StatelessWidget {
String? _validateInput(String? email) {
if (confirmController.value != originalController.value) {
return 'Passwords do not match';
return 'change_password_form_password_mismatch'.tr();
}
return null;
}
@@ -116,10 +143,10 @@ class ConfirmPasswordInput extends StatelessWidget {
return TextFormField(
obscureText: true,
controller: confirmController,
decoration: const InputDecoration(
labelText: 'Confirm Password',
hintText: 'Re-enter New Password',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: 'change_password_form_confirm_password'.tr(),
hintText: 'change_password_form_reenter_new_password'.tr(),
border: const OutlineInputBorder(),
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
@@ -129,12 +156,11 @@ class ConfirmPasswordInput extends StatelessWidget {
class ChangePasswordButton extends ConsumerWidget {
final TextEditingController passwordController;
final GlobalKey<FormState> formKey;
final VoidCallback onPressed;
const ChangePasswordButton({
Key? key,
required this.passwordController,
required this.formKey,
required this.onPressed,
}) : super(key: key);
@override
@@ -147,28 +173,10 @@ class ChangePasswordButton extends ConsumerWidget {
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
),
onPressed: () async {
if (formKey.currentState!.validate()) {
var isSuccess = await ref
.watch(authenticationProvider.notifier)
.changePassword(passwordController.value.text);
if (isSuccess) {
bool res =
await ref.watch(authenticationProvider.notifier).logout();
if (res) {
ref.watch(backupProvider.notifier).cancelBackup();
ref.watch(assetProvider.notifier).clearAllAsset();
ref.watch(websocketProvider.notifier).disconnect();
AutoRouter.of(context).replace(const LoginRoute());
}
}
}
},
child: const Text(
"Change Password",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
onPressed: onPressed,
child: Text(
'common_change_password'.tr(),
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
);
}

View File

@@ -298,10 +298,10 @@ class LoginButton extends ConsumerWidget {
),
onPressed: () async {
// This will remove current cache asset state of previous user login.
ref.watch(assetProvider.notifier).clearAllAsset();
ref.read(assetProvider.notifier).clearAllAsset();
var isAuthenticated =
await ref.watch(authenticationProvider.notifier).login(
await ref.read(authenticationProvider.notifier).login(
emailController.text,
passwordController.text,
serverEndpointController.text,
@@ -309,12 +309,11 @@ class LoginButton extends ConsumerWidget {
if (isAuthenticated) {
// Resume backup (if enable) then navigate
if (ref.watch(authenticationProvider).shouldChangePassword &&
!ref.watch(authenticationProvider).isAdmin) {
if (ref.read(authenticationProvider).shouldChangePassword &&
!ref.read(authenticationProvider).isAdmin) {
AutoRouter.of(context).push(const ChangePasswordRoute());
} else {
ref.watch(backupProvider.notifier).resumeBackup();
ref.read(backupProvider.notifier).resumeBackup();
AutoRouter.of(context).replace(const TabControllerRoute());
}
} else {

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