Compare commits
146 Commits
v1.20.2_30
...
v1.28.2_40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31739aca02 | ||
|
|
8f2e7b6f65 | ||
|
|
4ed647c43d | ||
|
|
f88ff4fb5c | ||
|
|
cc4881d633 | ||
|
|
d856b35afc | ||
|
|
b6d025da09 | ||
|
|
cc79ff1ca3 | ||
|
|
131aa2b6be | ||
|
|
02a6b73122 | ||
|
|
d87366c095 | ||
|
|
4f7a3afbfc | ||
|
|
6725954b70 | ||
|
|
4fe535e5e8 | ||
|
|
aed94bfc4c | ||
|
|
de996c0a81 | ||
|
|
1a39aa4da5 | ||
|
|
1f4ba73da7 | ||
|
|
836b174d33 | ||
|
|
853a65aef1 | ||
|
|
566039b93f | ||
|
|
18a7ff8726 | ||
|
|
6ffdf167fe | ||
|
|
6b702b13e4 | ||
|
|
f476bd985b | ||
|
|
92c4f0598b | ||
|
|
a337402124 | ||
|
|
209e6332b3 | ||
|
|
645bd8a109 | ||
|
|
9a471d80f7 | ||
|
|
de0c59efe7 | ||
|
|
c19d26f4f3 | ||
|
|
2edfc75c8a | ||
|
|
4c977d2c1f | ||
|
|
1425f2ec78 | ||
|
|
b081eda76f | ||
|
|
7f6837c751 | ||
|
|
a467936e73 | ||
|
|
2677ddccaa | ||
|
|
564ace3ddf | ||
|
|
a81ef7497c | ||
|
|
caa7b07398 | ||
|
|
6976a7241e | ||
|
|
172eda3ce5 | ||
|
|
552340add7 | ||
|
|
bd92dde117 | ||
|
|
617c54ab81 | ||
|
|
c76f7804ab | ||
|
|
0799aa2c72 | ||
|
|
b80dca74ef | ||
|
|
f5f00e0f6c | ||
|
|
75d2d82d05 | ||
|
|
5172242f88 | ||
|
|
25e68cf826 | ||
|
|
e527685ebf | ||
|
|
e745cb5e4b | ||
|
|
dfaa4969da | ||
|
|
f980a2f27a | ||
|
|
6b7c97c02a | ||
|
|
fdd9f37abd | ||
|
|
a09bba454c | ||
|
|
4be9aa091b | ||
|
|
33b810de74 | ||
|
|
44ccb1eec1 | ||
|
|
bef38c670c | ||
|
|
025d7bf192 | ||
|
|
5ad2d62039 | ||
|
|
a128833e68 | ||
|
|
87f7b0849a | ||
|
|
4596a8ee01 | ||
|
|
f9b1b12b10 | ||
|
|
68b1655e7f | ||
|
|
658b64df74 | ||
|
|
e344503834 | ||
|
|
bf2760ffef | ||
|
|
db2ed2d881 | ||
|
|
fb0fa742f5 | ||
|
|
3b55cdc0be | ||
|
|
0efcc99f3e | ||
|
|
7a85164a1e | ||
|
|
ba2cda8955 | ||
|
|
9048be4c8e | ||
|
|
83716ae1bc | ||
|
|
5cd4d2d158 | ||
|
|
13bb6d469b | ||
|
|
8e4c4c34e4 | ||
|
|
3125d04f32 | ||
|
|
c436c57cc9 | ||
|
|
7f9f825589 | ||
|
|
da9aed5c11 | ||
|
|
10ef3509dd | ||
|
|
3dc538f9e6 | ||
|
|
1e29ff322d | ||
|
|
9c30d58b10 | ||
|
|
013a0f8324 | ||
|
|
07b58f46f9 | ||
|
|
566e118a19 | ||
|
|
0e18c88534 | ||
|
|
068d06b9ee | ||
|
|
0cf7606ec9 | ||
|
|
25338ce02f | ||
|
|
4805d86a7c | ||
|
|
33b1410d82 | ||
|
|
f35ebec7c6 | ||
|
|
3aa6ee0320 | ||
|
|
cdb0aa00d8 | ||
|
|
9de7b8d3a7 | ||
|
|
4a28a46612 | ||
|
|
16561d15ff | ||
|
|
9642ad2820 | ||
|
|
e2169a26c2 | ||
|
|
f697922f32 | ||
|
|
1390d01763 | ||
|
|
86f780871c | ||
|
|
c1b22125fd | ||
|
|
30f069a5db | ||
|
|
2bf6cd9241 | ||
|
|
87d2a954a3 | ||
|
|
a388c5a642 | ||
|
|
4b34f017ca | ||
|
|
5c1d1dd5a1 | ||
|
|
1580d27c23 | ||
|
|
4b9187928c | ||
|
|
5b7236f6ad | ||
|
|
6fb439b580 | ||
|
|
a8334b5c27 | ||
|
|
e1cac93945 | ||
|
|
081f9f5bce | ||
|
|
25ccc5660d | ||
|
|
b6d3e578f2 | ||
|
|
52377c2dcf | ||
|
|
5c78f707fe | ||
|
|
bd5ed1b684 | ||
|
|
e89339b813 | ||
|
|
0b69feda40 | ||
|
|
339f7f776f | ||
|
|
7e6ccbad21 | ||
|
|
aac53e5cdc | ||
|
|
cbec75a175 | ||
|
|
bf04d9eb39 | ||
|
|
3058c894b1 | ||
|
|
e57e279fe1 | ||
|
|
f43c58fc6d | ||
|
|
dea304ac39 | ||
|
|
b46e834220 | ||
|
|
46f4905259 |
@@ -27,7 +27,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push Immich Mono Repo
|
- name: Build and push Immich Mono Repo
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Machine Learning
|
- name: Build and Push Machine Learning
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./machine-learning
|
context: ./machine-learning
|
||||||
file: ./machine-learning/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Web
|
- name: Build and Push Web
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Proxy
|
- name: Build and Push Proxy
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
file: ./nginx/Dockerfile
|
file: ./nginx/Dockerfile
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push Immich Mono Repo
|
- name: Build and push Immich Mono Repo
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Machine Learning
|
- name: Build and Push Machine Learning
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./machine-learning
|
context: ./machine-learning
|
||||||
file: ./machine-learning/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Web
|
- name: Build and Push Web
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Proxy
|
- name: Build and Push Proxy
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
file: ./nginx/Dockerfile
|
file: ./nginx/Dockerfile
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push immich-server release
|
- name: Build and push immich-server release
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Machine Learning
|
- name: Build and Push Machine Learning
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./machine-learning
|
context: ./machine-learning
|
||||||
file: ./machine-learning/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push immich-web release
|
- name: Build and push immich-web release
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push immich-proxy release
|
- name: Build and push immich-proxy release
|
||||||
uses: docker/build-push-action@v3.1.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
file: ./nginx/Dockerfile
|
file: ./nginx/Dockerfile
|
||||||
|
|||||||
19
.github/workflows/github-repo-stats.yml
vendored
Normal file
19
.github/workflows/github-repo-stats.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: github-repo-stats
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run this once per day, towards the end of the day for keeping the most
|
||||||
|
# recent data point most meaningful (hours are interpreted in UTC).
|
||||||
|
- cron: "0 23 * * *"
|
||||||
|
workflow_dispatch: # Allow for running this manually.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
j1:
|
||||||
|
name: github-repo-stats
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: run-ghrs
|
||||||
|
# Use latest release.
|
||||||
|
uses: jgehrcke/github-repo-stats@RELEASE
|
||||||
|
with:
|
||||||
|
ghtoken: ${{ secrets.GHRS_GITHUB_API_TOKEN }}
|
||||||
31
.github/workflows/test.yml
vendored
31
.github/workflows/test.yml
vendored
@@ -2,17 +2,40 @@ name: Test
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
push: { branches: master }
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-server-e2e:
|
e2e-tests:
|
||||||
name: Run test suite
|
name: Run end-to-end test suites
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Run Immich Server 2E2 Test
|
- name: Run Immich Server 2E2 Test
|
||||||
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
|
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
|
||||||
|
|
||||||
|
server-unit-tests:
|
||||||
|
name: Run server unit test suites and checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cd server && npm ci && npm run check:all
|
||||||
|
|
||||||
|
web-unit-tests:
|
||||||
|
name: Run web unit test suites and checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cd web && npm ci && npm run check:all
|
||||||
|
|||||||
134
CODE_OF_CONDUCT.md
Normal file
134
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation
|
||||||
|
in our community a harassment-free experience for everyone, regardless
|
||||||
|
of age, body size, visible or invisible disability, ethnicity, sex
|
||||||
|
characteristics, gender identity and expression, level of experience,
|
||||||
|
education, socio-economic status, nationality, personal appearance,
|
||||||
|
race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open,
|
||||||
|
welcoming, diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for
|
||||||
|
our community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our
|
||||||
|
mistakes, and learning from the experience
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or
|
||||||
|
political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in
|
||||||
|
a professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our
|
||||||
|
standards of acceptable behavior and will take appropriate and fair
|
||||||
|
corrective action in response to any behavior that they deem
|
||||||
|
inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit,
|
||||||
|
or reject comments, commits, code, wiki edits, issues, and other
|
||||||
|
contributions that are not aligned to this Code of Conduct, and will
|
||||||
|
communicate reasons for moderation decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also
|
||||||
|
applies when an individual is officially representing the community in
|
||||||
|
public spaces. Examples of representing our community include using an
|
||||||
|
official e-mail address, posting via an official social media account,
|
||||||
|
or acting as an appointed representative at an online or offline
|
||||||
|
event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||||
|
may be reported to the community leaders responsible for enforcement
|
||||||
|
at our Discord channel. All complaints
|
||||||
|
will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and
|
||||||
|
security of the reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in
|
||||||
|
determining the consequences for any action they deem in violation of
|
||||||
|
this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior
|
||||||
|
deemed unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders,
|
||||||
|
providing clarity around the nature of the violation and an
|
||||||
|
explanation of why the behavior was inappropriate. A public apology
|
||||||
|
may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued
|
||||||
|
behavior. No interaction with the people involved, including
|
||||||
|
unsolicited interaction with those enforcing the Code of Conduct, for
|
||||||
|
a specified period of time. This includes avoiding interactions in
|
||||||
|
community spaces as well as external channels like social
|
||||||
|
media. Violating these terms may lead to a temporary or permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards,
|
||||||
|
including sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or
|
||||||
|
public communication with the community for a specified period of
|
||||||
|
time. No public or private interaction with the people involved,
|
||||||
|
including unsolicited interaction with those enforcing the Code of
|
||||||
|
Conduct, is allowed during this period. Violating these terms may lead
|
||||||
|
to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of
|
||||||
|
community standards, including sustained inappropriate behavior,
|
||||||
|
harassment of an individual, or aggression toward or disparagement of
|
||||||
|
classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction
|
||||||
|
within the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor
|
||||||
|
Covenant][homepage], version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of
|
||||||
|
conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the
|
||||||
|
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
||||||
|
available at https://www.contributor-covenant.org/translations.
|
||||||
3
Makefile
3
Makefile
@@ -1,6 +1,9 @@
|
|||||||
dev:
|
dev:
|
||||||
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
|
dev-new:
|
||||||
|
rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
dev-update:
|
||||||
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
|||||||
244
README.md
244
README.md
@@ -1,3 +1,9 @@
|
|||||||
|
<h1 align="center"> Immich </h1>
|
||||||
|
<p align="center"> <b>High performance self-hosted photo and video backup solution.</b> </p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="design/feature-panel.png" title="Immich Logo">
|
||||||
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
|
||||||
<a href="https://github.com/alextran1502/immich"><img src="https://img.shields.io/github/stars/alextran1502/immich.svg?style=for-the-badge&logo=github&color=3F51B5&label=Stars&logoColor=000000&labelColor=ececec" alt="Star on Github"></a>
|
<a href="https://github.com/alextran1502/immich"><img src="https://img.shields.io/github/stars/alextran1502/immich.svg?style=for-the-badge&logo=github&color=3F51B5&label=Stars&logoColor=000000&labelColor=ececec" alt="Star on Github"></a>
|
||||||
@@ -15,50 +21,23 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="design/feature-panel.png" title="Immich Logo">
|
|
||||||
</p>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Immich
|
## Content
|
||||||
|
- [Features](#features)
|
||||||
**High performance self-hosted photo and video backup solution.**
|
- [Screenshots](#screenshots)
|
||||||
|
- [Installation](#installation)
|
||||||

|
- [Update](#update)
|
||||||
|
- [Mobile App](#-mobile-app)
|
||||||
Loading ~4000 images/videos
|
- [Development](#development)
|
||||||
|
- [Support](#support)
|
||||||
## Screenshots
|
- [Known Issues](#known-issues)
|
||||||
|
|
||||||
### Mobile
|
|
||||||
<p align="left">
|
|
||||||
<img src="design/login-screen.png" width="150" title="Login With Custom URL">
|
|
||||||
<img src="design/backup-screen.png" width="150" title="Backup Setting Info">
|
|
||||||
<img src="design/selective-backup-screen.png" width="150" title="Backup Setting Info">
|
|
||||||
<img src="design/home-screen.jpeg" width="150" title="Home Screen">
|
|
||||||
<img src="design/search-screen.jpeg" width="150" title="Curated Search Info">
|
|
||||||
<img src="design/shared-albums.png" width="150" title="Shared Albums">
|
|
||||||
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
### Web
|
|
||||||
<p align="left">
|
|
||||||
<img src="design/web-home.jpeg" width="49%" title="Home Dashboard">
|
|
||||||
<img src="design/web-detail.jpeg" width="49%" title="Detail">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# Note
|
|
||||||
|
|
||||||
**!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
|
|
||||||
|
|
||||||
This project is under heavy development, there will be continuous functions, features and api changes.
|
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
| | Mobile | Web |
|
> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development, there will be continuous functions, features and api changes.
|
||||||
|
|
||||||
|
| Features | Mobile | Web |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| Upload and view videos and photos | Yes | Yes
|
| Upload and view videos and photos | Yes | Yes
|
||||||
| Auto backup when the app is opened | Yes | N/A
|
| Auto backup when the app is opened | Yes | N/A
|
||||||
@@ -72,17 +51,38 @@ This project is under heavy development, there will be continuous functions, fea
|
|||||||
| Metadata view (EXIF, map) | Yes | Yes
|
| Metadata view (EXIF, map) | Yes | Yes
|
||||||
| Search by metadata, objects and image tags | Yes | No
|
| Search by metadata, objects and image tags | Yes | No
|
||||||
| Administrative functions (user management) | N/A | Yes
|
| Administrative functions (user management) | N/A | Yes
|
||||||
|
| Background backup | Android | N/A
|
||||||
|
| Virtual scroll | N/A | Yes
|
||||||
|
|
||||||
|
|
||||||
# System Requirement
|
<br/>
|
||||||
|
|
||||||
**OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
|
# Screenshots
|
||||||
|
|
||||||
**RAM**: At least 2GB, preffered 4GB.
|
### Mobile
|
||||||
|
| | | | | |
|
||||||
|
| - | - | - | - | - |
|
||||||
|
| <img src="design/login-screen.png" width="150" title="Login With Custom URL"> <p align="center"> Login with custom URL </p> | <img src="design/backup-screen.png" width="150" title="Backup Setting Info"> <p align="center"> Backup Settings </p> | <img src="design/selective-backup-screen.png" width="150" title="Backup Setting Info"> <p align="center"> Backup selection </p> | <img src="design/home-screen.jpeg" width="150" title="Home Screen"> <p align="center"> Home Screen </p> | <img src="design/search-screen.jpeg" width="150" title="Curated Search Info"> <p align="center"> Curated search </p> |
|
||||||
|
| <img src="design/shared-albums.png" width="150" title="Shared Albums"> <p align="center"> Shared albums </p> | <img src="design/nsc6.png" width="150" title="EXIF Info"> <p align="center"> EXIF info </p> | <img src="https://media.giphy.com/media/y8ZeaAigGmNvlSoKhU/giphy.gif" width="150" title="Loading ~4000 images/videos"> <p align="center"> Loading ~4000 images/videos </p> |
|
||||||
|
|
||||||
**Core**: At least 2 cores, preffered 4 cores.
|
### Web
|
||||||
|
| Home Dashboard | Image view |
|
||||||
|
| - | - |
|
||||||
|
|<img src="design/web-home.jpeg" width="100%" title="Home Dashboard"> | <img src="design/web-detail.jpeg" width="100%" title="Detail">|
|
||||||
|
|
||||||
# Technology Stack
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
# Project Details
|
||||||
|
## 💾 System Requirements
|
||||||
|
|
||||||
|
- **OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
|
||||||
|
|
||||||
|
- **RAM**: At least 2GB, preferred 4GB.
|
||||||
|
|
||||||
|
- **Core**: At least 2 cores, preferred 4 cores.
|
||||||
|
|
||||||
|
## 🔩 Technology Stack
|
||||||
|
|
||||||
There are several services that compose Immich:
|
There are several services that compose Immich:
|
||||||
|
|
||||||
@@ -93,15 +93,20 @@ There are several services that compose Immich:
|
|||||||
5. **Nginx** - Load balancing and optimized file uploading.
|
5. **Nginx** - Load balancing and optimized file uploading.
|
||||||
6. **TensorFlow** - Object Detection (COCO SSD) and Image Classification (ImageNet).
|
6. **TensorFlow** - Object Detection (COCO SSD) and Image Classification (ImageNet).
|
||||||
|
|
||||||
# Installing
|
|
||||||
|
|
||||||
## One-step installation - for evaluating only
|
<br/>
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
NOTE: When using a reverse proxy in front of Immich (such as NGINX), the reverse proxy might require extra configuration to allow large files to be uploaded (such as client_max_body_size in the case of NGINX).
|
||||||
|
|
||||||
|
## Testing One-step installation (not recommended for production)
|
||||||
|
|
||||||
|
> ⚠️ *This installation method is for evaluating Immich before futher customization to meet the users' needs.*
|
||||||
|
|
||||||
*Applicable system: Ubuntu, Debian, MacOS*
|
*Applicable system: Ubuntu, Debian, MacOS*
|
||||||
|
|
||||||
*This installation method is for evaluating Immich before futher customization to meet the users' needs.*
|
- In the shell, from the directory of your choice, run the following command:
|
||||||
|
|
||||||
In the shell, from the directory of your choice, run the following command:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -o- https://raw.githubusercontent.com/immich-app/immich/main/install.sh | bash
|
curl -o- https://raw.githubusercontent.com/immich-app/immich/main/install.sh | bash
|
||||||
@@ -114,116 +119,78 @@ The web application will be available at `http://<machine-ip-address>:2283`, and
|
|||||||
The directory which is used to store the backup file is `./immich-app/immich-data`.
|
The directory which is used to store the backup file is `./immich-app/immich-data`.
|
||||||
|
|
||||||
|
|
||||||
## Customize installation - for production usage
|
<br/>
|
||||||
|
|
||||||
|
## Custom installation (Recommended)
|
||||||
|
|
||||||
### Step 1 - Download necessary files
|
### Step 1 - Download necessary files
|
||||||
|
|
||||||
Create a directory called `immich-app` and cd into it. Then
|
- Create a directory called `immich-app` and cd into it.
|
||||||
|
|
||||||
Get `docker-compose.yml`
|
- Get `docker-compose.yml`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml
|
wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
Get `.env`
|
- Get `.env`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget -O .env wget https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
|
wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2 - Populate .env file with customed information
|
### Step 2 - Populate .env file with custom information
|
||||||
|
|
||||||
* Populate customised database information if necessary.
|
<a href="https://github.com/immich-app/immich/blob/main/docker/.env.example" target="_blank"><b>See the example <code>.env</code> file</b></a>
|
||||||
|
|
||||||
|
* Populate custom database information if necessary.
|
||||||
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
|
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
|
||||||
* Populate a secret value for `JWT_SECRET`
|
* Populate a secret value for `JWT_SECRET`, you can use this command: `openssl rand -base64 128`
|
||||||
* [Optional] Populate Mapbox value.
|
* [Optional] Populate Mapbox value to use reverse geocoding.
|
||||||
|
|
||||||
**Example**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
###################################################################################
|
|
||||||
# Database
|
|
||||||
###################################################################################
|
|
||||||
DB_USERNAME=postgres
|
|
||||||
DB_PASSWORD=postgres
|
|
||||||
DB_DATABASE_NAME=immich
|
|
||||||
|
|
||||||
###################################################################################
|
|
||||||
# Upload File Config
|
|
||||||
###################################################################################
|
|
||||||
UPLOAD_LOCATION=<put-the-path-of-the-upload-folder-here>
|
|
||||||
|
|
||||||
###################################################################################
|
|
||||||
# JWT SECRET
|
|
||||||
###################################################################################
|
|
||||||
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
|
||||||
|
|
||||||
###################################################################################
|
|
||||||
# MAPBOX
|
|
||||||
####################################################################################
|
|
||||||
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
|
||||||
ENABLE_MAPBOX=false
|
|
||||||
MAPBOX_KEY=
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3 - Start the containers
|
### Step 3 - Start the containers
|
||||||
|
|
||||||
Run `docker-compose up` or `docker compose up` (based on your docker's version)
|
- Run `docker-compose up` or `docker compose up` (based on your docker's version)
|
||||||
|
|
||||||
### Step 4 - Register admin user
|
### Step 4 - Register admin user
|
||||||
|
|
||||||
Navigate to the web at `http://<machine-ip-address>:2283` and follow the prompts to register admin user.
|
- Navigate to the web at `http://<machine-ip-address>:2283` and follow the prompts to register admin user.
|
||||||
|
<p align="center">
|
||||||
<p align="left">
|
|
||||||
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
||||||
<p/>
|
</p>
|
||||||
|
|
||||||
Additional accounts on the server can be created by the admin account.
|
- You can add and manage users from the administration page.
|
||||||
|
<p align="center">
|
||||||
<p align="left">
|
<img src="design/admin-interface.png" width="500" title="Admin User Management">
|
||||||
<img src="design/admin-interface.png" width="500" title="Admin User Management">
|
</p>
|
||||||
<p/>
|
|
||||||
|
|
||||||
### Step 5 - Access the mobile app
|
### Step 5 - Access the mobile app
|
||||||
|
|
||||||
Login the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283/api`
|
- Login the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283/api`
|
||||||
|
<p align="center">
|
||||||
<p align="left">
|
|
||||||
<img src="design/login-screen.jpeg" width="250" title="Example login screen">
|
<img src="design/login-screen.jpeg" width="250" title="Example login screen">
|
||||||
<p/>
|
</p>
|
||||||
|
|
||||||
## Mobile app
|
<br/>
|
||||||
|
|
||||||
## F-Droid
|
## Update
|
||||||
You can get the app on F-droid by clicking the image below.
|
|
||||||
|
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
If you have installed, you can update the application by navigate to the directory that contains the `docker-compose.yml` file and run the following command:
|
||||||
alt="Get it on F-Droid"
|
|
||||||
height="80">](https://f-droid.org/packages/app.alextran.immich)
|
```bash
|
||||||
|
docker-compose pull && docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
# Mobile app
|
||||||
|
|
||||||
|
| F-Droid | Google Play | iOS |
|
||||||
|
| - | - | - |
|
||||||
|
| <a href="https://f-droid.org/packages/app.alextran.immich"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> | <p align="left"> <img src="design/google-play-qr-code.png" width="200" title="Google Play Store"> <p/> | <p align="left"> <img src="design/ios-qr-code.png" width="200" title="Apple App Store"> <p/> |
|
||||||
|
|
||||||
|
> *The App version might be lagging behind the latest release due to the review process.*
|
||||||
|
|
||||||
|
|
||||||
## Android
|
<br/>
|
||||||
|
|
||||||
#### Get the app on Google Play Store [here](https://play.google.com/store/apps/details?id=app.alextran.immich)
|
|
||||||
|
|
||||||
*The App version might be lagging behind the latest release due to the review process.*
|
|
||||||
|
|
||||||
<p align="left">
|
|
||||||
<img src="design/google-play-qr-code.png" width="200" title="Google Play Store">
|
|
||||||
<p/>
|
|
||||||
|
|
||||||
## iOS
|
|
||||||
|
|
||||||
#### Get the app on Apple AppStore [here](https://apps.apple.com/us/app/immich/id1613945652):
|
|
||||||
|
|
||||||
*The App version might be lagging behind the latest release due to the review process.*
|
|
||||||
|
|
||||||
|
|
||||||
<p align="left">
|
|
||||||
<img src="design/ios-qr-code.png" width="200" title="Apple App Store">
|
|
||||||
<p/>
|
|
||||||
|
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
@@ -244,31 +211,28 @@ npm run api:generate # Run from server directory
|
|||||||
```
|
```
|
||||||
You can find the generated client SDK in the [`web/src/api`](web/src/api) for Typescript SDK and [`mobile/openapi`](mobile/openapi) for Dart SDK.
|
You can find the generated client SDK in the [`web/src/api`](web/src/api) for Typescript SDK and [`mobile/openapi`](mobile/openapi) for Dart SDK.
|
||||||
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
# Support
|
# Support
|
||||||
|
|
||||||
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**one time**](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or monthly donation from [**Github Sponsor**](https://github.com/sponsors/alextran1502)
|
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**one time**](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or monthly donation from [**Github Sponsor**](https://github.com/sponsors/alextran1502).
|
||||||
|
|
||||||
You can also donate using crypto currency with the following addresses:
|
You can also donate using crypto currency with the following addresses:
|
||||||
|
|
||||||
<p align="left" style="display: flex; place-items: center; gap: 20px" title="Bitcoin(BTC)">
|
<p align="" style="display: flex; place-items: center; gap: 15px" title="Bitcoin(BTC)"><img src="design/bitcoin.png" width="25" title="Bitcoin"> <b>Bitcoin</b>: <code>1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX</code></p>
|
||||||
<img src="design/bitcoin.png" width="25" title="Bitcoin">
|
|
||||||
<code>1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX</code>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
<p align="" style="display: flex; place-items: center; gap: 15px" title="Cardano(ADA)"> <img src="design/cardano.png" width="30" title="Cardano"> <b>Cardano</b>: <code>addr1qyy567vqhqrr3p7vpszr5p264gw89sqcwts2z8wqy4yek87cdmy79zazyjp7tmwhkluhk3krvslkzfvg0h43tytp3f5q49nycc</code> </p>
|
||||||
<p align="left" style="display: flex; place-items: center; gap: 15px" title="Cardano(ADA)">
|
|
||||||
<img src="design/cardano.png" width="30" title="Cardano">
|
|
||||||
<code>
|
|
||||||
addr1qyy567vqhqrr3p7vpszr5p264gw89sqcwts2z8wqy4yek87cdmy79zazyjp7tmwhkluhk3krvslkzfvg0h43tytp3f5q49nycc
|
|
||||||
</code>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
This is also a meaningful way to give me motivation and encouragement to continue working on the app.
|
This is also a meaningful way to give me motivation and encouragement to continue working on the app.
|
||||||
|
|
||||||
Cheers! 🎉
|
Cheers! 🎉
|
||||||
|
|
||||||
# Known Issue
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
# Known Issues
|
||||||
|
|
||||||
## TensorFlow Build Issue
|
## TensorFlow Build Issue
|
||||||
|
|
||||||
|
|||||||
32
dev-setup.md
Normal file
32
dev-setup.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Development Setup
|
||||||
|
|
||||||
|
## Lint / format extensions
|
||||||
|
|
||||||
|
Setting these in the IDE give a better developer experience auto-formatting code on save and providing instant feedback on lint issues.
|
||||||
|
|
||||||
|
### VSCode
|
||||||
|
Install Prettier, ESLint and Svelte extensions.
|
||||||
|
|
||||||
|
in User `settings.json` (`cmd + shift + p` and search for Open User Settings JSON) add the following:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"[javascript][typescript][css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"svelte.enable-ts-plugin": true,
|
||||||
|
"eslint.validate": ["javascript", "svelte"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running tests / checks
|
||||||
|
|
||||||
|
In both server and web:
|
||||||
|
`npm run check:all`
|
||||||
@@ -61,6 +61,6 @@ MAPBOX_KEY=
|
|||||||
####################################################################################
|
####################################################################################
|
||||||
|
|
||||||
# Custom message on the login page, should be written in HTML form.
|
# Custom message on the login page, should be written in HTML form.
|
||||||
# For example VITE_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
||||||
|
|
||||||
VITE_LOGIN_PAGE_MESSAGE=
|
PUBLIC_LOGIN_PAGE_MESSAGE=
|
||||||
@@ -6,6 +6,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
target: builder
|
||||||
command: npm run start:dev immich
|
command: npm run start:dev immich
|
||||||
volumes:
|
volumes:
|
||||||
- ../server:/usr/src/app
|
- ../server:/usr/src/app
|
||||||
@@ -24,6 +25,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../machine-learning
|
context: ../machine-learning
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
target: builder
|
||||||
command: npm run start:dev
|
command: npm run start:dev
|
||||||
volumes:
|
volumes:
|
||||||
- ../machine-learning:/usr/src/app
|
- ../machine-learning:/usr/src/app
|
||||||
@@ -41,6 +43,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
target: builder
|
||||||
command: npm run start:dev microservices
|
command: npm run start:dev microservices
|
||||||
volumes:
|
volumes:
|
||||||
- ../server:/usr/src/app
|
- ../server:/usr/src/app
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
target: builder
|
||||||
command: npm run test:e2e
|
command: npm run test:e2e
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
|
|||||||
25
install.sh
25
install.sh
@@ -6,10 +6,6 @@ RED='\033[0;31m'
|
|||||||
GREEN='\032[0;31m'
|
GREEN='\032[0;31m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
machine_has() {
|
|
||||||
type "$1" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
create_immich_directory() {
|
create_immich_directory() {
|
||||||
echo "Creating Immich directory..."
|
echo "Creating Immich directory..."
|
||||||
mkdir -p ./immich-app/immich-data
|
mkdir -p ./immich-app/immich-data
|
||||||
@@ -45,18 +41,21 @@ populate_upload_location() {
|
|||||||
start_docker_compose() {
|
start_docker_compose() {
|
||||||
echo "Starting Immich's docker containers"
|
echo "Starting Immich's docker containers"
|
||||||
|
|
||||||
if machine_has "docker compose"; then {
|
if docker compose &> /dev/null; then
|
||||||
docker compose up --remove-orphans -d
|
docker_bin="docker compose"
|
||||||
|
elif docker-compose &> /dev/null; then
|
||||||
show_friendly_message
|
docker_bin="docker-compose"
|
||||||
exit 0
|
else
|
||||||
}; fi
|
echo 'Cannot find `docker compose` or `docker-compose`.'
|
||||||
|
exit 1
|
||||||
if machine_has "docker-compose"; then
|
fi
|
||||||
docker-compose up --remove-orphans -d
|
|
||||||
|
|
||||||
|
if $docker_bin up --remove-orphans -d; then
|
||||||
show_friendly_message
|
show_friendly_message
|
||||||
exit 0
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Could not start. Check for errors above."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ upload:
|
|||||||
locale_code: de-DE
|
locale_code: de-DE
|
||||||
- file: mobile/assets/i18n/fr-FR.json
|
- file: mobile/assets/i18n/fr-FR.json
|
||||||
locale_code: fr-FR
|
locale_code: fr-FR
|
||||||
|
- file: mobile/assets/i18n/it-IT.json
|
||||||
|
locale_code: it-IT
|
||||||
|
- file: mobile/assets/i18n/nl-NL.json
|
||||||
|
locale_code: nl-NL
|
||||||
|
- file: mobile/assets/i18n/ko-KR.json
|
||||||
|
locale_code: ko-KR
|
||||||
|
- file: mobile/assets/i18n/da-DK.json
|
||||||
|
locale_code: da-DK
|
||||||
download:
|
download:
|
||||||
files:
|
files:
|
||||||
- file: mobile/assets/i18n/en-US.json
|
- file: mobile/assets/i18n/en-US.json
|
||||||
@@ -17,3 +25,11 @@ download:
|
|||||||
locale_code: de-DE
|
locale_code: de-DE
|
||||||
- file: mobile/assets/i18n/fr-FR.json
|
- file: mobile/assets/i18n/fr-FR.json
|
||||||
locale_code: fr-FR
|
locale_code: fr-FR
|
||||||
|
- file: mobile/assets/i18n/it-IT.json
|
||||||
|
locale_code: it-IT
|
||||||
|
- file: mobile/assets/i18n/nl-NL.json
|
||||||
|
locale_code: nl-NL
|
||||||
|
- file: mobile/assets/i18n/ko-KR.json
|
||||||
|
locale_code: ko-KR
|
||||||
|
- file: mobile/assets/i18n/da-DK.json
|
||||||
|
locale_code: da-DK
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM node:16-bullseye-slim
|
# Build stage
|
||||||
|
FROM node:16-bullseye-slim as builder
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
@@ -15,3 +16,27 @@ RUN npm rebuild @tensorflow/tfjs-node --build-from-source
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
# Prod stage
|
||||||
|
FROM node:16-bullseye-slim
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY entrypoint.sh ./
|
||||||
|
|
||||||
|
RUN mkdir -p /usr/src/app/dist \
|
||||||
|
&& mkdir -p /usr/src/app/node_modules \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y ffmpeg \
|
||||||
|
&& rm -rf /var/cache/apt/lists
|
||||||
|
|
||||||
|
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /usr/src/app/dist ./dist
|
||||||
|
|
||||||
|
RUN npm prune --production
|
||||||
|
|
||||||
|
# CMD [ "node", "dist/main" ]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
# npm run typeorm migration:run
|
# npm run typeorm migration:run
|
||||||
npm run build && npm run start:prod
|
# npm run start:prod
|
||||||
|
node dist/main.js
|
||||||
|
|||||||
3
mobile/android/.gitignore
vendored
3
mobile/android/.gitignore
vendored
@@ -11,3 +11,6 @@ GeneratedPluginRegistrant.java
|
|||||||
key.properties
|
key.properties
|
||||||
**/*.keystore
|
**/*.keystore
|
||||||
**/*.jks
|
**/*.jks
|
||||||
|
|
||||||
|
# Fastlane
|
||||||
|
/fastlane/report.xml
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "app.alextran.immich"
|
applicationId "app.alextran.immich"
|
||||||
minSdkVersion 21
|
minSdkVersion 23
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
@@ -80,5 +80,8 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
|
||||||
|
implementation "com.google.guava:guava:$guava_version"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
<service android:name=".AppClearedService" android:stopWithTask="false" />
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.example.immich_mobile
|
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package app.alextran.immich
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catches the event when either the system or the user kills the app
|
||||||
|
* (does not apply on force close!)
|
||||||
|
*/
|
||||||
|
class AppClearedService() : Service() {
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent) {
|
||||||
|
ContentObserverWorker.workManagerAppClearedWorkaround(applicationContext)
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package app.alextran.immich
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android plugin for Dart `BackgroundService`
|
||||||
|
*
|
||||||
|
* Receives messages/method calls from the foreground Dart side to manage
|
||||||
|
* the background service, e.g. start (enqueue), stop (cancel)
|
||||||
|
*/
|
||||||
|
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
|
private var methodChannel: MethodChannel? = null
|
||||||
|
private var context: Context? = null
|
||||||
|
|
||||||
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) {
|
||||||
|
context = ctx
|
||||||
|
methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
|
||||||
|
methodChannel?.setMethodCallHandler(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
onDetachedFromEngine()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDetachedFromEngine() {
|
||||||
|
methodChannel?.setMethodCallHandler(null)
|
||||||
|
methodChannel = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val ctx = context!!
|
||||||
|
when(call.method) {
|
||||||
|
"enable" -> {
|
||||||
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
|
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
|
||||||
|
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
|
||||||
|
.apply()
|
||||||
|
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
"configure" -> {
|
||||||
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
|
val requireUnmeteredNetwork = args.get(0) as Boolean
|
||||||
|
val requireCharging = args.get(1) as Boolean
|
||||||
|
ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging)
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
"disable" -> {
|
||||||
|
ContentObserverWorker.disable(ctx)
|
||||||
|
BackupWorker.stopWork(ctx)
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
"isEnabled" -> {
|
||||||
|
result.success(ContentObserverWorker.isEnabled(ctx))
|
||||||
|
}
|
||||||
|
"isIgnoringBatteryOptimizations" -> {
|
||||||
|
result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val TAG = "BackgroundServicePlugin"
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
package app.alextran.immich
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.concurrent.futures.ResolvableFuture
|
||||||
|
import androidx.work.BackoffPolicy
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
|
import androidx.work.ListenableWorker
|
||||||
|
import androidx.work.NetworkType
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkInfo
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.embedding.engine.dart.DartExecutor
|
||||||
|
import io.flutter.embedding.engine.loader.FlutterLoader
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.view.FlutterCallbackInformation
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker executed by Android WorkManager to perform backup in background
|
||||||
|
*
|
||||||
|
* Starts the Dart runtime/engine and calls `_nativeEntry` function in
|
||||||
|
* `background.service.dart` to run the actual backup logic.
|
||||||
|
* Called by Android WorkManager when all constraints for the work are met,
|
||||||
|
* i.e. battery is not low and optionally Wifi and charging are active.
|
||||||
|
*/
|
||||||
|
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
|
private val resolvableFuture = ResolvableFuture.create<Result>()
|
||||||
|
private var engine: FlutterEngine? = null
|
||||||
|
private lateinit var backgroundChannel: MethodChannel
|
||||||
|
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
|
||||||
|
private var timeBackupStarted: Long = 0L
|
||||||
|
|
||||||
|
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
||||||
|
|
||||||
|
Log.d(TAG, "startWork")
|
||||||
|
|
||||||
|
val ctx = applicationContext
|
||||||
|
|
||||||
|
if (!flutterLoader.initialized()) {
|
||||||
|
flutterLoader.startInitialization(ctx)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// Create a Notification channel if necessary
|
||||||
|
createChannel()
|
||||||
|
}
|
||||||
|
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
|
||||||
|
if (isIgnoringBatteryOptimizations) {
|
||||||
|
// normal background services can only up to 10 minutes
|
||||||
|
// foreground services are allowed to run indefinitely
|
||||||
|
// requires battery optimizations to be disabled (either manually by the user
|
||||||
|
// or by the system learning that immich is important to the user)
|
||||||
|
setForegroundAsync(createForegroundInfo(title))
|
||||||
|
} else {
|
||||||
|
showBackgroundInfo(title)
|
||||||
|
}
|
||||||
|
engine = FlutterEngine(ctx)
|
||||||
|
|
||||||
|
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||||
|
runDart()
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvableFuture
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the Dart runtime/engine and calls `_nativeEntry` function in
|
||||||
|
* `background.service.dart` to run the actual backup logic.
|
||||||
|
*/
|
||||||
|
private fun runDart() {
|
||||||
|
val callbackDispatcherHandle = applicationContext.getSharedPreferences(
|
||||||
|
SHARED_PREF_NAME, Context.MODE_PRIVATE).getLong(SHARED_PREF_CALLBACK_KEY, 0L)
|
||||||
|
val callbackInformation = FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle)
|
||||||
|
val appBundlePath = flutterLoader.findAppBundlePath()
|
||||||
|
|
||||||
|
engine?.let { engine ->
|
||||||
|
backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel")
|
||||||
|
backgroundChannel.setMethodCallHandler(this@BackupWorker)
|
||||||
|
engine.dartExecutor.executeDartCallback(
|
||||||
|
DartExecutor.DartCallback(
|
||||||
|
applicationContext.assets,
|
||||||
|
appBundlePath,
|
||||||
|
callbackInformation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopped() {
|
||||||
|
Log.d(TAG, "onStopped")
|
||||||
|
// called when the system has to stop this worker because constraints are
|
||||||
|
// no longer met or the system needs resources for more important tasks
|
||||||
|
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
||||||
|
backgroundChannel.invokeMethod("systemStop", null)
|
||||||
|
}
|
||||||
|
// cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException)
|
||||||
|
// instead, wait for 5 seconds until forcefully stopping backup work
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
stopEngine(null)
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun stopEngine(result: Result?) {
|
||||||
|
if (result != null) {
|
||||||
|
Log.d(TAG, "stopEngine result=${result}")
|
||||||
|
resolvableFuture.set(result)
|
||||||
|
}
|
||||||
|
engine?.destroy()
|
||||||
|
engine = null
|
||||||
|
clearBackgroundNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) {
|
||||||
|
when (call.method) {
|
||||||
|
"initialized" -> {
|
||||||
|
timeBackupStarted = SystemClock.uptimeMillis()
|
||||||
|
backgroundChannel.invokeMethod(
|
||||||
|
"onAssetsChanged",
|
||||||
|
null,
|
||||||
|
object : MethodChannel.Result {
|
||||||
|
override fun notImplemented() {
|
||||||
|
stopEngine(Result.failure())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||||
|
stopEngine(Result.failure())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun success(receivedResult: Any?) {
|
||||||
|
val success = receivedResult as Boolean
|
||||||
|
stopEngine(if(success) Result.success() else Result.retry())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"updateNotification" -> {
|
||||||
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
|
val title = args.get(0) as String
|
||||||
|
val content = args.get(1) as String
|
||||||
|
if (isIgnoringBatteryOptimizations) {
|
||||||
|
setForegroundAsync(createForegroundInfo(title, content))
|
||||||
|
} else {
|
||||||
|
showBackgroundInfo(title, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"showError" -> {
|
||||||
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
|
val title = args.get(0) as String
|
||||||
|
val content = args.get(1) as String
|
||||||
|
val individualTag = args.get(2) as String?
|
||||||
|
showError(title, content, individualTag)
|
||||||
|
}
|
||||||
|
"clearErrorNotifications" -> clearErrorNotifications()
|
||||||
|
"hasContentChanged" -> {
|
||||||
|
val lastChange = applicationContext
|
||||||
|
.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted)
|
||||||
|
val hasContentChanged = lastChange > timeBackupStarted;
|
||||||
|
timeBackupStarted = SystemClock.uptimeMillis()
|
||||||
|
r.success(hasContentChanged)
|
||||||
|
}
|
||||||
|
else -> r.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError(title: String, content: String, individualTag: String?) {
|
||||||
|
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setTicker(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.build()
|
||||||
|
notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearErrorNotifications() {
|
||||||
|
notificationManager.cancel(NOTIFICATION_ERROR_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showBackgroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null) {
|
||||||
|
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setTicker(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearBackgroundNotification() {
|
||||||
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
|
||||||
|
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setTicker(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
return ForegroundInfo(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun createChannel() {
|
||||||
|
val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
|
||||||
|
notificationManager.createNotificationChannel(foreground)
|
||||||
|
val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
|
notificationManager.createNotificationChannel(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SHARED_PREF_NAME = "immichBackgroundService"
|
||||||
|
const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
|
||||||
|
const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
|
||||||
|
const val SHARED_PREF_LAST_CHANGE = "lastChange"
|
||||||
|
|
||||||
|
private const val TASK_NAME_BACKUP = "immich/BackupWorker"
|
||||||
|
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
|
||||||
|
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
|
||||||
|
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
|
||||||
|
private const val NOTIFICATION_ID = 1
|
||||||
|
private const val NOTIFICATION_ERROR_ID = 2
|
||||||
|
private const val ONE_MINUTE = 60000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues the BackupWorker to run once the constraints are met
|
||||||
|
*/
|
||||||
|
fun enqueueBackupWorker(context: Context,
|
||||||
|
requireWifi: Boolean = false,
|
||||||
|
requireCharging: Boolean = false,
|
||||||
|
delayMilliseconds: Long = 0L) {
|
||||||
|
val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds)
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest)
|
||||||
|
Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the constraints of an already enqueued BackupWorker
|
||||||
|
*/
|
||||||
|
fun updateBackupWorker(context: Context,
|
||||||
|
requireWifi: Boolean = false,
|
||||||
|
requireCharging: Boolean = false) {
|
||||||
|
try {
|
||||||
|
val wm = WorkManager.getInstance(context)
|
||||||
|
val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP)
|
||||||
|
val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
|
||||||
|
if (workInfoList != null) {
|
||||||
|
for (workInfo in workInfoList) {
|
||||||
|
if (workInfo.getState() == WorkInfo.State.ENQUEUED) {
|
||||||
|
val workRequest = buildWorkRequest(requireWifi, requireCharging)
|
||||||
|
wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
|
||||||
|
Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, "updateBackupWorker failed: ${e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the currently running worker (if any) and removes it from the work queue
|
||||||
|
*/
|
||||||
|
fun stopWork(context: Context) {
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP)
|
||||||
|
Log.d(TAG, "stopWork: BackupWorker cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if the app is ignoring battery optimizations
|
||||||
|
*/
|
||||||
|
fun isIgnoringBatteryOptimizations(ctx: Context): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val pwrm = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
val name = ctx.packageName
|
||||||
|
return pwrm.isIgnoringBatteryOptimizations(name)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildWorkRequest(requireWifi: Boolean = false,
|
||||||
|
requireCharging: Boolean = false,
|
||||||
|
delayMilliseconds: Long = 0L): OneTimeWorkRequest {
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||||
|
.setRequiresBatteryNotLow(true)
|
||||||
|
.setRequiresCharging(requireCharging)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
|
||||||
|
.setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
return work
|
||||||
|
}
|
||||||
|
|
||||||
|
private val flutterLoader = FlutterLoader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val TAG = "BackupWorker"
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package app.alextran.immich
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.Operation
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker executed by Android WorkManager observing content changes (new photos/videos)
|
||||||
|
*
|
||||||
|
* Immediately enqueues the BackupWorker when running.
|
||||||
|
* As this work is not triggered periodically, but on content change, the
|
||||||
|
* worker enqueues itself again after each run.
|
||||||
|
*/
|
||||||
|
class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
if (!isEnabled(applicationContext)) {
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
if (getTriggeredContentUris().size > 0) {
|
||||||
|
startBackupWorker(applicationContext, delayMilliseconds = 0)
|
||||||
|
}
|
||||||
|
enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled"
|
||||||
|
const val SHARED_PREF_REQUIRE_WIFI = "requireWifi"
|
||||||
|
const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging"
|
||||||
|
|
||||||
|
private const val TASK_NAME_OBSERVER = "immich/ContentObserver"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues the `ContentObserverWorker`.
|
||||||
|
*
|
||||||
|
* @param context Android Context
|
||||||
|
*/
|
||||||
|
fun enable(context: Context, immediate: Boolean = false) {
|
||||||
|
// migration to remove any old active background task
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork("immich/photoListener")
|
||||||
|
|
||||||
|
enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
|
||||||
|
Log.d(TAG, "enabled ContentObserverWorker")
|
||||||
|
if (immediate) {
|
||||||
|
startBackupWorker(context, delayMilliseconds = 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the `BackupWorker` to run when all constraints are met.
|
||||||
|
*
|
||||||
|
* @param context Android Context
|
||||||
|
* @param requireWifi if true, task only runs if connected to wifi
|
||||||
|
* @param requireCharging if true, task only runs if device is charging
|
||||||
|
*/
|
||||||
|
fun configureWork(context: Context,
|
||||||
|
requireWifi: Boolean = false,
|
||||||
|
requireCharging: Boolean = false) {
|
||||||
|
context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(SHARED_PREF_SERVICE_ENABLED, true)
|
||||||
|
.putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi)
|
||||||
|
.putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging)
|
||||||
|
.apply()
|
||||||
|
BackupWorker.updateBackupWorker(context, requireWifi, requireCharging)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the currently running worker (if any) and removes it from the work queue
|
||||||
|
*/
|
||||||
|
fun disable(context: Context) {
|
||||||
|
context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply()
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER)
|
||||||
|
Log.d(TAG, "disabled ContentObserverWorker")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the user has enabled the background backup service
|
||||||
|
*/
|
||||||
|
fun isEnabled(ctx: Context): Boolean {
|
||||||
|
return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue and replace the worker without the content trigger but with a short delay
|
||||||
|
*/
|
||||||
|
fun workManagerAppClearedWorkaround(context: Context) {
|
||||||
|
val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
|
||||||
|
.setInitialDelay(500, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
WorkManager
|
||||||
|
.getInstance(context)
|
||||||
|
.enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work)
|
||||||
|
.getResult()
|
||||||
|
.get()
|
||||||
|
Log.d(TAG, "workManagerAppClearedWorkaround")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) {
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
||||||
|
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
|
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
||||||
|
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
|
.setTriggerContentUpdateDelay(5000, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startBackupWorker(context: Context, delayMilliseconds: Long) {
|
||||||
|
val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true)
|
||||||
|
val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
|
||||||
|
BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)
|
||||||
|
sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val TAG = "ContentObserverWorker"
|
||||||
@@ -1,6 +1,20 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
class MainActivity: FlutterActivity() {
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
flutterEngine.getPlugins().add(BackgroundServicePlugin())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
startService(Intent(getBaseContext(), AppClearedService::class.java));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.6.10'
|
ext.kotlin_version = '1.6.10'
|
||||||
|
ext.work_version = '2.7.1'
|
||||||
|
ext.concurrent_version = '1.1.0'
|
||||||
|
ext.guava_version = '31.0.1-android'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 30,
|
"android.injected.version.code" => 40,
|
||||||
"android.injected.version.name" => "1.20.0",
|
"android.injected.version.name" => "1.28.2",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
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')
|
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')
|
||||||
|
|||||||
@@ -15,13 +15,21 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
|
|||||||
|
|
||||||
## Android
|
## Android
|
||||||
|
|
||||||
|
### android build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android build
|
||||||
|
```
|
||||||
|
|
||||||
|
Build Android
|
||||||
|
|
||||||
### android release
|
### android release
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
[bundle exec] fastlane android release
|
[bundle exec] fastlane android release
|
||||||
```
|
```
|
||||||
|
|
||||||
Update AAB to PlayStore
|
Build and Release Android
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
* Improve performance
|
||||||
|
* Fix album title overflow
|
||||||
|
* New feature - Share asset from mobile to other apps
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Modify Album API endpoint to return count attribute instead of all assets to reduce network consumption and CPU processing.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Added setting screen
|
||||||
|
* Implemented dark mode
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
* Feature - [Android] Background backup.
|
||||||
|
* Fixed - [iOS] Dark mode not auto switch.
|
||||||
|
* Fixed - WebSocket not getting correct data on mobile.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
* Feature - Customization options for asset grid
|
||||||
|
* Added pt-BR Translation: Translation into Portuguese Brazil
|
||||||
|
* Feature - Show notifications on background backup errors
|
||||||
|
* Optimization - Use CachedNetworkImage and separate cache for thumbnails on library page
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Fixed rendering blank when failed to parse datetime on main timeline
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Add cache setting and improve caching mechanism
|
||||||
|
* Persist WiFi + charging settings of background backup
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Fixed remove empty translations
|
||||||
|
* Fixed search page crashes the app on some Android models
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Improve Android background service reliability
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Fix background service cannot run in release build
|
||||||
@@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000204">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000222">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="11.673502">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="55.311329">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="37.162935">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.070842">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
|
"album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
|
||||||
"album_info_card_backup_album_included": "EINGESCHLOSSEN",
|
"album_info_card_backup_album_included": "EINGESCHLOSSEN",
|
||||||
|
"album_thumbnail_card_item": "1 Element",
|
||||||
|
"album_thumbnail_card_items": "{} Elemente",
|
||||||
|
"album_thumbnail_card_shared": " · Geteilt",
|
||||||
"album_viewer_appbar_share_delete": "Album löschen",
|
"album_viewer_appbar_share_delete": "Album löschen",
|
||||||
"album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden",
|
"album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden",
|
||||||
"album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden",
|
"album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden",
|
||||||
@@ -46,6 +49,9 @@
|
|||||||
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
|
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
|
||||||
"backup_info_card_assets": "Elemente",
|
"backup_info_card_assets": "Elemente",
|
||||||
"control_bottom_app_bar_delete": "Löschen",
|
"control_bottom_app_bar_delete": "Löschen",
|
||||||
|
"control_bottom_app_bar_share": "Teilen",
|
||||||
|
"create_album_page_untitled": "Unbenannt",
|
||||||
|
"create_shared_album_page_create": "Erstellen",
|
||||||
"create_shared_album_page_share": "Teilen",
|
"create_shared_album_page_share": "Teilen",
|
||||||
"create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
|
"create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
|
||||||
"create_shared_album_page_share_select_photos": "Fotos auswählen",
|
"create_shared_album_page_share_select_photos": "Fotos auswählen",
|
||||||
@@ -59,6 +65,8 @@
|
|||||||
"exif_bottom_sheet_description": "Beschreibung hinzufügen...",
|
"exif_bottom_sheet_description": "Beschreibung hinzufügen...",
|
||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "STANDORT",
|
"exif_bottom_sheet_location": "STANDORT",
|
||||||
|
"library_page_albums": "Alben",
|
||||||
|
"library_page_new_album": "Neues Album",
|
||||||
"login_form_button_text": "Anmelden",
|
"login_form_button_text": "Anmelden",
|
||||||
"login_form_email_hint": "deine@email.de",
|
"login_form_email_hint": "deine@email.de",
|
||||||
"login_form_endpoint_hint": "http://deine-server-ip:port/api",
|
"login_form_endpoint_hint": "http://deine-server-ip:port/api",
|
||||||
@@ -67,13 +75,14 @@
|
|||||||
"login_form_err_invalid_email": "Ungültige E-Mail",
|
"login_form_err_invalid_email": "Ungültige E-Mail",
|
||||||
"login_form_err_leading_whitespace": "Führendes Leerzichen",
|
"login_form_err_leading_whitespace": "Führendes Leerzichen",
|
||||||
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
|
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
|
||||||
"login_form_failed_login": "Fehler bei der Anmeldung, überprüfen Sie Server URL, E-Mail und Passwort",
|
"login_form_failed_login": "Error logging you in, check server url, email and password",
|
||||||
"login_form_label_email": "E-Mail",
|
"login_form_label_email": "E-Mail",
|
||||||
"login_form_label_password": "Passwort",
|
"login_form_label_password": "Passwort",
|
||||||
"login_form_password_hint": "Passwort",
|
"login_form_password_hint": "password",
|
||||||
"login_form_save_login": "Angemeldet bleiben",
|
"login_form_save_login": "Angemeldet bleiben",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
|
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
|
||||||
|
"profile_drawer_settings": "Einstellungen",
|
||||||
"profile_drawer_sign_out": "Abmelden",
|
"profile_drawer_sign_out": "Abmelden",
|
||||||
"search_bar_hint": "Durchsuche deine Fotos",
|
"search_bar_hint": "Durchsuche deine Fotos",
|
||||||
"search_page_no_objects": "Keine Objektinformationen verfügbar",
|
"search_page_no_objects": "Keine Objektinformationen verfügbar",
|
||||||
@@ -83,20 +92,31 @@
|
|||||||
"search_result_page_new_search_hint": "Neue Suche",
|
"search_result_page_new_search_hint": "Neue Suche",
|
||||||
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
|
"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_err_album": "Album konnte nicht erstellt werden",
|
||||||
"select_user_for_sharing_page_share_suggestions": "Vorschläge",
|
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||||
|
"setting_pages_app_bar_settings": "Einstellungen",
|
||||||
"share_add": "Hinzufügen",
|
"share_add": "Hinzufügen",
|
||||||
"share_add_photos": "Fotos hinzufügen",
|
"share_add_photos": "Fotos hinzufügen",
|
||||||
"share_add_title": "Titel hinzufügen",
|
"share_add_title": "Titel hinzufügen",
|
||||||
"share_create_album": "Album erstellen",
|
"share_create_album": "Album erstellen",
|
||||||
|
"share_dialog_preparing": "Vorbereiten...",
|
||||||
"share_invite": "Zum Album einladen",
|
"share_invite": "Zum Album einladen",
|
||||||
"sharing_page_album": "Geteilte Alben",
|
"sharing_page_album": "Geteilte Alben",
|
||||||
"sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.",
|
"sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.",
|
||||||
"sharing_page_empty_list": "LEERE LISTE",
|
"sharing_page_empty_list": "LEERE LISTE",
|
||||||
"sharing_silver_appbar_create_shared_album": "Neues geteiltes Album",
|
"sharing_silver_appbar_create_shared_album": "Neues geteiltes Album",
|
||||||
"sharing_silver_appbar_share_partner": "Teile mit Partner",
|
"sharing_silver_appbar_share_partner": "Teile mit Partner",
|
||||||
|
"tab_controller_nav_library": "Bibliothek",
|
||||||
"tab_controller_nav_photos": "Fotos",
|
"tab_controller_nav_photos": "Fotos",
|
||||||
"tab_controller_nav_search": "Suche",
|
"tab_controller_nav_search": "Suche",
|
||||||
"tab_controller_nav_sharing": "Teilen",
|
"tab_controller_nav_sharing": "Teilen",
|
||||||
|
"theme_setting_dark_mode_switch": "Dunkler Modus",
|
||||||
|
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
|
||||||
|
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
|
||||||
|
"theme_setting_system_theme_switch": "Automatisch (Systemeinstellung folgen)",
|
||||||
|
"theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App",
|
||||||
|
"theme_setting_theme_title": "Theme",
|
||||||
|
"theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhöht allerdings den Datenverbrauch deutlich",
|
||||||
|
"theme_setting_three_stage_loading_title": "Dreistufiges Laden aktivieren",
|
||||||
"version_announcement_overlay_ack": "Ich habe verstanden",
|
"version_announcement_overlay_ack": "Ich habe verstanden",
|
||||||
"version_announcement_overlay_release_notes": "Änderungsprotokoll",
|
"version_announcement_overlay_release_notes": "Änderungsprotokoll",
|
||||||
"version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
|
"version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"album_info_card_backup_album_excluded": "EXCLUDED",
|
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||||
"album_info_card_backup_album_included": "INCLUDED",
|
"album_info_card_backup_album_included": "INCLUDED",
|
||||||
|
"album_thumbnail_card_item": "1 item",
|
||||||
|
"album_thumbnail_card_items": "{} items",
|
||||||
|
"album_thumbnail_card_shared": " · Shared",
|
||||||
"album_viewer_appbar_share_delete": "Delete album",
|
"album_viewer_appbar_share_delete": "Delete album",
|
||||||
"album_viewer_appbar_share_err_delete": "Failed to delete album",
|
"album_viewer_appbar_share_err_delete": "Failed to delete album",
|
||||||
"album_viewer_appbar_share_err_leave": "Failed to leave album",
|
"album_viewer_appbar_share_err_leave": "Failed to leave album",
|
||||||
@@ -9,6 +12,8 @@
|
|||||||
"album_viewer_appbar_share_leave": "Leave album",
|
"album_viewer_appbar_share_leave": "Leave album",
|
||||||
"album_viewer_appbar_share_remove": "Remove from album",
|
"album_viewer_appbar_share_remove": "Remove from album",
|
||||||
"album_viewer_page_share_add_users": "Add users",
|
"album_viewer_page_share_add_users": "Add users",
|
||||||
|
"asset_list_settings_subtitle": "Photo grid layout settings",
|
||||||
|
"asset_list_settings_title": "Photo Grid",
|
||||||
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||||
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||||
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||||
@@ -16,7 +21,26 @@
|
|||||||
"backup_album_selection_page_selection_info": "Selection Info",
|
"backup_album_selection_page_selection_info": "Selection Info",
|
||||||
"backup_album_selection_page_total_assets": "Total unique assets",
|
"backup_album_selection_page_total_assets": "Total unique assets",
|
||||||
"backup_all": "All",
|
"backup_all": "All",
|
||||||
|
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
|
||||||
|
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
|
||||||
|
"backup_background_service_current_upload_notification": "Uploading {}",
|
||||||
|
"backup_background_service_default_notification": "Checking for new assets…",
|
||||||
|
"backup_background_service_error_title": "Backup error",
|
||||||
|
"backup_background_service_in_progress_notification": "Backing up your assets…",
|
||||||
|
"backup_background_service_upload_failure_notification": "Failed to upload {}",
|
||||||
"backup_controller_page_albums": "Backup Albums",
|
"backup_controller_page_albums": "Backup Albums",
|
||||||
|
"backup_controller_page_background_battery_info_link": "Show me how",
|
||||||
|
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
|
||||||
|
"backup_controller_page_background_battery_info_ok": "OK",
|
||||||
|
"backup_controller_page_background_battery_info_title": "Battery optimizations",
|
||||||
|
"backup_controller_page_background_charging": "Only while charging",
|
||||||
|
"backup_controller_page_background_configure_error": "Failed to configure the background service",
|
||||||
|
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
|
||||||
|
"backup_controller_page_background_is_off": "Automatic background backup is off",
|
||||||
|
"backup_controller_page_background_is_on": "Automatic background backup is on",
|
||||||
|
"backup_controller_page_background_turn_off": "Turn off background service",
|
||||||
|
"backup_controller_page_background_turn_on": "Turn on background service",
|
||||||
|
"backup_controller_page_background_wifi": "Only on WiFi",
|
||||||
"backup_controller_page_backup": "Backup",
|
"backup_controller_page_backup": "Backup",
|
||||||
"backup_controller_page_backup_selected": "Selected: ",
|
"backup_controller_page_backup_selected": "Selected: ",
|
||||||
"backup_controller_page_backup_sub": "Backed up photos and videos",
|
"backup_controller_page_backup_sub": "Backed up photos and videos",
|
||||||
@@ -45,9 +69,24 @@
|
|||||||
"backup_controller_page_uploading_file_info": "Uploading file info",
|
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||||
"backup_err_only_album": "Cannot remove the only album",
|
"backup_err_only_album": "Cannot remove the only album",
|
||||||
"backup_info_card_assets": "assets",
|
"backup_info_card_assets": "assets",
|
||||||
|
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
|
||||||
|
"cache_settings_clear_cache_button": "Clear cache",
|
||||||
|
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
|
||||||
|
"cache_settings_image_cache_size": "Image cache size ({} assets)",
|
||||||
|
"cache_settings_statistics_album": "Library thumbnails",
|
||||||
|
"cache_settings_statistics_assets": "{} assets ({})",
|
||||||
|
"cache_settings_statistics_full": "Full images",
|
||||||
|
"cache_settings_statistics_shared": "Shared album thumbnails",
|
||||||
|
"cache_settings_statistics_thumbnail": "Thumbnails",
|
||||||
|
"cache_settings_statistics_title": "Cache usage",
|
||||||
|
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
|
||||||
|
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
|
||||||
|
"cache_settings_title": "Caching Settings",
|
||||||
"control_bottom_app_bar_delete": "Delete",
|
"control_bottom_app_bar_delete": "Delete",
|
||||||
"create_shared_album_page_share": "Share",
|
"control_bottom_app_bar_share": "Share",
|
||||||
|
"create_album_page_untitled": "Untitled",
|
||||||
"create_shared_album_page_create": "Create",
|
"create_shared_album_page_create": "Create",
|
||||||
|
"create_shared_album_page_share": "Share",
|
||||||
"create_shared_album_page_share_add_assets": "ADD ASSETS",
|
"create_shared_album_page_share_add_assets": "ADD ASSETS",
|
||||||
"create_shared_album_page_share_select_photos": "Select Photos",
|
"create_shared_album_page_share_select_photos": "Select Photos",
|
||||||
"daily_title_text_date": "E, MMM dd",
|
"daily_title_text_date": "E, MMM dd",
|
||||||
@@ -60,6 +99,8 @@
|
|||||||
"exif_bottom_sheet_description": "Add Description...",
|
"exif_bottom_sheet_description": "Add Description...",
|
||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "LOCATION",
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
|
"library_page_albums": "Albums",
|
||||||
|
"library_page_new_album": "New album",
|
||||||
"login_form_button_text": "Login",
|
"login_form_button_text": "Login",
|
||||||
"login_form_email_hint": "youremail@email.com",
|
"login_form_email_hint": "youremail@email.com",
|
||||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||||
@@ -75,6 +116,7 @@
|
|||||||
"login_form_save_login": "Stay logged in",
|
"login_form_save_login": "Stay logged in",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||||
|
"profile_drawer_settings": "Settings",
|
||||||
"profile_drawer_sign_out": "Sign Out",
|
"profile_drawer_sign_out": "Sign Out",
|
||||||
"search_bar_hint": "Search your photos",
|
"search_bar_hint": "Search your photos",
|
||||||
"search_page_no_objects": "No Objects Info Available",
|
"search_page_no_objects": "No Objects Info Available",
|
||||||
@@ -85,20 +127,39 @@
|
|||||||
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
||||||
"select_user_for_sharing_page_err_album": "Failed to create album",
|
"select_user_for_sharing_page_err_album": "Failed to create album",
|
||||||
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||||
|
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
|
||||||
|
"setting_notifications_notify_hours": "{} hours",
|
||||||
|
"setting_notifications_notify_immediately": "immediately",
|
||||||
|
"setting_notifications_notify_minutes": "{} minutes",
|
||||||
|
"setting_notifications_notify_never": "never",
|
||||||
|
"setting_notifications_subtitle": "Adjust your notification preferences",
|
||||||
|
"setting_notifications_title": "Notifications",
|
||||||
|
"setting_pages_app_bar_settings": "Settings",
|
||||||
"share_add": "Add",
|
"share_add": "Add",
|
||||||
"share_add_photos": "Add photos",
|
"share_add_photos": "Add photos",
|
||||||
"share_add_title": "Add a title",
|
"share_add_title": "Add a title",
|
||||||
"share_create_album": "Create album",
|
"share_create_album": "Create album",
|
||||||
|
"share_dialog_preparing": "Preparing...",
|
||||||
"share_invite": "Invite to album",
|
"share_invite": "Invite to album",
|
||||||
"sharing_page_album": "Shared albums",
|
"sharing_page_album": "Shared albums",
|
||||||
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
|
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
|
||||||
"sharing_page_empty_list": "EMPTY LIST",
|
"sharing_page_empty_list": "EMPTY LIST",
|
||||||
"sharing_silver_appbar_create_shared_album": "Create shared album",
|
"sharing_silver_appbar_create_shared_album": "Create shared album",
|
||||||
"sharing_silver_appbar_share_partner": "Share with partner",
|
"sharing_silver_appbar_share_partner": "Share with partner",
|
||||||
|
"tab_controller_nav_library": "Library",
|
||||||
"tab_controller_nav_photos": "Photos",
|
"tab_controller_nav_photos": "Photos",
|
||||||
"tab_controller_nav_search": "Search",
|
"tab_controller_nav_search": "Search",
|
||||||
"tab_controller_nav_sharing": "Sharing",
|
"tab_controller_nav_sharing": "Sharing",
|
||||||
"tab_controller_nav_library": "Library",
|
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
|
||||||
|
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
|
||||||
|
"theme_setting_dark_mode_switch": "Dark mode",
|
||||||
|
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
|
||||||
|
"theme_setting_image_viewer_quality_title": "Image viewer quality",
|
||||||
|
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
|
||||||
|
"theme_setting_theme_subtitle": "Choose the app's theme setting",
|
||||||
|
"theme_setting_theme_title": "Theme",
|
||||||
|
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
|
||||||
|
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
|
||||||
"version_announcement_overlay_ack": "Acknowledge",
|
"version_announcement_overlay_ack": "Acknowledge",
|
||||||
"version_announcement_overlay_release_notes": "release notes",
|
"version_announcement_overlay_release_notes": "release notes",
|
||||||
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
||||||
|
|||||||
@@ -21,12 +21,8 @@
|
|||||||
"backup_controller_page_backup_selected": "Seleccionado:",
|
"backup_controller_page_backup_selected": "Seleccionado:",
|
||||||
"backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos",
|
"backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos",
|
||||||
"backup_controller_page_cancel": "Cancelar",
|
"backup_controller_page_cancel": "Cancelar",
|
||||||
"backup_controller_page_created": "",
|
|
||||||
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.",
|
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.",
|
||||||
"backup_controller_page_excluded": "Excluido:",
|
"backup_controller_page_excluded": "Excluido:",
|
||||||
"backup_controller_page_failed": "",
|
|
||||||
"backup_controller_page_filename": "",
|
|
||||||
"backup_controller_page_id": "",
|
|
||||||
"backup_controller_page_info": "Información de la Copia de Seguridad",
|
"backup_controller_page_info": "Información de la Copia de Seguridad",
|
||||||
"backup_controller_page_none_selected": "Ninguno seleccionado",
|
"backup_controller_page_none_selected": "Ninguno seleccionado",
|
||||||
"backup_controller_page_remainder": "Remanente",
|
"backup_controller_page_remainder": "Remanente",
|
||||||
@@ -42,7 +38,6 @@
|
|||||||
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
|
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
|
||||||
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
|
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
|
||||||
"backup_controller_page_turn_on": "Activar la copia de seguridad",
|
"backup_controller_page_turn_on": "Activar la copia de seguridad",
|
||||||
"backup_controller_page_uploading_file_info": "",
|
|
||||||
"backup_err_only_album": "No se puede eliminar el único álbum",
|
"backup_err_only_album": "No se puede eliminar el único álbum",
|
||||||
"backup_info_card_assets": "activos",
|
"backup_info_card_assets": "activos",
|
||||||
"control_bottom_app_bar_delete": "Eliminar",
|
"control_bottom_app_bar_delete": "Eliminar",
|
||||||
@@ -67,7 +62,6 @@
|
|||||||
"login_form_err_invalid_email": "Correo electrónico no válido",
|
"login_form_err_invalid_email": "Correo electrónico no válido",
|
||||||
"login_form_err_leading_whitespace": "Espacio en blanco inicial",
|
"login_form_err_leading_whitespace": "Espacio en blanco inicial",
|
||||||
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
|
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
|
||||||
"login_form_failed_login": "",
|
|
||||||
"login_form_label_email": "Correo",
|
"login_form_label_email": "Correo",
|
||||||
"login_form_label_password": "Contraseña",
|
"login_form_label_password": "Contraseña",
|
||||||
"login_form_password_hint": "contraseña",
|
"login_form_password_hint": "contraseña",
|
||||||
@@ -76,14 +70,12 @@
|
|||||||
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
|
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
|
||||||
"profile_drawer_sign_out": "Cerrar Sesión",
|
"profile_drawer_sign_out": "Cerrar Sesión",
|
||||||
"search_bar_hint": "Busca tus fotos",
|
"search_bar_hint": "Busca tus fotos",
|
||||||
"search_page_no_objects": "",
|
|
||||||
"search_page_no_places": "No hay información de lugares disponibles",
|
"search_page_no_places": "No hay información de lugares disponibles",
|
||||||
"search_page_places": "Lugares",
|
"search_page_places": "Lugares",
|
||||||
"search_page_things": "Cosas",
|
"search_page_things": "Cosas",
|
||||||
"search_result_page_new_search_hint": "Nueva Busqueda",
|
"search_result_page_new_search_hint": "Nueva Busqueda",
|
||||||
"select_additional_user_for_sharing_page_suggestions": "Sugerencias",
|
"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_err_album": "Fallo al crear el álbum",
|
||||||
"select_user_for_sharing_page_share_suggestions": "",
|
|
||||||
"share_add": "Añadir",
|
"share_add": "Añadir",
|
||||||
"share_add_photos": "Añadir fotos",
|
"share_add_photos": "Añadir fotos",
|
||||||
"share_add_title": "Añadir un título",
|
"share_add_title": "Añadir un título",
|
||||||
|
|||||||
@@ -49,9 +49,6 @@
|
|||||||
"create_shared_album_page_share": "Jaa",
|
"create_shared_album_page_share": "Jaa",
|
||||||
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
|
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
|
||||||
"create_shared_album_page_share_select_photos": "Valitse kuvat",
|
"create_shared_album_page_share_select_photos": "Valitse kuvat",
|
||||||
"daily_title_text_date": "",
|
|
||||||
"daily_title_text_date_year": "",
|
|
||||||
"date_format": "",
|
|
||||||
"delete_dialog_alert": "Nämä kohteet poistetaan pysyvästi Immich:stä ja laitteeltasi",
|
"delete_dialog_alert": "Nämä kohteet poistetaan pysyvästi Immich:stä ja laitteeltasi",
|
||||||
"delete_dialog_cancel": "Peruuta",
|
"delete_dialog_cancel": "Peruuta",
|
||||||
"delete_dialog_ok": "Poista",
|
"delete_dialog_ok": "Poista",
|
||||||
@@ -72,7 +69,6 @@
|
|||||||
"login_form_label_password": "Salasana",
|
"login_form_label_password": "Salasana",
|
||||||
"login_form_password_hint": "salasana",
|
"login_form_password_hint": "salasana",
|
||||||
"login_form_save_login": "Pysy kirjautuneena",
|
"login_form_save_login": "Pysy kirjautuneena",
|
||||||
"monthly_title_text_date_format": "",
|
|
||||||
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
|
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
|
||||||
"profile_drawer_sign_out": "Kirjaudu ulos",
|
"profile_drawer_sign_out": "Kirjaudu ulos",
|
||||||
"search_bar_hint": "Etsi kuvia",
|
"search_bar_hint": "Etsi kuvia",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"album_info_card_backup_album_excluded": "EXCLU",
|
"album_info_card_backup_album_excluded": "EXCLU",
|
||||||
"album_info_card_backup_album_included": "INCLUS",
|
"album_info_card_backup_album_included": "INCLUS",
|
||||||
|
"album_thumbnail_card_item": "1 élément",
|
||||||
|
"album_thumbnail_card_items": "{} éléments",
|
||||||
|
"album_thumbnail_card_shared": " · Partagé",
|
||||||
"album_viewer_appbar_share_delete": "Supprimer l'album",
|
"album_viewer_appbar_share_delete": "Supprimer l'album",
|
||||||
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
|
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
|
||||||
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
|
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
|
||||||
@@ -46,6 +49,9 @@
|
|||||||
"backup_err_only_album": "Impossible de retirer le seul album",
|
"backup_err_only_album": "Impossible de retirer le seul album",
|
||||||
"backup_info_card_assets": "éléments",
|
"backup_info_card_assets": "éléments",
|
||||||
"control_bottom_app_bar_delete": "Supprimer",
|
"control_bottom_app_bar_delete": "Supprimer",
|
||||||
|
"control_bottom_app_bar_share": "Partager",
|
||||||
|
"create_album_page_untitled": "Sans titre",
|
||||||
|
"create_shared_album_page_create": "Créer",
|
||||||
"create_shared_album_page_share": "Partager",
|
"create_shared_album_page_share": "Partager",
|
||||||
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
|
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
|
||||||
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
|
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
|
||||||
@@ -59,6 +65,8 @@
|
|||||||
"exif_bottom_sheet_description": "Ajouter une description...",
|
"exif_bottom_sheet_description": "Ajouter une description...",
|
||||||
"exif_bottom_sheet_details": "DÉTAILS",
|
"exif_bottom_sheet_details": "DÉTAILS",
|
||||||
"exif_bottom_sheet_location": "LOCALISATION",
|
"exif_bottom_sheet_location": "LOCALISATION",
|
||||||
|
"library_page_albums": "Albums",
|
||||||
|
"library_page_new_album": "Nouvel album",
|
||||||
"login_form_button_text": "Connexion",
|
"login_form_button_text": "Connexion",
|
||||||
"login_form_email_hint": "votreemail@email.com",
|
"login_form_email_hint": "votreemail@email.com",
|
||||||
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
|
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
|
||||||
@@ -74,6 +82,7 @@
|
|||||||
"login_form_save_login": "Rester connecté",
|
"login_form_save_login": "Rester connecté",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
|
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
|
||||||
|
"profile_drawer_settings": "Paramètres",
|
||||||
"profile_drawer_sign_out": "Se déconnecter",
|
"profile_drawer_sign_out": "Se déconnecter",
|
||||||
"search_bar_hint": "Rechercher vos photos",
|
"search_bar_hint": "Rechercher vos photos",
|
||||||
"search_page_no_objects": "Aucune information disponible sur les objets",
|
"search_page_no_objects": "Aucune information disponible sur les objets",
|
||||||
@@ -88,12 +97,14 @@
|
|||||||
"share_add_photos": "Ajouter des photos",
|
"share_add_photos": "Ajouter des photos",
|
||||||
"share_add_title": "Ajouter un titre",
|
"share_add_title": "Ajouter un titre",
|
||||||
"share_create_album": "Créer un album",
|
"share_create_album": "Créer un album",
|
||||||
|
"share_dialog_preparing": "Préparation...",
|
||||||
"share_invite": "Inviter à l'album",
|
"share_invite": "Inviter à l'album",
|
||||||
"sharing_page_album": "Albums partagés",
|
"sharing_page_album": "Albums partagés",
|
||||||
"sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
|
"sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
|
||||||
"sharing_page_empty_list": "LISTE VIDE",
|
"sharing_page_empty_list": "LISTE VIDE",
|
||||||
"sharing_silver_appbar_create_shared_album": "Créer un album partagé",
|
"sharing_silver_appbar_create_shared_album": "Créer un album partagé",
|
||||||
"sharing_silver_appbar_share_partner": "Partager avec un partenaire",
|
"sharing_silver_appbar_share_partner": "Partager avec un partenaire",
|
||||||
|
"tab_controller_nav_library": "Bibliothèque",
|
||||||
"tab_controller_nav_photos": "Photos",
|
"tab_controller_nav_photos": "Photos",
|
||||||
"tab_controller_nav_search": "Recherche",
|
"tab_controller_nav_search": "Recherche",
|
||||||
"tab_controller_nav_sharing": "Partage",
|
"tab_controller_nav_sharing": "Partage",
|
||||||
|
|||||||
@@ -1,28 +1,52 @@
|
|||||||
{
|
{
|
||||||
"album_info_card_backup_album_excluded": "ESCLUSI",
|
"album_info_card_backup_album_excluded": "ESCLUSI",
|
||||||
"album_info_card_backup_album_included": "INCLUSI",
|
"album_info_card_backup_album_included": "INCLUSI",
|
||||||
|
"album_thumbnail_card_item": "1 elemento ",
|
||||||
|
"album_thumbnail_card_items": "{} elementi",
|
||||||
|
"album_thumbnail_card_shared": "Condiviso",
|
||||||
"album_viewer_appbar_share_delete": "Elimina album ",
|
"album_viewer_appbar_share_delete": "Elimina album ",
|
||||||
"album_viewer_appbar_share_err_delete": "Fallito nel cancellare l'album ",
|
"album_viewer_appbar_share_err_delete": "Errore nel cancellare l'album ",
|
||||||
"album_viewer_appbar_share_err_leave": "Fallito nel lasciare l'album ",
|
"album_viewer_appbar_share_err_leave": "Errore nel lasciare l'album ",
|
||||||
"album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album ",
|
"album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album ",
|
||||||
"album_viewer_appbar_share_err_title": "Fallito nel cambiare titolo dell'album ",
|
"album_viewer_appbar_share_err_title": "Errore nel cambiare il titolo dell'album ",
|
||||||
"album_viewer_appbar_share_leave": "Lascia l'album",
|
"album_viewer_appbar_share_leave": "Lascia album",
|
||||||
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
|
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
|
||||||
"album_viewer_page_share_add_users": "Aggiungi utenti",
|
"album_viewer_page_share_add_users": "Aggiungi utenti",
|
||||||
"backup_album_selection_page_albums_device": "Albums nel device ({})",
|
"asset_list_settings_subtitle": "Impostazion del layout della griglia delle foto",
|
||||||
|
"asset_list_settings_title": "Griglia foto",
|
||||||
|
"backup_album_selection_page_albums_device": "Albums sul device ({})",
|
||||||
"backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
|
"backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
|
||||||
"backup_album_selection_page_assets_scatter": "Stesse immagini e video possono trovarsi tra più album, così gli album possono essere inclusi o esclusi dal backup.",
|
"backup_album_selection_page_assets_scatter": "Stesse immagini e video possono trovarsi tra più album, così gli album possono essere inclusi o esclusi dal backup.",
|
||||||
"backup_album_selection_page_select_albums": "Seleziona gli album",
|
"backup_album_selection_page_select_albums": "Seleziona gli album",
|
||||||
"backup_album_selection_page_selection_info": "Informazioni sulla selezione ",
|
"backup_album_selection_page_selection_info": "Informazioni sulla selezione ",
|
||||||
"backup_album_selection_page_total_assets": "Numero totale di oggetti unici",
|
"backup_album_selection_page_total_assets": "Numero totale di oggetti unici",
|
||||||
"backup_all": "Tutti",
|
"backup_all": "Tutti",
|
||||||
"backup_controller_page_albums": "Backup album",
|
"backup_background_service_backup_failed_message": "Impossibile caricare contenuti. Nuovo tentativo…",
|
||||||
|
"backup_background_service_connection_failed_message": "Impossibile connettersi al server. Nuovo tentativo…",
|
||||||
|
"backup_background_service_current_upload_notification": "Caricamento {}",
|
||||||
|
"backup_background_service_default_notification": "Verifica di nuovi contenuti…",
|
||||||
|
"backup_background_service_error_title": "Errore di Backup",
|
||||||
|
"backup_background_service_in_progress_notification": "Backing dei tuoi contenuti…",
|
||||||
|
"backup_background_service_upload_failure_notification": "Impossibile caricare {}",
|
||||||
|
"backup_controller_page_albums": "Backup Album",
|
||||||
|
"backup_controller_page_background_battery_info_link": "Mostrami come",
|
||||||
|
"backup_controller_page_background_battery_info_message": "Per una migliore esperienza di backup, disabilita le ottimisazioni della batteria per l'app Immich.\n\nDal momento che è una funzionalità specifica del dispositivo, per favore consulta il manuale del produttore.",
|
||||||
|
"backup_controller_page_background_battery_info_ok": "OK",
|
||||||
|
"backup_controller_page_background_battery_info_title": "Ottimizzazioni batteria",
|
||||||
|
"backup_controller_page_background_charging": "Solo durante la ricarica",
|
||||||
|
"backup_controller_page_background_configure_error": "Impossibile configurare i servizi in background",
|
||||||
|
"backup_controller_page_background_description": "Abilita i servizi in background per sincronizzare tutti i nuovi contenuti senza la necessità di aprire l'app",
|
||||||
|
"backup_controller_page_background_is_off": "Backup automatico spento",
|
||||||
|
"backup_controller_page_background_is_on": "Backup automatico attivo",
|
||||||
|
"backup_controller_page_background_turn_off": "Disabilita servizi in background",
|
||||||
|
"backup_controller_page_background_turn_on": "Abilita servizi in background",
|
||||||
|
"backup_controller_page_background_wifi": "Solo su WiFi",
|
||||||
"backup_controller_page_backup": "Backup",
|
"backup_controller_page_backup": "Backup",
|
||||||
"backup_controller_page_backup_selected": "Selezionati:",
|
"backup_controller_page_backup_selected": "Selezionati:",
|
||||||
"backup_controller_page_backup_sub": "Photo e video salvati",
|
"backup_controller_page_backup_sub": "Foto e video caricati",
|
||||||
"backup_controller_page_cancel": "Cancella ",
|
"backup_controller_page_cancel": "Cancella ",
|
||||||
"backup_controller_page_created": "Creato il: {}",
|
"backup_controller_page_created": "Creato il: {}",
|
||||||
"backup_controller_page_desc_backup": "Attiva il backup automatico per eseguire upload sul server",
|
"backup_controller_page_desc_backup": "Attiva il backup per eseguire il caricamento automatico sul server",
|
||||||
"backup_controller_page_excluded": "Esclusi:",
|
"backup_controller_page_excluded": "Esclusi:",
|
||||||
"backup_controller_page_failed": "Falliti: ({})",
|
"backup_controller_page_failed": "Falliti: ({})",
|
||||||
"backup_controller_page_filename": "Nome del file: {} [{}]",
|
"backup_controller_page_filename": "Nome del file: {} [{}]",
|
||||||
@@ -30,39 +54,57 @@
|
|||||||
"backup_controller_page_info": "Informazioni sul backup",
|
"backup_controller_page_info": "Informazioni sul backup",
|
||||||
"backup_controller_page_none_selected": "Nessuna selezione",
|
"backup_controller_page_none_selected": "Nessuna selezione",
|
||||||
"backup_controller_page_remainder": "Promemoria ",
|
"backup_controller_page_remainder": "Promemoria ",
|
||||||
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da salvare",
|
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da caricare",
|
||||||
"backup_controller_page_select": "Seleziona ",
|
"backup_controller_page_select": "Seleziona ",
|
||||||
"backup_controller_page_server_storage": "Spazio nel server",
|
"backup_controller_page_server_storage": "Spazio sul server",
|
||||||
"backup_controller_page_start_backup": "Inizia backup ",
|
"backup_controller_page_start_backup": "Inizia backup ",
|
||||||
"backup_controller_page_status_off": "Backup è disattivato ",
|
"backup_controller_page_status_off": "Backup è disattivato ",
|
||||||
"backup_controller_page_status_on": "Backup è attivato",
|
"backup_controller_page_status_on": "Backup è attivato",
|
||||||
"backup_controller_page_storage_format": "{} di {} usati",
|
"backup_controller_page_storage_format": "{} di {} usati",
|
||||||
"backup_controller_page_to_backup": "Album da salvare",
|
"backup_controller_page_to_backup": "Album da caricare",
|
||||||
"backup_controller_page_total": "Totale",
|
"backup_controller_page_total": "Totale",
|
||||||
"backup_controller_page_total_sub": "Tutte le foto e i video unici salvati dagli album selezionati ",
|
"backup_controller_page_total_sub": "Tutte le foto e i video unici caricati dagli album selezionati ",
|
||||||
"backup_controller_page_turn_off": "Disattiva backup",
|
"backup_controller_page_turn_off": "Disattiva backup",
|
||||||
"backup_controller_page_turn_on": "Attiva backup ",
|
"backup_controller_page_turn_on": "Attiva backup ",
|
||||||
"backup_controller_page_uploading_file_info": "Info sul file caricato",
|
"backup_controller_page_uploading_file_info": "Info sul file caricato",
|
||||||
"backup_err_only_album": "Non è possibile rimuovere l'unico album",
|
"backup_err_only_album": "Non è possibile rimuovere l'unico album",
|
||||||
"backup_info_card_assets": "Oggetti ",
|
"backup_info_card_assets": "oggetti ",
|
||||||
|
"cache_settings_album_thumbnails": "Anteprime pagine librerie ({} assets)",
|
||||||
|
"cache_settings_clear_cache_button": "Cancella cache",
|
||||||
|
"cache_settings_clear_cache_button_title": "Cancella cache app. Questo impatterà sulle prestazioni applicative fino a quando la cache non sarà rigenerata.",
|
||||||
|
"cache_settings_image_cache_size": "Dimensione cache foto ({} assets)",
|
||||||
|
"cache_settings_statistics_album": "Anteprime librerie",
|
||||||
|
"cache_settings_statistics_assets": "{} contenuti ({})",
|
||||||
|
"cache_settings_statistics_full": "Immagini complete",
|
||||||
|
"cache_settings_statistics_shared": "Anteprime album condivisi",
|
||||||
|
"cache_settings_statistics_thumbnail": "Anteprime",
|
||||||
|
"cache_settings_statistics_title": "Uso della cache",
|
||||||
|
"cache_settings_subtitle": "Controlla il comportamento della cache",
|
||||||
|
"cache_settings_thumbnail_size": "Dimensione cache anteprime ({} assets)",
|
||||||
|
"cache_settings_title": "Impostazioni della Cache",
|
||||||
"control_bottom_app_bar_delete": "Elimina",
|
"control_bottom_app_bar_delete": "Elimina",
|
||||||
|
"control_bottom_app_bar_share": "Condividi",
|
||||||
|
"create_album_page_untitled": "Senza titolo",
|
||||||
|
"create_shared_album_page_create": "Crea",
|
||||||
"create_shared_album_page_share": "Condividi",
|
"create_shared_album_page_share": "Condividi",
|
||||||
"create_shared_album_page_share_add_assets": "AGGIUNGI OGGETTI",
|
"create_shared_album_page_share_add_assets": "AGGIUNGI OGGETTI",
|
||||||
"create_shared_album_page_share_select_photos": "Seleziona foto",
|
"create_shared_album_page_share_select_photos": "Seleziona foto",
|
||||||
"daily_title_text_date": "E, dd MMM",
|
"daily_title_text_date": "E, dd MMM",
|
||||||
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||||
"date_format": "E, d LLL, y • hh:mm",
|
"date_format": "E, d LLL, y • hh:mm",
|
||||||
"delete_dialog_alert": "Questi oggetti saranno cancellati permanentemente da Immich e dal tuo device",
|
"delete_dialog_alert": "Questi oggetti saranno cancellati definitivamente da Immich e dal tuo device",
|
||||||
"delete_dialog_cancel": "Annulla",
|
"delete_dialog_cancel": "Annulla",
|
||||||
"delete_dialog_ok": "Elimina",
|
"delete_dialog_ok": "Elimina",
|
||||||
"delete_dialog_title": "Cancella in modo permanente ",
|
"delete_dialog_title": "Cancella definitivamente",
|
||||||
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
|
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
|
||||||
"exif_bottom_sheet_details": "DETTAGLI",
|
"exif_bottom_sheet_details": "DETTAGLI",
|
||||||
"exif_bottom_sheet_location": "POSIZIONE",
|
"exif_bottom_sheet_location": "POSIZIONE",
|
||||||
"login_form_button_text": "Accedi",
|
"library_page_albums": "Album",
|
||||||
|
"library_page_new_album": "Nuovo Album",
|
||||||
|
"login_form_button_text": "Login",
|
||||||
"login_form_email_hint": "tuaemail@email.com",
|
"login_form_email_hint": "tuaemail@email.com",
|
||||||
"login_form_endpoint_hint": "http://tuo-ip-del-server:port/api",
|
"login_form_endpoint_hint": "http://tuo-ip-del-server:port/api",
|
||||||
"login_form_endpoint_url": "URL del Server Endpoint",
|
"login_form_endpoint_url": "Server Endpoint URL",
|
||||||
"login_form_err_http": "Per favore specificare http:// o https://",
|
"login_form_err_http": "Per favore specificare http:// o https://",
|
||||||
"login_form_err_invalid_email": "Email non valida",
|
"login_form_err_invalid_email": "Email non valida",
|
||||||
"login_form_err_leading_whitespace": "Spazio bianco all'inizio ",
|
"login_form_err_leading_whitespace": "Spazio bianco all'inizio ",
|
||||||
@@ -74,33 +116,54 @@
|
|||||||
"login_form_save_login": "Rimani connesso ",
|
"login_form_save_login": "Rimani connesso ",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
|
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
|
||||||
"profile_drawer_sign_out": "Esci",
|
"profile_drawer_settings": "Impostazioni ",
|
||||||
|
"profile_drawer_sign_out": "Logout",
|
||||||
"search_bar_hint": "Cerca le tue foto",
|
"search_bar_hint": "Cerca le tue foto",
|
||||||
"search_page_no_objects": "Nessuna Informazione relativa all'Oggetto Disponibile",
|
"search_page_no_objects": "Nessuna informazione relativa all'oggetto disponibile",
|
||||||
"search_page_no_places": "Nessun informazione sulla posizione ",
|
"search_page_no_places": "Nessun informazione sul luogo disponibile",
|
||||||
"search_page_places": "Luoghi",
|
"search_page_places": "Luoghi",
|
||||||
"search_page_things": "Oggetti",
|
"search_page_things": "Oggetti",
|
||||||
"search_result_page_new_search_hint": "Nuova ricerca ",
|
"search_result_page_new_search_hint": "Nuova ricerca ",
|
||||||
"select_additional_user_for_sharing_page_suggestions": "Suggerimenti ",
|
"select_additional_user_for_sharing_page_suggestions": "Suggerimenti ",
|
||||||
"select_user_for_sharing_page_err_album": "Fallito nel creare l'album ",
|
"select_user_for_sharing_page_err_album": "Errore nel creare l'album ",
|
||||||
"select_user_for_sharing_page_share_suggestions": "Suggerimenti",
|
"select_user_for_sharing_page_share_suggestions": "Suggerimenti",
|
||||||
|
"setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {}",
|
||||||
|
"setting_notifications_notify_hours": "{} Ore",
|
||||||
|
"setting_notifications_notify_immediately": "Immediatamente",
|
||||||
|
"setting_notifications_notify_minutes": "{} Minuti",
|
||||||
|
"setting_notifications_notify_never": "Mai",
|
||||||
|
"setting_notifications_subtitle": "Cambia le impostazioni di notifica",
|
||||||
|
"setting_notifications_title": "Notifiche",
|
||||||
|
"setting_pages_app_bar_settings": "Impostazioni",
|
||||||
"share_add": "Aggiungi",
|
"share_add": "Aggiungi",
|
||||||
"share_add_photos": "Aggiungi foto",
|
"share_add_photos": "Aggiungi foto",
|
||||||
"share_add_title": "Aggiungi un titolo ",
|
"share_add_title": "Aggiungi un titolo ",
|
||||||
"share_create_album": "Crea album",
|
"share_create_album": "Crea album",
|
||||||
"share_invite": "Invitare all'album ",
|
"share_dialog_preparing": "Preparo…",
|
||||||
|
"share_invite": "Invitare nell'album ",
|
||||||
"sharing_page_album": "Album condivisi",
|
"sharing_page_album": "Album condivisi",
|
||||||
"sharing_page_description": "Crea un album condiviso per condividere foto e video con gente nel tuo network",
|
"sharing_page_description": "Crea un album condiviso per condividere foto e video con persone nel tuo network",
|
||||||
"sharing_page_empty_list": "LISTA VUOTA",
|
"sharing_page_empty_list": "LISTA VUOTA",
|
||||||
"sharing_silver_appbar_create_shared_album": "Crea album condiviso",
|
"sharing_silver_appbar_create_shared_album": "Crea album condiviso",
|
||||||
"sharing_silver_appbar_share_partner": "Condividi con il partner",
|
"sharing_silver_appbar_share_partner": "Condividi con il partner",
|
||||||
|
"tab_controller_nav_library": "Libreria",
|
||||||
"tab_controller_nav_photos": "Foto",
|
"tab_controller_nav_photos": "Foto",
|
||||||
"tab_controller_nav_search": "Cerca",
|
"tab_controller_nav_search": "Cerca",
|
||||||
"tab_controller_nav_sharing": "Condividi",
|
"tab_controller_nav_sharing": "Condividi",
|
||||||
|
"theme_setting_asset_list_storage_indicator_title": "Mostra indicatore dello storage nei titoli dei contenuti",
|
||||||
|
"theme_setting_asset_list_tiles_per_row_title": "Numero di contenuti per riga ({})",
|
||||||
|
"theme_setting_dark_mode_switch": "Dark mode",
|
||||||
|
"theme_setting_image_viewer_quality_subtitle": "Cambia la qualità del dettaglio dell'immagine",
|
||||||
|
"theme_setting_image_viewer_quality_title": "Qualità immagine",
|
||||||
|
"theme_setting_system_theme_switch": "Automatico (Vai alle impostazioni di sistema)",
|
||||||
|
"theme_setting_theme_subtitle": "Scegli un'impostazione per il tema",
|
||||||
|
"theme_setting_theme_title": "Tema",
|
||||||
|
"theme_setting_three_stage_loading_subtitle": "Il caricamento in 3 stage aumenterà le performance di caricamento ma anche il consumo di banda",
|
||||||
|
"theme_setting_three_stage_loading_title": "Abilita il caricamento a tre stage",
|
||||||
"version_announcement_overlay_ack": "Riconosci ",
|
"version_announcement_overlay_ack": "Riconosci ",
|
||||||
"version_announcement_overlay_release_notes": "le note di rilascio ",
|
"version_announcement_overlay_release_notes": "note di rilascio ",
|
||||||
"version_announcement_overlay_text_1": "Ciao amico, c'è una nuova versione di",
|
"version_announcement_overlay_text_1": "Ciao amico, c'è una nuova versione di",
|
||||||
"version_announcement_overlay_text_2": "prova a controllare ",
|
"version_announcement_overlay_text_2": "per favore prenditi il tuo tempo per controllare il",
|
||||||
"version_announcement_overlay_text_3": "e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore nella configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico delle immagini docker.",
|
"version_announcement_overlay_text_3": "e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore nella configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico delle immagini docker.",
|
||||||
"version_announcement_overlay_title": "Nuova versione di server disponibile! \uD83C\uDF89"
|
"version_announcement_overlay_title": "Nuova versione del server disponibile! \uD83C\uDF89"
|
||||||
}
|
}
|
||||||
152
mobile/assets/i18n/ko-KR.json
Normal file
152
mobile/assets/i18n/ko-KR.json
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "제외됨",
|
||||||
|
"album_info_card_backup_album_included": "포함됨",
|
||||||
|
"album_viewer_appbar_share_delete": "앨범 삭제",
|
||||||
|
"album_viewer_appbar_share_err_delete": "앨범 삭제 실패",
|
||||||
|
"album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다",
|
||||||
|
"album_viewer_appbar_share_err_remove": "앨범에서 미디어를 제거하는 데 문제가 있습니다",
|
||||||
|
"album_viewer_appbar_share_err_title": "앨범 제목 변경 실패",
|
||||||
|
"album_viewer_appbar_share_leave": "앨범 나가기",
|
||||||
|
"album_viewer_appbar_share_remove": "앨범에서 제거",
|
||||||
|
"album_viewer_page_share_add_users": "사용자 추가",
|
||||||
|
"backup_album_selection_page_albums_device": "기기의 앨범({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "포함하려면 탭하고 제외하려면 두 번 탭하세요",
|
||||||
|
"backup_album_selection_page_assets_scatter": "미디어파일은 여러 앨범에 분산될 수 있습니다. 따라서 백업 프로세스 중에 앨범에서 포함하거나 제외할 수 있습니다.",
|
||||||
|
"backup_album_selection_page_select_albums": "앨범 선택",
|
||||||
|
"backup_album_selection_page_selection_info": "선택 정보",
|
||||||
|
"backup_album_selection_page_total_assets": "총 미디어파일 수",
|
||||||
|
"backup_all": "모두",
|
||||||
|
"backup_background_service_default_notification": "새 미디어파일 확인중...",
|
||||||
|
"backup_background_service_upload_failure_notification": "{} 업로드 실패",
|
||||||
|
"backup_background_service_in_progress_notification": "미디어파일 백업 중...",
|
||||||
|
"backup_background_service_current_upload_notification": "{} 업로드 중",
|
||||||
|
"backup_background_service_error_title": "백업 오류",
|
||||||
|
"backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중...",
|
||||||
|
"backup_background_service_backup_failed_message": "미디어파일을 백업하지 못했습니다. 다시 시도하는 중...",
|
||||||
|
"backup_controller_page_albums": "백업대상",
|
||||||
|
"backup_controller_page_backup": "백업",
|
||||||
|
"backup_controller_page_backup_selected": "선택됨: ",
|
||||||
|
"backup_controller_page_backup_sub": "백업된 사진 및 비디오",
|
||||||
|
"backup_controller_page_background_description": "백그라운드 서비스를 켜서 앱을 열지 않고도 새 미디어파일을 자동으로 백업합니다.",
|
||||||
|
"backup_controller_page_background_wifi": "WiFi에서만",
|
||||||
|
"backup_controller_page_background_charging": "충전 중일 때만",
|
||||||
|
"backup_controller_page_background_is_on": "자동 백그라운드 백업이 켜져 있습니다",
|
||||||
|
"backup_controller_page_background_is_off": "자동 백그라운드 백업이 꺼져 있습니다",
|
||||||
|
"backup_controller_page_background_turn_on": "백그라운드 서비스 켜기",
|
||||||
|
"backup_controller_page_background_turn_off": "백그라운드 서비스 끄기",
|
||||||
|
"backup_controller_page_background_configure_error": "백그라운드 서비스를 구성하지 못했습니다",
|
||||||
|
"backup_controller_page_cancel": "취소",
|
||||||
|
"backup_controller_page_created": "생성일: {}",
|
||||||
|
"backup_controller_page_desc_backup": "새 미디어파일을 서버에 자동으로 업로드하려면 백업을 켜주세요.",
|
||||||
|
"backup_controller_page_excluded": "제외됨: ",
|
||||||
|
"backup_controller_page_failed": "실패함 ({})",
|
||||||
|
"backup_controller_page_filename": "파일 이름: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "정보",
|
||||||
|
"backup_controller_page_none_selected": "선택되지 않음",
|
||||||
|
"backup_controller_page_remainder": "남은 백업파일",
|
||||||
|
"backup_controller_page_remainder_sub": "백업 대기중인 남은 사진 및 비디오",
|
||||||
|
"backup_controller_page_select": "선택",
|
||||||
|
"backup_controller_page_server_storage": "서버 저장소",
|
||||||
|
"backup_controller_page_start_backup": "백업 시작",
|
||||||
|
"backup_controller_page_status_off": "백업이 꺼져 있습니다",
|
||||||
|
"backup_controller_page_status_on": "백업이 켜져 있습니다",
|
||||||
|
"backup_controller_page_storage_format": "{}/{} 사용",
|
||||||
|
"backup_controller_page_to_backup": "백업할 앨범",
|
||||||
|
"backup_controller_page_total": "전체 백업대상",
|
||||||
|
"backup_controller_page_total_sub": "선택한 앨범의 모든 사진 및 비디오",
|
||||||
|
"backup_controller_page_turn_off": "백업 끄기",
|
||||||
|
"backup_controller_page_turn_on": "백업 켜기",
|
||||||
|
"backup_controller_page_uploading_file_info": "파일 정보 업로드 중",
|
||||||
|
"backup_err_only_album": "유일한 앨범은 제거할 수 없습니다",
|
||||||
|
"backup_info_card_assets": "미디어",
|
||||||
|
"control_bottom_app_bar_delete": "삭제",
|
||||||
|
"create_shared_album_page_share": "공유",
|
||||||
|
"create_shared_album_page_create": "만들기",
|
||||||
|
"create_shared_album_page_share_add_assets": "사진 추가",
|
||||||
|
"create_shared_album_page_share_select_photos": "사진 선택",
|
||||||
|
"daily_title_text_date": "E, M월 d일",
|
||||||
|
"daily_title_text_date_year": "E, M월 d일, yyyy",
|
||||||
|
"date_format": "yyyy년 M월 d일, EEEE • a h:mm",
|
||||||
|
"delete_dialog_alert": "이 항목은 Immich 및 휴대폰에서 영구적으로 삭제됩니다",
|
||||||
|
"delete_dialog_cancel": "취소",
|
||||||
|
"delete_dialog_ok": "삭제",
|
||||||
|
"delete_dialog_title": "영구적으로 삭제",
|
||||||
|
"exif_bottom_sheet_description": "설명 추가...",
|
||||||
|
"exif_bottom_sheet_details": "상세정보",
|
||||||
|
"exif_bottom_sheet_location": "위치",
|
||||||
|
"login_form_button_text": "로그인",
|
||||||
|
"login_form_email_hint": "youremail@email.com",
|
||||||
|
"login_form_endpoint_hint": "https://your-server-ip:port/api",
|
||||||
|
"login_form_endpoint_url": "서버 엔드포인트 URL",
|
||||||
|
"login_form_err_http": "엔드포인트는 http:// 또는 https://로 시작해야 합니다",
|
||||||
|
"login_form_err_invalid_email": "잘못된 이메일 형식입니다",
|
||||||
|
"login_form_err_leading_whitespace": "이메일 앞에 공백문자가 포함되어 있습니다",
|
||||||
|
"login_form_err_trailing_whitespace": "이메일 뒤에 공백문자가 포함되어 있습니다",
|
||||||
|
"login_form_failed_login": "로그인 오류, 서버 URL, 이메일 및 비밀번호를 확인하세요",
|
||||||
|
"login_form_label_email": "이메일",
|
||||||
|
"login_form_label_password": "비밀번호",
|
||||||
|
"login_form_password_hint": "비밀번호",
|
||||||
|
"login_form_save_login": "로그인상태 유지",
|
||||||
|
"monthly_title_text_date_format": "y년 M월",
|
||||||
|
"profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신 상태입니다",
|
||||||
|
"profile_drawer_sign_out": "로그아웃",
|
||||||
|
"profile_drawer_settings": "설정",
|
||||||
|
"search_bar_hint": "사진 검색",
|
||||||
|
"search_page_no_objects": "발견된 사물이\n없습니다",
|
||||||
|
"search_page_no_places": "발견된 장소가\n없습니다",
|
||||||
|
"search_page_places": "장소",
|
||||||
|
"search_page_things": "사물",
|
||||||
|
"search_result_page_new_search_hint": "새 검색",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "초대 가능한 사용자 제안",
|
||||||
|
"select_user_for_sharing_page_err_album": "앨범 생성 실패",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "초대 가능한 사용자 제안",
|
||||||
|
"share_add": "추가",
|
||||||
|
"share_add_photos": "사진 추가",
|
||||||
|
"share_add_title": "새 앨범제목",
|
||||||
|
"share_create_album": "앨범 만들기",
|
||||||
|
"share_invite": "앨범에 초대",
|
||||||
|
"sharing_page_album": "공유앨범",
|
||||||
|
"sharing_page_description": "공유앨범을 만들어 다른 사용자들과 사진 및 비디오를 공유합니다.",
|
||||||
|
"sharing_page_empty_list": "공유앨범 없음",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "공유앨범 만들기",
|
||||||
|
"sharing_silver_appbar_share_partner": "파트너와 공유",
|
||||||
|
"tab_controller_nav_photos": "사진",
|
||||||
|
"tab_controller_nav_search": "검색",
|
||||||
|
"tab_controller_nav_sharing": "공유",
|
||||||
|
"tab_controller_nav_library": "라이브러리",
|
||||||
|
"version_announcement_overlay_ack": "승인",
|
||||||
|
"version_announcement_overlay_release_notes": "릴리스 정보",
|
||||||
|
"version_announcement_overlay_text_1": "안녕하세요!",
|
||||||
|
"version_announcement_overlay_text_2": "앱에 새로운 업데이트가 있습니다!",
|
||||||
|
"version_announcement_overlay_text_3": "특히 WatchTower 또는 서버 응용 프로그램 자동 업데이트를 처리하는 메커니즘을 사용하는 경우 잘못된 구성을 방지하기 위해 docker-compose 및 .env 설정이 최신 상태인지 확인하세요.",
|
||||||
|
"version_announcement_overlay_title": "새 서버 버전 사용 가능 \uD83C\uDF89",
|
||||||
|
"album_thumbnail_card_item": "1개 항목",
|
||||||
|
"album_thumbnail_card_items": "{}개 항목",
|
||||||
|
"album_thumbnail_card_shared": " · 공유",
|
||||||
|
"library_page_albums": "앨범",
|
||||||
|
"library_page_new_album": "새 앨범",
|
||||||
|
"create_album_page_untitled": "제목없음",
|
||||||
|
"share_dialog_preparing": "준비중...",
|
||||||
|
"control_bottom_app_bar_share": "공유",
|
||||||
|
"setting_pages_app_bar_settings": "설정",
|
||||||
|
"theme_setting_theme_title": "테마",
|
||||||
|
"theme_setting_theme_subtitle": "앱테마 선택",
|
||||||
|
"theme_setting_system_theme_switch": "자동(시스템 설정에 따름)",
|
||||||
|
"theme_setting_dark_mode_switch": "다크모드",
|
||||||
|
"theme_setting_image_viewer_quality_title": "이미지 뷰어 품질",
|
||||||
|
"theme_setting_image_viewer_quality_subtitle": "디테일 이미지 뷰어 품질 조정",
|
||||||
|
"theme_setting_three_stage_loading_title": "3단계 로딩 활성화",
|
||||||
|
"theme_setting_three_stage_loading_subtitle": "이 기능은 로딩 성능을 향상시킬 수 있지만 훨씬 더 많은 데이터를 사용합니다.",
|
||||||
|
"asset_list_settings_title": "사진 배열",
|
||||||
|
"asset_list_settings_subtitle": "사진 배열 레이아웃 설정",
|
||||||
|
"theme_setting_asset_list_storage_indicator_title": "미디어 타일에 스토리지 싱크여부 표시",
|
||||||
|
"theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 미디어 수 ({})",
|
||||||
|
"setting_notifications_title": "알림",
|
||||||
|
"setting_notifications_subtitle": "알림 기본 설정 조정",
|
||||||
|
"setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {}",
|
||||||
|
"setting_notifications_notify_immediately": "즉시",
|
||||||
|
"setting_notifications_notify_minutes": "{}분 뒤",
|
||||||
|
"setting_notifications_notify_hours": "{}시간 뒤",
|
||||||
|
"setting_notifications_notify_never": "알리지 않음"
|
||||||
|
}
|
||||||
152
mobile/assets/i18n/nl-NL.json
Normal file
152
mobile/assets/i18n/nl-NL.json
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "UITGESLOTEN",
|
||||||
|
"album_info_card_backup_album_included": "INGESLOTEN",
|
||||||
|
"album_viewer_appbar_share_delete": "Verwijder album",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Fout bij verwijderen album",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Fout bij verlaten album",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Er gaat iets mis bij het verwijderen van items uit het album",
|
||||||
|
"album_viewer_appbar_share_err_title": "Fout bij wijzigen album titel",
|
||||||
|
"album_viewer_appbar_share_leave": "Verlaat album",
|
||||||
|
"album_viewer_appbar_share_remove": "Verwijder uit album",
|
||||||
|
"album_viewer_page_share_add_users": "Voeg gebruiker toe",
|
||||||
|
"backup_album_selection_page_albums_device": "Albums op apparaat ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Tik om in te voegen, dubbel tik om uit te sluiten",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Items kunnen over verschillende albums verdeeld zijn, dus albums kunnen ingesloten of uitgesloten zijn van het backup proces.",
|
||||||
|
"backup_album_selection_page_select_albums": "Selecteer albums",
|
||||||
|
"backup_album_selection_page_selection_info": "Selectie info",
|
||||||
|
"backup_album_selection_page_total_assets": "Totaal unieke items",
|
||||||
|
"backup_all": "Alle",
|
||||||
|
"backup_background_service_default_notification": "Controleren op nieuw items…",
|
||||||
|
"backup_background_service_upload_failure_notification": "Fout bij upload {}",
|
||||||
|
"backup_background_service_in_progress_notification": "Backuppen van items…",
|
||||||
|
"backup_background_service_current_upload_notification": "Uploaden {}",
|
||||||
|
"backup_background_service_error_title": "Backup fout",
|
||||||
|
"backup_background_service_connection_failed_message": "Fout bij verbinden server. Opnieuw proberen…",
|
||||||
|
"backup_background_service_backup_failed_message": "Fout bij backuppen items. Opnieuw proberen…",
|
||||||
|
"backup_controller_page_albums": "Backup Albums",
|
||||||
|
"backup_controller_page_backup": "Backup",
|
||||||
|
"backup_controller_page_backup_selected": "Geselecteerd: ",
|
||||||
|
"backup_controller_page_backup_sub": "Foto's en video's gebackupped",
|
||||||
|
"backup_controller_page_background_description": "Gebruik achtergrondservice om automatisch nieuwe items te uploaden naar server zonder de app te openen",
|
||||||
|
"backup_controller_page_background_wifi": "Alleen op WiFi",
|
||||||
|
"backup_controller_page_background_charging": "Alleen tijdens opladen",
|
||||||
|
"backup_controller_page_background_is_on": "Automatische achtergrond backup staat aan",
|
||||||
|
"backup_controller_page_background_is_off": "Automatische achtergrond backup staat uit",
|
||||||
|
"backup_controller_page_background_turn_on": "Zet achtergrondservice aan",
|
||||||
|
"backup_controller_page_background_turn_off": "Zet achtergrondservice uit",
|
||||||
|
"backup_controller_page_background_configure_error": "Achtergrondservice configuratie mislukt",
|
||||||
|
"backup_controller_page_cancel": "Annuleren",
|
||||||
|
"backup_controller_page_created": "Gemaakt op: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Configureer backup om automatisch nieuwe items te uploaden naar server.",
|
||||||
|
"backup_controller_page_excluded": "Uitgezonderd: ",
|
||||||
|
"backup_controller_page_failed": "Mislukt ({})",
|
||||||
|
"backup_controller_page_filename": "Bestandsnaam: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Backup informatie",
|
||||||
|
"backup_controller_page_none_selected": "Geen geselecteerd",
|
||||||
|
"backup_controller_page_remainder": "Rest",
|
||||||
|
"backup_controller_page_remainder_sub": "Overgebleven foto's en video's om te backuppen uit selectie",
|
||||||
|
"backup_controller_page_select": "Selecteer",
|
||||||
|
"backup_controller_page_server_storage": "Server Opslag",
|
||||||
|
"backup_controller_page_start_backup": "Start Backup",
|
||||||
|
"backup_controller_page_status_off": "Backup staat uit",
|
||||||
|
"backup_controller_page_status_on": "Backup staat aan",
|
||||||
|
"backup_controller_page_storage_format": "{} van {} gebruikt",
|
||||||
|
"backup_controller_page_to_backup": "Albums om te backuppen",
|
||||||
|
"backup_controller_page_total": "Totaal",
|
||||||
|
"backup_controller_page_total_sub": "Alle unieke foto's en video's uit geselecteerde albums",
|
||||||
|
"backup_controller_page_turn_off": "Backup uitzetten",
|
||||||
|
"backup_controller_page_turn_on": "Backup aanzetten",
|
||||||
|
"backup_controller_page_uploading_file_info": "Bestandsgegevens uploaden",
|
||||||
|
"backup_err_only_album": "Kan niet alleen het album verwijderen",
|
||||||
|
"backup_info_card_assets": "items",
|
||||||
|
"control_bottom_app_bar_delete": "Verwijderen",
|
||||||
|
"create_shared_album_page_share": "Delen",
|
||||||
|
"create_shared_album_page_create": "Aanmaken",
|
||||||
|
"create_shared_album_page_share_add_assets": "VOEG FOTO'S TOE",
|
||||||
|
"create_shared_album_page_share_select_photos": "Selecteer Foto's",
|
||||||
|
"daily_title_text_date": "E, MMM dd",
|
||||||
|
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||||
|
"date_format": "E, LLL d, y • h:mm a",
|
||||||
|
"delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat",
|
||||||
|
"delete_dialog_cancel": "Annuleren",
|
||||||
|
"delete_dialog_ok": "Verwijderen",
|
||||||
|
"delete_dialog_title": "Verwijder permanent",
|
||||||
|
"exif_bottom_sheet_description": "Voeg beschrijving toe...",
|
||||||
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
|
"exif_bottom_sheet_location": "LOCATIE",
|
||||||
|
"login_form_button_text": "Login",
|
||||||
|
"login_form_email_hint": "jouwemail@email.com",
|
||||||
|
"login_form_endpoint_hint": "http://jouw-server-ip:port/api",
|
||||||
|
"login_form_endpoint_url": "Server URL",
|
||||||
|
"login_form_err_http": "Voer http:// of https:// in",
|
||||||
|
"login_form_err_invalid_email": "Ongeldige Email",
|
||||||
|
"login_form_err_leading_whitespace": "Spatie aan het begin",
|
||||||
|
"login_form_err_trailing_whitespace": "Spatie aan het eind",
|
||||||
|
"login_form_failed_login": "Fout bij inloggen, controleer server url, email en wachtwoord",
|
||||||
|
"login_form_label_email": "Email",
|
||||||
|
"login_form_label_password": "Wachtwoord",
|
||||||
|
"login_form_password_hint": "wachtwoord",
|
||||||
|
"login_form_save_login": "Ingelogd blijven",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "Client en Server zijn up-to-date",
|
||||||
|
"profile_drawer_sign_out": "Uitloggen",
|
||||||
|
"profile_drawer_settings": "Instellingen",
|
||||||
|
"search_bar_hint": "Zoek je foto's",
|
||||||
|
"search_page_no_objects": "Geen object gegevens beschikbaar",
|
||||||
|
"search_page_no_places": "Geen locatie gegevens beschikbaar",
|
||||||
|
"search_page_places": "Plaatsen",
|
||||||
|
"search_page_things": "Dingen",
|
||||||
|
"search_result_page_new_search_hint": "Nieuw resultaat",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Suggesties",
|
||||||
|
"select_user_for_sharing_page_err_album": "Album aanmaken mislukt",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Suggesties",
|
||||||
|
"share_add": "Toevoegen",
|
||||||
|
"share_add_photos": "Foto's toevoegen",
|
||||||
|
"share_add_title": "Titel toevoegen",
|
||||||
|
"share_create_album": "Album aanmaken",
|
||||||
|
"share_invite": "Uitnodigen voor album",
|
||||||
|
"sharing_page_album": "Gedeelde albums",
|
||||||
|
"sharing_page_description": "Maak gedeelde albums om foto's en video's te delen met mensen in je netwerk.",
|
||||||
|
"sharing_page_empty_list": "LEGE LIJST",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Maak gedeeld album",
|
||||||
|
"sharing_silver_appbar_share_partner": "Delen met partner",
|
||||||
|
"tab_controller_nav_photos": "Foto's",
|
||||||
|
"tab_controller_nav_search": "Zoeken",
|
||||||
|
"tab_controller_nav_sharing": "Delen",
|
||||||
|
"tab_controller_nav_library": "Bibliotheek",
|
||||||
|
"version_announcement_overlay_ack": "Bevestig",
|
||||||
|
"version_announcement_overlay_release_notes": "release opmerkingen",
|
||||||
|
"version_announcement_overlay_text_1": "Er is een nieuwe versie beschikbaar van",
|
||||||
|
"version_announcement_overlay_text_2": "neem je tijd en bezoek de ",
|
||||||
|
"version_announcement_overlay_text_3": " controleer of je docker-compose en .env up-to-date zijn om te voorkomen dat er misconfiguraties zijn, in het bijzonder als je gebruik maakt van WatchTower of een ander mechanisme dat je server automatisch configureert.",
|
||||||
|
"version_announcement_overlay_title": "Nieuwe server versie beschikbaar \uD83C\uDF89",
|
||||||
|
"album_thumbnail_card_item": "1 item",
|
||||||
|
"album_thumbnail_card_items": "{} items",
|
||||||
|
"album_thumbnail_card_shared": " · Gedeeld",
|
||||||
|
"library_page_albums": "Albums",
|
||||||
|
"library_page_new_album": "Nieuw album",
|
||||||
|
"create_album_page_untitled": "Naamloos",
|
||||||
|
"share_dialog_preparing": "Voorbereiden...",
|
||||||
|
"control_bottom_app_bar_share": "Delen",
|
||||||
|
"setting_pages_app_bar_settings": "Instellingen",
|
||||||
|
"theme_setting_theme_title": "Thema",
|
||||||
|
"theme_setting_theme_subtitle": "Kies de thema instelling van de app",
|
||||||
|
"theme_setting_system_theme_switch": "Automatisch (volg systeeminstelling)",
|
||||||
|
"theme_setting_dark_mode_switch": "Donkere modus",
|
||||||
|
"theme_setting_image_viewer_quality_title": "Foto weergave kwaliteit",
|
||||||
|
"theme_setting_image_viewer_quality_subtitle": "Pas de kwaliteit aan van de gedetailleerde foto weergave",
|
||||||
|
"theme_setting_three_stage_loading_title": "Drie-laags laden inschakelen",
|
||||||
|
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
|
||||||
|
"asset_list_settings_title": "Foto Grid",
|
||||||
|
"asset_list_settings_subtitle": "Foto grid layout instellingen",
|
||||||
|
"theme_setting_asset_list_storage_indicator_title": "Laat ruimte indicator zien bij item tegels",
|
||||||
|
"theme_setting_asset_list_tiles_per_row_title": "Aantal items per rij ({})",
|
||||||
|
"setting_notifications_title": "Notificaties",
|
||||||
|
"setting_notifications_subtitle": "Werk je notificatievoorkeuren bij",
|
||||||
|
"setting_notifications_notify_failures_grace_period": "Melding achtergrond backup fouten: {}",
|
||||||
|
"setting_notifications_notify_immediately": "meteen",
|
||||||
|
"setting_notifications_notify_minutes": "{} minuten",
|
||||||
|
"setting_notifications_notify_hours": "{} uur",
|
||||||
|
"setting_notifications_notify_never": "nooit"
|
||||||
|
}
|
||||||
139
mobile/assets/i18n/pt-BR.json
Normal file
139
mobile/assets/i18n/pt-BR.json
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "EXCLUÍDO",
|
||||||
|
"album_info_card_backup_album_included": "INCLUÍDO",
|
||||||
|
"album_viewer_appbar_share_delete": "Excluir álbum",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Falha ao excluir álbum",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Falha ao sair do álbum",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Há problemas ao remover recursos do álbum",
|
||||||
|
"album_viewer_appbar_share_err_title": "Falha ao alterar o título do álbum",
|
||||||
|
"album_viewer_appbar_share_leave": "Sair do álbum",
|
||||||
|
"album_viewer_appbar_share_remove": "Remover do álbum",
|
||||||
|
"album_viewer_page_share_add_users": "Adicionar usuários",
|
||||||
|
"backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Toque para incluir, toque duas vezes para excluir",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Os recursos podem se espalhar por vários álbuns. Assim, os álbuns podem ser incluídos ou excluídos durante o processo de backup.",
|
||||||
|
"backup_album_selection_page_select_albums": "Selecionar álbuns",
|
||||||
|
"backup_album_selection_page_selection_info": "Informações da Seleção",
|
||||||
|
"backup_album_selection_page_total_assets": "Total de recursos exclusivos",
|
||||||
|
"backup_all": "Todos",
|
||||||
|
"backup_background_service_default_notification": "Checking for new assets…",
|
||||||
|
"backup_background_service_upload_failure_notification": "Falha ao carregar {}",
|
||||||
|
"backup_background_service_in_progress_notification": "Fazendo backup de seus ativos…",
|
||||||
|
"backup_background_service_current_upload_notification": "Enviando {}",
|
||||||
|
"backup_controller_page_albums": "Álbuns de backup",
|
||||||
|
"backup_controller_page_backup": "Backup",
|
||||||
|
"backup_controller_page_backup_selected": "Selecionado: ",
|
||||||
|
"backup_controller_page_backup_sub": "Backup de fotos e vídeos",
|
||||||
|
"backup_controller_page_background_description": "Ative o serviço em segundo plano para fazer backup automático de novos ativos sem precisar abrir o aplicativo",
|
||||||
|
"backup_controller_page_background_wifi": "Apenas em Wi-Fi",
|
||||||
|
"backup_controller_page_background_charging": "Apenas durante o carregamento",
|
||||||
|
"backup_controller_page_background_is_on": "O backup automático em segundo plano está ativado",
|
||||||
|
"backup_controller_page_background_is_off": "O backup automático em segundo plano está desativado",
|
||||||
|
"backup_controller_page_background_turn_on": "Ativar o serviço em segundo plano",
|
||||||
|
"backup_controller_page_background_turn_off": "Desativar o serviço em segundo plano",
|
||||||
|
"backup_controller_page_background_configure_error": "Falha ao configurar o serviço em segundo plano",
|
||||||
|
"backup_controller_page_cancel": "Cancelar",
|
||||||
|
"backup_controller_page_created": "Criado em: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Ative o backup para carregar automaticamente novos ativos no servidor.",
|
||||||
|
"backup_controller_page_excluded": "Excluído: ",
|
||||||
|
"backup_controller_page_failed": "Falhou ({})",
|
||||||
|
"backup_controller_page_filename": "Nome do arquivo: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Informações de backup",
|
||||||
|
"backup_controller_page_none_selected": "Nenhum selecionado",
|
||||||
|
"backup_controller_page_remainder": "Restante",
|
||||||
|
"backup_controller_page_remainder_sub": "Fotos e vídeos restantes para fazer backup da seleção",
|
||||||
|
"backup_controller_page_select": "Selecionar",
|
||||||
|
"backup_controller_page_server_storage": "Armazenamento do servidor",
|
||||||
|
"backup_controller_page_start_backup": "Iniciar backup",
|
||||||
|
"backup_controller_page_status_off": "O backup está desativado",
|
||||||
|
"backup_controller_page_status_on": "O backup está ativado",
|
||||||
|
"backup_controller_page_storage_format": "{} de {} usado",
|
||||||
|
"backup_controller_page_to_backup": "Álbuns para backup",
|
||||||
|
"backup_controller_page_total": "Total",
|
||||||
|
"backup_controller_page_total_sub": "Todas as fotos e vídeos únicos dos álbuns selecionados",
|
||||||
|
"backup_controller_page_turn_off": "Desativar o backup",
|
||||||
|
"backup_controller_page_turn_on": "Ativar Backup",
|
||||||
|
"backup_controller_page_uploading_file_info": "Carregando informações do arquivo",
|
||||||
|
"backup_err_only_album": "Não é possível remover o único álbum",
|
||||||
|
"backup_info_card_assets": "ativos",
|
||||||
|
"control_bottom_app_bar_delete": "Excluir",
|
||||||
|
"create_shared_album_page_share": "Compartilhar",
|
||||||
|
"create_shared_album_page_create": "Criar",
|
||||||
|
"create_shared_album_page_share_add_assets": "ADICIONAR FOTOS",
|
||||||
|
"create_shared_album_page_share_select_photos": "Selecionar fotos",
|
||||||
|
"daily_title_text_date": "E, MMM dd",
|
||||||
|
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||||
|
"date_format": "E, LLL d, y • h:mm a",
|
||||||
|
"delete_dialog_alert": "Esses itens serão excluídos permanentemente do Immich e do seu dispositivo",
|
||||||
|
"delete_dialog_cancel": "Cancelar",
|
||||||
|
"delete_dialog_ok": "Excluir",
|
||||||
|
"delete_dialog_title": "Excluir permanentemente",
|
||||||
|
"exif_bottom_sheet_description": "Adicionar descrição...",
|
||||||
|
"exif_bottom_sheet_details": "DETALHES",
|
||||||
|
"exif_bottom_sheet_location": "LOCALIZAÇÃO",
|
||||||
|
"login_form_button_text": "Login",
|
||||||
|
"login_form_email_hint": "youremail@email.com",
|
||||||
|
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||||
|
"login_form_endpoint_url": "Server Endpoint URL",
|
||||||
|
"login_form_err_http": "Please specify http:// or https://",
|
||||||
|
"login_form_err_invalid_email": "E-mail inválido",
|
||||||
|
"login_form_err_leading_whitespace": "Leading whitespace",
|
||||||
|
"login_form_err_trailing_whitespace": "Trailing whitespace",
|
||||||
|
"login_form_failed_login": "Erro ao fazer login, verifique a url do servidor, e-mail e senha",
|
||||||
|
"login_form_label_email": "Email",
|
||||||
|
"login_form_label_password": "Password",
|
||||||
|
"login_form_password_hint": "password",
|
||||||
|
"login_form_save_login": "Permaneçer conectado",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "Cliente e Servidor estão atualizados",
|
||||||
|
"profile_drawer_sign_out": "Sair",
|
||||||
|
"profile_drawer_settings": "Configurações",
|
||||||
|
"search_bar_hint": "Procurar fotos",
|
||||||
|
"search_page_no_objects": "Nenhuma informação de objeto disponível",
|
||||||
|
"search_page_no_places": "Nenhuma informação de lugares disponível",
|
||||||
|
"search_page_places": "Lugares",
|
||||||
|
"search_page_things": "Coisas",
|
||||||
|
"search_result_page_new_search_hint": "Nova pesquisa",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Sugestões",
|
||||||
|
"select_user_for_sharing_page_err_album": "Falha ao criar álbum",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Sugestões",
|
||||||
|
"share_add": "Adicionar",
|
||||||
|
"share_add_photos": "Adicionar fotos",
|
||||||
|
"share_add_title": "Adicione um título",
|
||||||
|
"share_create_album": "Criar álbum",
|
||||||
|
"share_invite": "Convidar para o álbum",
|
||||||
|
"sharing_page_album": "Álbuns compartilhados",
|
||||||
|
"sharing_page_description": "Crie álbuns compartilhados para compartilhar fotos e vídeos com pessoas em sua rede.",
|
||||||
|
"sharing_page_empty_list": "LISTA VAZIA",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Criar álbum compartilhado",
|
||||||
|
"sharing_silver_appbar_share_partner": "Compartilhe com o parceiro",
|
||||||
|
"tab_controller_nav_photos": "Fotos",
|
||||||
|
"tab_controller_nav_search": "Procurar",
|
||||||
|
"tab_controller_nav_sharing": "Compartilhamento",
|
||||||
|
"tab_controller_nav_library": "Biblioteca",
|
||||||
|
"version_announcement_overlay_ack": "Confirmar",
|
||||||
|
"version_announcement_overlay_release_notes": "notas de lançamento",
|
||||||
|
"version_announcement_overlay_text_1": "Oi amigo, há um novo lançamento de",
|
||||||
|
"version_announcement_overlay_text_2": "reserve um tempo para visitar o ",
|
||||||
|
"version_announcement_overlay_text_3": " e verifique se a configuração do docker-compose e do .env está atualizada para evitar configurações incorretas, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização automática do aplicativo do servidor.",
|
||||||
|
"version_announcement_overlay_title": "Nova versão do servidor disponível \uD83C\uDF89",
|
||||||
|
"album_thumbnail_card_item": "1 item",
|
||||||
|
"album_thumbnail_card_items": "{} items",
|
||||||
|
"album_thumbnail_card_shared": " · Compartilhado",
|
||||||
|
"library_page_albums": "Albums",
|
||||||
|
"library_page_new_album": "Novo album",
|
||||||
|
"create_album_page_untitled": "Sem título",
|
||||||
|
"share_dialog_preparing": "Preparando...",
|
||||||
|
"control_bottom_app_bar_share": "Compartilhar",
|
||||||
|
"setting_pages_app_bar_settings": "Configurações",
|
||||||
|
"theme_setting_theme_title": "Tema",
|
||||||
|
"theme_setting_theme_subtitle": "Escolha a configuração de tema do app",
|
||||||
|
"theme_setting_system_theme_switch": "Automático (seguir a configuração do sistema)",
|
||||||
|
"theme_setting_dark_mode_switch": "Dark mode",
|
||||||
|
"theme_setting_image_viewer_quality_title": "Qualidade das imagens do visualizador",
|
||||||
|
"theme_setting_image_viewer_quality_subtitle": "Ajuste a qualidade de imagens detalhadas do visualizador",
|
||||||
|
"theme_setting_three_stage_loading_title": "Ative o carregamento em três estágios",
|
||||||
|
"theme_setting_three_stage_loading_subtitle": "O carregamento em três estágios oferece a imagem de melhor qualidade em troca de uma velocidade de carregamento mais lenta"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>9.0</string>
|
<string>11.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
- shared_preferences_ios (0.0.1):
|
- shared_preferences_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- sqflite (0.0.2):
|
- sqflite (0.0.2):
|
||||||
@@ -40,6 +42,7 @@ DEPENDENCIES:
|
|||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
@@ -67,6 +70,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
|
share_plus:
|
||||||
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_ios:
|
shared_preferences_ios:
|
||||||
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||||
sqflite:
|
sqflite:
|
||||||
@@ -79,7 +84,7 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/wakelock/ios"
|
:path: ".symlinks/plugins/wakelock/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||||
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
||||||
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
|
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
@@ -88,6 +93,7 @@ SPEC CHECKSUMS:
|
|||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
|
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
|
|||||||
@@ -360,7 +360,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 38;
|
CURRENT_PROJECT_VERSION = 52;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 38;
|
CURRENT_PROJECT_VERSION = 52;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -522,7 +522,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 38;
|
CURRENT_PROJECT_VERSION = 52;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.20.0</string>
|
<string>1.27.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>38</string>
|
<string>52</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true />
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
@@ -66,8 +66,7 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIUserInterfaceStyle</key>
|
|
||||||
<string>Light</string>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true />
|
<true />
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
@@ -93,7 +92,10 @@
|
|||||||
<string>it</string>
|
<string>it</string>
|
||||||
<string>fi</string>
|
<string>fi</string>
|
||||||
<string>ja</string>
|
<string>ja</string>
|
||||||
|
<string>ko</string>
|
||||||
|
<string>nl</string>
|
||||||
<string>pl</string>
|
<string>pl</string>
|
||||||
|
<string>pt</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.20.0"
|
version_number: "1.28.2"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -5,34 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000213">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.088407">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.499192">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="22.635867">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="30.057077">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.376681">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.438506">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="91.762747">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="91.259106">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="49.149884">
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="102.092139">
|
||||||
|
|
||||||
<failure message="/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `chdir' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:30:in `block (2 levels) in parsing_binding' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane.rb:33:in `call' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `chdir' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `execute' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/bin/fastlane:23:in `<top (required)>' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `load' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `<main>' Error uploading ipa file: [Transporter Error Output]: ERROR ITMS-90186: Invalid Pre-Release Train. The train version '1.19.0' is closed for new build submissions
|
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,12 @@ const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1
|
|||||||
// Github Release Info
|
// Github Release Info
|
||||||
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box
|
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box
|
||||||
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
|
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
|
||||||
|
|
||||||
|
// User Setting Info
|
||||||
|
const String userSettingInfoBox = "immichUserSettingInfoBox";
|
||||||
|
|
||||||
|
// Background backup Info
|
||||||
|
const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
|
||||||
|
const String backupFailedSince = "immichBackupFailedSince"; // Key 1
|
||||||
|
const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2
|
||||||
|
const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const immichBackgroundColor = Color(0xFFf6f8fe);
|
Color immichBackgroundColor = const Color(0xFFf6f8fe);
|
||||||
|
Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0);
|
||||||
|
Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250);
|
||||||
|
|||||||
20
mobile/lib/constants/locales.dart
Normal file
20
mobile/lib/constants/locales.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
const List<Locale> locales = [
|
||||||
|
// Default locale
|
||||||
|
Locale('en', 'US'),
|
||||||
|
// Additional locales
|
||||||
|
Locale('da', 'DK'),
|
||||||
|
Locale('de', 'DE'),
|
||||||
|
Locale('es', 'ES'),
|
||||||
|
Locale('fi', 'FI'),
|
||||||
|
Locale('fr', 'FR'),
|
||||||
|
Locale('it', 'IT'),
|
||||||
|
Locale('ja', 'JP'),
|
||||||
|
Locale('nl', 'NL'),
|
||||||
|
Locale('pl', 'PL'),
|
||||||
|
Locale('pt', 'PR'),
|
||||||
|
Locale('ko', 'KR'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const String translationsPath = 'assets/i18n';
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
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/models/hive_saved_login_info.model.dart';
|
||||||
@@ -17,6 +23,7 @@ import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
|||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
||||||
|
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||||
import 'constants/hive_box.dart';
|
import 'constants/hive_box.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
@@ -29,6 +36,7 @@ void main() async {
|
|||||||
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
||||||
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
await Hive.openBox(hiveGithubReleaseInfoBox);
|
await Hive.openBox(hiveGithubReleaseInfoBox);
|
||||||
|
await Hive.openBox(userSettingInfoBox);
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
@@ -38,21 +46,18 @@ void main() async {
|
|||||||
|
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
|
|
||||||
var locales = const [
|
if (kReleaseMode && Platform.isAndroid) {
|
||||||
// Default locale
|
try {
|
||||||
Locale('en', 'US'),
|
await FlutterDisplayMode.setHighRefreshRate();
|
||||||
// Additional locales
|
} catch (e) {
|
||||||
Locale('da', 'DK'),
|
debugPrint("Error setting high refresh rate: $e");
|
||||||
Locale('de', 'DE'),
|
}
|
||||||
Locale('es', 'ES'),
|
}
|
||||||
Locale('fr', 'FR'),
|
|
||||||
Locale('it', 'IT'),
|
|
||||||
];
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
EasyLocalization(
|
EasyLocalization(
|
||||||
supportedLocales: locales,
|
supportedLocales: locales,
|
||||||
path: 'assets/i18n',
|
path: translationsPath,
|
||||||
useFallbackTranslations: true,
|
useFallbackTranslations: true,
|
||||||
fallbackLocale: locales.first,
|
fallbackLocale: locales.first,
|
||||||
child: const ProviderScope(child: ImmichApp()),
|
child: const ProviderScope(child: ImmichApp()),
|
||||||
@@ -79,6 +84,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
|
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
|
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
ref.watch(assetProvider.notifier).getAllAsset();
|
ref.watch(assetProvider.notifier).getAllAsset();
|
||||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
@@ -117,8 +123,11 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
initApp().then((_) => debugPrint("App Init Completed"));
|
initApp().then((_) => debugPrint("App Init Completed"));
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// needs to be delayed so that EasyLocalization is working
|
||||||
|
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -142,23 +151,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
MaterialApp.router(
|
MaterialApp.router(
|
||||||
title: 'Immich',
|
title: 'Immich',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
themeMode: ref.watch(immichThemeProvider),
|
||||||
useMaterial3: true,
|
darkTheme: immichDarkTheme,
|
||||||
brightness: Brightness.light,
|
theme: immichLightTheme,
|
||||||
primarySwatch: Colors.indigo,
|
|
||||||
fontFamily: 'WorkSans',
|
|
||||||
snackBarTheme: const SnackBarThemeData(
|
|
||||||
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
|
|
||||||
),
|
|
||||||
scaffoldBackgroundColor: immichBackgroundColor,
|
|
||||||
appBarTheme: const AppBarTheme(
|
|
||||||
backgroundColor: immichBackgroundColor,
|
|
||||||
foregroundColor: Colors.indigo,
|
|
||||||
elevation: 1,
|
|
||||||
centerTitle: true,
|
|
||||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
routeInformationParser: router.defaultRouteParser(),
|
routeInformationParser: router.defaultRouteParser(),
|
||||||
routerDelegate: router.delegate(
|
routerDelegate: router.delegate(
|
||||||
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
|
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
@@ -22,19 +24,23 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(25),
|
borderRadius: BorderRadius.circular(25),
|
||||||
),
|
),
|
||||||
side: const BorderSide(
|
side: BorderSide(
|
||||||
width: 1,
|
width: 1,
|
||||||
color: Color.fromARGB(255, 215, 215, 215),
|
color: isDarkTheme
|
||||||
|
? const Color.fromARGB(255, 63, 63, 63)
|
||||||
|
: const Color.fromARGB(255, 206, 206, 206),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
icon: Icon(iconData, size: 15),
|
icon: Icon(
|
||||||
|
iconData,
|
||||||
|
size: 15,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
labelText,
|
labelText,
|
||||||
style: const TextStyle(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
fontSize: 12,
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/cache.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:transparent_image/transparent_image.dart';
|
|
||||||
|
|
||||||
class AlbumThumbnailCard extends StatelessWidget {
|
class AlbumThumbnailCard extends StatelessWidget {
|
||||||
const AlbumThumbnailCard({Key? key, required this.album}) : super(key: key);
|
const AlbumThumbnailCard({
|
||||||
|
Key? key,
|
||||||
|
required this.album,
|
||||||
|
required this.cacheService,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final AlbumResponseDto album;
|
final AlbumResponseDto album;
|
||||||
|
final CacheService cacheService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
|
final cardSize = MediaQuery.of(context).size.width / 2 - 18;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
|
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
|
||||||
@@ -26,28 +38,30 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: FadeInImage(
|
child: CachedNetworkImage(
|
||||||
width: MediaQuery.of(context).size.width / 2 - 18,
|
cacheManager: cacheService.getCache(CacheType.albumThumbnail),
|
||||||
height: MediaQuery.of(context).size.width / 2 - 18,
|
memCacheHeight: max(400, cardSize.toInt() * 3),
|
||||||
|
width: cardSize,
|
||||||
|
height: cardSize,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: MemoryImage(kTransparentImage),
|
|
||||||
image: NetworkImage(
|
|
||||||
'${box.get(serverEndpointKey)}/asset/thumbnail/${album.albumThumbnailAssetId}?format=JPEG',
|
|
||||||
headers: {
|
|
||||||
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
|
||||||
},
|
|
||||||
),
|
|
||||||
fadeInDuration: const Duration(milliseconds: 200),
|
fadeInDuration: const Duration(milliseconds: 200),
|
||||||
fadeOutDuration: const Duration(milliseconds: 200),
|
imageUrl:
|
||||||
|
getAlbumThumbnailUrl(album, type: ThumbnailFormat.JPEG),
|
||||||
|
httpHeaders: {
|
||||||
|
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
||||||
|
},
|
||||||
|
cacheKey: "${album.albumThumbnailAssetId}",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: SizedBox(
|
||||||
album.albumName,
|
width: cardSize,
|
||||||
style: const TextStyle(
|
child: Text(
|
||||||
fontWeight: FontWeight.bold,
|
album.albumName,
|
||||||
fontSize: 12,
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -55,18 +69,20 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${album.assets.length} item${album.assets.length > 1 ? 's' : ''}',
|
album.assetCount == 1
|
||||||
|
? 'album_thumbnail_card_item'
|
||||||
|
: 'album_thumbnail_card_items',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
).tr(args: ['${album.assetCount}']),
|
||||||
if (album.shared)
|
if (album.shared)
|
||||||
const Text(
|
const Text(
|
||||||
' · Shared',
|
'album_thumbnail_card_shared',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
)
|
).tr()
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return TextField(
|
return TextField(
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
if (v.isEmpty) {
|
if (v.isEmpty) {
|
||||||
@@ -51,7 +53,10 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
albumTitleController.clear();
|
albumTitleController.clear();
|
||||||
isAlbumTitleEmpty.value = true;
|
isAlbumTitleEmpty.value = true;
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.cancel_rounded),
|
icon: Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
splashRadius: 10,
|
splashRadius: 10,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -65,7 +70,9 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
hintText: 'share_add_title'.tr(),
|
hintText: 'share_add_title'.tr(),
|
||||||
focusColor: Colors.grey[300],
|
focusColor: Colors.grey[300],
|
||||||
fillColor: Colors.grey[200],
|
fillColor: isDarkTheme
|
||||||
|
? const Color.fromARGB(255, 32, 33, 35)
|
||||||
|
: Colors.grey[200],
|
||||||
filled: isAlbumTitleTextFieldFocus.value,
|
filled: isAlbumTitleTextFieldFocus.value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
|
|
||||||
void _buildBottomSheet() {
|
void _buildBottomSheet() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
backgroundColor: immichBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
isScrollControlled: false,
|
isScrollControlled: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final titleTextEditController =
|
final titleTextEditController =
|
||||||
useTextEditingController(text: albumInfo.albumName);
|
useTextEditingController(text: albumInfo.albumName);
|
||||||
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
void onFocusModeChange() {
|
void onFocusModeChange() {
|
||||||
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
||||||
@@ -65,7 +66,10 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
titleTextEditController.clear();
|
titleTextEditController.clear();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.cancel_rounded),
|
icon: Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
splashRadius: 10,
|
splashRadius: 10,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -78,7 +82,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
focusColor: Colors.grey[300],
|
focusColor: Colors.grey[300],
|
||||||
fillColor: Colors.grey[200],
|
fillColor: isDarkTheme
|
||||||
|
? const Color.fromARGB(255, 32, 33, 35)
|
||||||
|
: Colors.grey[200],
|
||||||
filled: titleFocusNode.hasFocus,
|
filled: titleFocusNode.hasFocus,
|
||||||
hintText: 'share_add_title'.tr(),
|
hintText: 'share_add_title'.tr(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -8,24 +9,28 @@ import 'package:immich_mobile/constants/hive_box.dart';
|
|||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/cache.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerThumbnail extends HookConsumerWidget {
|
class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
final AssetResponseDto asset;
|
final AssetResponseDto asset;
|
||||||
final List<AssetResponseDto> assetList;
|
final List<AssetResponseDto> assetList;
|
||||||
|
final bool showStorageIndicator;
|
||||||
|
final BaseCacheManager? cacheManager;
|
||||||
|
|
||||||
const AlbumViewerThumbnail({
|
const AlbumViewerThumbnail({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.assetList,
|
required this.assetList,
|
||||||
|
this.cacheManager,
|
||||||
|
this.showStorageIndicator = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final cacheKey = useState(1);
|
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
||||||
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
final selectedAssetsInAlbumViewer =
|
final selectedAssetsInAlbumViewer =
|
||||||
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
@@ -37,7 +42,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
GalleryViewerRoute(
|
GalleryViewerRoute(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
assetList: assetList,
|
assetList: assetList,
|
||||||
thumbnailRequestUrl: thumbnailRequestUrl,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,7 +126,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(border: drawBorderColor()),
|
decoration: BoxDecoration(border: drawBorderColor()),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
cacheManager: cacheManager,
|
||||||
|
cacheKey: asset.id,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
memCacheHeight: 200,
|
memCacheHeight: 200,
|
||||||
@@ -167,7 +172,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildThumbnailImage(),
|
_buildThumbnailImage(),
|
||||||
_buildAssetStoreLocationIcon(),
|
if (showStorageIndicator) _buildAssetStoreLocationIcon(),
|
||||||
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
|
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
|
||||||
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/cache.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SelectionThumbnailImage extends HookConsumerWidget {
|
class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
@@ -15,15 +17,14 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final cacheKey = useState(1);
|
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
||||||
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
|
||||||
var selectedAsset =
|
var selectedAsset =
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||||
var newAssetsForAlbum =
|
var newAssetsForAlbum =
|
||||||
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
||||||
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||||
|
final cacheService = ref.watch(cacheServiceProvider);
|
||||||
|
|
||||||
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
||||||
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
@@ -113,7 +114,8 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(border: drawBorderColor()),
|
decoration: BoxDecoration(border: drawBorderColor()),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
cacheManager: cacheService.getCache(CacheType.thumbnail),
|
||||||
|
cacheKey: asset.id,
|
||||||
width: 150,
|
width: 150,
|
||||||
height: 150,
|
height: 150,
|
||||||
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,
|
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/cache.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
||||||
@@ -14,11 +16,8 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final cacheKey = useState(1);
|
final cacheService = ref.watch(cacheServiceProvider);
|
||||||
|
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
|
||||||
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -27,12 +26,13 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
cacheManager: cacheService.getCache(CacheType.thumbnail),
|
||||||
|
cacheKey: asset.id,
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 500,
|
height: 500,
|
||||||
memCacheHeight: 500,
|
memCacheHeight: 500,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: getThumbnailUrl(asset),
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
fadeInDuration: const Duration(milliseconds: 250),
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
||||||
|
|||||||
@@ -35,13 +35,7 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
child: TextButton.icon(
|
child: ElevatedButton.icon(
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: MaterialStateProperty.all(
|
|
||||||
Theme.of(context).primaryColor.withAlpha(20),
|
|
||||||
),
|
|
||||||
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context)
|
AutoRouter.of(context)
|
||||||
.push(CreateAlbumRoute(isSharedAlbum: true));
|
.push(CreateAlbumRoute(isSharedAlbum: true));
|
||||||
@@ -52,8 +46,12 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"sharing_silver_appbar_create_shared_album",
|
"sharing_silver_appbar_create_shared_album",
|
||||||
style:
|
maxLines: 1,
|
||||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 11,
|
||||||
|
// color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -61,13 +59,7 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 4.0),
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
child: TextButton.icon(
|
child: ElevatedButton.icon(
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: MaterialStateProperty.all(
|
|
||||||
Theme.of(context).primaryColor.withAlpha(20),
|
|
||||||
),
|
|
||||||
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
|
||||||
),
|
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.swap_horizontal_circle_outlined,
|
Icons.swap_horizontal_circle_outlined,
|
||||||
@@ -75,8 +67,11 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"sharing_silver_appbar_share_partner",
|
"sharing_silver_appbar_share_partner",
|
||||||
style:
|
style: TextStyle(
|
||||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
@@ -14,7 +13,10 @@ import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart
|
|||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/cache.service.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
@@ -187,23 +189,31 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImageGrid(AlbumResponseDto albumInfo) {
|
Widget _buildImageGrid(AlbumResponseDto albumInfo) {
|
||||||
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
|
final bool showStorageIndicator =
|
||||||
|
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
|
||||||
|
final cacheService = ref.watch(cacheServiceProvider);
|
||||||
|
|
||||||
if (albumInfo.assets.isNotEmpty) {
|
if (albumInfo.assets.isNotEmpty) {
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
sliver: SliverGrid(
|
sliver: SliverGrid(
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 3,
|
crossAxisCount:
|
||||||
|
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
crossAxisSpacing: 5.0,
|
crossAxisSpacing: 5.0,
|
||||||
mainAxisSpacing: 5,
|
mainAxisSpacing: 5,
|
||||||
),
|
),
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return AlbumViewerThumbnail(
|
return AlbumViewerThumbnail(
|
||||||
|
cacheManager: cacheService.getCache(CacheType.thumbnail),
|
||||||
asset: albumInfo.assets[index],
|
asset: albumInfo.assets[index],
|
||||||
assetList: albumInfo.assets,
|
assetList: albumInfo.assets,
|
||||||
|
showStorageIndicator: showStorageIndicator,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
childCount: albumInfo.assets.length,
|
childCount: albumInfo.assetCount,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -242,7 +252,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
titleFocusNode.unfocus();
|
titleFocusNode.unfocus();
|
||||||
},
|
},
|
||||||
child: DraggableScrollbar.semicircle(
|
child: DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).hintColor,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
@@ -255,7 +265,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
minHeight: 50,
|
minHeight: 50,
|
||||||
maxHeight: 50,
|
maxHeight: 50,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: immichBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
child: _buildControlButton(albumInfo),
|
child: _buildControlButton(albumInfo),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
DraggableScrollbar.semicircle(
|
DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).hintColor,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
final isAlbumTitleEmpty = useState(true);
|
final isAlbumTitleEmpty = useState(true);
|
||||||
final selectedAssets =
|
final selectedAssets =
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||||
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
_showSelectUserPage() {
|
_showSelectUserPage() {
|
||||||
AutoRouter.of(context).push(const SelectUserForSharingRoute());
|
AutoRouter.of(context).push(const SelectUserForSharingRoute());
|
||||||
@@ -37,8 +38,10 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
isAlbumTitleTextFieldFocus.value = false;
|
isAlbumTitleTextFieldFocus.value = false;
|
||||||
|
|
||||||
if (albumTitleController.text.isEmpty) {
|
if (albumTitleController.text.isEmpty) {
|
||||||
albumTitleController.text = 'Untitled';
|
albumTitleController.text = 'create_album_page_untitled'.tr();
|
||||||
ref.watch(albumTitleProvider.notifier).setAlbumTitle('Untitled');
|
ref
|
||||||
|
.watch(albumTitleProvider.notifier)
|
||||||
|
.setAlbumTitle('create_album_page_untitled'.tr());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +76,12 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 200, left: 18),
|
padding: const EdgeInsets.only(top: 200, left: 18),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'create_shared_album_page_share_add_assets',
|
'create_shared_album_page_share_add_assets',
|
||||||
style: TextStyle(fontSize: 12),
|
style: Theme.of(context).textTheme.headline2?.copyWith(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -94,24 +100,28 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
||||||
side: const BorderSide(
|
side: BorderSide(
|
||||||
color: Color.fromARGB(255, 206, 206, 206),
|
color: isDarkTheme
|
||||||
|
? const Color.fromARGB(255, 63, 63, 63)
|
||||||
|
: const Color.fromARGB(255, 206, 206, 206),
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: _onSelectPhotosButtonPressed,
|
onPressed: _onSelectPhotosButtonPressed,
|
||||||
icon: const Icon(Icons.add_rounded),
|
icon: Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
label: Padding(
|
label: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'create_shared_album_page_share_select_photos',
|
'create_shared_album_page_share_select_photos',
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.grey[700],
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
),
|
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -188,6 +198,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||||
@@ -195,9 +206,11 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'share_create_album',
|
'share_create_album',
|
||||||
style: TextStyle(color: Colors.black),
|
style: Theme.of(context).textTheme.headline2?.copyWith(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
actions: [
|
actions: [
|
||||||
if (isSharedAlbum)
|
if (isSharedAlbum)
|
||||||
@@ -207,8 +220,9 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
: null,
|
: null,
|
||||||
child: Text(
|
child: Text(
|
||||||
'create_shared_album_page_share'.tr(),
|
'create_shared_album_page_share'.tr(),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -232,9 +246,9 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
// leading: Container(),
|
|
||||||
pinned: true,
|
pinned: true,
|
||||||
floating: false,
|
floating: false,
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/cache.service.dart';
|
||||||
|
|
||||||
class LibraryPage extends HookConsumerWidget {
|
class LibraryPage extends HookConsumerWidget {
|
||||||
const LibraryPage({Key? key}) : super(key: key);
|
const LibraryPage({Key? key}) : super(key: key);
|
||||||
@@ -12,6 +14,7 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final albums = ref.watch(albumProvider);
|
final albums = ref.watch(albumProvider);
|
||||||
|
final cacheService = ref.watch(cacheServiceProvider);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@@ -22,7 +25,7 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAppBar() {
|
Widget _buildAppBar() {
|
||||||
return SliverAppBar(
|
return const SliverAppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
floating: true,
|
floating: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
@@ -34,7 +37,6 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
fontFamily: 'SnowburstOne',
|
fontFamily: 'SnowburstOne',
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -66,15 +68,14 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: const Text(
|
||||||
"New album",
|
'library_page_new_album',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -85,13 +86,13 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
_buildAppBar(),
|
_buildAppBar(),
|
||||||
const SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Albums",
|
'library_page_albums',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
@@ -103,6 +104,7 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
_buildCreateAlbumButton(),
|
_buildCreateAlbumButton(),
|
||||||
for (var album in albums)
|
for (var album in albums)
|
||||||
AlbumThumbnailCard(
|
AlbumThumbnailCard(
|
||||||
|
cacheService: cacheService,
|
||||||
album: album,
|
album: album,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -136,9 +136,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: Text(
|
||||||
'share_invite',
|
'share_invite',
|
||||||
style: TextStyle(color: Colors.black),
|
style: TextStyle(color: Theme.of(context).primaryColor),
|
||||||
).tr(),
|
).tr(),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
@@ -150,11 +150,18 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
primary: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
onPressed:
|
onPressed:
|
||||||
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
|
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"share_create_album",
|
"share_create_album",
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
// color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@@ -8,8 +9,9 @@ import 'package:immich_mobile/constants/hive_box.dart';
|
|||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
|
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/cache.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:transparent_image/transparent_image.dart';
|
|
||||||
|
|
||||||
class SharingPage extends HookConsumerWidget {
|
class SharingPage extends HookConsumerWidget {
|
||||||
const SharingPage({Key? key}) : super(key: key);
|
const SharingPage({Key? key}) : super(key: key);
|
||||||
@@ -19,6 +21,7 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
||||||
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
|
final CacheService cacheService = ref.watch(cacheServiceProvider);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@@ -32,40 +35,35 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
return SliverList(
|
return SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
String thumbnailUrl = sharedAlbums[index].albumThumbnailAssetId !=
|
final album = sharedAlbums[index];
|
||||||
null
|
|
||||||
? "$thumbnailRequestUrl/${sharedAlbums[index].albumThumbnailAssetId}"
|
|
||||||
: "https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60";
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: FadeInImage(
|
child: CachedNetworkImage(
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
|
memCacheHeight: 200,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: MemoryImage(kTransparentImage),
|
cacheManager:
|
||||||
image: NetworkImage(
|
cacheService.getCache(CacheType.sharedAlbumThumbnail),
|
||||||
thumbnailUrl,
|
imageUrl: getAlbumThumbnailUrl(album),
|
||||||
headers: {
|
cacheKey: album.albumThumbnailAssetId,
|
||||||
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
httpHeaders: {
|
||||||
},
|
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
||||||
),
|
},
|
||||||
fadeInDuration: const Duration(milliseconds: 200),
|
fadeInDuration: const Duration(milliseconds: 200),
|
||||||
fadeOutDuration: const Duration(milliseconds: 200),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
sharedAlbums[index].albumName,
|
sharedAlbums[index].albumName,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
fontSize: 16,
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
color: Colors.grey.shade800,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context)
|
AutoRouter.of(context)
|
||||||
@@ -87,7 +85,7 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10), // if you need this
|
borderRadius: BorderRadius.circular(10), // if you need this
|
||||||
side: const BorderSide(
|
side: const BorderSide(
|
||||||
color: Colors.black12,
|
color: Colors.grey,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -102,25 +100,21 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.offline_share_outlined,
|
Icons.offline_share_outlined,
|
||||||
size: 50,
|
size: 50,
|
||||||
color: Theme.of(context).primaryColor.withAlpha(200),
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'sharing_page_empty_list',
|
'sharing_page_empty_list',
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.headline3,
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'sharing_page_description',
|
'sharing_page_description',
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
final ImageViewerService _imageViewerService;
|
final ImageViewerService _imageViewerService;
|
||||||
|
final ShareService _shareService;
|
||||||
|
|
||||||
ImageViewerStateNotifier(this._imageViewerService)
|
ImageViewerStateNotifier(this._imageViewerService, this._shareService)
|
||||||
: super(
|
: super(
|
||||||
ImageViewerPageState(
|
ImageViewerPageState(
|
||||||
downloadAssetStatus: DownloadAssetStatus.idle,
|
downloadAssetStatus: DownloadAssetStatus.idle,
|
||||||
@@ -42,9 +46,23 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
|||||||
|
|
||||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void shareAsset(AssetResponseDto asset, BuildContext context) async {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext buildContext) {
|
||||||
|
_shareService
|
||||||
|
.shareAsset(asset)
|
||||||
|
.then((_) => Navigator.of(buildContext).pop());
|
||||||
|
return const ShareDialog();
|
||||||
|
},
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final imageViewerStateProvider =
|
final imageViewerStateProvider =
|
||||||
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
|
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
|
||||||
((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))),
|
((ref) => ImageViewerStateNotifier(
|
||||||
|
ref.watch(imageViewerServiceProvider), ref.watch(shareServiceProvider))),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
enum _RemoteImageStatus { empty, thumbnail, full }
|
enum _RemoteImageStatus { empty, thumbnail, preview, full }
|
||||||
|
|
||||||
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
late CachedNetworkImageProvider _imageProvider;
|
late CachedNetworkImageProvider _imageProvider;
|
||||||
@@ -15,13 +16,16 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool allowMoving = _status == _RemoteImageStatus.full;
|
bool allowMoving = _status == _RemoteImageStatus.full;
|
||||||
return PhotoView(
|
|
||||||
imageProvider: _imageProvider,
|
return IgnorePointer(
|
||||||
minScale: PhotoViewComputedScale.contained,
|
ignoring: !allowMoving,
|
||||||
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
child: PhotoView(
|
||||||
enablePanAlways: true,
|
imageProvider: _imageProvider,
|
||||||
scaleStateChangedCallback: _scaleStateChanged,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
onScaleEnd: _onScaleListener,
|
enablePanAlways: true,
|
||||||
|
scaleStateChangedCallback: _scaleStateChanged,
|
||||||
|
onScaleEnd: _onScaleListener,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +56,21 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
widget.isZoomedFunction();
|
widget.isZoomedFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
void _fireStartLoadingEvent() {
|
||||||
|
widget.onLoadingStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fireFinishedLoadingEvent() {
|
||||||
|
widget.onLoadingCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedNetworkImageProvider _authorizedImageProvider(
|
||||||
|
String url, String cacheKey, BaseCacheManager? cacheManager) {
|
||||||
return CachedNetworkImageProvider(
|
return CachedNetworkImageProvider(
|
||||||
url,
|
url,
|
||||||
headers: {"Authorization": widget.authToken},
|
headers: {"Authorization": widget.authToken},
|
||||||
cacheKey: url,
|
cacheKey: cacheKey,
|
||||||
|
cacheManager: cacheManager,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,14 +78,25 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
_RemoteImageStatus newStatus,
|
_RemoteImageStatus newStatus,
|
||||||
CachedNetworkImageProvider provider,
|
CachedNetworkImageProvider provider,
|
||||||
) {
|
) {
|
||||||
// Transition to same status is forbidden
|
|
||||||
if (_status == newStatus) return;
|
if (_status == newStatus) return;
|
||||||
// Transition full -> thumbnail is forbidden
|
|
||||||
if (_status == _RemoteImageStatus.full &&
|
if (_status == _RemoteImageStatus.full &&
|
||||||
newStatus == _RemoteImageStatus.thumbnail) return;
|
newStatus == _RemoteImageStatus.thumbnail) return;
|
||||||
|
|
||||||
|
if (_status == _RemoteImageStatus.preview &&
|
||||||
|
newStatus == _RemoteImageStatus.thumbnail) return;
|
||||||
|
|
||||||
|
if (_status == _RemoteImageStatus.full &&
|
||||||
|
newStatus == _RemoteImageStatus.preview) return;
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (newStatus != _RemoteImageStatus.full) {
|
||||||
|
_fireStartLoadingEvent();
|
||||||
|
} else {
|
||||||
|
_fireFinishedLoadingEvent();
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_status = newStatus;
|
_status = newStatus;
|
||||||
_imageProvider = provider;
|
_imageProvider = provider;
|
||||||
@@ -79,8 +104,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _loadImages() {
|
void _loadImages() {
|
||||||
CachedNetworkImageProvider thumbnailProvider =
|
CachedNetworkImageProvider thumbnailProvider = _authorizedImageProvider(
|
||||||
_authorizedImageProvider(widget.thumbnailUrl);
|
widget.thumbnailUrl,
|
||||||
|
widget.cacheKey,
|
||||||
|
widget.thumbnailCacheManager,
|
||||||
|
);
|
||||||
_imageProvider = thumbnailProvider;
|
_imageProvider = thumbnailProvider;
|
||||||
|
|
||||||
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
@@ -92,8 +120,24 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
CachedNetworkImageProvider fullProvider =
|
if (widget.previewUrl != null) {
|
||||||
_authorizedImageProvider(widget.imageUrl);
|
CachedNetworkImageProvider previewProvider = _authorizedImageProvider(
|
||||||
|
widget.previewUrl!,
|
||||||
|
"${widget.cacheKey}_previewStage",
|
||||||
|
widget.previewCacheManager,
|
||||||
|
);
|
||||||
|
previewProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
|
_performStateTransition(_RemoteImageStatus.preview, previewProvider);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedNetworkImageProvider fullProvider = _authorizedImageProvider(
|
||||||
|
widget.imageUrl,
|
||||||
|
"${widget.cacheKey}_fullStage",
|
||||||
|
widget.fullCacheManager,
|
||||||
|
);
|
||||||
fullProvider.resolve(const ImageConfiguration()).addListener(
|
fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
||||||
@@ -118,11 +162,25 @@ class RemotePhotoView extends StatefulWidget {
|
|||||||
required this.isZoomedListener,
|
required this.isZoomedListener,
|
||||||
required this.onSwipeDown,
|
required this.onSwipeDown,
|
||||||
required this.onSwipeUp,
|
required this.onSwipeUp,
|
||||||
|
this.previewUrl,
|
||||||
|
required this.onLoadingCompleted,
|
||||||
|
required this.onLoadingStart,
|
||||||
|
this.thumbnailCacheManager,
|
||||||
|
this.previewCacheManager,
|
||||||
|
this.fullCacheManager,
|
||||||
|
required this.cacheKey,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final String authToken;
|
final String authToken;
|
||||||
|
final String? previewUrl;
|
||||||
|
final Function onLoadingCompleted;
|
||||||
|
final Function onLoadingStart;
|
||||||
|
final BaseCacheManager? thumbnailCacheManager;
|
||||||
|
final BaseCacheManager? previewCacheManager;
|
||||||
|
final BaseCacheManager? fullCacheManager;
|
||||||
|
final String cacheKey;
|
||||||
|
|
||||||
final void Function() onSwipeDown;
|
final void Function() onSwipeDown;
|
||||||
final void Function() onSwipeUp;
|
final void Function() onSwipeUp;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -11,17 +9,23 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
required this.asset,
|
required this.asset,
|
||||||
required this.onMoreInfoPressed,
|
required this.onMoreInfoPressed,
|
||||||
required this.onDownloadPressed,
|
required this.onDownloadPressed,
|
||||||
|
required this.onSharePressed,
|
||||||
|
this.loading = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
final AssetResponseDto asset;
|
||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
final Function onDownloadPressed;
|
final Function onDownloadPressed;
|
||||||
|
final Function onSharePressed;
|
||||||
|
final bool loading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
double iconSize = 18.0;
|
double iconSize = 18.0;
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
|
// iconTheme: IconThemeData(color: Colors.grey[100]),
|
||||||
|
// actionsIconTheme: IconThemeData(color: Colors.grey[100]),
|
||||||
foregroundColor: Colors.grey[100],
|
foregroundColor: Colors.grey[100],
|
||||||
toolbarHeight: 60,
|
toolbarHeight: 60,
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
@@ -29,29 +33,43 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).pop();
|
AutoRouter.of(context).pop();
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
Icons.arrow_back_ios_new_rounded,
|
Icons.arrow_back_ios_new_rounded,
|
||||||
size: 20.0,
|
size: 20.0,
|
||||||
|
color: Colors.grey[200],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
if (loading)
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
child: const CircularProgressIndicator(strokeWidth: 2.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onDownloadPressed();
|
onDownloadPressed();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.cloud_download_rounded),
|
icon: Icon(
|
||||||
|
Icons.cloud_download_rounded,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
log("favorite");
|
onSharePressed();
|
||||||
},
|
},
|
||||||
icon: asset.isFavorite
|
icon: Icon(
|
||||||
? const Icon(Icons.favorite_rounded)
|
Icons.share,
|
||||||
: const Icon(Icons.favorite_border_rounded),
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
@@ -59,7 +77,10 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
onMoreInfoPressed();
|
onMoreInfoPressed();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.more_horiz_rounded),
|
icon: Icon(
|
||||||
|
Icons.more_horiz_rounded,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,36 +11,51 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class GalleryViewerPage extends HookConsumerWidget {
|
class GalleryViewerPage extends HookConsumerWidget {
|
||||||
late List<AssetResponseDto> assetList;
|
late List<AssetResponseDto> assetList;
|
||||||
final AssetResponseDto asset;
|
final AssetResponseDto asset;
|
||||||
final String thumbnailRequestUrl;
|
|
||||||
|
|
||||||
GalleryViewerPage({
|
GalleryViewerPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.assetList,
|
required this.assetList,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.thumbnailRequestUrl,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AssetResponseDto? assetDetail;
|
AssetResponseDto? assetDetail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final Box<dynamic> box = Hive.box(userInfoBox);
|
final Box<dynamic> box = Hive.box(userInfoBox);
|
||||||
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
|
final threeStageLoading = useState(false);
|
||||||
|
final loading = useState(false);
|
||||||
|
final isZoomed = useState<bool>(false);
|
||||||
|
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
|
||||||
|
|
||||||
int indexOfAsset = assetList.indexOf(asset);
|
int indexOfAsset = assetList.indexOf(asset);
|
||||||
|
|
||||||
@override
|
|
||||||
void initState(int index) {
|
|
||||||
indexOfAsset = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
PageController controller =
|
PageController controller =
|
||||||
PageController(initialPage: assetList.indexOf(asset));
|
PageController(initialPage: assetList.indexOf(asset));
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
threeStageLoading.value = appSettingService
|
||||||
|
.getSetting<bool>(AppSettingsEnum.threeStageLoading);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState(int index) {
|
||||||
|
indexOfAsset = index;
|
||||||
|
}
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail = await ref
|
assetDetail = await ref
|
||||||
.watch(assetServiceProvider)
|
.watch(assetServiceProvider)
|
||||||
@@ -59,9 +74,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final isZoomed = useState<bool>(false);
|
|
||||||
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
|
|
||||||
|
|
||||||
//make isZoomed listener call instead
|
//make isZoomed listener call instead
|
||||||
void isZoomedMethod() {
|
void isZoomedMethod() {
|
||||||
if (isZoomedListener.value) {
|
if (isZoomedListener.value) {
|
||||||
@@ -74,6 +86,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
appBar: TopControlAppBar(
|
appBar: TopControlAppBar(
|
||||||
|
loading: loading.value,
|
||||||
asset: assetList[indexOfAsset],
|
asset: assetList[indexOfAsset],
|
||||||
onMoreInfoPressed: () {
|
onMoreInfoPressed: () {
|
||||||
showInfo();
|
showInfo();
|
||||||
@@ -83,6 +96,11 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
.watch(imageViewerStateProvider.notifier)
|
.watch(imageViewerStateProvider.notifier)
|
||||||
.downloadAsset(assetList[indexOfAsset], context);
|
.downloadAsset(assetList[indexOfAsset], context);
|
||||||
},
|
},
|
||||||
|
onSharePressed: () {
|
||||||
|
ref
|
||||||
|
.watch(imageViewerStateProvider.notifier)
|
||||||
|
.shareAsset(assetList[indexOfAsset], context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
@@ -95,18 +113,19 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
initState(index);
|
initState(index);
|
||||||
|
|
||||||
getAssetExif();
|
getAssetExif();
|
||||||
|
|
||||||
if (assetList[index].type == AssetTypeEnum.IMAGE) {
|
if (assetList[index].type == AssetTypeEnum.IMAGE) {
|
||||||
return ImageViewerPage(
|
return ImageViewerPage(
|
||||||
thumbnailUrl:
|
|
||||||
'${box.get(serverEndpointKey)}/asset/thumbnail/${assetList[index].id}',
|
|
||||||
imageUrl:
|
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}&isThumb=false',
|
|
||||||
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
||||||
isZoomedFunction: isZoomedMethod,
|
isZoomedFunction: isZoomedMethod,
|
||||||
isZoomedListener: isZoomedListener,
|
isZoomedListener: isZoomedListener,
|
||||||
|
onLoadingCompleted: () => {},
|
||||||
|
onLoadingStart: () => {},
|
||||||
asset: assetList[index],
|
asset: assetList[index],
|
||||||
heroTag: assetList[index].id,
|
heroTag: assetList[index].id,
|
||||||
|
threeStageLoading: threeStageLoading.value,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return SwipeDetector(
|
return SwipeDetector(
|
||||||
|
|||||||
@@ -8,34 +8,40 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/cache.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class ImageViewerPage extends HookConsumerWidget {
|
class ImageViewerPage extends HookConsumerWidget {
|
||||||
final String imageUrl;
|
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
final String thumbnailUrl;
|
|
||||||
final AssetResponseDto asset;
|
final AssetResponseDto asset;
|
||||||
final String authToken;
|
final String authToken;
|
||||||
final ValueNotifier<bool> isZoomedListener;
|
final ValueNotifier<bool> isZoomedListener;
|
||||||
final void Function() isZoomedFunction;
|
final void Function() isZoomedFunction;
|
||||||
|
final void Function() onLoadingCompleted;
|
||||||
|
final void Function() onLoadingStart;
|
||||||
|
final bool threeStageLoading;
|
||||||
|
|
||||||
ImageViewerPage({
|
ImageViewerPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.imageUrl,
|
|
||||||
required this.heroTag,
|
required this.heroTag,
|
||||||
required this.thumbnailUrl,
|
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.authToken,
|
required this.authToken,
|
||||||
required this.isZoomedFunction,
|
required this.isZoomedFunction,
|
||||||
required this.isZoomedListener,
|
required this.isZoomedListener,
|
||||||
|
required this.onLoadingCompleted,
|
||||||
|
required this.onLoadingStart,
|
||||||
|
required this.threeStageLoading,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AssetResponseDto? assetDetail;
|
AssetResponseDto? assetDetail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final downloadAssetStatus =
|
final downloadAssetStatus =
|
||||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
|
final cacheService = ref.watch(cacheServiceProvider);
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail =
|
assetDetail =
|
||||||
@@ -68,13 +74,25 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
child: Hero(
|
child: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: RemotePhotoView(
|
child: RemotePhotoView(
|
||||||
thumbnailUrl: thumbnailUrl,
|
thumbnailUrl: getThumbnailUrl(asset),
|
||||||
imageUrl: imageUrl,
|
cacheKey: asset.id,
|
||||||
|
imageUrl: getImageUrl(asset),
|
||||||
|
previewUrl: threeStageLoading
|
||||||
|
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
|
||||||
|
: null,
|
||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
isZoomedFunction: isZoomedFunction,
|
isZoomedFunction: isZoomedFunction,
|
||||||
isZoomedListener: isZoomedListener,
|
isZoomedListener: isZoomedListener,
|
||||||
onSwipeDown: () => AutoRouter.of(context).pop(),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
onSwipeUp: () => showInfo(),
|
onSwipeUp: () => showInfo(),
|
||||||
|
onLoadingCompleted: onLoadingCompleted,
|
||||||
|
onLoadingStart: onLoadingStart,
|
||||||
|
thumbnailCacheManager:
|
||||||
|
cacheService.getCache(CacheType.thumbnail),
|
||||||
|
previewCacheManager:
|
||||||
|
cacheService.getCache(CacheType.imageViewerPreview),
|
||||||
|
fullCacheManager:
|
||||||
|
cacheService.getCache(CacheType.imageViewerFull),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,456 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:ui' show IsolateNameServer, PluginUtilities;
|
||||||
|
import 'package:cancellation_token_http/http.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/background_service/localization.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
final backgroundServiceProvider = Provider(
|
||||||
|
(ref) => BackgroundService(),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Background backup service
|
||||||
|
class BackgroundService {
|
||||||
|
static const String _portNameLock = "immichLock";
|
||||||
|
BackgroundService();
|
||||||
|
static const MethodChannel _foregroundChannel =
|
||||||
|
MethodChannel('immich/foregroundChannel');
|
||||||
|
static const MethodChannel _backgroundChannel =
|
||||||
|
MethodChannel('immich/backgroundChannel');
|
||||||
|
bool _isBackgroundInitialized = false;
|
||||||
|
CancellationToken? _cancellationToken;
|
||||||
|
bool _canceledBySystem = false;
|
||||||
|
int _wantsLockTime = 0;
|
||||||
|
bool _hasLock = false;
|
||||||
|
SendPort? _waitingIsolate;
|
||||||
|
ReceivePort? _rp;
|
||||||
|
bool _errorGracePeriodExceeded = true;
|
||||||
|
|
||||||
|
bool get isBackgroundInitialized {
|
||||||
|
return _isBackgroundInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures that the background service is enqueued if enabled in settings
|
||||||
|
Future<bool> resumeServiceIfEnabled() async {
|
||||||
|
return await isBackgroundBackupEnabled() && await enableService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 =
|
||||||
|
"backup_background_service_default_notification".tr();
|
||||||
|
final bool ok = await _foregroundChannel
|
||||||
|
.invokeMethod('enable', [callback.toRawHandle(), title, immediate]);
|
||||||
|
return ok;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configures the background service
|
||||||
|
Future<bool> configureService({
|
||||||
|
bool requireUnmetered = true,
|
||||||
|
bool requireCharging = false,
|
||||||
|
}) async {
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final bool ok = await _foregroundChannel.invokeMethod(
|
||||||
|
'configure',
|
||||||
|
[requireUnmetered, requireCharging],
|
||||||
|
);
|
||||||
|
return ok;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if battery optimizations are disabled
|
||||||
|
Future<bool> isIgnoringBatteryOptimizations() async {
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await _foregroundChannel
|
||||||
|
.invokeMethod('isIgnoringBatteryOptimizations');
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the notification shown by the background service
|
||||||
|
Future<bool> _updateNotification({
|
||||||
|
required String title,
|
||||||
|
String? content,
|
||||||
|
}) async {
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (_isBackgroundInitialized) {
|
||||||
|
return await _backgroundChannel
|
||||||
|
.invokeMethod('updateNotification', [title, content]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("[_updateNotification] failed to communicate with plugin");
|
||||||
|
}
|
||||||
|
return Future.value(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a new priority notification
|
||||||
|
Future<bool> _showErrorNotification({
|
||||||
|
required String title,
|
||||||
|
String? content,
|
||||||
|
String? individualTag,
|
||||||
|
}) async {
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (_isBackgroundInitialized && _errorGracePeriodExceeded) {
|
||||||
|
return await _backgroundChannel
|
||||||
|
.invokeMethod('showError', [title, content, individualTag]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("[_showErrorNotification] failed to communicate with plugin");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _clearErrorNotifications() async {
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (_isBackgroundInitialized) {
|
||||||
|
return await _backgroundChannel.invokeMethod('clearErrorNotifications');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint(
|
||||||
|
"[_clearErrorNotifications] failed to communicate with plugin",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// await to ensure this thread (foreground or background) has exclusive access
|
||||||
|
Future<bool> acquireLock() async {
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final int lockTime = Timeline.now;
|
||||||
|
_wantsLockTime = lockTime;
|
||||||
|
final ReceivePort rp = ReceivePort(_portNameLock);
|
||||||
|
_rp = rp;
|
||||||
|
final SendPort sp = rp.sendPort;
|
||||||
|
|
||||||
|
while (!IsolateNameServer.registerPortWithName(sp, _portNameLock)) {
|
||||||
|
try {
|
||||||
|
await _checkLockReleasedWithHeartbeat(lockTime);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_wantsLockTime != lockTime) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_hasLock = true;
|
||||||
|
rp.listen(_heartbeatListener);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkLockReleasedWithHeartbeat(final int lockTime) async {
|
||||||
|
SendPort? other = IsolateNameServer.lookupPortByName(_portNameLock);
|
||||||
|
if (other != null) {
|
||||||
|
final ReceivePort tempRp = ReceivePort();
|
||||||
|
final SendPort tempSp = tempRp.sendPort;
|
||||||
|
final bs = tempRp.asBroadcastStream();
|
||||||
|
while (_wantsLockTime == lockTime) {
|
||||||
|
other.send(tempSp);
|
||||||
|
final dynamic answer = await bs.first
|
||||||
|
.timeout(const Duration(seconds: 5), onTimeout: () => null);
|
||||||
|
if (_wantsLockTime != lockTime) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (answer == null) {
|
||||||
|
// other isolate failed to answer, assuming it exited without releasing the lock
|
||||||
|
if (other == IsolateNameServer.lookupPortByName(_portNameLock)) {
|
||||||
|
IsolateNameServer.removePortNameMapping(_portNameLock);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} else if (answer == true) {
|
||||||
|
// other isolate released the lock
|
||||||
|
break;
|
||||||
|
} else if (answer == false) {
|
||||||
|
// other isolate is still active
|
||||||
|
}
|
||||||
|
final dynamic isFinished = await bs.first
|
||||||
|
.timeout(const Duration(seconds: 5), onTimeout: () => false);
|
||||||
|
if (isFinished == true) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempRp.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _heartbeatListener(dynamic msg) {
|
||||||
|
if (msg is SendPort) {
|
||||||
|
_waitingIsolate = msg;
|
||||||
|
msg.send(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// releases the exclusive access lock
|
||||||
|
void releaseLock() {
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_wantsLockTime = 0;
|
||||||
|
if (_hasLock) {
|
||||||
|
IsolateNameServer.removePortNameMapping(_portNameLock);
|
||||||
|
_waitingIsolate?.send(true);
|
||||||
|
_waitingIsolate = null;
|
||||||
|
_hasLock = false;
|
||||||
|
}
|
||||||
|
_rp?.close();
|
||||||
|
_rp = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupBackgroundCallHandler() {
|
||||||
|
_backgroundChannel.setMethodCallHandler(_callHandler);
|
||||||
|
_isBackgroundInitialized = true;
|
||||||
|
_backgroundChannel.invokeMethod('initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _callHandler(MethodCall call) async {
|
||||||
|
switch (call.method) {
|
||||||
|
case "onAssetsChanged":
|
||||||
|
final Future<bool> translationsLoaded = loadTranslations();
|
||||||
|
try {
|
||||||
|
final bool hasAccess = await acquireLock();
|
||||||
|
if (!hasAccess) {
|
||||||
|
debugPrint("[_callHandler] could not acquire lock, exiting");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await translationsLoaded;
|
||||||
|
final bool ok = await _onAssetsChanged();
|
||||||
|
return ok;
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint(error.toString());
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
await Hive.close();
|
||||||
|
releaseLock();
|
||||||
|
}
|
||||||
|
case "systemStop":
|
||||||
|
_canceledBySystem = true;
|
||||||
|
_cancellationToken?.cancel();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
debugPrint("Unknown method ${call.method}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _onAssetsChanged() async {
|
||||||
|
await Hive.initFlutter();
|
||||||
|
|
||||||
|
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
||||||
|
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
||||||
|
await Hive.openBox(userInfoBox);
|
||||||
|
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
||||||
|
await Hive.openBox(userSettingInfoBox);
|
||||||
|
await Hive.openBox(backgroundBackupInfoBox);
|
||||||
|
|
||||||
|
ApiService apiService = ApiService();
|
||||||
|
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
||||||
|
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
|
||||||
|
BackupService backupService = BackupService(apiService);
|
||||||
|
|
||||||
|
final Box<HiveBackupAlbums> box =
|
||||||
|
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
|
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
||||||
|
if (backupAlbumInfo == null) {
|
||||||
|
_clearErrorNotifications();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await PhotoManager.setIgnorePermissionCheck(true);
|
||||||
|
|
||||||
|
do {
|
||||||
|
final bool backupOk = await _runBackup(backupService, backupAlbumInfo);
|
||||||
|
if (backupOk) {
|
||||||
|
await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
|
||||||
|
await box.put(
|
||||||
|
backupInfoKey,
|
||||||
|
backupAlbumInfo,
|
||||||
|
);
|
||||||
|
} else if (Hive.box(backgroundBackupInfoBox).get(backupFailedSince) ==
|
||||||
|
null) {
|
||||||
|
Hive.box(backgroundBackupInfoBox)
|
||||||
|
.put(backupFailedSince, DateTime.now());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// check for new assets added while performing backup
|
||||||
|
} while (true ==
|
||||||
|
await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _runBackup(
|
||||||
|
BackupService backupService,
|
||||||
|
HiveBackupAlbums backupAlbumInfo,
|
||||||
|
) async {
|
||||||
|
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
|
||||||
|
|
||||||
|
if (_canceledBySystem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AssetEntity> toUpload =
|
||||||
|
await backupService.buildUploadCandidates(backupAlbumInfo);
|
||||||
|
|
||||||
|
try {
|
||||||
|
toUpload = await backupService.removeAlreadyUploadedAssets(toUpload);
|
||||||
|
} catch (e) {
|
||||||
|
_showErrorNotification(
|
||||||
|
title: "backup_background_service_error_title".tr(),
|
||||||
|
content: "backup_background_service_connection_failed_message".tr(),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_canceledBySystem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toUpload.isEmpty) {
|
||||||
|
_clearErrorNotifications();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cancellationToken = CancellationToken();
|
||||||
|
final bool ok = await backupService.backupAsset(
|
||||||
|
toUpload,
|
||||||
|
_cancellationToken!,
|
||||||
|
_onAssetUploaded,
|
||||||
|
_onProgress,
|
||||||
|
_onSetCurrentBackupAsset,
|
||||||
|
_onBackupError,
|
||||||
|
);
|
||||||
|
if (ok) {
|
||||||
|
_clearErrorNotifications();
|
||||||
|
} else {
|
||||||
|
_showErrorNotification(
|
||||||
|
title: "backup_background_service_error_title".tr(),
|
||||||
|
content: "backup_background_service_backup_failed_message".tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||||
|
debugPrint("Uploaded $deviceAssetId from $deviceId");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onProgress(int sent, int total) {}
|
||||||
|
|
||||||
|
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||||
|
_showErrorNotification(
|
||||||
|
title: "Upload failed",
|
||||||
|
content: "backup_background_service_upload_failure_notification"
|
||||||
|
.tr(args: [errorAssetInfo.fileName]),
|
||||||
|
individualTag: errorAssetInfo.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
||||||
|
_updateNotification(
|
||||||
|
title: "backup_background_service_in_progress_notification".tr(),
|
||||||
|
content: "backup_background_service_current_upload_notification"
|
||||||
|
.tr(args: [currentUploadAsset.fileName]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isErrorGracePeriodExceeded() {
|
||||||
|
final int value = AppSettingsService()
|
||||||
|
.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod);
|
||||||
|
if (value == 0) {
|
||||||
|
return true;
|
||||||
|
} else if (value == 5) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final DateTime? failedSince =
|
||||||
|
Hive.box(backgroundBackupInfoBox).get(backupFailedSince);
|
||||||
|
if (failedSince == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Duration duration = DateTime.now().difference(failedSince);
|
||||||
|
if (value == 1) {
|
||||||
|
return duration > const Duration(minutes: 30);
|
||||||
|
} else if (value == 2) {
|
||||||
|
return duration > const Duration(hours: 2);
|
||||||
|
} else if (value == 3) {
|
||||||
|
return duration > const Duration(hours: 8);
|
||||||
|
} else if (value == 4) {
|
||||||
|
return duration > const Duration(hours: 24);
|
||||||
|
}
|
||||||
|
assert(false, "Invalid value");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
void _nativeEntry() {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
BackgroundService backgroundService = BackgroundService();
|
||||||
|
backgroundService._setupBackgroundCallHandler();
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:easy_localization/src/asset_loader.dart';
|
||||||
|
import 'package:easy_localization/src/easy_localization_controller.dart';
|
||||||
|
import 'package:easy_localization/src/localization.dart';
|
||||||
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
|
|
||||||
|
/// Workaround to manually load translations in another Isolate
|
||||||
|
Future<bool> loadTranslations() async {
|
||||||
|
await EasyLocalizationController.initEasyLocation();
|
||||||
|
|
||||||
|
final controller = EasyLocalizationController(
|
||||||
|
supportedLocales: locales,
|
||||||
|
useFallbackTranslations: true,
|
||||||
|
saveLocale: true,
|
||||||
|
assetLoader: const RootBundleAssetLoader(),
|
||||||
|
path: translationsPath,
|
||||||
|
useOnlyLangCode: false,
|
||||||
|
onLoadError: (e) => debugPrint(e.toString()),
|
||||||
|
fallbackLocale: locales.first,
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.loadTranslations();
|
||||||
|
|
||||||
|
return Localization.load(controller.locale,
|
||||||
|
translations: controller.translations,
|
||||||
|
fallbackTranslations: controller.fallbackTranslations);
|
||||||
|
}
|
||||||
@@ -4,35 +4,45 @@ import 'package:photo_manager/photo_manager.dart';
|
|||||||
|
|
||||||
class AvailableAlbum {
|
class AvailableAlbum {
|
||||||
final AssetPathEntity albumEntity;
|
final AssetPathEntity albumEntity;
|
||||||
|
final DateTime? lastBackup;
|
||||||
final Uint8List? thumbnailData;
|
final Uint8List? thumbnailData;
|
||||||
AvailableAlbum({
|
AvailableAlbum({
|
||||||
required this.albumEntity,
|
required this.albumEntity,
|
||||||
|
this.lastBackup,
|
||||||
this.thumbnailData,
|
this.thumbnailData,
|
||||||
});
|
});
|
||||||
|
|
||||||
AvailableAlbum copyWith({
|
AvailableAlbum copyWith({
|
||||||
AssetPathEntity? albumEntity,
|
AssetPathEntity? albumEntity,
|
||||||
|
DateTime? lastBackup,
|
||||||
Uint8List? thumbnailData,
|
Uint8List? thumbnailData,
|
||||||
}) {
|
}) {
|
||||||
return AvailableAlbum(
|
return AvailableAlbum(
|
||||||
albumEntity: albumEntity ?? this.albumEntity,
|
albumEntity: albumEntity ?? this.albumEntity,
|
||||||
|
lastBackup: lastBackup ?? this.lastBackup,
|
||||||
thumbnailData: thumbnailData ?? this.thumbnailData,
|
thumbnailData: thumbnailData ?? this.thumbnailData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get name => albumEntity.name;
|
||||||
|
|
||||||
|
int get assetCount => albumEntity.assetCount;
|
||||||
|
|
||||||
|
String get id => albumEntity.id;
|
||||||
|
|
||||||
|
bool get isAll => albumEntity.isAll;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
|
'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup, thumbnailData: $thumbnailData)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is AvailableAlbum &&
|
return other is AvailableAlbum && other.albumEntity == albumEntity;
|
||||||
other.albumEntity == albumEntity &&
|
|
||||||
other.thumbnailData == thumbnailData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => albumEntity.hashCode ^ thumbnailData.hashCode;
|
int get hashCode => albumEntity.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:photo_manager/photo_manager.dart';
|
|||||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
|
|
||||||
enum BackUpProgressEnum { idle, inProgress, done }
|
enum BackUpProgressEnum { idle, inProgress, inBackground, done }
|
||||||
|
|
||||||
class BackUpState {
|
class BackUpState {
|
||||||
// enum
|
// enum
|
||||||
@@ -15,11 +15,14 @@ class BackUpState {
|
|||||||
final double progressInPercentage;
|
final double progressInPercentage;
|
||||||
final CancellationToken cancelToken;
|
final CancellationToken cancelToken;
|
||||||
final ServerInfoResponseDto serverInfo;
|
final ServerInfoResponseDto serverInfo;
|
||||||
|
final bool backgroundBackup;
|
||||||
|
final bool backupRequireWifi;
|
||||||
|
final bool backupRequireCharging;
|
||||||
|
|
||||||
/// All available albums on the device
|
/// All available albums on the device
|
||||||
final List<AvailableAlbum> availableAlbums;
|
final List<AvailableAlbum> availableAlbums;
|
||||||
final Set<AssetPathEntity> selectedBackupAlbums;
|
final Set<AvailableAlbum> selectedBackupAlbums;
|
||||||
final Set<AssetPathEntity> excludedBackupAlbums;
|
final Set<AvailableAlbum> excludedBackupAlbums;
|
||||||
|
|
||||||
/// Assets that are not overlapping in selected backup albums and excluded backup albums
|
/// Assets that are not overlapping in selected backup albums and excluded backup albums
|
||||||
final Set<AssetEntity> allUniqueAssets;
|
final Set<AssetEntity> allUniqueAssets;
|
||||||
@@ -36,6 +39,9 @@ class BackUpState {
|
|||||||
required this.progressInPercentage,
|
required this.progressInPercentage,
|
||||||
required this.cancelToken,
|
required this.cancelToken,
|
||||||
required this.serverInfo,
|
required this.serverInfo,
|
||||||
|
required this.backgroundBackup,
|
||||||
|
required this.backupRequireWifi,
|
||||||
|
required this.backupRequireCharging,
|
||||||
required this.availableAlbums,
|
required this.availableAlbums,
|
||||||
required this.selectedBackupAlbums,
|
required this.selectedBackupAlbums,
|
||||||
required this.excludedBackupAlbums,
|
required this.excludedBackupAlbums,
|
||||||
@@ -50,9 +56,12 @@ class BackUpState {
|
|||||||
double? progressInPercentage,
|
double? progressInPercentage,
|
||||||
CancellationToken? cancelToken,
|
CancellationToken? cancelToken,
|
||||||
ServerInfoResponseDto? serverInfo,
|
ServerInfoResponseDto? serverInfo,
|
||||||
|
bool? backgroundBackup,
|
||||||
|
bool? backupRequireWifi,
|
||||||
|
bool? backupRequireCharging,
|
||||||
List<AvailableAlbum>? availableAlbums,
|
List<AvailableAlbum>? availableAlbums,
|
||||||
Set<AssetPathEntity>? selectedBackupAlbums,
|
Set<AvailableAlbum>? selectedBackupAlbums,
|
||||||
Set<AssetPathEntity>? excludedBackupAlbums,
|
Set<AvailableAlbum>? excludedBackupAlbums,
|
||||||
Set<AssetEntity>? allUniqueAssets,
|
Set<AssetEntity>? allUniqueAssets,
|
||||||
Set<String>? selectedAlbumsBackupAssetsIds,
|
Set<String>? selectedAlbumsBackupAssetsIds,
|
||||||
CurrentUploadAsset? currentUploadAsset,
|
CurrentUploadAsset? currentUploadAsset,
|
||||||
@@ -63,6 +72,10 @@ class BackUpState {
|
|||||||
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
|
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
|
||||||
cancelToken: cancelToken ?? this.cancelToken,
|
cancelToken: cancelToken ?? this.cancelToken,
|
||||||
serverInfo: serverInfo ?? this.serverInfo,
|
serverInfo: serverInfo ?? this.serverInfo,
|
||||||
|
backgroundBackup: backgroundBackup ?? this.backgroundBackup,
|
||||||
|
backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi,
|
||||||
|
backupRequireCharging:
|
||||||
|
backupRequireCharging ?? this.backupRequireCharging,
|
||||||
availableAlbums: availableAlbums ?? this.availableAlbums,
|
availableAlbums: availableAlbums ?? this.availableAlbums,
|
||||||
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
|
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
|
||||||
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
|
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
|
||||||
@@ -75,7 +88,7 @@ class BackUpState {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
|
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -89,6 +102,9 @@ class BackUpState {
|
|||||||
other.progressInPercentage == progressInPercentage &&
|
other.progressInPercentage == progressInPercentage &&
|
||||||
other.cancelToken == cancelToken &&
|
other.cancelToken == cancelToken &&
|
||||||
other.serverInfo == serverInfo &&
|
other.serverInfo == serverInfo &&
|
||||||
|
other.backgroundBackup == backgroundBackup &&
|
||||||
|
other.backupRequireWifi == backupRequireWifi &&
|
||||||
|
other.backupRequireCharging == backupRequireCharging &&
|
||||||
collectionEquals(other.availableAlbums, availableAlbums) &&
|
collectionEquals(other.availableAlbums, availableAlbums) &&
|
||||||
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
|
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
|
||||||
collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
|
collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
|
||||||
@@ -107,6 +123,9 @@ class BackUpState {
|
|||||||
progressInPercentage.hashCode ^
|
progressInPercentage.hashCode ^
|
||||||
cancelToken.hashCode ^
|
cancelToken.hashCode ^
|
||||||
serverInfo.hashCode ^
|
serverInfo.hashCode ^
|
||||||
|
backgroundBackup.hashCode ^
|
||||||
|
backupRequireWifi.hashCode ^
|
||||||
|
backupRequireCharging.hashCode ^
|
||||||
availableAlbums.hashCode ^
|
availableAlbums.hashCode ^
|
||||||
selectedBackupAlbums.hashCode ^
|
selectedBackupAlbums.hashCode ^
|
||||||
excludedBackupAlbums.hashCode ^
|
excludedBackupAlbums.hashCode ^
|
||||||
|
|||||||
@@ -13,9 +13,17 @@ class HiveBackupAlbums {
|
|||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
List<String> excludedAlbumsIds;
|
List<String> excludedAlbumsIds;
|
||||||
|
|
||||||
|
@HiveField(2, defaultValue: [])
|
||||||
|
List<DateTime> lastSelectedBackupTime;
|
||||||
|
|
||||||
|
@HiveField(3, defaultValue: [])
|
||||||
|
List<DateTime> lastExcludedBackupTime;
|
||||||
|
|
||||||
HiveBackupAlbums({
|
HiveBackupAlbums({
|
||||||
required this.selectedAlbumIds,
|
required this.selectedAlbumIds,
|
||||||
required this.excludedAlbumsIds,
|
required this.excludedAlbumsIds,
|
||||||
|
required this.lastSelectedBackupTime,
|
||||||
|
required this.lastExcludedBackupTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -25,10 +33,16 @@ class HiveBackupAlbums {
|
|||||||
HiveBackupAlbums copyWith({
|
HiveBackupAlbums copyWith({
|
||||||
List<String>? selectedAlbumIds,
|
List<String>? selectedAlbumIds,
|
||||||
List<String>? excludedAlbumsIds,
|
List<String>? excludedAlbumsIds,
|
||||||
|
List<DateTime>? lastSelectedBackupTime,
|
||||||
|
List<DateTime>? lastExcludedBackupTime,
|
||||||
}) {
|
}) {
|
||||||
return HiveBackupAlbums(
|
return HiveBackupAlbums(
|
||||||
selectedAlbumIds: selectedAlbumIds ?? this.selectedAlbumIds,
|
selectedAlbumIds: selectedAlbumIds ?? this.selectedAlbumIds,
|
||||||
excludedAlbumsIds: excludedAlbumsIds ?? this.excludedAlbumsIds,
|
excludedAlbumsIds: excludedAlbumsIds ?? this.excludedAlbumsIds,
|
||||||
|
lastSelectedBackupTime:
|
||||||
|
lastSelectedBackupTime ?? this.lastSelectedBackupTime,
|
||||||
|
lastExcludedBackupTime:
|
||||||
|
lastExcludedBackupTime ?? this.lastExcludedBackupTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +51,8 @@ class HiveBackupAlbums {
|
|||||||
|
|
||||||
result.addAll({'selectedAlbumIds': selectedAlbumIds});
|
result.addAll({'selectedAlbumIds': selectedAlbumIds});
|
||||||
result.addAll({'excludedAlbumsIds': excludedAlbumsIds});
|
result.addAll({'excludedAlbumsIds': excludedAlbumsIds});
|
||||||
|
result.addAll({'lastSelectedBackupTime': lastSelectedBackupTime});
|
||||||
|
result.addAll({'lastExcludedBackupTime': lastExcludedBackupTime});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -45,6 +61,10 @@ class HiveBackupAlbums {
|
|||||||
return HiveBackupAlbums(
|
return HiveBackupAlbums(
|
||||||
selectedAlbumIds: List<String>.from(map['selectedAlbumIds']),
|
selectedAlbumIds: List<String>.from(map['selectedAlbumIds']),
|
||||||
excludedAlbumsIds: List<String>.from(map['excludedAlbumsIds']),
|
excludedAlbumsIds: List<String>.from(map['excludedAlbumsIds']),
|
||||||
|
lastSelectedBackupTime:
|
||||||
|
List<DateTime>.from(map['lastSelectedBackupTime']),
|
||||||
|
lastExcludedBackupTime:
|
||||||
|
List<DateTime>.from(map['lastExcludedBackupTime']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +80,15 @@ class HiveBackupAlbums {
|
|||||||
|
|
||||||
return other is HiveBackupAlbums &&
|
return other is HiveBackupAlbums &&
|
||||||
listEquals(other.selectedAlbumIds, selectedAlbumIds) &&
|
listEquals(other.selectedAlbumIds, selectedAlbumIds) &&
|
||||||
listEquals(other.excludedAlbumsIds, excludedAlbumsIds);
|
listEquals(other.excludedAlbumsIds, excludedAlbumsIds) &&
|
||||||
|
listEquals(other.lastSelectedBackupTime, lastSelectedBackupTime) &&
|
||||||
|
listEquals(other.lastExcludedBackupTime, lastExcludedBackupTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => selectedAlbumIds.hashCode ^ excludedAlbumsIds.hashCode;
|
int get hashCode =>
|
||||||
|
selectedAlbumIds.hashCode ^
|
||||||
|
excludedAlbumsIds.hashCode ^
|
||||||
|
lastSelectedBackupTime.hashCode ^
|
||||||
|
lastExcludedBackupTime.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,17 +19,25 @@ class HiveBackupAlbumsAdapter extends TypeAdapter<HiveBackupAlbums> {
|
|||||||
return HiveBackupAlbums(
|
return HiveBackupAlbums(
|
||||||
selectedAlbumIds: (fields[0] as List).cast<String>(),
|
selectedAlbumIds: (fields[0] as List).cast<String>(),
|
||||||
excludedAlbumsIds: (fields[1] as List).cast<String>(),
|
excludedAlbumsIds: (fields[1] as List).cast<String>(),
|
||||||
|
lastSelectedBackupTime:
|
||||||
|
fields[2] == null ? [] : (fields[2] as List).cast<DateTime>(),
|
||||||
|
lastExcludedBackupTime:
|
||||||
|
fields[3] == null ? [] : (fields[3] as List).cast<DateTime>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, HiveBackupAlbums obj) {
|
void write(BinaryWriter writer, HiveBackupAlbums obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(2)
|
..writeByte(4)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.selectedAlbumIds)
|
..write(obj.selectedAlbumIds)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
..write(obj.excludedAlbumsIds);
|
..write(obj.excludedAlbumsIds)
|
||||||
|
..writeByte(2)
|
||||||
|
..write(obj.lastSelectedBackupTime)
|
||||||
|
..writeByte(3)
|
||||||
|
..write(obj.lastExcludedBackupTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
@@ -9,9 +11,11 @@ import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.d
|
|||||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
@@ -21,6 +25,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
this._backupService,
|
this._backupService,
|
||||||
this._serverInfoService,
|
this._serverInfoService,
|
||||||
this._authState,
|
this._authState,
|
||||||
|
this._backgroundService,
|
||||||
this.ref,
|
this.ref,
|
||||||
) : super(
|
) : super(
|
||||||
BackUpState(
|
BackUpState(
|
||||||
@@ -28,6 +33,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
allAssetsInDatabase: const [],
|
allAssetsInDatabase: const [],
|
||||||
progressInPercentage: 0,
|
progressInPercentage: 0,
|
||||||
cancelToken: CancellationToken(),
|
cancelToken: CancellationToken(),
|
||||||
|
backgroundBackup: false,
|
||||||
|
backupRequireWifi: true,
|
||||||
|
backupRequireCharging: false,
|
||||||
serverInfo: ServerInfoResponseDto(
|
serverInfo: ServerInfoResponseDto(
|
||||||
diskAvailable: "0",
|
diskAvailable: "0",
|
||||||
diskAvailableRaw: 0,
|
diskAvailableRaw: 0,
|
||||||
@@ -56,6 +64,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final BackupService _backupService;
|
final BackupService _backupService;
|
||||||
final ServerInfoService _serverInfoService;
|
final ServerInfoService _serverInfoService;
|
||||||
final AuthenticationState _authState;
|
final AuthenticationState _authState;
|
||||||
|
final BackgroundService _backgroundService;
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -66,7 +75,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// We have method to include and exclude albums
|
/// We have method to include and exclude albums
|
||||||
/// The total unique assets will be used for backing mechanism
|
/// The total unique assets will be used for backing mechanism
|
||||||
///
|
///
|
||||||
void addAlbumForBackup(AssetPathEntity album) {
|
void addAlbumForBackup(AvailableAlbum album) {
|
||||||
if (state.excludedBackupAlbums.contains(album)) {
|
if (state.excludedBackupAlbums.contains(album)) {
|
||||||
removeExcludedAlbumForBackup(album);
|
removeExcludedAlbumForBackup(album);
|
||||||
}
|
}
|
||||||
@@ -76,7 +85,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
_updateBackupAssetCount();
|
_updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
void addExcludedAlbumForBackup(AssetPathEntity album) {
|
void addExcludedAlbumForBackup(AvailableAlbum album) {
|
||||||
if (state.selectedBackupAlbums.contains(album)) {
|
if (state.selectedBackupAlbums.contains(album)) {
|
||||||
removeAlbumForBackup(album);
|
removeAlbumForBackup(album);
|
||||||
}
|
}
|
||||||
@@ -85,8 +94,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
_updateBackupAssetCount();
|
_updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAlbumForBackup(AssetPathEntity album) {
|
void removeAlbumForBackup(AvailableAlbum album) {
|
||||||
Set<AssetPathEntity> currentSelectedAlbums = state.selectedBackupAlbums;
|
Set<AvailableAlbum> currentSelectedAlbums = state.selectedBackupAlbums;
|
||||||
|
|
||||||
currentSelectedAlbums.removeWhere((a) => a == album);
|
currentSelectedAlbums.removeWhere((a) => a == album);
|
||||||
|
|
||||||
@@ -94,8 +103,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
_updateBackupAssetCount();
|
_updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeExcludedAlbumForBackup(AssetPathEntity album) {
|
void removeExcludedAlbumForBackup(AvailableAlbum album) {
|
||||||
Set<AssetPathEntity> currentExcludedAlbums = state.excludedBackupAlbums;
|
Set<AvailableAlbum> currentExcludedAlbums = state.excludedBackupAlbums;
|
||||||
|
|
||||||
currentExcludedAlbums.removeWhere((a) => a == album);
|
currentExcludedAlbums.removeWhere((a) => a == album);
|
||||||
|
|
||||||
@@ -103,6 +112,60 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
_updateBackupAssetCount();
|
_updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void configureBackgroundBackup({
|
||||||
|
bool? enabled,
|
||||||
|
bool? requireWifi,
|
||||||
|
bool? requireCharging,
|
||||||
|
required void Function(String msg) onError,
|
||||||
|
required void Function() onBatteryInfo,
|
||||||
|
}) async {
|
||||||
|
assert(enabled != null || requireWifi != null || requireCharging != null);
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final bool wasEnabled = state.backgroundBackup;
|
||||||
|
final bool wasWifi = state.backupRequireWifi;
|
||||||
|
final bool wasCharing = state.backupRequireCharging;
|
||||||
|
state = state.copyWith(
|
||||||
|
backgroundBackup: enabled,
|
||||||
|
backupRequireWifi: requireWifi,
|
||||||
|
backupRequireCharging: requireCharging,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state.backgroundBackup) {
|
||||||
|
bool success = true;
|
||||||
|
if (!wasEnabled) {
|
||||||
|
if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
|
||||||
|
onBatteryInfo();
|
||||||
|
}
|
||||||
|
success &= await _backgroundService.enableService(immediate: true);
|
||||||
|
}
|
||||||
|
success &= success &&
|
||||||
|
await _backgroundService.configureService(
|
||||||
|
requireUnmetered: state.backupRequireWifi,
|
||||||
|
requireCharging: state.backupRequireCharging,
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
await Hive.box(backgroundBackupInfoBox)
|
||||||
|
.put(backupRequireWifi, state.backupRequireWifi);
|
||||||
|
await Hive.box(backgroundBackupInfoBox)
|
||||||
|
.put(backupRequireCharging, state.backupRequireCharging);
|
||||||
|
} else {
|
||||||
|
state = state.copyWith(
|
||||||
|
backgroundBackup: wasEnabled,
|
||||||
|
backupRequireWifi: wasWifi,
|
||||||
|
backupRequireCharging: wasCharing,
|
||||||
|
);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Get all album on the device
|
/// Get all album on the device
|
||||||
/// Get all selected and excluded album from the user's persistent storage
|
/// Get all selected and excluded album from the user's persistent storage
|
||||||
@@ -144,6 +207,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
defaultValue: HiveBackupAlbums(
|
defaultValue: HiveBackupAlbums(
|
||||||
selectedAlbumIds: [],
|
selectedAlbumIds: [],
|
||||||
excludedAlbumsIds: [],
|
excludedAlbumsIds: [],
|
||||||
|
lastSelectedBackupTime: [],
|
||||||
|
lastExcludedBackupTime: [],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -173,6 +238,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
HiveBackupAlbums(
|
HiveBackupAlbums(
|
||||||
selectedAlbumIds: [albumHasAllAssets.id],
|
selectedAlbumIds: [albumHasAllAssets.id],
|
||||||
excludedAlbumsIds: [],
|
excludedAlbumsIds: [],
|
||||||
|
lastSelectedBackupTime: [
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)
|
||||||
|
],
|
||||||
|
lastExcludedBackupTime: [],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -181,19 +250,37 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Generate AssetPathEntity from id to add to local state
|
// Generate AssetPathEntity from id to add to local state
|
||||||
try {
|
try {
|
||||||
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
Set<AvailableAlbum> selectedAlbums = {};
|
||||||
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
for (var i = 0; i < backupAlbumInfo!.selectedAlbumIds.length; i++) {
|
||||||
state = state.copyWith(
|
var albumAsset =
|
||||||
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset},
|
await AssetPathEntity.fromId(backupAlbumInfo.selectedAlbumIds[i]);
|
||||||
|
selectedAlbums.add(
|
||||||
|
AvailableAlbum(
|
||||||
|
albumEntity: albumAsset,
|
||||||
|
lastBackup: backupAlbumInfo.lastSelectedBackupTime.length > i
|
||||||
|
? backupAlbumInfo.lastSelectedBackupTime[i]
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
Set<AvailableAlbum> excludedAlbums = {};
|
||||||
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
for (var i = 0; i < backupAlbumInfo.excludedAlbumsIds.length; i++) {
|
||||||
state = state.copyWith(
|
var albumAsset =
|
||||||
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset},
|
await AssetPathEntity.fromId(backupAlbumInfo.excludedAlbumsIds[i]);
|
||||||
|
excludedAlbums.add(
|
||||||
|
AvailableAlbum(
|
||||||
|
albumEntity: albumAsset,
|
||||||
|
lastBackup: backupAlbumInfo.lastExcludedBackupTime.length > i
|
||||||
|
? backupAlbumInfo.lastExcludedBackupTime[i]
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
state = state.copyWith(
|
||||||
|
selectedBackupAlbums: selectedAlbums,
|
||||||
|
excludedBackupAlbums: excludedAlbums,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] Failed to generate album from id $e");
|
debugPrint("[ERROR] Failed to generate album from id $e");
|
||||||
}
|
}
|
||||||
@@ -209,14 +296,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
||||||
|
|
||||||
for (var album in state.selectedBackupAlbums) {
|
for (var album in state.selectedBackupAlbums) {
|
||||||
var assets =
|
var assets = await album.albumEntity
|
||||||
await album.getAssetListRange(start: 0, end: album.assetCount);
|
.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
assetsFromSelectedAlbums.addAll(assets);
|
assetsFromSelectedAlbums.addAll(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var album in state.excludedBackupAlbums) {
|
for (var album in state.excludedBackupAlbums) {
|
||||||
var assets =
|
var assets = await album.albumEntity
|
||||||
await album.getAssetListRange(start: 0, end: album.assetCount);
|
.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
assetsFromExcludedAlbums.addAll(assets);
|
assetsFromExcludedAlbums.addAll(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,12 +350,16 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// and then update the UI according to those information
|
/// and then update the UI according to those information
|
||||||
///
|
///
|
||||||
Future<void> getBackupInfo() async {
|
Future<void> getBackupInfo() async {
|
||||||
await Future.wait([
|
final bool isEnabled = await _backgroundService.isBackgroundBackupEnabled();
|
||||||
_getBackupAlbumsInfo(),
|
state = state.copyWith(backgroundBackup: isEnabled);
|
||||||
_updateServerInfo(),
|
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
||||||
]);
|
await Future.wait([
|
||||||
|
_getBackupAlbumsInfo(),
|
||||||
|
_updateServerInfo(),
|
||||||
|
]);
|
||||||
|
|
||||||
await _updateBackupAssetCount();
|
await _updateBackupAssetCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -276,6 +367,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// Hive database
|
/// Hive database
|
||||||
///
|
///
|
||||||
void _updatePersistentAlbumsSelection() {
|
void _updatePersistentAlbumsSelection() {
|
||||||
|
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||||
Box<HiveBackupAlbums> backupAlbumInfoBox =
|
Box<HiveBackupAlbums> backupAlbumInfoBox =
|
||||||
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
backupAlbumInfoBox.put(
|
backupAlbumInfoBox.put(
|
||||||
@@ -283,6 +375,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
HiveBackupAlbums(
|
HiveBackupAlbums(
|
||||||
selectedAlbumIds: state.selectedBackupAlbums.map((e) => e.id).toList(),
|
selectedAlbumIds: state.selectedBackupAlbums.map((e) => e.id).toList(),
|
||||||
excludedAlbumsIds: state.excludedBackupAlbums.map((e) => e.id).toList(),
|
excludedAlbumsIds: state.excludedBackupAlbums.map((e) => e.id).toList(),
|
||||||
|
lastSelectedBackupTime: state.selectedBackupAlbums
|
||||||
|
.map((e) => e.lastBackup ?? epoch)
|
||||||
|
.toList(),
|
||||||
|
lastExcludedBackupTime: state.excludedBackupAlbums
|
||||||
|
.map((e) => e.lastBackup ?? epoch)
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -290,7 +388,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
///
|
///
|
||||||
/// Invoke backup process
|
/// Invoke backup process
|
||||||
///
|
///
|
||||||
void startBackupProcess() async {
|
Future<void> startBackupProcess() async {
|
||||||
|
assert(state.backupProgress == BackUpProgressEnum.idle);
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
||||||
|
|
||||||
await getBackupInfo();
|
await getBackupInfo();
|
||||||
@@ -318,7 +417,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Perform Backup
|
// Perform Backup
|
||||||
state = state.copyWith(cancelToken: CancellationToken());
|
state = state.copyWith(cancelToken: CancellationToken());
|
||||||
_backupService.backupAsset(
|
await _backupService.backupAsset(
|
||||||
assetsWillBeBackup,
|
assetsWillBeBackup,
|
||||||
state.cancelToken,
|
state.cancelToken,
|
||||||
_onAssetUploaded,
|
_onAssetUploaded,
|
||||||
@@ -326,6 +425,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
_onSetCurrentBackupAsset,
|
_onSetCurrentBackupAsset,
|
||||||
_onBackupError,
|
_onBackupError,
|
||||||
);
|
);
|
||||||
|
await _notifyBackgroundServiceCanRun();
|
||||||
} else {
|
} else {
|
||||||
PhotoManager.openSetting();
|
PhotoManager.openSetting();
|
||||||
}
|
}
|
||||||
@@ -340,6 +440,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void cancelBackup() {
|
void cancelBackup() {
|
||||||
|
if (state.backupProgress != BackUpProgressEnum.inProgress) {
|
||||||
|
_notifyBackgroundServiceCanRun();
|
||||||
|
}
|
||||||
state.cancelToken.cancel();
|
state.cancelToken.cancel();
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
@@ -359,10 +462,21 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
if (state.allUniqueAssets.length -
|
if (state.allUniqueAssets.length -
|
||||||
state.selectedAlbumsBackupAssetsIds.length ==
|
state.selectedAlbumsBackupAssetsIds.length ==
|
||||||
0) {
|
0) {
|
||||||
|
final latestAssetBackup =
|
||||||
|
state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce(
|
||||||
|
(v, e) => e.isAfter(v) ? e : v,
|
||||||
|
);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
|
selectedBackupAlbums: state.selectedBackupAlbums
|
||||||
|
.map((e) => e.copyWith(lastBackup: latestAssetBackup))
|
||||||
|
.toSet(),
|
||||||
|
excludedBackupAlbums: state.excludedBackupAlbums
|
||||||
|
.map((e) => e.copyWith(lastBackup: latestAssetBackup))
|
||||||
|
.toSet(),
|
||||||
backupProgress: BackUpProgressEnum.done,
|
backupProgress: BackUpProgressEnum.done,
|
||||||
progressInPercentage: 0.0,
|
progressInPercentage: 0.0,
|
||||||
);
|
);
|
||||||
|
_updatePersistentAlbumsSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateServerInfo();
|
_updateServerInfo();
|
||||||
@@ -385,7 +499,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resumeBackup() {
|
Future<void> _resumeBackup() async {
|
||||||
// Check if user is login
|
// Check if user is login
|
||||||
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
|
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
|
||||||
@@ -404,13 +518,101 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.backupProgress == BackUpProgressEnum.inBackground) {
|
||||||
|
debugPrint("[resumeBackup] Background backup is running - abort");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Run backup
|
// Run backup
|
||||||
debugPrint("[resumeBackup] Start back up");
|
debugPrint("[resumeBackup] Start back up");
|
||||||
startBackupProcess();
|
await startBackupProcess();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Box<HiveBackupAlbums> box =
|
||||||
|
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
|
HiveBackupAlbums? albums = box.get(backupInfoKey);
|
||||||
|
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||||
|
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||||
|
if (albums != null) {
|
||||||
|
selectedAlbums = _updateAlbumsBackupTime(
|
||||||
|
selectedAlbums,
|
||||||
|
albums.selectedAlbumIds,
|
||||||
|
albums.lastSelectedBackupTime,
|
||||||
|
);
|
||||||
|
excludedAlbums = _updateAlbumsBackupTime(
|
||||||
|
excludedAlbums,
|
||||||
|
albums.excludedAlbumsIds,
|
||||||
|
albums.lastExcludedBackupTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
|
||||||
|
state = state.copyWith(
|
||||||
|
backupProgress: previous,
|
||||||
|
selectedBackupAlbums: selectedAlbums,
|
||||||
|
excludedBackupAlbums: excludedAlbums,
|
||||||
|
backupRequireWifi: backgroundBox.get(backupRequireWifi),
|
||||||
|
backupRequireCharging: backgroundBox.get(backupRequireCharging),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _resumeBackup();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<AvailableAlbum> _updateAlbumsBackupTime(
|
||||||
|
Set<AvailableAlbum> albums,
|
||||||
|
List<String> ids,
|
||||||
|
List<DateTime> times,
|
||||||
|
) {
|
||||||
|
Set<AvailableAlbum> result = {};
|
||||||
|
for (int i = 0; i < ids.length; i++) {
|
||||||
|
try {
|
||||||
|
AvailableAlbum a = albums.firstWhere((e) => e.id == ids[i]);
|
||||||
|
result.add(a.copyWith(lastBackup: times[i]));
|
||||||
|
} on StateError {
|
||||||
|
debugPrint("[_updateAlbumBackupTime] failed to find album in state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _notifyBackgroundServiceCanRun() async {
|
||||||
|
const allowedStates = [
|
||||||
|
AppStateEnum.inactive,
|
||||||
|
AppStateEnum.paused,
|
||||||
|
AppStateEnum.detached,
|
||||||
|
];
|
||||||
|
if (Platform.isAndroid &&
|
||||||
|
allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
|
||||||
|
try {
|
||||||
|
if (Hive.isBoxOpen(hiveBackupInfoBox)) {
|
||||||
|
await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
|
||||||
|
await Hive.box(backgroundBackupInfoBox).close();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
||||||
|
}
|
||||||
|
_backgroundService.releaseLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final backupProvider =
|
final backupProvider =
|
||||||
@@ -419,6 +621,7 @@ final backupProvider =
|
|||||||
ref.watch(backupServiceProvider),
|
ref.watch(backupServiceProvider),
|
||||||
ref.watch(serverInfoServiceProvider),
|
ref.watch(serverInfoServiceProvider),
|
||||||
ref.watch(authenticationProvider),
|
ref.watch(authenticationProvider),
|
||||||
|
ref.watch(backgroundServiceProvider),
|
||||||
ref,
|
ref,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -9,6 +10,7 @@ import 'package:immich_mobile/constants/hive_box.dart';
|
|||||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/files_helper.dart';
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -39,8 +41,129 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backupAsset(
|
/// Returns all assets newer than the last successful backup per album
|
||||||
Set<AssetEntity> assetList,
|
Future<List<AssetEntity>> buildUploadCandidates(
|
||||||
|
HiveBackupAlbums backupAlbums,
|
||||||
|
) async {
|
||||||
|
final filter = FilterOptionGroup(
|
||||||
|
containsPathModified: true,
|
||||||
|
orders: [const OrderOption(type: OrderOptionType.updateDate)],
|
||||||
|
);
|
||||||
|
final now = DateTime.now();
|
||||||
|
final List<AssetPathEntity?> selectedAlbums =
|
||||||
|
await _loadAlbumsWithTimeFilter(
|
||||||
|
backupAlbums.selectedAlbumIds,
|
||||||
|
backupAlbums.lastSelectedBackupTime,
|
||||||
|
filter,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
if (selectedAlbums.every((e) => e == null)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll);
|
||||||
|
if (allIdx != -1) {
|
||||||
|
final List<AssetPathEntity?> excludedAlbums =
|
||||||
|
await _loadAlbumsWithTimeFilter(
|
||||||
|
backupAlbums.excludedAlbumsIds,
|
||||||
|
backupAlbums.lastExcludedBackupTime,
|
||||||
|
filter,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
final List<AssetEntity> toAdd = await _fetchAssetsAndUpdateLastBackup(
|
||||||
|
selectedAlbums.slice(allIdx, allIdx + 1),
|
||||||
|
backupAlbums.lastSelectedBackupTime.slice(allIdx, allIdx + 1),
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
final List<AssetEntity> toRemove = await _fetchAssetsAndUpdateLastBackup(
|
||||||
|
excludedAlbums,
|
||||||
|
backupAlbums.lastExcludedBackupTime,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
return toAdd.toSet().difference(toRemove.toSet()).toList();
|
||||||
|
} else {
|
||||||
|
return await _fetchAssetsAndUpdateLastBackup(
|
||||||
|
selectedAlbums,
|
||||||
|
backupAlbums.lastSelectedBackupTime,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AssetPathEntity?>> _loadAlbumsWithTimeFilter(
|
||||||
|
List<String> albumIds,
|
||||||
|
List<DateTime> lastBackups,
|
||||||
|
FilterOptionGroup filter,
|
||||||
|
DateTime now,
|
||||||
|
) async {
|
||||||
|
List<AssetPathEntity?> result = List.filled(albumIds.length, null);
|
||||||
|
for (int i = 0; i < albumIds.length; i++) {
|
||||||
|
try {
|
||||||
|
final AssetPathEntity album =
|
||||||
|
await AssetPathEntity.obtainPathFromProperties(
|
||||||
|
id: albumIds[i],
|
||||||
|
optionGroup: filter.copyWith(
|
||||||
|
updateTimeCond: DateTimeCond(
|
||||||
|
// subtract 2 seconds to prevent missing assets due to rounding issues
|
||||||
|
min: lastBackups[i].subtract(const Duration(seconds: 2)),
|
||||||
|
max: now,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
maxDateTimeToNow: false,
|
||||||
|
);
|
||||||
|
result[i] = album;
|
||||||
|
} on StateError {
|
||||||
|
// either there are no assets matching the filter criteria OR the album no longer exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AssetEntity>> _fetchAssetsAndUpdateLastBackup(
|
||||||
|
List<AssetPathEntity?> albums,
|
||||||
|
List<DateTime> lastBackup,
|
||||||
|
DateTime now,
|
||||||
|
) async {
|
||||||
|
List<AssetEntity> result = [];
|
||||||
|
for (int i = 0; i < albums.length; i++) {
|
||||||
|
final AssetPathEntity? a = albums[i];
|
||||||
|
if (a != null && a.lastModified?.isBefore(lastBackup[i]) != true) {
|
||||||
|
result.addAll(await a.getAssetListRange(start: 0, end: a.assetCount));
|
||||||
|
lastBackup[i] = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new list of assets not yet uploaded
|
||||||
|
Future<List<AssetEntity>> removeAlreadyUploadedAssets(
|
||||||
|
List<AssetEntity> candidates,
|
||||||
|
) async {
|
||||||
|
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
if (candidates.length < 10) {
|
||||||
|
final List<CheckDuplicateAssetResponseDto?> duplicateResponse =
|
||||||
|
await Future.wait(
|
||||||
|
candidates.map(
|
||||||
|
(e) => _apiService.assetApi.checkDuplicateAsset(
|
||||||
|
CheckDuplicateAssetDto(deviceAssetId: e.id, deviceId: deviceId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return candidates
|
||||||
|
.whereIndexed((i, e) => duplicateResponse[i]?.isExist == false)
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
final List<String>? allAssetsInDatabase = await getDeviceBackupAsset();
|
||||||
|
|
||||||
|
if (allAssetsInDatabase == null) {
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
final Set<String> inDb = allAssetsInDatabase.toSet();
|
||||||
|
return candidates.whereNot((e) => inDb.contains(e.id)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> backupAsset(
|
||||||
|
Iterable<AssetEntity> assetList,
|
||||||
http.CancellationToken cancelToken,
|
http.CancellationToken cancelToken,
|
||||||
Function(String, String) singleAssetDoneCb,
|
Function(String, String) singleAssetDoneCb,
|
||||||
Function(int, int) uploadProgressCb,
|
Function(int, int) uploadProgressCb,
|
||||||
@@ -50,6 +173,7 @@ class BackupService {
|
|||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
File? file;
|
File? file;
|
||||||
|
bool anyErrors = false;
|
||||||
|
|
||||||
for (var entity in assetList) {
|
for (var entity in assetList) {
|
||||||
try {
|
try {
|
||||||
@@ -134,9 +258,10 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
} on http.CancelledException {
|
} on http.CancelledException {
|
||||||
debugPrint("Backup was cancelled by the user");
|
debugPrint("Backup was cancelled by the user");
|
||||||
return;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("ERROR backupAsset: ${e.toString()}");
|
debugPrint("ERROR backupAsset: ${e.toString()}");
|
||||||
|
anyErrors = true;
|
||||||
continue;
|
continue;
|
||||||
} finally {
|
} finally {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
@@ -144,6 +269,7 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return !anyErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getAssetType(AssetType assetType) {
|
String _getAssetType(AssetType assetType) {
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.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/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
|
|
||||||
class AlbumInfoCard extends HookConsumerWidget {
|
class AlbumInfoCard extends HookConsumerWidget {
|
||||||
final Uint8List? imageData;
|
final Uint8List? imageData;
|
||||||
final AssetPathEntity albumInfo;
|
final AvailableAlbum albumInfo;
|
||||||
|
|
||||||
const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo})
|
const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -24,6 +24,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
||||||
final bool isExcluded =
|
final bool isExcluded =
|
||||||
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||||
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
ColorFilter selectedFilter = ColorFilter.mode(
|
ColorFilter selectedFilter = ColorFilter.mode(
|
||||||
Theme.of(context).primaryColor.withAlpha(100),
|
Theme.of(context).primaryColor.withAlpha(100),
|
||||||
@@ -39,11 +40,11 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
return Chip(
|
return Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
label: const Text(
|
label: Text(
|
||||||
"album_info_card_backup_album_included",
|
"album_info_card_backup_album_included",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.white,
|
color: isDarkTheme ? Colors.black : Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
@@ -53,11 +54,11 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
return Chip(
|
return Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
label: const Text(
|
label: Text(
|
||||||
"album_info_card_backup_album_excluded",
|
"album_info_card_backup_album_excluded",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.white,
|
color: isDarkTheme ? Colors.black : Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
@@ -122,7 +123,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (albumInfo.id == 'isAll') {
|
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'Cannot exclude album contains all assets',
|
msg: 'Cannot exclude album contains all assets',
|
||||||
@@ -141,8 +142,10 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
margin: const EdgeInsets.all(1),
|
margin: const EdgeInsets.all(1),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12), // if you need this
|
borderRadius: BorderRadius.circular(12), // if you need this
|
||||||
side: const BorderSide(
|
side: BorderSide(
|
||||||
color: Color(0xFFC9C9C9),
|
color: isDarkTheme
|
||||||
|
? const Color.fromARGB(255, 37, 35, 35)
|
||||||
|
: const Color(0xFFC9C9C9),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -219,8 +222,9 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context)
|
AutoRouter.of(context).push(
|
||||||
.push(AlbumPreviewRoute(album: albumInfo));
|
AlbumPreviewRoute(album: albumInfo.albumEntity),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.image_outlined,
|
Icons.image_outlined,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class BackupInfoCard extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
style: const TextStyle(color: Color(0xFF808080), fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: Column(
|
trailing: Column(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.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/modules/backup/ui/album_info_card.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
@@ -16,6 +17,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
final availableAlbums = ref.watch(backupProvider).availableAlbums;
|
final availableAlbums = ref.watch(backupProvider).availableAlbums;
|
||||||
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@@ -46,7 +48,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
: const EdgeInsets.all(0),
|
: const EdgeInsets.all(0),
|
||||||
child: AlbumInfoCard(
|
child: AlbumInfoCard(
|
||||||
imageData: thumbnailData,
|
imageData: thumbnailData,
|
||||||
albumInfo: availableAlbums[index].albumEntity,
|
albumInfo: availableAlbums[index],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -81,14 +83,16 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.white,
|
color: Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Colors.black
|
||||||
|
: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
deleteIconColor: Colors.white,
|
deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
|
||||||
deleteIcon: const Icon(
|
deleteIcon: const Icon(
|
||||||
Icons.cancel_rounded,
|
Icons.cancel_rounded,
|
||||||
size: 15,
|
size: 15,
|
||||||
@@ -119,14 +123,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.white,
|
color: isDarkTheme ? Colors.black : immichBackgroundColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.red[300],
|
backgroundColor: Colors.red[300],
|
||||||
deleteIconColor: Colors.white,
|
deleteIconColor:
|
||||||
|
isDarkTheme ? Colors.black : immichBackgroundColor,
|
||||||
deleteIcon: const Icon(
|
deleteIcon: const Icon(
|
||||||
Icons.cancel_rounded,
|
Icons.cancel_rounded,
|
||||||
size: 15,
|
size: 15,
|
||||||
@@ -154,11 +159,16 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
physics: const ClampingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
vertical: 8.0,
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"backup_album_selection_page_selection_info",
|
"backup_album_selection_page_selection_info",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
// Selected Album Chips
|
// Selected Album Chips
|
||||||
@@ -178,9 +188,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
child: Card(
|
child: Card(
|
||||||
margin: const EdgeInsets.all(0),
|
margin: const EdgeInsets.all(0),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5), // if you need this
|
borderRadius: BorderRadius.circular(5),
|
||||||
side: const BorderSide(
|
side: BorderSide(
|
||||||
color: Color.fromARGB(255, 235, 235, 235),
|
color: isDarkTheme
|
||||||
|
? const Color.fromARGB(255, 0, 0, 0)
|
||||||
|
: const Color.fromARGB(255, 235, 235, 235),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -190,12 +202,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
title: Text(
|
title: const Text(
|
||||||
"backup_album_selection_page_total_assets",
|
"backup_album_selection_page_total_assets",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[700],
|
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
@@ -257,11 +268,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: ListBody(
|
child: ListBody(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
const Text(
|
||||||
'backup_album_selection_page_assets_scatter',
|
'backup_album_selection_page_assets_scatter',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[700],
|
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -12,6 +14,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
|
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
|
||||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class BackupControllerPage extends HookConsumerWidget {
|
class BackupControllerPage extends HookConsumerWidget {
|
||||||
const BackupControllerPage({Key? key}) : super(key: key);
|
const BackupControllerPage({Key? key}) : super(key: key);
|
||||||
@@ -20,9 +23,12 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
BackUpState backupState = ref.watch(backupProvider);
|
BackUpState backupState = ref.watch(backupProvider);
|
||||||
AuthenticationState authenticationState = ref.watch(authenticationProvider);
|
AuthenticationState authenticationState = ref.watch(authenticationProvider);
|
||||||
|
bool hasExclusiveAccess =
|
||||||
|
backupState.backupProgress != BackUpProgressEnum.inBackground;
|
||||||
bool shouldBackup = backupState.allUniqueAssets.length -
|
bool shouldBackup = backupState.allUniqueAssets.length -
|
||||||
backupState.selectedAlbumsBackupAssetsIds.length ==
|
backupState.selectedAlbumsBackupAssetsIds.length ==
|
||||||
0
|
0 ||
|
||||||
|
!hasExclusiveAccess
|
||||||
? false
|
? false
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
@@ -82,7 +88,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ListTile _buildBackupController() {
|
ListTile _buildAutoBackupController() {
|
||||||
var backUpOption = authenticationState.deviceInfo.isAutoBackup
|
var backUpOption = authenticationState.deviceInfo.isAutoBackup
|
||||||
? "backup_controller_page_status_on".tr()
|
? "backup_controller_page_status_on".tr()
|
||||||
: "backup_controller_page_status_off".tr();
|
: "backup_controller_page_status_off".tr();
|
||||||
@@ -114,13 +120,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
).tr(),
|
).tr(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: OutlinedButton(
|
child: ElevatedButton(
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
side: const BorderSide(
|
|
||||||
width: 1,
|
|
||||||
color: Color.fromARGB(255, 220, 220, 220),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (isAutoBackup) {
|
if (isAutoBackup) {
|
||||||
ref
|
ref
|
||||||
@@ -134,7 +134,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
backupBtnText,
|
backupBtnText,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -144,6 +147,142 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showErrorToUser(String msg) {
|
||||||
|
final snackBar = SnackBar(
|
||||||
|
content: Text(
|
||||||
|
msg.tr(),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showBatteryOptimizationInfoToUser() {
|
||||||
|
final buttonTextColor = Theme.of(context).primaryColor;
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text(
|
||||||
|
'backup_controller_page_background_battery_info_title',
|
||||||
|
).tr(),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: const Text(
|
||||||
|
'backup_controller_page_background_battery_info_message',
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () => launchUrl(
|
||||||
|
Uri.parse('https://dontkillmyapp.com'),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"backup_controller_page_background_battery_info_link",
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text(
|
||||||
|
'backup_controller_page_background_battery_info_ok',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
|
).tr(),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ListTile _buildBackgroundBackupController() {
|
||||||
|
final bool isBackgroundEnabled = backupState.backgroundBackup;
|
||||||
|
final bool isWifiRequired = backupState.backupRequireWifi;
|
||||||
|
final bool isChargingRequired = backupState.backupRequireCharging;
|
||||||
|
final Color activeColor = Theme.of(context).primaryColor;
|
||||||
|
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)
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSelectedAlbumName() {
|
Widget _buildSelectedAlbumName() {
|
||||||
var text = "backup_controller_page_backup_selected".tr();
|
var text = "backup_controller_page_backup_selected".tr();
|
||||||
var albums = ref.watch(backupProvider).selectedBackupAlbums;
|
var albums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
@@ -232,33 +371,27 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
"backup_controller_page_to_backup",
|
"backup_controller_page_to_backup",
|
||||||
style: TextStyle(color: Color(0xFF808080), fontSize: 12),
|
style: TextStyle(fontSize: 12),
|
||||||
).tr(),
|
).tr(),
|
||||||
_buildSelectedAlbumName(),
|
_buildSelectedAlbumName(),
|
||||||
_buildExcludedAlbumName()
|
_buildExcludedAlbumName()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: OutlinedButton(
|
trailing: ElevatedButton(
|
||||||
style: OutlinedButton.styleFrom(
|
onPressed: hasExclusiveAccess
|
||||||
enableFeedback: true,
|
? () {
|
||||||
side: const BorderSide(
|
AutoRouter.of(context)
|
||||||
width: 1,
|
.push(const BackupAlbumSelectionRoute());
|
||||||
color: Color.fromARGB(255, 220, 220, 220),
|
}
|
||||||
|
: null,
|
||||||
|
child: const Text(
|
||||||
|
"backup_controller_page_select",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
onPressed: () {
|
|
||||||
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 16.0,
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
"backup_controller_page_select",
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -324,14 +457,14 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Table(
|
child: Table(
|
||||||
border: TableBorder.all(
|
border: TableBorder.all(
|
||||||
color: Colors.black12,
|
color: Theme.of(context).primaryColorLight,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TableRow(
|
TableRow(
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.grey[100],
|
// color: Colors.grey[100],
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TableCell(
|
TableCell(
|
||||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
@@ -355,9 +488,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
TableRow(
|
TableRow(
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.grey[200],
|
// color: Colors.grey[200],
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TableCell(
|
TableCell(
|
||||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
@@ -384,9 +517,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
TableRow(
|
TableRow(
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.grey[100],
|
// color: Colors.grey[100],
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TableCell(
|
TableCell(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -412,7 +545,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
void startBackup() {
|
void startBackup() {
|
||||||
ref.watch(errorBackupListProvider.notifier).empty();
|
ref.watch(errorBackupListProvider.notifier).empty();
|
||||||
ref.watch(backupProvider.notifier).startBackupProcess();
|
if (ref.watch(backupProvider).backupProgress !=
|
||||||
|
BackUpProgressEnum.inBackground) {
|
||||||
|
ref.watch(backupProvider.notifier).startBackupProcess();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -445,6 +581,27 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
hasExclusiveAccess
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(5), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.black12,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
borderOnForeground: false,
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
"Background backup is currently running, some actions are disabled",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
_buildFolderSelectionTile(),
|
_buildFolderSelectionTile(),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "backup_controller_page_total".tr(),
|
title: "backup_controller_page_total".tr(),
|
||||||
@@ -463,7 +620,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
_buildBackupController(),
|
_buildAutoBackupController(),
|
||||||
|
if (Platform.isAndroid) const Divider(),
|
||||||
|
if (Platform.isAndroid) _buildBackgroundBackupController(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
_buildStorageInformation(),
|
_buildStorageInformation(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@@ -477,9 +636,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
? ElevatedButton(
|
? ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: Colors.red[300],
|
foregroundColor: Colors.grey[50],
|
||||||
onPrimary: Colors.grey[50],
|
backgroundColor: Colors.red[300],
|
||||||
padding: const EdgeInsets.all(14),
|
// padding: const EdgeInsets.all(14),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
@@ -493,11 +652,6 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
).tr(),
|
).tr(),
|
||||||
)
|
)
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
primary: Theme.of(context).primaryColor,
|
|
||||||
onPrimary: Colors.grey[50],
|
|
||||||
padding: const EdgeInsets.all(14),
|
|
||||||
),
|
|
||||||
onPressed: shouldBackup ? startBackup : null,
|
onPressed: shouldBackup ? startBackup : null,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"backup_controller_page_start_backup",
|
"backup_controller_page_start_backup",
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
|
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
||||||
HomePageStateNotifier()
|
|
||||||
|
final ShareService _shareService;
|
||||||
|
|
||||||
|
HomePageStateNotifier(this._shareService)
|
||||||
: super(
|
: super(
|
||||||
HomePageState(
|
HomePageState(
|
||||||
isMultiSelectEnable: false,
|
isMultiSelectEnable: false,
|
||||||
@@ -64,9 +71,22 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
|||||||
|
|
||||||
state = state.copyWith(selectedItems: currentList);
|
state = state.copyWith(selectedItems: currentList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void shareAssets(List<AssetResponseDto> assets, BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext buildContext) {
|
||||||
|
_shareService
|
||||||
|
.shareAssets(assets)
|
||||||
|
.then((_) => Navigator.of(buildContext).pop());
|
||||||
|
return const ShareDialog();
|
||||||
|
},
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final homePageStateProvider =
|
final homePageStateProvider =
|
||||||
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
|
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
|
||||||
((ref) => HomePageStateNotifier()),
|
((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
|
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
|
||||||
|
|
||||||
class ControlBottomAppBar extends StatelessWidget {
|
class ControlBottomAppBar extends ConsumerWidget {
|
||||||
const ControlBottomAppBar({Key? key}) : super(key: key);
|
const ControlBottomAppBar({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -15,17 +17,17 @@ class ControlBottomAppBar extends StatelessWidget {
|
|||||||
height: MediaQuery.of(context).size.height * 0.15,
|
height: MediaQuery.of(context).size.height * 0.15,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(15),
|
topLeft: Radius.circular(8),
|
||||||
topRight: Radius.circular(15),
|
topRight: Radius.circular(8),
|
||||||
),
|
),
|
||||||
color: Colors.grey[300]?.withOpacity(0.98),
|
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.95),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.delete_forever_rounded,
|
iconData: Icons.delete_forever_rounded,
|
||||||
@@ -39,6 +41,20 @@ class ControlBottomAppBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ControlBoxButton(
|
||||||
|
iconData: Icons.share,
|
||||||
|
label: "control_bottom_app_bar_share".tr(),
|
||||||
|
onPressed: () {
|
||||||
|
final homePageState = ref.watch(homePageStateProvider);
|
||||||
|
ref.watch(homePageStateProvider.notifier).shareAssets(
|
||||||
|
homePageState.selectedItems.toList(),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.disableMultiSelect();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -67,7 +83,7 @@ class ControlBoxButton extends StatelessWidget {
|
|||||||
width: 60,
|
width: 60,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ class DailyTitleText extends ConsumerWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.black87,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|||||||
@@ -14,32 +14,22 @@ class DisableMultiSelectButton extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 0,
|
top: 10,
|
||||||
left: 0,
|
left: 0,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 46),
|
padding: const EdgeInsets.only(left: 16.0, top: 46),
|
||||||
child: Material(
|
child: Padding(
|
||||||
elevation: 20,
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
borderRadius: BorderRadius.circular(35),
|
child: ElevatedButton.icon(
|
||||||
child: Container(
|
onPressed: () {
|
||||||
decoration: BoxDecoration(
|
onPressed();
|
||||||
borderRadius: BorderRadius.circular(35),
|
},
|
||||||
color: Colors.grey[100],
|
icon: const Icon(Icons.close_rounded),
|
||||||
),
|
label: Text(
|
||||||
child: Padding(
|
'$selectedItemCount',
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
style: const TextStyle(
|
||||||
child: TextButton.icon(
|
fontWeight: FontWeight.w600,
|
||||||
onPressed: () {
|
fontSize: 18,
|
||||||
onPressed();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.close_rounded),
|
|
||||||
label: Text(
|
|
||||||
'$selectedItemCount',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 18,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
|
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -7,11 +8,17 @@ import 'package:openapi/api.dart';
|
|||||||
class ImageGrid extends ConsumerWidget {
|
class ImageGrid extends ConsumerWidget {
|
||||||
final List<AssetResponseDto> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
final List<AssetResponseDto> sortedAssetGroup;
|
final List<AssetResponseDto> sortedAssetGroup;
|
||||||
|
final int tilesPerRow;
|
||||||
|
final bool showStorageIndicator;
|
||||||
|
final BaseCacheManager? cacheManager;
|
||||||
|
|
||||||
ImageGrid({
|
ImageGrid({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.assetGroup,
|
required this.assetGroup,
|
||||||
required this.sortedAssetGroup,
|
required this.sortedAssetGroup,
|
||||||
|
this.cacheManager,
|
||||||
|
this.tilesPerRow = 4,
|
||||||
|
this.showStorageIndicator = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
List<AssetResponseDto> imageSortedList = [];
|
List<AssetResponseDto> imageSortedList = [];
|
||||||
@@ -19,8 +26,8 @@ class ImageGrid extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return SliverGrid(
|
return SliverGrid(
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 4,
|
crossAxisCount: tilesPerRow,
|
||||||
crossAxisSpacing: 5.0,
|
crossAxisSpacing: 5.0,
|
||||||
mainAxisSpacing: 5,
|
mainAxisSpacing: 5,
|
||||||
),
|
),
|
||||||
@@ -32,8 +39,10 @@ class ImageGrid extends ConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ThumbnailImage(
|
ThumbnailImage(
|
||||||
|
cacheManager: cacheManager,
|
||||||
asset: assetGroup[index],
|
asset: assetGroup[index],
|
||||||
assetList: sortedAssetGroup,
|
assetList: sortedAssetGroup,
|
||||||
|
showStorageIndicator: showStorageIndicator,
|
||||||
),
|
),
|
||||||
if (assetType != AssetTypeEnum.IMAGE)
|
if (assetType != AssetTypeEnum.IMAGE)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final BackUpState backupState = ref.watch(backupProvider);
|
final BackUpState backupState = ref.watch(backupProvider);
|
||||||
bool isEnableAutoBackup =
|
bool isEnableAutoBackup = backupState.backgroundBackup ||
|
||||||
ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
||||||
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
floating: true,
|
floating: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
snap: false,
|
snap: false,
|
||||||
|
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||||
),
|
),
|
||||||
@@ -41,9 +42,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
top: 5,
|
top: 5,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
Icons.face_outlined,
|
Icons.face_outlined,
|
||||||
size: 30,
|
size: 30,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Scaffold.of(context).openDrawer();
|
Scaffold.of(context).openDrawer();
|
||||||
@@ -57,7 +59,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => Scaffold.of(context).openDrawer(),
|
onTap: () => Scaffold.of(context).openDrawer(),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.grey[200],
|
// color: Colors.grey[200],
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
@@ -77,13 +79,12 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: Text(
|
title: const Text(
|
||||||
'IMMICH',
|
'IMMICH',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'SnowburstOne',
|
fontFamily: 'SnowburstOne',
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -109,17 +110,24 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
iconSize: 30,
|
iconSize: 30,
|
||||||
icon: isEnableAutoBackup
|
icon: isEnableAutoBackup
|
||||||
? const Icon(Icons.backup_rounded)
|
? Icon(
|
||||||
|
Icons.backup_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
)
|
||||||
: Badge(
|
: Badge(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
elevation: 2,
|
elevation: 3,
|
||||||
position: BadgePosition.bottomEnd(bottom: -4, end: -4),
|
position: BadgePosition.bottomEnd(bottom: -4, end: -4),
|
||||||
badgeColor: Colors.white,
|
badgeColor: Colors.white,
|
||||||
badgeContent: const Icon(
|
badgeContent: const Icon(
|
||||||
Icons.cloud_off_rounded,
|
Icons.cloud_off_rounded,
|
||||||
size: 8,
|
size: 8,
|
||||||
|
color: Colors.indigo,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.backup_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.backup_rounded),
|
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var onPop = await AutoRouter.of(context)
|
var onPop = await AutoRouter.of(context)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class MonthlyTitleText extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).textTheme.headline1?.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,303 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
class ProfileDrawer extends HookConsumerWidget {
|
|
||||||
const ProfileDrawer({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
|
||||||
AuthenticationState authState = ref.watch(authenticationProvider);
|
|
||||||
ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
|
||||||
final uploadProfileImageStatus =
|
|
||||||
ref.watch(uploadProfileImageProvider).status;
|
|
||||||
final appInfo = useState({});
|
|
||||||
var dummmy = Random().nextInt(1024);
|
|
||||||
|
|
||||||
_getPackageInfo() async {
|
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
|
||||||
|
|
||||||
appInfo.value = {
|
|
||||||
"version": packageInfo.version,
|
|
||||||
"buildNumber": packageInfo.buildNumber,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildUserProfileImage() {
|
|
||||||
if (authState.profileImagePath.isEmpty) {
|
|
||||||
return const CircleAvatar(
|
|
||||||
radius: 35,
|
|
||||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
|
||||||
if (authState.profileImagePath.isNotEmpty) {
|
|
||||||
return CircleAvatar(
|
|
||||||
radius: 35,
|
|
||||||
backgroundImage: NetworkImage(
|
|
||||||
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const CircleAvatar(
|
|
||||||
radius: 35,
|
|
||||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
|
||||||
return CircleAvatar(
|
|
||||||
radius: 35,
|
|
||||||
backgroundImage: NetworkImage(
|
|
||||||
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
|
|
||||||
return const CircleAvatar(
|
|
||||||
radius: 35,
|
|
||||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
|
|
||||||
return const ImmichLoadingIndicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
_pickUserProfileImage() async {
|
|
||||||
final XFile? image = await ImagePicker().pickImage(
|
|
||||||
source: ImageSource.gallery,
|
|
||||||
maxHeight: 1024,
|
|
||||||
maxWidth: 1024,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (image != null) {
|
|
||||||
var success =
|
|
||||||
await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
|
|
||||||
ref.read(uploadProfileImageProvider).profileImagePath,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
_getPackageInfo();
|
|
||||||
_buildUserProfileImage();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
return Drawer(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
ListView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
children: [
|
|
||||||
DrawerHeader(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color.fromARGB(255, 216, 219, 238),
|
|
||||||
Color.fromARGB(255, 226, 230, 231)
|
|
||||||
],
|
|
||||||
begin: Alignment.centerRight,
|
|
||||||
end: Alignment.centerLeft,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
_buildUserProfileImage(),
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
right: -5,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: _pickUserProfileImage,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.grey[50],
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(5.0),
|
|
||||||
child: Icon(
|
|
||||||
Icons.edit,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${authState.firstName} ${authState.lastName}",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
authState.userEmail,
|
|
||||||
style: TextStyle(color: Colors.grey[800], fontSize: 12),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
tileColor: Colors.grey[100],
|
|
||||||
leading: const Icon(
|
|
||||||
Icons.logout_rounded,
|
|
||||||
color: Colors.black54,
|
|
||||||
),
|
|
||||||
title: const Text(
|
|
||||||
"profile_drawer_sign_out",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.black54,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
onTap: () async {
|
|
||||||
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).popUntilRoot();
|
|
||||||
AutoRouter.of(context).replace(const LoginRoute());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: Colors.grey[100],
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(5), // if you need this
|
|
||||||
side: const BorderSide(
|
|
||||||
color: Color.fromARGB(101, 201, 201, 201),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
serverInfoState.isVersionMismatch
|
|
||||||
? serverInfoState.versionMismatchErrorMessage
|
|
||||||
: "profile_drawer_client_server_up_to_date".tr(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"App Version",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey[500],
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey[500],
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Server Version",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey[500],
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey[500],
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
|
||||||
|
class ProfileDrawer extends HookConsumerWidget {
|
||||||
|
const ProfileDrawer({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
_buildSignoutButton() {
|
||||||
|
return ListTile(
|
||||||
|
horizontalTitleGap: 0,
|
||||||
|
leading: SizedBox(
|
||||||
|
height: double.infinity,
|
||||||
|
child: Icon(
|
||||||
|
Icons.logout_rounded,
|
||||||
|
color: Theme.of(context).textTheme.labelMedium?.color,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"profile_drawer_sign_out",
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
onTap: () async {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildSettingButton() {
|
||||||
|
return ListTile(
|
||||||
|
horizontalTitleGap: 0,
|
||||||
|
leading: SizedBox(
|
||||||
|
height: double.infinity,
|
||||||
|
child: Icon(
|
||||||
|
Icons.settings_rounded,
|
||||||
|
color: Theme.of(context).textTheme.labelMedium?.color,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"profile_drawer_settings",
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
onTap: () {
|
||||||
|
AutoRouter.of(context).push(const SettingsRoute());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Drawer(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
const ProfileDrawerHeader(),
|
||||||
|
_buildSettingButton(),
|
||||||
|
_buildSignoutButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const ServerInfoBox()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
|
||||||
|
class ProfileDrawerHeader extends HookConsumerWidget {
|
||||||
|
const ProfileDrawerHeader({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
|
AuthenticationState authState = ref.watch(authenticationProvider);
|
||||||
|
final uploadProfileImageStatus =
|
||||||
|
ref.watch(uploadProfileImageProvider).status;
|
||||||
|
var dummmy = Random().nextInt(1024);
|
||||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
_buildUserProfileImage() {
|
||||||
|
if (authState.profileImagePath.isEmpty) {
|
||||||
|
return const CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
||||||
|
if (authState.profileImagePath.isNotEmpty) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: NetworkImage(
|
||||||
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: NetworkImage(
|
||||||
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
|
||||||
|
return const CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
|
||||||
|
return const ImmichLoadingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
_pickUserProfileImage() async {
|
||||||
|
final XFile? image = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
maxHeight: 1024,
|
||||||
|
maxWidth: 1024,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (image != null) {
|
||||||
|
var success =
|
||||||
|
await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
|
||||||
|
ref.read(uploadProfileImageProvider).profileImagePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
_buildUserProfileImage();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return DrawerHeader(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: isDarkMode
|
||||||
|
? [
|
||||||
|
const Color.fromARGB(255, 22, 25, 48),
|
||||||
|
const Color.fromARGB(255, 13, 13, 13),
|
||||||
|
const Color.fromARGB(255, 0, 0, 0),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
const Color.fromARGB(255, 216, 219, 238),
|
||||||
|
const Color.fromARGB(255, 242, 242, 242),
|
||||||
|
Colors.white,
|
||||||
|
],
|
||||||
|
begin: Alignment.centerRight,
|
||||||
|
end: Alignment.centerLeft,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
_buildUserProfileImage(),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: -5,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _pickUserProfileImage,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
elevation: 3,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(5.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.edit,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${authState.firstName} ${authState.lastName}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
authState.userEmail,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user