Compare commits

..

131 Commits

Author SHA1 Message Date
Neeraj Gupta
3950d51b86 [auth] Minor fixes for custom order (#4319)
## Description

## Tests
2024-12-05 16:56:08 +05:30
Neeraj Gupta
792ffbbdac [auth] Bump version 4.1.4 2024-12-05 16:52:13 +05:30
Ashil
7c00863663 [mob][photos] Fix clipping of TextInputWidget (#4318)
## Description

#### Before
Text is clipped more towards the right end
<img width="312" alt="Screenshot 2024-12-05 at 4 18 49 PM"
src="https://github.com/user-attachments/assets/56577526-defb-4c03-a31c-03a34283c886">


#### After

<img width="312" alt="Screenshot 2024-12-05 at 4 17 36 PM"
src="https://github.com/user-attachments/assets/add18d28-6d75-4cf0-9c6d-1924f70b699d">
2024-12-05 16:33:42 +05:30
ashilkn
1cdaf7e023 [mob][photos] Minor UI improvements to TextInputWidget 2024-12-05 16:14:10 +05:30
ashilkn
98c604a73e [mob][photos] Fix clipping of TextInputWidget toward the right end when there is no suffix icon 2024-12-05 16:08:42 +05:30
Neeraj Gupta
aa4e1b8165 [auth] Show edit icon for custom order 2024-12-05 15:58:15 +05:30
Neeraj Gupta
25548e8850 [auth] Change copy 2024-12-05 15:53:42 +05:30
Neeraj Gupta
1e95c6833a [auth] Go to change order on custom is already selected 2024-12-05 15:51:03 +05:30
Neeraj Gupta
a78cd0e70f [auth] Copy change 2024-12-05 15:43:45 +05:30
Ashil
ab80128690 [mob][photos] Remove 'Descriptions' section from search tab (#4317) 2024-12-05 15:20:34 +05:30
ashilkn
df7071a130 [mob][photos] Remove unused extensions on String 2024-12-05 15:17:37 +05:30
ashilkn
ceeac4222e [mob][photos] Remove 'Descriptions' section from search tab 2024-12-05 15:12:03 +05:30
Neeraj Gupta
a92e74d65c [auth] Fix search for fav enteries 2024-12-05 13:15:32 +05:30
Neeraj Gupta
239e6d3131 [auth] Keep favorite enteries on top in all cases except custom sort (#4316)
## Description

## Tests
2024-12-05 12:06:45 +05:30
Neeraj Gupta
c7b031272a [auth] Bump version 4.1.3 2024-12-05 11:58:09 +05:30
Neeraj Gupta
6d9dbcb952 [auth] Keep fav enteries at top for non-custom order 2024-12-05 11:57:15 +05:30
Neeraj Gupta
77f30b38af [auth] Add padding for fav icon 2024-12-05 11:48:01 +05:30
Neeraj Gupta
00f13f585f [auth] Use text as lable for favorite 2024-12-05 11:47:03 +05:30
Neeraj Gupta
ef39170628 [mob][photos] Fix state issue when reloading avatar (#4311) (#4312)
## Description

- Fixed by adding a key to the PersonGridItem, that way it gets
refreshed when the person is changed

(related issue : #4311)

## Tests
2024-12-05 10:42:15 +05:30
Neeraj Gupta
e5a1624540 [auth] Fix sorting & bump version (#4305)
## Description
Previously sorting was not updating the indexed correctly. Have tested
the changes locally, and verified that correct order is being
maintained. Also, reduced redundant update by passing correctly filtered
list while sorting (only non-trashed and non-error codes)
## Tests
2024-12-05 09:37:31 +05:30
Alexis L
918d03aae9 [mob][photos] Fix state issue when reloading avatar (#4311)
- Fixed by adding a key to the PersonGridItem, that way it gets refreshed when the person is changed
2024-12-04 18:50:13 +01:00
Neeraj Gupta
406ec9c2e4 [auth] Bump version 2024-12-04 16:05:52 +05:30
Vasilis Toumpelis
e9a6af4a29 [auth] Add SpaceHey and Wolvesville icons (#4291)
https://spacehey.com
https://wolvesville.com
2024-12-04 16:01:42 +05:30
Neeraj Gupta
d947dd02cc [auth] Fix custom sort 2024-12-04 15:56:18 +05:30
Neeraj Gupta
7eeeafa03e [auth] Fix sorting + add option to sort by recently and most frequently used (#4304)
## Description

## Tests
2024-12-04 15:03:23 +05:30
Neeraj Gupta
5296665451 [auth] Handle null value 2024-12-04 14:45:52 +05:30
Neeraj Gupta
0814f048a0 [auth] Lint fix 2024-12-04 14:22:12 +05:30
Neeraj Gupta
9a50915678 [auth] Track tapCount & lastUsed in e2ee manner 2024-12-04 14:16:45 +05:30
Neeraj Gupta
c99a465c85 [auth] Show sorting menu on home screen 2024-12-04 13:59:52 +05:30
Neeraj Gupta
1644b1cd89 [auth] Show sort option on home screen 2024-12-04 13:42:22 +05:30
Neeraj Gupta
aa154189a0 [auth] Rename pin/unpin to fav/unfav (#4302)
## Description

## Tests
2024-12-04 13:08:05 +05:30
Neeraj Gupta
4accc796e6 [auth] Fix UI for fav chip on top 2024-12-04 12:29:28 +05:30
Neeraj Gupta
94b09acaaa [auth] Rename pin/unpin to fav/unfav 2024-12-04 12:17:13 +05:30
Neeraj Gupta
bc72872f26 [mob][photos] Fix dialog that is supposed to be shown when turning off downloads coming up only when turning on downloads in manage link screen (#4284) 2024-12-04 11:13:17 +05:30
Neeraj Gupta
bdf31e99e2 [auth] Allow only lower_case letter and underscore in custom icon name (#4301)
## Description

## Tests
2024-12-04 10:21:51 +05:30
Neeraj Gupta
1f29efb060 [auth] Fix case issue for git 2024-12-04 10:19:44 +05:30
Neeraj Gupta
689045d06b [auth] Fix case issue for git 2024-12-04 10:18:20 +05:30
Neeraj Gupta
e3a4f419f4 [auth] Move custom icon validation on top 2024-12-04 10:15:21 +05:30
Neeraj Gupta
c4970e9df8 [auth] Fix icon case for git 2024-12-04 10:14:50 +05:30
Neeraj Gupta
2562fd05ae [auth] Rename enom icon 2024-12-04 09:54:17 +05:30
Neeraj Gupta
095c22b565 [auth] Rename existing icons to match lint rule 2024-12-04 09:50:00 +05:30
Neeraj Gupta
71fcce5f01 [auth] Add Lint validation for custom icon name 2024-12-04 09:48:07 +05:30
Ashil
7b002c857e [mob][photos] Fix UX issue with video controls when viewing a video memory (#4294) 2024-12-03 21:35:50 +05:30
ashilkn
a905a1fa44 [mob][photos] Fix UX issue with video controls when viewing a video memory 2024-12-03 21:33:18 +05:30
Manav Rathi
129d110543 [web] Minor refactoring - Move types closer to use (#4290) 2024-12-03 14:27:52 +05:30
Manav Rathi
6160446335 Inline 2024-12-03 14:19:32 +05:30
Manav Rathi
1457440c9f Inline 2024-12-03 14:16:09 +05:30
Manav Rathi
e0fae2e499 Move 2024-12-03 14:07:56 +05:30
Manav Rathi
4ea63940ef [web] auth / fix iteration on Safari (#4289)
.map is not defined on an iterator
2024-12-03 11:47:29 +05:30
Manav Rathi
daba61b69b [web] auth / fix iteration on Safari
.map is not defined on an iterator
2024-12-03 11:43:48 +05:30
Manav Rathi
43848d5618 [auth] Remove duplicated icon (#4288)
There is both `binance.svg` and `Binance.svg`, which cause havoc with
git on macOSes due to case insensitivity. Remove one of the duplicates
(arbitrarily, which one).
2024-12-02 22:59:27 +05:30
Manav Rathi
d40da1c6f5 Remove dupes 2024-12-02 22:55:47 +05:30
Prateek Sunal
759f600ca6 [auth][migration] migrate db correctly (#4250)
## Description

Migration was failing for windows, this will fix the migration for newer
releases.

## Tests
2024-12-02 22:24:56 +05:30
ashilkn
6544f0b839 [mob][photos] Fix dialog that is supposed to be shown when turning off downloads coming up only when turning on downloads in manage link screen 2024-12-02 20:03:19 +05:30
Prateek Sunal
c1ea512355 [auth][ios] fix compatibility for ios < 13 (#4252)
## Description

fix compatibility for ios < 13 and ios >=12

Reference:
https://stackoverflow.com/questions/57907817/dyld-library-not-loaded-swiftui-when-app-runs-on-ios-12-using-availableios-13

## Tests

Co-authored-by: Prateek Sunal <prateekmedia@users.noreply.github.com>
2024-12-02 18:41:27 +05:30
0xedward
9055eee0a6 [auth] Add Bugzilla icon (#4243)
https://www.bugzilla.org/assets/img/logo-header.svg
2024-12-02 18:40:10 +05:30
0xedward
6fb2e2966c Add Pushover icon (#4242)
Logo from https://pushover.net/images/pushover-logo.svg
2024-12-02 18:39:36 +05:30
Manav Rathi
af988d5f69 [server] Remove unnecessary logging when mailing lists are not configured (#4279) 2024-12-02 18:39:06 +05:30
Nikunj Kumar Nakum
f864db50c0 [auth] Updated binance and added dropbox (#4230)
## Description
added binance and dropbox icon, last updated icons were duplicated and
there were multiple entries of a same services which are removed.
## Tests
2024-12-02 18:38:41 +05:30
Neeraj Gupta
42ac90027a [auth] Add Fidelity icon (#4195)
Include icon for Fidelity Investments found on
https://www.fidelity.com/bin-public/600_Fidelity_Com_English/images/homepage/icon_fidelity_pyramid.svg
2024-12-02 18:37:27 +05:30
Neeraj Gupta
b775981e3f [auth] New translations (#4278)
New translations from
[Crowdin](https://crowdin.com/project/ente-authenticator-app)
2024-12-02 18:36:27 +05:30
Manav Rathi
43e458afdd [web] Update to latest libheif-js 1.18.2 (#4283) 2024-12-02 17:39:42 +05:30
Manav Rathi
5bcf8f9a58 Move GC pause voodoo to worker 2024-12-02 17:34:04 +05:30
Manav Rathi
bfbaa71c68 [web] Update to latest libheif-js 2024-12-02 17:28:01 +05:30
Manav Rathi
91e851523d [web] Auth: Paginate API responses, Filter out trashed, Pinned at top (#4281) 2024-12-02 14:24:16 +05:30
Manav Rathi
ea0bd31480 Pinned at the top 2024-12-02 14:16:55 +05:30
Manav Rathi
0ef71268fe Filter out trashed 2024-12-02 14:07:50 +05:30
Manav Rathi
f2c56ad843 Parse codeDisplay 2024-12-02 14:05:34 +05:30
Manav Rathi
1b38bdacfa Tweak 2024-12-02 13:51:17 +05:30
Manav Rathi
58ecbd5311 Pagination 2024-12-02 13:48:53 +05:30
Manav Rathi
2fc5594e8d [web] Minor cleanup (#4280) 2024-12-02 13:27:40 +05:30
Manav Rathi
e664b7ac30 sp 2024-12-02 11:58:19 +05:30
Manav Rathi
269b911cbf Prune things not related to auth 2024-12-02 10:49:09 +05:30
Manav Rathi
6d3f177d91 Allow map tiles to be loaded 2024-12-02 10:47:01 +05:30
Manav Rathi
ac15a502b9 [server] Remove unnecessary logging when mailing lists are not configured 2024-12-02 10:08:01 +05:30
Manav Rathi
7ddc8a6593 Remove unused file
Last use was stopped in c2191515ee
2024-12-02 09:44:39 +05:30
Manav Rathi
d4cebca274 Empty 2024-12-02 09:27:52 +05:30
Manav Rathi
bccb162f08 [web] New translations (#4275)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2024-12-02 09:03:01 +05:30
Manav Rathi
56cb724dce Dashes in ENV var names not supported by docker compose (#4269)
When I was trying to set this up, I was getting errors from `docker
compose`:

```
failed to read /var/hosts/ente/.credentials.env: line 11: unexpected character "-" in variable name "ENTE_APPS_PUBLIC-ALBUMS=https://<redacted>"
```

There was, apparently, a brief period in time when `docker compose`
supported dashes in ENV var names, [but this is no longer the
case](https://github.com/docker/compose/issues/12302), hence some vars
need to be defined in a `museum.yml` and mounted into the instance.
Maybe there is some rewriting in place that allows to redefine
`museum.yml` entries with dashes in ENV vars, however I couldn't find it
(and if there isn't, there probably should be, but this change should
make the guide usable again at least).
2024-12-02 09:01:43 +05:30
Crowdin Bot
57987e604e New Crowdin translations by GitHub Action 2024-12-02 01:17:49 +00:00
Crowdin Bot
4d2d613013 New Crowdin translations by GitHub Action 2024-12-02 00:38:09 +00:00
Neeraj Gupta
ca3d795a80 [auth] Add "Favorites" Tag to Display Pinned Codes (#4266)
## Description
This PR introduces a new Favorites tag, allowing users to quickly view
all their pinned codes in one place.
2024-12-01 06:49:03 +05:30
István Szmozsánszky
45bccc907e Fix typo 2024-12-01 00:13:58 +02:00
István Szmozsánszky
d8dfc6b5cc Dashes in ENV var names not supported by docker compose
Docker compose no longer supports dashes in ENV var names, hence some vars need to be defined in museum.yml
See https://github.com/docker/compose/issues/12302 for details
2024-11-30 23:47:08 +02:00
Aman Raj
b0c2efa2bc [auth] minor fixes 2024-11-30 17:11:02 +05:30
Aman Raj
ce7e6f1518 [auth] Added a favourite tag which shows all pinned codes 2024-11-30 15:00:59 +05:30
Neeraj Gupta
03de70d5f8 [mob] Move manage device cache option from Advance to Free up space (#4265)
## Description

## Tests
2024-11-30 14:56:08 +05:30
Neeraj Gupta
5b873b794d [mob] Rename copy 2024-11-30 14:53:03 +05:30
Neeraj Gupta
de6caf9958 [mob] Move manage device storage under free up section 2024-11-30 14:48:52 +05:30
Neeraj Gupta
33227f1b71 [mob] SaveOrEditPerson: Dismiss keyboard on tapping outside (#4263)
## Description

## Tests
2024-11-30 13:08:53 +05:30
Manav Rathi
b957b817a5 [web] General refactoring, tint towards public albums - Part x/x (#4260) 2024-11-30 07:02:08 +05:30
Manav Rathi
35b58f9804 Combine 2024-11-30 06:52:38 +05:30
Manav Rathi
a02dd4859d Combine 2024-11-30 06:41:39 +05:30
Manav Rathi
352b19b860 Add more details to the doc for the internal.disable-registration (#4259) 2024-11-30 06:12:38 +05:30
Manav Rathi
79f6c78015 Mention in FAQ 2024-11-30 06:10:37 +05:30
Manav Rathi
f950870459 [server] Add more details to the doc for the internal.disable-registration 2024-11-30 06:07:48 +05:30
Manav Rathi
c5ffdfd091 Add internal.disable-registration to config (#4238)
## Description
As discussed in #2476, I added an option `internal.disable-registration`
to allow disabling the registration of new users on selfhosted
instances.
Users can still go through the registration flow, but when entering the
ott they received via mail, they get an unauthorized error.

Should this be documented in any of the self-hosting documentation?
2024-11-30 05:56:43 +05:30
Neeraj Gupta
bbbb49f401 [mob] Dismiss keyboard on tapping outside 2024-11-29 21:41:59 +05:30
Neeraj Gupta
d2335d7491 [server] Add missing nil check (#4257)
## Description

## Tests
2024-11-29 20:42:46 +05:30
Neeraj Gupta
98c2e98e16 [server] Add missing nil check 2024-11-29 20:06:47 +05:30
Vishnu Mohandas
40abd89b97 [server] Limit unverifeid sessions (#4256)
## Description

This (cosmetic?) change is to reduce the brute-force attempt on password
cracking.

It feels cosmetic because each create-session call requires an expensive
KDF steps for different password. It's also not prone to dictionary
attack because of the nonce.

Still, this change felt harmless to make, and it's hardens the security
further.

## Tests
Tested locally
2024-11-29 19:55:18 +05:30
Neeraj Gupta
d3d419e12d [server] RateLimit max sessions in an hour 2024-11-29 19:33:36 +05:30
Julian Pollinger
aed7075a13 invert if condition 2024-11-29 14:50:46 +01:00
Manav Rathi
027cfbd883 [doc] Remove deprecated exception, 1.7.7 is already released (#4255) 2024-11-29 17:26:41 +05:30
Manav Rathi
378aba5b9a [doc] Remove deprecated exception, 1.7.7 is already released 2024-11-29 17:25:02 +05:30
Manav Rathi
830cae96c3 [web] General refactoring, focus on public albums (#4254) 2024-11-29 17:24:31 +05:30
Manav Rathi
33bda1a4a1 Doc 2024-11-29 17:20:54 +05:30
Manav Rathi
52d5ab31aa Retain order 2024-11-29 17:18:16 +05:30
Manav Rathi
b47d541a09 Missing dep 2024-11-29 17:14:18 +05:30
Manav Rathi
929224a2cb Reorder 2024-11-29 17:03:38 +05:30
Manav Rathi
12fbde45b3 Propogate 2024-11-29 16:57:24 +05:30
Manav Rathi
70eb18fe3b Combine 2024-11-29 16:17:35 +05:30
Manav Rathi
30d8ebd946 Reorder 2024-11-29 15:03:39 +05:30
Manav Rathi
85673177f5 Keep JSX together 2024-11-29 15:01:36 +05:30
Manav Rathi
0dbd8a0f8c Memo context to avoid unnecessary renders 2024-11-29 14:50:33 +05:30
Manav Rathi
8e1450c6c6 Convert deprecated method 2024-11-29 14:46:39 +05:30
Neeraj Gupta
b36a2dec45 [auth][search] fix focus on search icon tap (#4249)
## Description

Fixes #[3153](https://github.com/ente-io/ente/discussions/3153) and
#[3272](https://github.com/ente-io/ente/discussions/3272)

## Tests
2024-11-29 14:45:23 +05:30
Manav Rathi
2fbc3d1b90 Tweak 2024-11-29 14:43:30 +05:30
Prateek Sunal
29adca2a99 fix: temp dir scope 2024-11-29 14:39:59 +05:30
Prateek Sunal
fa333eff76 fix: mergable changes 2024-11-29 14:33:24 +05:30
Manav Rathi
d9bd79e5ef Default 2024-11-29 14:32:47 +05:30
Prateek Sunal
378a0bf0b1 chore: bump pod 2024-11-29 14:29:35 +05:30
Prateek Sunal
d9fc9e3c76 fix: request focus on search for all platforms 2024-11-29 14:28:16 +05:30
Manav Rathi
9fffbdb6fa Don't pass an undefined anchorEl 2024-11-29 14:25:14 +05:30
Manav Rathi
8642c4abe1 Tweak 2024-11-29 13:50:35 +05:30
Manav Rathi
cf96fe1553 Tweak 2024-11-29 13:39:51 +05:30
Manav Rathi
e02b76d2d2 Prune 2024-11-29 13:32:11 +05:30
Julian Pollinger
39f2d03e74 add internal.disable-registration to config 2024-11-29 00:44:33 +01:00
Edward
9b1dd3aa64 Add Fidelity icon 2024-11-26 19:45:40 -05:00
Prateek Sunal
a2854b344c Merge remote-tracking branch 'origin/main' into win-quickfix 2024-11-23 01:57:15 +05:30
Prateek Sunal
96d9637de1 feat: win quick fix 2024-11-23 01:42:43 +05:30
138 changed files with 2506 additions and 2042 deletions

View File

@@ -22,6 +22,18 @@ jobs:
with:
submodules: recursive
- name: Verify custom icons are lowercase including optional understores, and end with .svg
run: |
find assets/custom-icons -type f -name "*.svg" | while read -r file; do
if [[ "$(basename "$file")" != "$(basename "$file" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')" ]]; then
echo "File name is not lowercase: $file"
exit 1
fi
done
- name: Verify custom icon JSON
run: cat assets/custom-icons/_data/custom-icons.json | jq empty
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
uses: subosito/flutter-action@v2
with:
@@ -32,6 +44,3 @@ jobs:
- run: flutter pub get
- run: flutter analyze --no-fatal-infos
- name: Verify custom icon JSON
run: cat assets/custom-icons/_data/custom-icons.json | jq empty

View File

@@ -1,7 +1,8 @@
{
"icons": [
{
"title": "1xBet"
"title": "1xBet",
"slug": "1x_bet"
},
{
"title": "23andme"
@@ -45,13 +46,15 @@
},
{
"title": "BaiduCloud",
"slug": "baidu_cloud",
"altNames": [
"百度云",
"baiduyun"
]
},
{
"title": "Band"
"title": "Band",
"slug": "band"
},
{
"title": "Battle.net",
@@ -63,6 +66,7 @@
},
{
"title": "BBS.NGA",
"slug": "bbs_nga",
"altNames": [
"NGA玩家社区",
"NGA社区"
@@ -76,21 +80,11 @@
},
{
"title": "Binance",
"slug": "binance_exchange",
"altNames": [
"币安"
]
},
{
"title": "Binance TR",
"slug": "binance_tr"
},
{
"title": "BinanceUS",
"slug": "binance_us",
"altNames": [
"Binance US"
]
},
{
"title": "Bitfinex"
},
@@ -123,13 +117,6 @@
{
"title": "Bitwarden"
},
{
"title": "Bloom Host",
"slug": "bloom_host",
"altNames": [
"Bloom Host Billing"
]
},
{
"title": "Blockchain",
"altNames": [
@@ -146,7 +133,8 @@
]
},
{
"title": "Bluesky"
"title": "Bluesky",
"slug": "blue_sky"
},
{
"title": "Booking",
@@ -156,16 +144,11 @@
},
{
"title": "BorgBase",
"slug": "borg_base",
"altNames": [
"borg"
]
},
{
"title": "Booking",
"altNames": [
"Booking.com"
]
},
{
"title": "Brave Creators",
"slug": "brave_creators",
@@ -175,6 +158,9 @@
"Brave Browser"
]
},
{
"title": "Bugzilla"
},
{
"title": "Bybit"
},
@@ -191,7 +177,8 @@
]
},
{
"title": "CERN"
"title": "CERN",
"slug": "cern"
},
{
"title": "ChangeNOW"
@@ -268,6 +255,9 @@
{
"title": "Doppler"
},
{
"title": "Dropbox"
},
{
"title": "dus.net",
"slug": "dusnet"
@@ -290,7 +280,8 @@
"hex": "1DB954"
},
{
"title": "enom"
"title": "enom",
"slug": "enom_v1"
},
{
"title": "Epic Games",
@@ -303,6 +294,13 @@
{
"title": "Estateguru"
},
{
"title": "Fidelity",
"slug": "fidelity",
"altNames": [
"Fidelity Investments"
]
},
{
"title": "Filen"
},
@@ -330,7 +328,7 @@
},
{
"title": "Gosuslugi",
"slug": "Gosuslugi",
"slug": "gosuslugi",
"altNames": [
"Госуслуги"
]
@@ -377,11 +375,12 @@
]
},
{
"title": "IceDrive"
"title": "IceDrive",
"slug": "ice_drive"
},
{
"title": "ID.me",
"slug": "IDme"
"slug": "id_me"
},
{
"title": "Infomaniak"
@@ -409,7 +408,8 @@
"hex": "000000"
},
{
"title": "IVPN"
"title": "IVPN",
"slug": "ivpn"
},
{
"title": "Jagex",
@@ -439,6 +439,7 @@
},
{
"title": "Ko-fi",
"slug": "ko_fi",
"altNames": [
"Ko fi",
"Kofi"
@@ -466,6 +467,7 @@
},
{
"title": "Lark",
"slug": "lark",
"altNames": [
"飞书"
]
@@ -475,7 +477,7 @@
},
{
"title": "Linux.Do",
"slug": "LINUX_DO",
"slug": "linux_do",
"altNames": [
"LINUX DO",
"LinxDo"
@@ -574,6 +576,7 @@
},
{
"title": "NeteaseMail",
"slug": "netease_mail",
"altNames": [
"网易邮箱",
"Mail.163"
@@ -643,10 +646,12 @@
"hex": "f08222"
},
{
"title": "pCloud"
"title": "pCloud",
"slug": "pcloud"
},
{
"title": "PebbleHost",
"slug": "pebble_host",
"altNames": [
"Pebble Host"
]
@@ -684,6 +689,10 @@
{
"title": "Proxmox"
},
{
"title": "Pushover",
"slug": "pushover"
},
{
"title": "qiniuyun",
"altNames": [
@@ -719,6 +728,7 @@
},
{
"title": "Restream",
"slug": "restream",
"altNames": [
"restream.io"
]
@@ -788,6 +798,9 @@
{
"title": "Snapchat"
},
{
"title": "SpaceHey"
},
{
"title": "Standard Notes",
"slug": "standardnotes"
@@ -841,6 +854,7 @@
},
{
"title": "Terabit",
"slug": "terabit",
"altNames": [
"Terabit Hosting",
"terabit.io"
@@ -955,6 +969,9 @@
{
"title": "Wise"
},
{
"title": "Wolvesville"
},
{
"title": "WorkOS",
"altNames": [
@@ -969,6 +986,7 @@
},
{
"title": "Yandex",
"slug": "yandex",
"altNames": [
"Ya",
"Яндекс"

View File

Before

Width:  |  Height:  |  Size: 481 B

After

Width:  |  Height:  |  Size: 481 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" vector-effect="non-scaling-stroke" viewBox="0 0 500 500">
<g fill="#F0B90B">
<path d="m0 250 56.452-56.451L112.903 250l-56.451 56.451L0 250zm96.774-96.774L250 0l153.226 153.226-56.452 56.451L250 112.903l-96.774 96.774-56.452-56.451z"/>
<path d="M193.549 250 250 193.549 306.451 250 250 306.451 193.549 250z"/>
<path d="m153.226 290.323-56.452 56.451L250 500l153.226-153.226-56.452-56.451L250 387.097l-96.774-96.774zM387.097 250l56.452-56.451L500 250l-56.451 56.451L387.097 250z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 561 B

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 662 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" vector-effect="non-scaling-stroke" viewBox="0 0 500 500">
<g fill="#0061fe">
<path d="M125.022 38.07.055 117.545l124.967 79.473 124.988-79.473 124.968 79.473 124.967-79.473-124.967-79.473-124.968 79.473-124.988-79.473z"/>
<path d="M125.022 355.967.055 276.492l124.967-79.475 124.988 79.475-124.988 79.475zm124.988-79.475 124.968-79.475 124.967 79.475-124.967 79.475-124.968-79.475zm0 185.438-124.988-79.474 124.988-79.473 124.968 79.473L250.01 461.93z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 531 B

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,11 @@
<svg width="49" height="48" viewBox="0 0 49 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_933_28141)">
<path d="M24.9434 48C38.1983 48 48.9434 37.2548 48.9434 24C48.9434 10.7452 38.1983 0 24.9434 0C11.6886 0 0.94342 10.7452 0.94342 24C0.94342 37.2548 11.6886 48 24.9434 48Z" fill="white"/>
<path d="M22.5996 27.795H29.2838L31.2396 29.0631H22.3059L18.3011 41.4571C16.0258 40.544 13.9543 39.1883 12.2067 37.4686C10.4592 35.7489 9.07035 33.6995 8.12074 31.4391C7.17113 29.1787 6.67963 26.7523 6.67472 24.3005C6.66982 21.8487 7.15161 19.4204 8.09217 17.1562C9.94318 12.7412 13.423 9.20846 17.8095 7.29094C22.196 5.37342 27.1523 5.2184 31.6501 6.85805C36.1478 8.4977 39.8416 11.8061 41.9649 16.0968C44.0882 20.3874 44.478 25.3309 43.0534 29.9013C41.9853 33.3728 39.9048 36.4463 37.0784 38.7276L32.1423 30.5031L39.0128 34.3359C39.0128 34.3359 39.0701 34.2929 39.0128 34.2499L30.0576 26.484L41.685 28.2607C41.728 28.2607 41.7423 28.2607 41.7423 28.2607L41.6707 28.1962L30.6952 24.3204L42.1005 21.5622C42.1005 21.5622 42.1005 21.5622 42.1005 21.5049L30.4229 22.2714L39.5787 15.3365C39.5787 15.3365 39.5214 15.2792 39.4999 15.3365L29.2122 20.552L34.879 10.5222C34.879 10.5222 34.879 10.4649 34.8432 10.5222L27.4355 19.1837L28.8683 7.721C28.8683 7.721 28.8253 7.67801 28.7895 7.721L25.3722 18.8326L22.3847 7.36995C22.3847 7.36995 22.3202 7.33413 22.2987 7.36995L23.1226 19.2625L16.0802 10.0923C16.0802 10.0923 16.0373 10.0923 16.0158 10.0923L21.3459 20.244L11.2731 14.749C11.2731 14.749 11.2158 14.749 11.2158 14.792L20.1781 22.1496L8.71545 20.9389C8.67247 20.9389 8.67247 20.9962 8.71545 21.0177L19.8343 24.2631L8.79426 27.5013C8.78949 27.506 8.78571 27.5115 8.78312 27.5177C8.78054 27.5238 8.77921 27.5304 8.77921 27.5371C8.77921 27.5438 8.78054 27.5504 8.78312 27.5565C8.78571 27.5627 8.78949 27.5683 8.79426 27.5729L20.1781 26.2834L11.2373 33.8416C11.2373 33.8416 11.1943 33.8846 11.2373 33.9061L11.3519 33.8631L18.5161 30.1162L20.1925 27.444H22.1053L20.7799 26.5055L25.2074 19.3986L22.5996 27.795Z" fill="#368727"/>
</g>
<defs>
<clipPath id="clip0_933_28141">
<rect width="49" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 902 B

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="600px" height="600px" viewBox="0 0 600 600" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="layer1" stroke="none" stroke-width="1">
<ellipse style="fill: rgb(36, 157, 241); fill-rule: evenodd; stroke: rgb(255, 255, 255); stroke-width: 0;" transform="matrix(-0.674571, 0.73821, -0.73821, -0.674571, 556.833239, 241.613465)" cx="216.308" cy="152.076" rx="296.855" ry="296.855"/>
<path d="M 280.949 172.514 L 355.429 162.714 L 282.909 326.374 L 282.909 326.374 C 295.649 325.394 308.142 321.067 320.389 313.394 L 320.389 313.394 L 320.389 313.394 C 332.642 305.714 343.916 296.077 354.209 284.484 L 354.209 284.484 L 354.209 284.484 C 364.496 272.884 373.396 259.981 380.909 245.774 L 380.909 245.774 L 380.909 245.774 C 388.422 231.561 393.812 217.594 397.079 203.874 L 397.079 203.874 L 397.079 203.874 C 399.039 195.381 399.939 187.214 399.779 179.374 L 399.779 179.374 L 399.779 179.374 C 399.612 171.534 397.569 164.674 393.649 158.794 L 393.649 158.794 L 393.649 158.794 C 389.729 152.914 383.766 148.177 375.759 144.584 L 375.759 144.584 L 375.759 144.584 C 367.759 140.991 356.899 139.194 343.179 139.194 L 343.179 139.194 L 343.179 139.194 C 327.172 139.194 311.409 141.807 295.889 147.034 L 295.889 147.034 L 295.889 147.034 C 280.376 152.261 266.002 159.857 252.769 169.824 L 252.769 169.824 L 252.769 169.824 C 239.542 179.784 228.029 192.197 218.229 207.064 L 218.229 207.064 L 218.229 207.064 C 208.429 221.924 201.406 238.827 197.159 257.774 L 197.159 257.774 L 197.159 257.774 C 195.526 263.981 194.546 268.961 194.219 272.714 L 194.219 272.714 L 194.219 272.714 C 193.892 276.474 193.812 279.577 193.979 282.024 L 193.979 282.024 L 193.979 282.024 C 194.139 284.477 194.462 286.357 194.949 287.664 L 194.949 287.664 L 194.949 287.664 C 195.442 288.971 195.852 290.277 196.179 291.584 L 196.179 291.584 L 196.179 291.584 C 179.519 291.584 167.349 288.234 159.669 281.534 L 159.669 281.534 L 159.669 281.534 C 151.996 274.841 150.119 263.164 154.039 246.504 L 154.039 246.504 L 154.039 246.504 C 157.959 229.191 166.862 212.694 180.749 197.014 L 180.749 197.014 L 180.749 197.014 C 194.629 181.334 211.122 167.531 230.229 155.604 L 230.229 155.604 L 230.229 155.604 C 249.342 143.684 270.249 134.214 292.949 127.194 L 292.949 127.194 L 292.949 127.194 C 315.656 120.167 337.789 116.654 359.349 116.654 L 359.349 116.654 L 359.349 116.654 C 378.296 116.654 394.219 119.347 407.119 124.734 L 407.119 124.734 L 407.119 124.734 C 420.026 130.127 430.072 137.234 437.259 146.054 L 437.259 146.054 L 437.259 146.054 C 444.446 154.874 448.936 165.164 450.729 176.924 L 450.729 176.924 L 450.729 176.924 C 452.529 188.684 451.959 200.934 449.019 213.674 L 449.019 213.674 L 449.019 213.674 C 445.426 229.027 438.646 244.464 428.679 259.984 L 428.679 259.984 L 428.679 259.984 C 418.719 275.497 406.226 289.544 391.199 302.124 L 391.199 302.124 L 391.199 302.124 C 376.172 314.697 358.939 324.904 339.499 332.744 L 339.499 332.744 L 339.499 332.744 C 320.066 340.584 299.406 344.504 277.519 344.504 L 277.519 344.504 L 275.069 344.504 L 212.839 484.154 L 142.279 484.154 L 280.949 172.514 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill: rgb(255, 255, 255); fill-rule: nonzero; white-space: pre;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1 @@
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 720 720" xml:space="preserve"><style>.st0{fill:#1d4ed8}</style><path class="st0" d="M402.8 425H705v259.9H402.8z"/><circle vector-effect="non-scaling-stroke" class="st0" cx="553.9" cy="429.9" r="151.1"/><circle vector-effect="non-scaling-stroke" class="st0" cx="553.9" cy="144.8" r="109.7"/><path d="M174.2 180.3H383v140.6c0 87.8-71.3 159.2-159.2 159.2h-49.7C86.3 480 15 412.9 15 330.2c0-82.8 71.3-149.9 159.2-149.9m-66.7 92v28.9c2.5-2.9 4.9-5.1 7.2-6.8 2.4-1.7 5-2.9 7.8-3.7 2.9-.8 5.9-1.3 9.2-1.3 5 0 9.3 1 13.2 3.1 3.8 2.1 6.8 5.1 9 9.1 1.4 2.3 2.3 4.9 2.8 7.8s.7 6.2.7 10v37.3c0 3.9-.9 6.9-2.7 8.9s-4.2 3-7.1 3c-6.4 0-9.6-4-9.6-11.9v-32.9c0-6.2-.9-11-2.8-14.4s-5.4-5-10.6-5c-3.5 0-6.6 1-9.4 3s-4.9 4.7-6.2 8.1c-1 2.9-1.6 8.1-1.6 15.5v25.8c0 3.9-.9 6.8-2.6 8.8s-4.2 3-7.2 3c-6.4 0-9.6-4-9.6-11.9v-84.5c0-4 .8-6.9 2.5-8.9s4.1-3 7.1-3q4.65 0 7.2 3 2.7 3.15 2.7 9m85.5 50.4h35.3q-.75-10.05-5.4-15c-3.1-3.3-7.2-5-12.3-5-4.9 0-8.8 1.7-12 5-3 3.5-4.9 8.5-5.6 15m84.8 48.4 1.8-4.4-24-60.4c-1.5-3.5-2.2-6-2.2-7.6 0-1.7.4-3.2 1.3-4.6s2-2.6 3.5-3.4c1.5-.9 3.1-1.3 4.7-1.3 2.8 0 4.9.9 6.4 2.7 1.4 1.8 2.7 4.3 3.8 7.7l16.5 48 15.6-44.6c1.2-3.6 2.4-6.4 3.4-8.5 1-2 2.1-3.4 3.2-4.1s2.7-1.1 4.7-1.1q2.25 0 4.2 1.2c1.4.8 2.4 1.8 3.1 3.2.7 1.3 1.1 2.7 1.1 4.2q-.3 1.35-.9 3.9c-.4 1.7-1 3.4-1.6 5.1L297 373.4c-2.2 5.9-4.3 10.4-6.4 13.8-2.1 3.3-4.9 5.9-8.3 7.7-3.5 1.8-8.1 2.7-14 2.7-5.7 0-10-.6-12.8-1.9-2.9-1.2-4.3-3.5-4.3-6.8 0-2.2.7-4 2-5.2q2.1-1.8 6-1.8c1 0 2.1.1 3.1.4 1.2.3 2.3.4 3.2.4 2.2 0 4-.3 5.3-1s2.4-1.8 3.5-3.5c1-1.5 2.2-3.9 3.5-7.1m-46.7-37.3H193q0 6.6 2.7 11.7c1.7 3.4 4 5.9 6.9 7.6s6.1 2.6 9.5 2.6c2.3 0 4.5-.3 6.4-.8s3.8-1.4 5.6-2.6 3.5-2.4 5-3.7 3.5-3.1 5.9-5.4c1-.9 2.4-1.3 4.3-1.3 2 0 3.6.5 4.9 1.6 1.2 1.1 1.9 2.6 1.9 4.6 0 1.8-.7 3.8-2.1 6.2s-3.5 4.6-6.2 6.8q-4.2 3.3-10.5 5.4c-4.2 1.4-9.1 2.1-14.5 2.1-12.5 0-22.3-3.6-29.2-10.7s-10.4-16.8-10.4-29.1c0-5.8.9-11.1 2.6-16s4.2-9.1 7.5-12.7c3.3-3.5 7.3-6.2 12.1-8.1s10.1-2.8 16-2.8c7.6 0 14.1 1.6 19.6 4.8 5.4 3.2 9.5 7.4 12.2 12.5s4.1 10.3 4.1 15.6c0 4.9-1.4 8.1-4.2 9.5-2.9 1.5-6.9 2.2-12 2.2" style="fill-rule:evenodd;clip-rule:evenodd;fill:#1d4ed8"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1 @@
<svg data-name="Layer 1" viewBox="0 0 100 100" width="100" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M36.907 4.866s-.886 17.406 2.163 25.78c-4.782 4.193-10.152 8.97-10.992 10.12-1.383 1.896-.351 10.035-5.008 13.495-2.347 1.742-7.423 5.036-11.881 7.875-4.395 2.798-8.19 5.152-8.19 5.152s1.472 1.471 3.786 3.432c3.927 3.327 10.273 8.063 15.937 9.408 2.482.589 5.448.303 8.553-.315a73 73 0 0 0 3.092-.693c6.223-1.522 12.542-3.63 16.312-2.143a4.9 4.9 0 0 1 1.662 1.063c1.966 1.935 3.105 3.853 3.76 5.494.219.547.382 1.063.506 1.54.248.95.332 1.74.354 2.292a6 6 0 0 1-.018.867s.319-.53.797-1.414c1.434-2.652 4.304-8.49 4.316-12.775 0 0 .186.098.504.299s.768.502 1.295.912c1.318 1.024 3.117 2.716 4.543 5.146a16.7 16.7 0 0 1 1.979 5.186c.535 2.672.503 5.843-.534 9.549 0 0 .228-.22.62-.627 2.738-2.857 13.431-15.019 9.24-26.795 0 0 2.258 1.105 4.638 2.925.595.456 1.197.954 1.774 1.493 1.441 1.347 2.723 2.937 3.318 4.674q.18.52.27 1.058s1.178-4.477.185-9.732a21 21 0 0 0-.435-1.84c-1.019-3.569-3.157-7.29-7.395-10.09 0 0 .61-.09 1.606-.156s2.38-.108 3.923-.012c.772.048 1.584.13 2.409.262 2.474.394 5.062 1.229 7.004 2.886 0 0-.038-.183-.125-.51-.611-2.284-3.648-11.622-13.131-14.804 0 0 8.243-2.074 12.117-7.633 0 0-3.578-.009-7.402-.763a28 28 0 0 1-2.297-.55c-1.516-.432-2.965-1.009-4.133-1.775l-.012-.031c-.207-.64-3.59-10.958-7.644-16.82a82 82 0 0 0-9.17-10.867s-6.478 9.16-7.971 17.072c0 0-1.614-.505-3.99-.363l.01-.02s-3.634-3.942-8.832-10.092-7.512-7.16-7.512-7.16z" fill="#50474d" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" paint-order="stroke fill markers"/><path d="M36.91 4.863s-1.118 21.902 3.455 28.392c4.572 6.49 12.89-11.14 12.89-11.14s-3.633-3.943-8.831-10.092-7.514-7.16-7.514-7.16" fill="#50474d"/><path d="M89.706 77.861c-.962-5.735-10.001-10.15-10.001-10.15 4.79 13.459-9.858 27.42-9.858 27.42 4.148-14.82-7.788-21.09-7.788-21.09-.016 5.712-5.112 14.19-5.112 14.19s.639-5.034-4.603-10.194c-3.797-3.735-12.908.157-21.065 1.772-3.105.617-6.071.905-8.554.316-5.664-1.345-12.009-6.081-15.937-9.409-2.314-1.96-3.786-3.432-3.786-3.432s3.796-2.353 8.19-5.152c4.459-2.839 9.535-6.133 11.882-7.875 4.657-3.46 3.624-11.598 5.007-13.493 1.385-1.897 17.458-15.771 17.458-15.771 5.937-4.396 11.697-2.495 11.697-2.495 1.493-7.912 7.97-17.071 7.97-17.071a82 82 0 0 1 9.17 10.866c4.055 5.862 7.438 16.18 7.645 16.82l.01.032c4.672 3.064 13.833 3.086 13.833 3.086-3.874 5.559-12.117 7.633-12.117 7.633C94.585 47.5 97.002 59.178 97.002 59.178c-5.178-4.42-14.941-2.98-14.941-2.98 11.37 7.511 7.645 21.663 7.645 21.663" fill="#5f5358"/><path d="M32.896 53.522s2.977-5.13 4.849-5.962 8.812-1.69 8.812-1.69-3.436 4.739-5.825 5.297c-2.388.558-7.836 2.355-7.836 2.355" fill="#efe5b6"/><path d="M11.392 67.886c-.287.572-2.023 1.429-2.21 1.58-.184.153-.24-.822-.24-.822l-1.482-.19-.306 1.254-.366 1.008c-2.314-1.96-3.786-3.432-3.786-3.432s3.796-2.353 8.19-5.152c0 0 .489 5.184.2 5.754m78.314 9.975c-.962-5.735-10.001-10.15-10.001-10.15 4.79 13.459-9.858 27.42-9.858 27.42 4.148-14.82-7.788-21.09-7.788-21.09-.016 5.712-5.112 14.19-5.112 14.19s.639-5.034-4.603-10.194c-3.797-3.735-12.908.157-21.065 1.772 0 0 10.42-6.481 18.06-6.289s8.868 4.436 8.868 4.436 2.056-6.791.157-10.31c0 0 7.45-.641 13.206 3.23 0 0-2.3-9.532-7.585-11.71 0 0 8.575-1.607 13.548.622 0 0 .966-5.154-6.411-8.438 0 0 6.675-1.915 12.723-.041 0 0-4.525-7.29-10.312-9.015 0 0 9.039-1.497 11.896-3.51a15.6 15.6 0 0 1-3.398-5.639c4.672 3.064 13.833 3.086 13.833 3.086-3.874 5.559-12.117 7.633-12.117 7.633 10.838 3.637 13.255 15.314 13.255 15.314-5.178-4.42-14.941-2.98-14.941-2.98 11.37 7.511 7.645 21.663 7.645 21.663M66.061 11.993s6.107 8.076 6.856 13.876c.75 5.801-.285 8.9-.285 8.9s-3.086-5.19-8.535-8.297c-5.45-3.107 1.964-14.48 1.964-14.48z" fill="#50474d"/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 503 B

After

Width:  |  Height:  |  Size: 503 B

View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -260,4 +260,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: b4e3a7eabb03395b66e81fc061789f61526ee6bb
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2

View File

@@ -444,6 +444,100 @@
"$(inherited)",
"@executable_path/Frameworks",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-l\"swiftCoreGraphics\"",
"-framework",
"\"AVFoundation\"",
"-framework",
"\"AVKit\"",
"-framework",
"\"DKImagePickerController\"",
"-framework",
"\"DKPhotoGallery\"",
"-framework",
"\"Foundation\"",
"-framework",
"\"ImageIO\"",
"-framework",
"\"LocalAuthentication\"",
"-framework",
"\"MTBBarcodeScanner\"",
"-framework",
"\"OrderedSet\"",
"-framework",
"\"Photos\"",
"-framework",
"\"QuartzCore\"",
"-framework",
"\"SDWebImage\"",
"-framework",
"\"Sentry\"",
"-framework",
"\"SwiftyGif\"",
"-framework",
"\"Toast\"",
"-framework",
"\"UIKit\"",
"-framework",
"\"app_links\"",
"-framework",
"\"connectivity_plus\"",
"-framework",
"\"device_info_plus\"",
"-framework",
"\"file_picker\"",
"-framework",
"\"file_saver\"",
"-framework",
"\"fk_user_agent\"",
"-framework",
"\"flutter_email_sender\"",
"-framework",
"\"flutter_inappwebview_ios\"",
"-framework",
"\"flutter_local_authentication\"",
"-framework",
"\"flutter_local_notifications\"",
"-framework",
"\"flutter_native_splash\"",
"-framework",
"\"flutter_secure_storage\"",
"-framework",
"\"fluttertoast\"",
"-framework",
"\"local_auth_darwin\"",
"-framework",
"\"move_to_background\"",
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"privacy_screen\"",
"-framework",
"\"qr_code_scanner\"",
"-framework",
"\"sentry_flutter\"",
"-framework",
"\"share_plus\"",
"-framework",
"\"shared_preferences_foundation\"",
"-framework",
"\"sodium_libs\"",
"-framework",
"\"sqflite\"",
"-framework",
"\"sqlite3\"",
"-framework",
"\"sqlite3_flutter_libs\"",
"-framework",
"\"url_launcher_ios\"",
"-weak_framework",
"\"LinkPresentation\"",
"-weak_framework",
SwiftUI,
);
PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -592,6 +686,100 @@
"$(inherited)",
"@executable_path/Frameworks",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-l\"swiftCoreGraphics\"",
"-framework",
"\"AVFoundation\"",
"-framework",
"\"AVKit\"",
"-framework",
"\"DKImagePickerController\"",
"-framework",
"\"DKPhotoGallery\"",
"-framework",
"\"Foundation\"",
"-framework",
"\"ImageIO\"",
"-framework",
"\"LocalAuthentication\"",
"-framework",
"\"MTBBarcodeScanner\"",
"-framework",
"\"OrderedSet\"",
"-framework",
"\"Photos\"",
"-framework",
"\"QuartzCore\"",
"-framework",
"\"SDWebImage\"",
"-framework",
"\"Sentry\"",
"-framework",
"\"SwiftyGif\"",
"-framework",
"\"Toast\"",
"-framework",
"\"UIKit\"",
"-framework",
"\"app_links\"",
"-framework",
"\"connectivity_plus\"",
"-framework",
"\"device_info_plus\"",
"-framework",
"\"file_picker\"",
"-framework",
"\"file_saver\"",
"-framework",
"\"fk_user_agent\"",
"-framework",
"\"flutter_email_sender\"",
"-framework",
"\"flutter_inappwebview_ios\"",
"-framework",
"\"flutter_local_authentication\"",
"-framework",
"\"flutter_local_notifications\"",
"-framework",
"\"flutter_native_splash\"",
"-framework",
"\"flutter_secure_storage\"",
"-framework",
"\"fluttertoast\"",
"-framework",
"\"local_auth_darwin\"",
"-framework",
"\"move_to_background\"",
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"privacy_screen\"",
"-framework",
"\"qr_code_scanner\"",
"-framework",
"\"sentry_flutter\"",
"-framework",
"\"share_plus\"",
"-framework",
"\"shared_preferences_foundation\"",
"-framework",
"\"sodium_libs\"",
"-framework",
"\"sqflite\"",
"-framework",
"\"sqlite3\"",
"-framework",
"\"sqlite3_flutter_libs\"",
"-framework",
"\"url_launcher_ios\"",
"-weak_framework",
"\"LinkPresentation\"",
"-weak_framework",
SwiftUI,
);
PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View File

@@ -328,6 +328,10 @@
}
}
},
"manualSort": "Custom",
"editOrder": "Edit order",
"mostFrequentlyUsed": "Frequently used",
"mostRecentlyUsed": "Recently used",
"activeSessions": "Active sessions",
"somethingWentWrongPleaseTryAgain": "Something went wrong, please try again",
"thisWillLogYouOutOfThisDevice": "This will log you out of this device!",
@@ -447,8 +451,11 @@
"customEndpoint": "Connected to {endpoint}",
"pinText": "Pin",
"unpinText": "Unpin",
"pinnedCodeMessage": "{code} has been pinned",
"unpinnedCodeMessage": "{code} has been unpinned",
"fav": "Favorite",
"favorites": "Favorites",
"unfav": "Unfavorite",
"favoritedCodeMessage": "{code} has been added to favorites",
"unfavoritedCodeMessage": "{code} has been removed from favorites",
"tags": "Tags",
"createNewTag": "Create New Tag",
"tag": "Tag",

View File

@@ -6,34 +6,51 @@
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
},
"onBoardingBody": "Asegure sus códigos 2FA",
"onBoardingBody": "Realice una copia de seguridad segura de sus códigos 2FA",
"onBoardingGetStarted": "Primeros pasos",
"setupFirstAccount": "Configura tu primera cuenta",
"importScanQrCode": "Escanear un código QR",
"qrCode": "Código QR",
"importEnterSetupKey": "Ingrese una llave de configuración",
"importEnterSetupKey": "Ingrese una clave de configuración",
"importAccountPageTitle": "Ingrese los detalles de la cuenta",
"secretCanNotBeEmpty": "El secreto no puede estar vacío",
"bothIssuerAndAccountCanNotBeEmpty": "El emisor y la cuenta no pueden estar vacíos",
"bothIssuerAndAccountCanNotBeEmpty": "Ni el emisor ni la cuenta pueden estar vacíos",
"incorrectDetails": "Detalles incorrectos",
"pleaseVerifyDetails": "Por favor, confirma los detalles e intentar otra vez",
"pleaseVerifyDetails": "Por favor, confirma los detalles e inténtalo de nuevo",
"codeIssuerHint": "Emisor",
"codeSecretKeyHint": "Llave Secreta",
"codeSecretKeyHint": "Clave secreta",
"secret": "Secreto",
"all": "Todos",
"notes": "Notas",
"notesLengthLimit": "Las notas pueden tener como máximo {count} caracteres",
"@notesLengthLimit": {
"description": "Text to indicate the maximum number of characters allowed for notes",
"placeholders": {
"count": {
"description": "The maximum number of characters allowed for notes",
"type": "int",
"example": "100"
}
}
},
"codeAccountHint": "Cuenta (tu@dominio.com)",
"codeTagHint": "Marcado",
"accountKeyType": "Tipo de llave",
"codeTagHint": "Etiqueta",
"accountKeyType": "Tipo de clave",
"sessionExpired": "La sesión ha expirado",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
},
"pleaseLoginAgain": "Por favor, vuelva a iniciar sesión",
"pleaseLoginAgain": "Por favor, vuelve a iniciar sesión",
"loggingOut": "Cerrando sesión...",
"timeBasedKeyType": "Basado en el tiempo (TOTP)",
"counterBasedKeyType": "Basado en Contador (HOTP)",
"counterBasedKeyType": "Basado en contador (HOTP)",
"saveAction": "Guardar",
"nextTotpTitle": "siguiente",
"deleteCodeTitle": "¿Eliminar código?",
"deleteCodeMessage": "¿Está seguro que desea eliminar este código? Esta acción es irreversible.",
"deleteCodeMessage": "¿Estás seguro de que quieres eliminar este código? Esta acción es irreversible.",
"trashCode": "¿Enviar código a la papelera?",
"trashCodeMessage": "¿Estás seguro de que quieres enviar el código de la cuenta {account} a la papelera?",
"trash": "Papelera",
"viewLogsAction": "Ver Registros",
"sendLogsDescription": "Esto enviará registros para ayudarnos a depurar su problema. Aunque tomamos precauciones para asegurarnos que no se registre información sensible, le recomendamos que consulte estos registros antes de compartirlos.",
"preparingLogsTitle": "Preparando registros...",
@@ -65,9 +82,9 @@
"merchandise": "Mercancías",
"verifyPassword": "Verificar contraseña",
"pleaseWait": "Por favor, espere...",
"generatingEncryptionKeysTitle": "Generando claves de encriptación...",
"generatingEncryptionKeysTitle": "Generando claves de cifrado...",
"recreatePassword": "Recrear contraseña",
"recreatePasswordMessage": "El dispositivo actual no es lo suficientemente potente para verificar su contraseña, así que necesitamos regenerarlo una vez de una manera que funcione con todos los dispositivos.\n\nPor favor Inicie sesión usando su clave de recuperación y regenere su contraseña (puede volver a utilizar la misma si lo desea).",
"recreatePasswordMessage": "El dispositivo actual no es lo suficientemente potente como para verificar tu contraseña, por lo que debemos regenerarlo una vez de una manera que funcione con todos los dispositivos.\n\nPor favor, inicia sesión con tu clave de recuperación y regenera tu contraseña (puedes volver a usar la misma si lo deseas).",
"useRecoveryKey": "Usar clave de recuperación",
"incorrectPasswordTitle": "Contraseña incorrecta",
"welcomeBack": "¡Te damos la bienvenida otra vez!",
@@ -79,32 +96,33 @@
"data": "Datos",
"importCodes": "Importar códigos",
"importTypePlainText": "Texto sin formato",
"importTypeEnteEncrypted": "Exportación cifrada Ente",
"passwordForDecryptingExport": "Contraseña para descifrar exportación",
"importTypeEnteEncrypted": "Exportación cifrada de Ente",
"passwordForDecryptingExport": "Contraseña para descifrar la exportación",
"passwordEmptyError": "La contraseña no puede estar vacía",
"importFromApp": "Importar códigos de {appName}",
"importGoogleAuthGuide": "Exportar tus cuentas desde Google Authenticator a un código QR usando la opción \"Transferir Cuentas\". A continuación, usando otro dispositivo, escanee el código QR.\n\nConsejo: Puede usar la webcam de su portátil para tomar una foto del código QR.",
"importGoogleAuthGuide": "Exporta tus cuentas desde Google Authenticator a un código QR usando la opción \"Transferir Cuentas\". A continuación, usando otro dispositivo, escanea el código QR.\n\nConsejo: Puedes usar la webcam de tu portátil para tomar una foto del código QR.",
"importSelectJsonFile": "Seleccione el archivo JSON",
"importSelectAppExport": "Seleccione el archivo de exportación de {appName}",
"importEnteEncGuide": "Seleccione el archivo JSON cifrado exportado desde Ente",
"importRaivoGuide": "Utilice la opción \"Exportar códigos a un archivo de Zip\" en la configuración de Raivo.\n\nExtraiga el archivo zip e importe el archivo JSON.",
"importBitwardenGuide": "Use la opción \"Exportar caja fuerte\" dentro del menú Herramientas de Bitwarden e importe el fichero JSON no cifrado.",
"importAegisGuide": "Utilice la opción \"Exportar la bóveda\" en ajustes de Aegis.\n\nSi tu bóveda es cifrada, necesitara entrar contraseña de bóveda para descifrar la bóveda.",
"import2FasGuide": "Use la opción \"Configuración→Copia de seguridad→Exportar\" en 2FAS\n\nSi su copia de seguridad está cifrada, necesitará introducir la contraseña para descifrarla",
"importLastpassGuide": "Utilice la opción \"Transferir cuentas\" en la configuración del autenticador de Lastpass y pulse \"Exportar cuentas al archivo\". Importe el archivo JSON descargado.",
"importRaivoGuide": "Utiliza la opción \"Exportar códigos a un archivo Zip\" en la configuración de Raivo.\n\nExtrae el archivo zip e importa el archivo JSON.",
"importBitwardenGuide": "Usa la opción \"Exportar caja fuerte\" dentro del menú Herramientas de Bitwarden e importe el fichero JSON sin cifrar.",
"importAegisGuide": "Utilice la opción \"Exportar la bóveda\" en ajustes de Aegis.\n\nSi tu bóveda es cifrada, necesitaras introducir la contraseña de la bóveda para descifrarla.",
"import2FasGuide": "Usa la opción \"Configuración→Copia de seguridad→Exportar\" en 2FAS\n\nSi tu copia de seguridad está cifrada, necesitará introducir la contraseña para descifrarla",
"importLastpassGuide": "Utiliza la opción \"Transferir cuentas\" en la configuración del autenticador de Lastpass y pulse \"Exportar cuentas al archivo\". Importe el archivo JSON descargado.",
"exportCodes": "Exportar códigos",
"importLabel": "Importar",
"importInstruction": "Por favor, seleccione un archivo que contenga una lista de sus códigos en el siguiente formato",
"importCodeDelimiterInfo": "Los códigos pueden separarse por una coma o una nueva línea",
"selectFile": "Seleccionar archivo",
"emailVerificationToggle": "Verificación de correo electrónico",
"emailVerificationEnableWarning": "Si estás guardando la autenticación de dos factores en tu correo electrónico con nosotros, activar la verificación de correo electrónico podría resultar en un punto muerto. Si está bloqueado fuera de un servicio, puede que no pueda iniciar sesión en el otro.",
"authToChangeEmailVerificationSetting": "Por favor, autentifíquese para cambiar su correo electrónico",
"authToViewYourRecoveryKey": "Por favor, autentifíquese para ver su clave de recuperación",
"authToChangeYourEmail": "Por favor, autentifíquese para cambiar su correo electrónico",
"authToChangeYourPassword": "Por favor, autentifíquese para cambiar su contraseña",
"authToViewSecrets": "Por favor, autentifíquese para ver sus secretos",
"authToInitiateSignIn": "Por favor, autentifíquese para iniciar la sesión para realizar la copia de seguridad.",
"emailVerificationEnableWarning": "Para evitar quedarte bloqueado fuera de tu cuenta, asegúrate de guardar una copia de su código 2FA de tu correo electrónico fuera de Ente Auth antes de habilitar la verificación de correo electrónico.",
"authToChangeEmailVerificationSetting": "Por favor, autentícate para cambiar tu correo electrónico",
"authenticateGeneric": "Por favor, autentícate",
"authToViewYourRecoveryKey": "Por favor, autentícate para ver tu clave de recuperación",
"authToChangeYourEmail": "Por favor, autentícate para cambiar tu correo electrónico",
"authToChangeYourPassword": "Por favor, autentícate para cambiar tu contraseña",
"authToViewSecrets": "Por favor, autentícate para ver tus secretos",
"authToInitiateSignIn": "Por favor, autentícate para iniciar la sesión para realizar la copia de seguridad.",
"ok": "Ok",
"cancel": "Cancelar",
"yes": "Si",
@@ -114,32 +132,35 @@
"general": "General",
"settings": "Configuración",
"copied": "Copiado",
"pleaseTryAgain": "Por favor, inténtalo nuevamente",
"pleaseTryAgain": "Por favor, inténtalo de nuevo",
"existingUser": "Usuario existente",
"newUser": "Nuevo a Ente",
"newUser": "Nuevo en Ente",
"delete": "Borrar",
"enterYourPasswordHint": "Ingrese su contraseña",
"enterYourPasswordHint": "Introduce tu contraseña",
"forgotPassword": "Olvidé mi contraseña",
"oops": "Ups",
"suggestFeatures": "Sugerir funcionalidades",
"faq": "Preguntas Frecuentes",
"somethingWentWrongMessage": "Algo ha ido mal, por favor, prueba otra vez",
"somethingWentWrongMessage": "Algo ha ido mal, por favor, inténtalo de nuevo",
"leaveFamily": "Dejar plan familiar",
"leaveFamilyMessage": "¿Está seguro de que desea abandonar el plan familiar?",
"leaveFamilyMessage": "¿Estás seguro de que quieres abandonar el plan familiar?",
"inFamilyPlanMessage": "¡Estás en un plan familiar!",
"hintForMobile": "Mantén pulsado un código para editarlo o eliminarlo.",
"hintForDesktop": "Haz clic derecho en un código para editarlo o eliminarlo.",
"scan": "Escanear",
"scanACode": "Escanear un código",
"verify": "Verificar",
"verifyEmail": "Verificar correo electrónico",
"enterCodeHint": "Ingrese el código de seis dígitos de su aplicación de autenticación",
"lostDeviceTitle": "¿Perdió su dispositivo?",
"lostDeviceTitle": "¿Dispositivo perdido?",
"twoFactorAuthTitle": "Autenticación de dos factores",
"passkeyAuthTitle": "Verificación de llave de acceso",
"verifyPasskey": "Verificar llave de acceso",
"passkeyAuthTitle": "Verificación de clave de acceso",
"verifyPasskey": "Verificar clave de acceso",
"loginWithTOTP": "Inicio de sesión con TOTP",
"recoverAccount": "Recuperar cuenta",
"enterRecoveryKeyHint": "Introduzca su clave de recuperación",
"enterRecoveryKeyHint": "Introduce tu clave de recuperación",
"recover": "Recuperar",
"contactSupportViaEmailMessage": "Por favor, envíe un email a {email} desde su dirección de correo electrónico registrada",
"contactSupportViaEmailMessage": "Por favor, envía un email a {email} desde la dirección de correo electrónico que usó durante el registro",
"@contactSupportViaEmailMessage": {
"placeholders": {
"email": {
@@ -148,20 +169,20 @@
}
},
"invalidQRCode": "Código QR no válido",
"noRecoveryKeyTitle": "¿Sin clave de recuperación?",
"noRecoveryKeyTitle": "¿No tienes la clave de recuperación?",
"enterEmailHint": "Introduce tu dirección de correo electrónico",
"invalidEmailTitle": "Dirección de correo electrónico no válida",
"invalidEmailMessage": "Por favor, introduzca una dirección de correo electrónico válida.",
"invalidEmailMessage": "Por favor, introduce una dirección de correo electrónico válida.",
"deleteAccount": "Eliminar cuenta",
"deleteAccountQuery": "Lamentaremos que te vayas. ¿Estás enfrentando algún inconveniente?",
"deleteAccountQuery": "Lamentamos que te vayas. ¿Estás teniendo algún problema?",
"yesSendFeedbackAction": "Sí, enviar comentarios",
"noDeleteAccountAction": "No, eliminar cuenta",
"initiateAccountDeleteTitle": "Por favor, autentifíquese para iniciar la eliminación de la cuenta",
"initiateAccountDeleteTitle": "Por favor, autentícate para iniciar la eliminación de la cuenta",
"sendEmail": "Enviar correo electrónico",
"createNewAccount": "Crear nueva cuenta",
"createNewAccount": "Crear cuenta nueva",
"weakStrength": "Poco segura",
"strongStrength": "Fuerte",
"moderateStrength": "Moderado",
"strongStrength": "Segura",
"moderateStrength": "Moderada",
"confirmPassword": "Confirmar contraseña",
"close": "Cerrar",
"oopsSomethingWentWrong": "Vaya, algo salió mal.",
@@ -170,10 +191,10 @@
"social": "Social",
"security": "Seguridad",
"lockscreen": "Pantalla de bloqueo",
"authToChangeLockscreenSetting": "Por favor autentifíquese para cambiar la configuración de bloqueo de pantalla",
"deviceLockEnablePreSteps": "Para activar el bloqueo de la aplicación, por favor configure el código de acceso del dispositivo o el bloqueo de pantalla en los ajustes del sistema.",
"authToChangeLockscreenSetting": "Por favor autentícate para cambiar la configuración de bloqueo de pantalla",
"deviceLockEnablePreSteps": "Para activar el bloqueo de la aplicación, por favor configura el código de acceso del dispositivo o el bloqueo de pantalla en los ajustes de tu sistema.",
"viewActiveSessions": "Ver sesiones activas",
"authToViewYourActiveSessions": "Por favor, autentifíquese para ver sus sesiones activas",
"authToViewYourActiveSessions": "Por favor, autentícate para ver tus sesiones activas",
"searchHint": "Buscar...",
"search": "Buscar",
"sorryUnableToGenCode": "Lo sentimos, no se puede generar un código para {issuerName}",
@@ -182,18 +203,22 @@
"scanAQrCode": "Escanear un código QR",
"enterDetailsManually": "Ingrese los detalles manualmente",
"edit": "Editar",
"share": "Compartir",
"shareCodes": "Compartir código",
"shareCodesDuration": "Selecciona la duración durante la cual deseas compartir el código.",
"restore": "Restaurar",
"copiedToClipboard": "Copiado al portapapeles",
"copiedNextToClipboard": "Copiado el siguiente código al portapapeles",
"error": "Error",
"recoveryKeyCopiedToClipboard": "Llave de recuperación copiada al portapapeles",
"recoveryKeyOnForgotPassword": "Si olvida su contraseña, la única forma de recuperar sus datos es con esta clave.",
"recoveryKeySaveDescription": "Nosotros no almacenamos esta clave, por favor guarde dicha clave de 24 palabras en un lugar seguro.",
"recoveryKeyCopiedToClipboard": "Clave de recuperación copiada al portapapeles",
"recoveryKeyOnForgotPassword": "Si olvidas tu contraseña, la única forma de recuperar tus datos es con esta clave.",
"recoveryKeySaveDescription": "Nosotros no almacenamos esta clave, por favor guarda esta clave de 24 palabras en un lugar seguro.",
"doThisLater": "Hacer esto más tarde",
"saveKey": "Guardar Clave",
"saveKey": "Guardar clave",
"save": "Guardar",
"send": "Enviar",
"saveOrSendDescription": "¿Desea guardar esto en el almacenamiento (carpeta Descargas por defecto) o enviarlo a otras aplicaciones?",
"saveOnlyDescription": "¿Desea guardar esto en el almacenamiento (carpeta Descargas por defecto)?",
"saveOrSendDescription": "¿Desea guardar el archivo en el almacenamiento (carpeta Descargas por defecto) o enviarlo a otras aplicaciones?",
"saveOnlyDescription": "¿Desea guardar el archivo en el almacenamiento (carpeta Descargas por defecto)?",
"back": "Atrás",
"createAccount": "Crear cuenta",
"passwordStrength": "Fortaleza de la contraseña: {passwordStrengthValue}",
@@ -217,11 +242,11 @@
"changePasswordTitle": "Cambiar contraseña",
"resetPasswordTitle": "Restablecer contraseña",
"encryptionKeys": "Claves de cifrado",
"passwordWarning": "No almacenamos esta contraseña, así que si la olvidas, <underline>no podemos descifrar tus datos</underline>",
"passwordWarning": "No almacenamos esta contraseña, así que si la olvidas, <underline>no podremos descifrar tus datos</underline>",
"enterPasswordToEncrypt": "Introduzca una contraseña que podamos usar para cifrar sus datos",
"enterNewPasswordToEncrypt": "Introduzca una nueva contraseña que podamos usar para cifrar sus datos",
"enterNewPasswordToEncrypt": "Introduzca una contraseña nueva que podamos usar para cifrar sus datos",
"passwordChangedSuccessfully": "Contraseña cambiada correctamente",
"generatingEncryptionKeys": "Generando claves de encriptación...",
"generatingEncryptionKeys": "Generando claves de cifrado...",
"continueLabel": "Continuar",
"insecureDevice": "Dispositivo inseguro",
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Lo sentimos, no hemos podido generar claves seguras en este dispositivo.\n\nRegístrate desde un dispositivo diferente.",
@@ -230,31 +255,31 @@
"loginTerms": "Al hacer clic en iniciar sesión, acepto los <u-terms>términos de servicio</u-terms> y <u-policy>la política de privacidad</u-policy>",
"logInLabel": "Iniciar sesión",
"logout": "Cerrar sesión",
"areYouSureYouWantToLogout": "¿Seguro que quiere cerrar la sesión?",
"yesLogout": "Sí, cerrar sesión",
"areYouSureYouWantToLogout": "¿Seguro que quieres cerrar la sesión?",
"yesLogout": "Sí, cerrar la sesión",
"exit": "Salir",
"verifyingRecoveryKey": "Verificando clave de recuperación...",
"recoveryKeyVerified": "Clave de recuperación verificada",
"recoveryKeySuccessBody": "¡Genial! Su clave de recuperación es válida. Gracias por verificar.\n\nPor favor, recuerde mantener su clave de recuperación segura.",
"invalidRecoveryKey": "La clave de recuperación introducida no es válida. Por favor, asegúrese de que contiene 24 palabras y compruebe la ortografía de cada una.\n\nSi ha introducido un código de recuperación antiguo, asegúrese de que tiene 64 caracteres de largo y compruebe cada uno de ellos.",
"invalidRecoveryKey": "La clave de recuperación introducida no es válida. Por favor, asegúrate de que contiene 24 palabras y comprueba la ortografía de cada una.\n\nSi has introducido un código de recuperación antiguo, asegúrate de que tiene 64 caracteres de largo y comprueba cada uno de ellos.",
"recreatePasswordTitle": "Recrear contraseña",
"recreatePasswordBody": "El dispositivo actual no es lo suficientemente potente para verificar su contraseña, pero podemos regenerarla de una manera que funcione con todos los dispositivos.\n\nPor favor inicie sesión usando su clave de recuperación y regenere su contraseña (puede volver a utilizar la misma si lo desea).",
"invalidKey": "Clave inválida",
"recreatePasswordBody": "El dispositivo actual no es lo suficientemente potente para verificar su contraseña, pero podemos regenerarla de manera que funcione con todos los dispositivos.\n\nPor favor inicie sesión usando su clave de recuperación y regenere su contraseña (puede volver a utilizar la misma si lo desea).",
"invalidKey": "Clave no válida",
"tryAgain": "Inténtelo de nuevo",
"viewRecoveryKey": "Ver código de recuperación",
"viewRecoveryKey": "Ver clave de recuperación",
"confirmRecoveryKey": "Confirmar clave de recuperación",
"recoveryKeyVerifyReason": "Su clave de recuperación es la única forma de recuperar sus fotos si olvida su contraseña. Puede encontrar su clave de recuperación en Ajustes > Cuenta.\n\nPor favor, introduzca su clave de recuperación aquí para verificar que la ha guardado correctamente.",
"confirmYourRecoveryKey": "Confirmar su clave de recuperación",
"recoveryKeyVerifyReason": "Tu clave de recuperación es la única forma de recuperar tus fotos si olvidas tu contraseña. Puedes encontrar tu clave de recuperación en Ajustes > Cuenta.\n\nPor favor, introduce tu clave de recuperación aquí para verificar que la has guardado correctamente.",
"confirmYourRecoveryKey": "Confirmar tu clave de recuperación",
"confirm": "Confirmar",
"emailYourLogs": "Envíe sus registros por correo electrónico",
"pleaseSendTheLogsTo": "Por favor, envíe los registros a {toEmail}",
"copyEmailAddress": "Copiar dirección de correo electrónico",
"exportLogs": "Exportar registros",
"enterYourRecoveryKey": "Introduzca su clave de recuperación",
"tempErrorContactSupportIfPersists": "Parece que algo salió mal. Por favor, vuelve a intentarlo después de algún tiempo. Si el error persiste, ponte en contacto con nuestro equipo de soporte.",
"enterYourRecoveryKey": "Introduce tu clave de recuperación",
"tempErrorContactSupportIfPersists": "Parece que algo salió mal. Por favor, vuelve a intentarlo pasado un tiempo. Si el error persiste, ponte en contacto con nuestro equipo de soporte.",
"networkHostLookUpErr": "No se puede conectar a Ente. Por favor, comprueba tu configuración de red y ponte en contacto con el soporte técnico si el error persiste.",
"networkConnectionRefusedErr": "No se puede conectar a Ente. Por favor, vuelve a intentarlo pasado un tiempo. Si el error persiste, ponte en contacto con el soporte técnico.",
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Parece que algo salió mal. Por favor, vuelve a intentarlo después de algún tiempo. Si el error persiste, ponte en contacto con nuestro equipo de soporte.",
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Parece que algo salió mal. Por favor, vuelve a intentarlo pasado un tiempo. Si el error persiste, ponte en contacto con nuestro equipo de soporte.",
"about": "Acerca de",
"weAreOpenSource": "¡Somos de código abierto!",
"privacy": "Privacidad",
@@ -266,14 +291,14 @@
"updateAvailable": "Actualización disponible",
"update": "Actualizar",
"checking": "Comprobando...",
"youAreOnTheLatestVersion": "Está usando la versión más reciente",
"youAreOnTheLatestVersion": "Estás usando la versión más reciente",
"warning": "Atención",
"exportWarningDesc": "El archivo exportado contiene información confidencial. Por favor, guárdelo de forma segura.",
"iUnderStand": "Entiendo",
"exportWarningDesc": "El archivo exportado contiene información confidencial. Por favor, guárdalo de forma segura.",
"iUnderStand": "Lo entiendo",
"@iUnderStand": {
"description": "Text for the button to confirm the user understands the warning"
},
"authToExportCodes": "Por favor, autentifíquese para exportar sus códigos",
"authToExportCodes": "Por favor, autentícate para exportar tus códigos",
"importSuccessTitle": "¡Hurra!",
"importSuccessDesc": "¡Has importado {count} códigos!",
"@importSuccessDesc": {
@@ -288,7 +313,7 @@
"sorry": "Lo sentimos",
"importFailureDesc": "No se pudo analizar el archivo seleccionado.\n¡Por favor escriba a support@ente.io si necesita ayuda!",
"pendingSyncs": "Atención",
"pendingSyncsWarningBody": "Algunos de sus códigos no han sido respaldados.\n\nPor favor, asegúrese de tener una copia de seguridad de estos códigos antes de cerrar la sesión.",
"pendingSyncsWarningBody": "Algunos de tus códigos no tienen copia de seguridad.\n\nPor favor, asegúrate de tener una copia de seguridad de estos códigos antes de cerrar la sesión.",
"checkInboxAndSpamFolder": "Por favor revisa tu bandeja de entrada (y spam) para completar la verificación",
"tapToEnterCode": "Toca para introducir el código",
"resendEmail": "Reenviar correo electrónico",
@@ -304,18 +329,18 @@
}
},
"activeSessions": "Sesiones activas",
"somethingWentWrongPleaseTryAgain": "Algo ha ido mal, por favor, prueba otra vez",
"somethingWentWrongPleaseTryAgain": "Algo ha ido mal, por favor, inténtelo de nuevo",
"thisWillLogYouOutOfThisDevice": "¡Esto cerrará la sesión de este dispositivo!",
"thisWillLogYouOutOfTheFollowingDevice": "Esto cerrará la sesión del siguiente dispositivo:",
"terminateSession": "¿Terminar sesión?",
"terminate": "Terminar",
"thisDevice": "Este dispositivo",
"toResetVerifyEmail": "Para restablecer su contraseña, por favor verifique su correo electrónico primero.",
"toResetVerifyEmail": "Para restablecer tu contraseña, por favor verifica tu correo electrónico primero.",
"thisEmailIsAlreadyInUse": "Este correo electrónico ya está en uso",
"verificationFailedPleaseTryAgain": "Verificación fallida, por favor inténtalo de nuevo",
"yourVerificationCodeHasExpired": "Tu código de verificación ha expirado",
"incorrectCode": "Código incorrecto",
"sorryTheCodeYouveEnteredIsIncorrect": "Lo sentimos, el código que ha introducido es incorrecto",
"sorryTheCodeYouveEnteredIsIncorrect": "Lo sentimos, el código que has introducido es incorrecto",
"emailChangedTo": "Correo electrónico cambiado a {newEmail}",
"authenticationFailedPleaseTryAgain": "Error de autenticación, por favor inténtalo de nuevo",
"authenticationSuccessful": "¡Autenticación exitosa!",
@@ -330,30 +355,31 @@
"passwordToEncryptExport": "Contraseña para cifrar la exportación",
"export": "Exportar",
"useOffline": "Usar sin copias de seguridad",
"signInToBackup": "Inicia sesión para hacer copia de tus códigos",
"signInToBackup": "Inicia sesión para hacer una copia de seguridad tus códigos",
"singIn": "Iniciar sesión",
"sigInBackupReminder": "Por favor, exporte sus códigos para asegurarse de que tiene una copia de seguridad de la que puede restaurar.",
"offlineModeWarning": "Ha elegido proceder sin copia de seguridad. Por favor, tome copias de seguridad manuales para asegurarse de que sus códigos están seguros.",
"sigInBackupReminder": "Por favor, exporta tus códigos para asegurarte de que tienes una copia de seguridad de la que puedas restaurarlos.",
"offlineModeWarning": "Ha elegido proceder sin copia de seguridad. Por favor, realice copias de seguridad manuales para asegurarse de que sus códigos están seguros.",
"showLargeIcons": "Mostrar iconos grandes",
"compactMode": "Modo compacto",
"shouldHideCode": "Ocultar códigos",
"doubleTapToViewHiddenCode": "Puedes tocar dos veces en una entrada para ver el código",
"focusOnSearchBar": "Enfocar búsqueda al iniciar la aplicación",
"confirmUpdatingkey": "¿Estás seguro de que deseas actualizar la clave secreto?",
"confirmUpdatingkey": "¿Estás seguro de que deseas actualizar la clave secreta?",
"minimizeAppOnCopy": "Minimizar aplicación al copiar",
"editCodeAuthMessage": "Autenticar para editar código",
"deleteCodeAuthMessage": "Autenticar para borrar código",
"showQRAuthMessage": "Autenticar para mostrar código QR",
"confirmAccountDeleteTitle": "Confirmar eliminación de la cuenta",
"confirmAccountDeleteMessage": "Esta cuenta está vinculada a otras aplicaciones de Ente, si utilizas alguna. Se programará la eliminación de los datos cargados en todas las aplicaciones de Ente, y tu cuenta se eliminará permanentemente.",
"confirmAccountDeleteMessage": "Esta cuenta está vinculada a otras aplicaciones de Ente, si utilizas alguna. \n\nSe programará la eliminación de los datos cargados en todas las aplicaciones de Ente, y tu cuenta se eliminará permanentemente.",
"androidBiometricHint": "Verificar identidad",
"@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
},
"androidBiometricNotRecognized": "No reconocido. Inténtelo de nuevo.",
"androidBiometricNotRecognized": "No reconocido. Inténtalo de nuevo.",
"@androidBiometricNotRecognized": {
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
},
"androidBiometricSuccess": "Realizado correctamente",
"androidBiometricSuccess": "Autenticación exitosa",
"@androidBiometricSuccess": {
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
},
@@ -361,11 +387,11 @@
"@androidCancelButton": {
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
},
"androidSignInTitle": "Se requiere autenticación",
"androidSignInTitle": "Se necesita autenticación biométrica",
"@androidSignInTitle": {
"description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
},
"androidBiometricRequiredTitle": "Biométrica necesaria",
"androidBiometricRequiredTitle": "Se necesita autenticación biométrica",
"@androidBiometricRequiredTitle": {
"description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
},
@@ -381,11 +407,11 @@
"@goToSettings": {
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
},
"androidGoToSettingsDescription": "La autenticación biométrica no está configurada en su dispositivo. Vaya a 'Ajustes > Seguridad' para añadir autenticación biométrica.",
"androidGoToSettingsDescription": "La autenticación biométrica no está configurada en tu dispositivo. Ve a 'Ajustes > Seguridad' para configurar la autenticación biométrica.",
"@androidGoToSettingsDescription": {
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
},
"iOSLockOut": "La autenticación biométrica está deshabilitada. Por favor bloquee y desbloquee la pantalla para habilitarla.",
"iOSLockOut": "La autenticación biométrica está deshabilitada. Por favor bloquea y desbloquea la pantalla para habilitarla.",
"@iOSLockOut": {
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
},
@@ -399,27 +425,27 @@
},
"noInternetConnection": "No hay conexión a Internet",
"pleaseCheckYourInternetConnectionAndTryAgain": "Compruebe su conexión a Internet e inténtelo de nuevo.",
"signOutFromOtherDevices": "Cerrar sesión desde otros dispositivos",
"signOutOtherBody": "Si cree que alguien puede conocer su contraseña, puede forzar a todos los demás dispositivos que usen su cuenta a cerrar la sesión.",
"signOutOtherDevices": "Cerrar la sesión de otros dispositivos",
"signOutFromOtherDevices": "Cerrar sesión en otros dispositivos",
"signOutOtherBody": "Si crees que alguien puede conocer tu contraseña, puedes forzar a todos los demás dispositivos que usen tu cuenta a cerrar la sesión.",
"signOutOtherDevices": "Cerrar la sesión en otros dispositivos",
"doNotSignOut": "No cerrar la sesión",
"hearUsWhereTitle": "¿Cómo conoció Ente? (opcional)",
"hearUsExplanation": "No rastreamos las aplicaciones instaladas. ¡Nos ayudaría si nos dijera dónde nos encontró!",
"hearUsExplanation": "No rastreamos la instalación de las aplicaciones. ¡Nos ayudaría si nos dijera dónde nos encontró!",
"recoveryKeySaved": "¡Clave de recuperación guardada en la carpeta Descargas!",
"waitingForBrowserRequest": "Esperando la solicitud del navegador...",
"waitingForVerification": "Esperando verificación...",
"passkey": "Llave de acceso",
"passkey": "Clave de acceso",
"passKeyPendingVerification": "La verificación todavía está pendiente",
"loginSessionExpired": "La sesión ha expirado",
"loginSessionExpiredDetails": "Tu sesión ha expirado. Por favor, vuelve a iniciar sesión.",
"developerSettingsWarning": "¿Estás seguro de que quieres modificar los ajustes de desarrollador?",
"developerSettings": "Ajustes de desarrollador",
"serverEndpoint": "Punto final del servidor",
"invalidEndpoint": "Punto final no válido",
"invalidEndpointMessage": "Lo sentimos, el punto final introducido no es válido. Por favor, introduce un punto final válido y vuelve a intentarlo.",
"endpointUpdatedMessage": "Punto final actualizado con éxito",
"serverEndpoint": "Endpoint del servidor",
"invalidEndpoint": "Endpoint no válido",
"invalidEndpointMessage": "Lo sentimos, el endpoint introducido no es válido. Por favor, introduce un endpoint válido y vuelve a intentarlo.",
"endpointUpdatedMessage": "Endpoint actualizado con éxito",
"customEndpoint": "Conectado a {endpoint}",
"pinText": "Fijar",
"pinText": "Anclar",
"unpinText": "Desanclar",
"pinnedCodeMessage": "{code} ha sido anclado",
"unpinnedCodeMessage": "{code} ha sido desanclado",
@@ -427,33 +453,37 @@
"createNewTag": "Crear Nueva Etiqueta",
"tag": "Etiqueta",
"create": "Crear",
"editTag": "Editar Etiqueta",
"editTag": "Editar etiqueta",
"deleteTagTitle": "¿Eliminar etiqueta?",
"deleteTagMessage": "¿Estás seguro de que quieres eliminar esta etiqueta? Esta acción es irreversible.",
"somethingWentWrongParsingCode": "No se han podido analizar los códigos {x}.",
"updateNotAvailable": "Actualización no disponible",
"viewRawCodes": "Ver códigos raw",
"rawCodes": "Códigos raw",
"rawCodeData": "Datos del código raw",
"viewRawCodes": "Ver códigos en bruto",
"rawCodes": "Códigos en bruto",
"rawCodeData": "Datos de código en bruto",
"appLock": "Bloqueo de aplicación",
"noSystemLockFound": "Bloqueo del sistema no encontrado",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Para activar el bloqueo de la aplicación, por favor configure el código de acceso del dispositivo o el bloqueo de pantalla en los ajustes del sistema.",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Para activar el bloqueo de la aplicación, por favor configura el código de acceso del dispositivo o el bloqueo de pantalla en los ajustes del sistema.",
"autoLock": "Bloqueo automático",
"immediately": "De inmediato",
"immediately": "Inmediatamente",
"reEnterPassword": "Reescribe tu contraseña",
"reEnterPin": "Reescribe tu PIN",
"next": "Siguiente",
"tooManyIncorrectAttempts": "Demasiados intentos incorrectos",
"tapToUnlock": "Toca para desbloquear",
"setNewPassword": "Establece una nueva contraseña",
"deviceLock": "Dispositivo bloqueado",
"deviceLock": "Bloqueo del dispositivo",
"hideContent": "Ocultar contenido",
"hideContentDescriptionAndroid": "Oculta el contenido de la aplicación en el seleccionador de aplicaciones y desactiva las capturas de pantalla",
"hideContentDescriptioniOS": "Ocultar el contenido de la aplicación en el seleccionador de aplicaciones",
"hideContentDescriptionAndroid": "Oculta el contenido de la aplicación en el selector de aplicaciones y desactiva las capturas de pantalla",
"hideContentDescriptioniOS": "Ocultar el contenido de la aplicación en el selector de aplicaciones",
"autoLockFeatureDescription": "Tiempo tras el cual la aplicación se bloquea después de ser colocada en segundo plano",
"appLockDescription": "Elija entre la pantalla de bloqueo por defecto de su dispositivo y una pantalla de bloqueo personalizada con un PIN o contraseña.",
"pinLock": "Bloquear pin",
"pinLock": "Bloqueo con Pin",
"enterPin": "Ingresa el PIN",
"setNewPin": "Establecer nuevo PIN",
"importFailureDescNew": "No se pudo analizar el archivo seleccionado."
"importFailureDescNew": "No se pudo analizar el archivo seleccionado.",
"appLockNotEnabled": "Bloqueo de aplicación no activado",
"appLockNotEnabledDescription": "Por favor, activa el bloqueo de aplicación desde Seguridad > Bloqueo de aplicación",
"authToViewPasskey": "Por favor, autentícate para ver tu clave de acceso",
"appLockOfflineModeWarning": "Has elegido proceder sin copia de seguridad. Si olvidas el código de desbloqueo de la aplicación, se bloqueará el acceso a sus datos."
}

View File

@@ -1,4 +1,83 @@
{
"account": "खाता",
"unlock": "खोलें"
"unlock": "खोलें",
"recoveryKey": "पुनःप्राप्ति कुंजी",
"counterAppBarTitle": "काउंटर",
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
},
"onBoardingGetStarted": "प्रारंभ करें",
"setupFirstAccount": "अपना पहला अकाउंट सेटअप करें",
"importScanQrCode": "QR कोड स्कैन करें",
"qrCode": "QR कोड",
"importEnterSetupKey": "",
"importAccountPageTitle": "अकाउंट विवरण डालें",
"incorrectDetails": "ग़लत विवरण",
"pleaseVerifyDetails": "कृपया विवरण सत्यापित करें और पुनः प्रयास करें",
"codeIssuerHint": "जारीकर्ता",
"codeSecretKeyHint": "सीक्रेट कुंजी",
"secret": "सीक्रेट",
"all": "सभी",
"notes": "नोट्स",
"notesLengthLimit": "नोट्स अधिकतम {count} अक्षर लम्बे हो सकते हैं",
"@notesLengthLimit": {
"description": "Text to indicate the maximum number of characters allowed for notes",
"placeholders": {
"count": {
"description": "The maximum number of characters allowed for notes",
"type": "int",
"example": "100"
}
}
},
"codeAccountHint": "अकाउंट (you@domain.com)",
"codeTagHint": "टैग",
"sessionExpired": "सत्र की अवधि समाप्त",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
},
"pleaseLoginAgain": "कृपया फिर से लॉगिन करें",
"loggingOut": "लॉग आउट हो रहा है...",
"saveAction": "सेव करें",
"viewLogsAction": "लॉग देखें",
"preparingLogsTitle": "लॉग तैयार किये जा रहे हैं...",
"emailLogsTitle": "लॉग ईमेल करें",
"emailLogsMessage": "कृपया {email} पर लॉग ईमेल करें",
"@emailLogsMessage": {
"placeholders": {
"email": {
"type": "String"
}
}
},
"exportLogsAction": "लॉग एक्सपोर्ट करें",
"reportABug": "बग रिपोर्ट करें",
"reportBug": "बग रिपोर्ट करें",
"emailUsMessage": "कृपया हमें {email} पर ईमेल करें",
"@emailUsMessage": {
"placeholders": {
"email": {
"type": "String"
}
}
},
"contactSupport": "सपोर्ट टीम से संपर्क करें",
"rateUsOnStore": "हमें {storeName} पर रेट करें",
"blog": "ब्लॉग",
"verifyPassword": "पासवर्ड सत्यापित करें",
"pleaseWait": "कृपया प्रतीक्षा करें...",
"incorrectPasswordTitle": "ग़लत पासवर्ड",
"welcomeBack": "आपका पुनः स्वागत है!",
"changeEmail": "ईमेल बदलें",
"changePassword": "पासवर्ड बदलें",
"data": "डेटा",
"passwordEmptyError": "पासवर्ड रिक्त नहीं हो सकता है",
"importLabel": "इंपोर्ट",
"selectFile": "फ़ाइल का चयन करें",
"emailVerificationToggle": "ईमेल सत्यापन",
"ok": "ठीक है",
"cancel": "रद्द करें",
"yes": "हाँ",
"no": "नहीं",
"settings": "सेटिंग"
}

View File

@@ -8,8 +8,8 @@
},
"onBoardingBody": "2단계 인증 코드를 안전하게 백업하세요",
"onBoardingGetStarted": "시작하기",
"setupFirstAccount": "첫번째 계정을 설정하세요",
"importScanQrCode": "QR 코드 스캔",
"setupFirstAccount": "첫 번째 계정을 설정하세요",
"importScanQrCode": "QR 코드 스캔하기",
"qrCode": "QR 코드",
"importEnterSetupKey": "설정 키 입력",
"importAccountPageTitle": "계정 상세 정보 입력",
@@ -19,7 +19,20 @@
"pleaseVerifyDetails": "입력된 정보를 확인하고 다시 시도하세요",
"codeIssuerHint": "발행인",
"codeSecretKeyHint": "비밀 키",
"secret": "비밀",
"all": "모든 항목",
"notes": "메모",
"notesLengthLimit": "메모는 최대 {count}자까지 적을 수 있습니다",
"@notesLengthLimit": {
"description": "Text to indicate the maximum number of characters allowed for notes",
"placeholders": {
"count": {
"description": "The maximum number of characters allowed for notes",
"type": "int",
"example": "100"
}
}
},
"codeAccountHint": "계정 (you@domain.com)",
"codeTagHint": "태그",
"accountKeyType": "키 종류",
@@ -55,7 +68,7 @@
"reportABug": "버그 제보",
"crashAndErrorReporting": "충돌 & 에러 보고",
"reportBug": "버그 제보",
"emailUsMessage": "{email} 로 이메일을 보내주세요.",
"emailUsMessage": "{email}로 이메일을 보내주세요.",
"@emailUsMessage": {
"placeholders": {
"email": {
@@ -64,7 +77,7 @@
}
},
"contactSupport": "지원 문의",
"rateUsOnStore": "{storeName} 에서 평가해주세요",
"rateUsOnStore": "{storeName}에서 평가해주세요",
"blog": "블로그",
"merchandise": "제품",
"verifyPassword": "비밀번호 확인",
@@ -75,7 +88,7 @@
"useRecoveryKey": "복구 키 사용",
"incorrectPasswordTitle": "올바르지 않은 비밀번호",
"welcomeBack": "돌아오신 것을 환영합니다!",
"madeWithLoveAtPrefix": "made with ❤️ at ",
"madeWithLoveAtPrefix": "❤️을 담아 만들었습니다 ",
"supportDevs": "<bold-green>ente</bold-green>를 구독하고 저희를 지원해주세요",
"supportDiscount": "쿠폰 코드 \"AUTH\"를 사용하고 첫 해 10% 할인 혜택을 받으세요",
"changeEmail": "이메일 변경",
@@ -86,22 +99,391 @@
"importTypeEnteEncrypted": "Ente로 암호화된 내보내기",
"passwordForDecryptingExport": "복호화용 비밀번호",
"passwordEmptyError": "비밀번호는 비어있을 수 없습니다",
"importFromApp": "{appName} 로부터 코드 불러오기",
"importFromApp": "{appName}로부터 코드 불러오기",
"importGoogleAuthGuide": "Google 인증기에서 \"계정 이전\" 옵션을 선택해 생성된 QR 코드를 이용해 계정들을 옮길 수 있습니다. 그 다음, 다른 디바이스를 이용하여 QR 코드를 스캔해주세요.\n\n힌트: 웹캠을 이용해 QR 코드를 촬영할 수 있습니다.",
"importSelectJsonFile": "JSON 파일 선택",
"importSelectAppExport": "{appName}의 내보낸 파일 선택하기",
"importEnteEncGuide": "Ente에서 내보낸 암호화된 JSON 파일 선택하기",
"importRaivoGuide": "Raivo의 설정에서 \"Zip 파일로 OTP 내보내기\"를 이용해주세요.\n\nZip 파일의 압축을 풀고 JSON 파일을 가져오세요.",
"importBitwardenGuide": "Bitwarden의 도구에서 \"보관함 내보내기\"를 선택하여 암호화되지 않은 JSON 파일을 불러오세요.",
"importAegisGuide": "Aegis의 설정에서 \"보관함 내보내기\"를 이용하세요.\n\n보관함이 암호화된 경우, 보관함의 복호화를 위해 보관함 비밀번호를 입력해야 할 수도 있습니다.",
"import2FasGuide": "2FAS의 옵션에서 \"설정 -> 백업 -> 내보내기\"를 이용하세요.\n\n백업이 암호화되었다면, 백업의 복호화를 위해 암호를 입력해야 할 수도 있습니다.",
"importLastpassGuide": "Lastpass 인증기의 설정에서 \"계정 이전하기\" 옵션 중 \"파일로 계정들 내보내기\"를 이용하세요. 다운로드 된 JSON 파일을 불러오세요.",
"exportCodes": "코드 내보내기",
"importLabel": "불러오기",
"importInstruction": "다음의 포맷에 맞춰 여러분의 코드가 들어있는 파일을 선택해주세요",
"importCodeDelimiterInfo": "코드는 쉼표 혹은 줄 단위로 구분할 수 있습니다",
"selectFile": "파일 선택",
"emailVerificationToggle": "이메일 검증",
"emailVerificationEnableWarning": "계정이 잠겨 손을 쓸 수 없는 상황에 대비하여, 이메일을 검증하기 전 메일로 전송된 Ente Auth의 2차 인증을 별도로 보관해두세요.",
"authToChangeEmailVerificationSetting": "이메일 검증을 변경하시려면 인증절차를 거쳐주세요",
"authenticateGeneric": "인증절차를 거쳐주세요",
"authToViewYourRecoveryKey": "당신의 복구 키를 확인하려면 인증절차를 거쳐주세요",
"authToChangeYourEmail": "이메일을 변경하려면 인증절차를 거쳐주세요",
"authToChangeYourPassword": "암호를 변경하려면 인증절차를 거쳐주세요",
"authToViewSecrets": "비밀 부분을 확인하려면 인증절차를 거쳐주세요",
"authToInitiateSignIn": "백업을 위해 로그인 상태를 초기화하려면 인증절차를 거쳐주세요",
"ok": "확인",
"cancel": "취소",
"yes": "네",
"no": "아니오",
"email": "이메일",
"support": "지원",
"general": "일반 설정",
"settings": "설정",
"copied": "복사 됨",
"pleaseTryAgain": "다시 시도해주세요.",
"existingUser": "기존 유저",
"pleaseTryAgain": "다시 시도해주세요",
"existingUser": "기존 사용자",
"newUser": "Ente에 새로 가입",
"delete": "삭제",
"enterYourPasswordHint": "패스워드 입력",
"forgotPassword": "패스워드 분실",
"enterYourPasswordHint": "암호 입력",
"forgotPassword": "암호 분실",
"oops": "이런!",
"suggestFeatures": "기능 제안",
"faq": "FAQ"
"faq": "FAQ",
"somethingWentWrongMessage": "뭔가 잘못된 것 같습니다, 다시 시도해주세요",
"leaveFamily": "패밀리에서 떠나기",
"leaveFamilyMessage": "가족 요금제에서 떠나시겠습니까?",
"inFamilyPlanMessage": "가족 요금제에 가입하셨습니다!",
"hintForMobile": "수정이나 삭제를 원하시면 코드를 길게 눌러주세요.",
"hintForDesktop": "수정이나 삭제를 원하시면 코드를 우클릭해주세요.",
"scan": "스캔하기",
"scanACode": "코드 스캔하기",
"verify": "인증",
"verifyEmail": "이메일 인증하기",
"enterCodeHint": "인증기에 적힌 여섯 자리 코드를 입력해주세요",
"lostDeviceTitle": "기기를 잃어버리셨나요?",
"twoFactorAuthTitle": "2단계 인증",
"passkeyAuthTitle": "패스키 검증",
"verifyPasskey": "패스키 확인",
"loginWithTOTP": "TOTP로 로그인 하기",
"recoverAccount": "계정 복구",
"enterRecoveryKeyHint": "복구 키를 입력하세요",
"recover": "복구",
"contactSupportViaEmailMessage": "당신이 등록한 이메일 주소에서 {email}로 메일 한 통을 보내주세요",
"@contactSupportViaEmailMessage": {
"placeholders": {
"email": {
"type": "String"
}
}
},
"invalidQRCode": "맞지 않는 QR 코드",
"noRecoveryKeyTitle": "복구 키가 없으세요?",
"enterEmailHint": "이메일 주소 입력",
"invalidEmailTitle": "잘못 된 이메일 주소",
"invalidEmailMessage": "유효한 이메일 주소를 입력해주세요",
"deleteAccount": "계정 삭제하기",
"deleteAccountQuery": "떠나신다니 아쉽습니다. 뭔가 문제가 있으셨나요?",
"yesSendFeedbackAction": "네, 피드백을 보냅니다",
"noDeleteAccountAction": "아니오, 계정을 지웁니다",
"initiateAccountDeleteTitle": "계정 삭제 절차를 시작하려면 인증절차를 거쳐주세요",
"sendEmail": "이메일 보내기",
"createNewAccount": "새 계정 만들기",
"weakStrength": "약함",
"strongStrength": "강함",
"moderateStrength": "보통",
"confirmPassword": "암호 확인",
"close": "닫기",
"oopsSomethingWentWrong": "이런, 뭔가 꼬였습니다.",
"selectLanguage": "언어 선택",
"language": "언어",
"social": "소셜",
"security": "보안",
"lockscreen": "화면 잠금",
"authToChangeLockscreenSetting": "화면 잠금 설정을 변경하시려면 인증절차를 거쳐주세요",
"deviceLockEnablePreSteps": "기기 잠금을 활성화하시려면, 기기의 암호를 만들거나 시스템 설정에서 화면 잠금을 설정해주세요.",
"viewActiveSessions": "활성화된 세션 확인하기",
"authToViewYourActiveSessions": "활성화된 세션을 확인하시려면 인증절차를 거쳐주세요",
"searchHint": "검색...",
"search": "찾기",
"sorryUnableToGenCode": "죄송합니다, {issuerName}의 코드를 생성할 수 없습니다",
"noResult": "결과 없음",
"addCode": "코드 추가하기",
"scanAQrCode": "QR 코드 스캔하기",
"enterDetailsManually": "속성을 수동으로 입력하기",
"edit": "수정",
"share": "공유",
"shareCodes": "코드 공유하기",
"shareCodesDuration": "코드 공유를 허용할 시간을 선택해주세요.",
"restore": "복구",
"copiedToClipboard": "클립보드에 복사 됨",
"copiedNextToClipboard": "클립보드에 다음번 코드 복사 됨",
"error": "에러",
"recoveryKeyCopiedToClipboard": "클립보드에 복구 키 복사 됨",
"recoveryKeyOnForgotPassword": "암호를 잊어버린 경우, 데이터를 복구하려면 이 키를 이용하는 방법 뿐입니다.",
"recoveryKeySaveDescription": "저희는 이 키를 보관하지 않사오니, 여기에 있는 24 단어로 구성된 키를 안전하게 보관해주세요.",
"doThisLater": "나중에 하기",
"saveKey": "키 저장하기",
"save": "저장",
"send": "보내기",
"saveOrSendDescription": "이것을 당신의 스토리지 (일반적으로 다운로드 폴더) 에 저장하시겠습니까, 아니면 다른 앱으로 전송하시겠습니까?",
"saveOnlyDescription": "이것을 당신의 스토리지 (일반적으로 다운로드 폴더) 에 저장하시겠습니까?",
"back": "뒤로 가기",
"createAccount": "계정 만들기",
"passwordStrength": "암호 보안 강도: {passwordStrengthValue}",
"@passwordStrength": {
"description": "Text to indicate the password strength",
"placeholders": {
"passwordStrengthValue": {
"description": "The strength of the password as a string",
"type": "String",
"example": "Weak or Moderate or Strong"
}
},
"message": "Password Strength: {passwordStrengthText}"
},
"password": "암호",
"signUpTerms": "나는 <u-terms>사용자 약관</u-terms>과 <u-policy>개인정보 취급방침</u-policy>에 동의합니다.",
"privacyPolicyTitle": "개인정보 취급방침",
"termsOfServicesTitle": "약관",
"encryption": "암호화",
"setPasswordTitle": "암호 지정",
"changePasswordTitle": "암호 변경",
"resetPasswordTitle": "암호 초기화",
"encryptionKeys": "암호화 키",
"passwordWarning": "저희는 이 암호를 저장하지 않사오니, 만약 잊어버리시게 되면, <underline>데이터를 복호화해드릴 수 없습니다</underline>",
"enterPasswordToEncrypt": "데이터 암호화를 위한 암호 입력",
"enterNewPasswordToEncrypt": "데이터 암호화를 위한 새로운 암호 입력",
"passwordChangedSuccessfully": "암호가 성공적으로 변경되었습니다",
"generatingEncryptionKeys": "암호화 키 생성 중...",
"continueLabel": "계속",
"insecureDevice": "보안이 허술한 기기",
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "죄송합니다, 이 기기에서 보안 키를 생성할 수 없습니다.\n\n다른 기기에서 계정을 생성해주세요.",
"howItWorks": "작동 원리",
"ackPasswordLostWarning": "나는 암호를 분실한 경우, 데이터가 <underline>종단간 암호화</underline>되어있기에 데이터를 손실할 수 있음을 이해합니다.",
"loginTerms": "로그인을 누름으로써, 나는 <u-terms>사용자 약관</u-terms>과 <u-policy>개인정보 취급방침</u-policy>에 동의합니다.",
"logInLabel": "로그인",
"logout": "로그아웃",
"areYouSureYouWantToLogout": "로그아웃 하시겠습니까?",
"yesLogout": "네, 로그아웃하기",
"exit": "나가기",
"verifyingRecoveryKey": "복구 키 확인 중...",
"recoveryKeyVerified": "복구 키 확인 됨",
"recoveryKeySuccessBody": "좋습니다! 복구 키가 확인되었습니다. 확인 절차를 거쳐주셔서 감사합니다.\n\n잊지 마시고 꼭 복구 키를 안전하게 보관해주세요.",
"invalidRecoveryKey": "입력하신 복구 키가 유효하지 않습니다. 24 단어가 입력됐는지, 그리고 철자가 모두 정확한지 확인해주세요.\n\n구형 복구 코드를 입력하신 경우, 64 자가 맞는지, 그리고 문자가 각각 맞는지 확인해주세요.",
"recreatePasswordTitle": "암호 재생성",
"recreatePasswordBody": "현재 사용 중인 기기는 암호를 확인하기에 적합하지 않으나, 모든 기기에서 작동하는 방식으로 비밀번호를 재생성할 수 있습니다.\n\n복구 키를 사용하여 로그인하고 암호를 재생성해주세요. (원하시면 현재 사용 중인 암호와 같은 암호를 재사용하실 수 있습니다.)",
"invalidKey": "유효하지 않은 키",
"tryAgain": "다시 시도해주세요",
"viewRecoveryKey": "복구 키 보기",
"confirmRecoveryKey": "복구 키 확인",
"recoveryKeyVerifyReason": "암호를 잃어버리셨을 경우, 복구 키만이 당신의 사진들을 복원할 유일한 방법이 됩니다. 설정 > 계정으로 들어가셔서 복구 키를 확인하세요.\n\n여기에 복구 키를 입력하셔서, 확인하셨던 복구 키가 정상인지 체크해보세요.",
"confirmYourRecoveryKey": "복구 키 확인",
"confirm": "확인",
"emailYourLogs": "로그를 이메일로 보내기",
"pleaseSendTheLogsTo": "이 로그를 {toEmail}으로 보내주세요",
"copyEmailAddress": "이메일 주소 복사",
"exportLogs": "로그 내보내기",
"enterYourRecoveryKey": "복구 키를 입력하세요",
"tempErrorContactSupportIfPersists": "뭔가 잘못된 것 같습니다. 잠시 후에 다시 시도해주세요. 에러가 반복되는 경우, 저희 지원 팀에 문의해주세요.",
"networkHostLookUpErr": "Ente에 접속할 수 없습니다, 네트워크 설정을 확인해주시고 에러가 반복되는 경우 저희 지원 팀에 문의해주세요.",
"networkConnectionRefusedErr": "Ente에 접속할 수 없습니다, 잠시 후에 다시 시도해주세요. 에러가 반복되는 경우, 저희 지원 팀에 문의해주세요.",
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "뭔가 잘못된 것 같습니다. 잠시 후에 다시 시도해주세요. 에러가 반복되는 경우, 저희 지원 팀에 문의해주세요.",
"about": "소개",
"weAreOpenSource": "저희는 오픈 소스로 운영됩니다!",
"privacy": "개인 정보",
"terms": "약관",
"checkForUpdates": "업데이트 확인",
"checkStatus": "상태 확인",
"downloadUpdate": "다운로드",
"criticalUpdateAvailable": "중요 업데이트 있음",
"updateAvailable": "업데이트 가능",
"update": "업데이트",
"checking": "확인 중...",
"youAreOnTheLatestVersion": "최신 버전을 사용 중이십니다",
"warning": "경고",
"exportWarningDesc": "내보낸 파일은 민감한 정보들을 담고 있습니다. 안전하게 보관해주세요.",
"iUnderStand": "알겠습니다",
"@iUnderStand": {
"description": "Text for the button to confirm the user understands the warning"
},
"authToExportCodes": "코드를 내보내려면 인증절차를 거쳐주세요",
"importSuccessTitle": "얏호!",
"importSuccessDesc": "코드 {count} 개를 내보내셨습니다!",
"@importSuccessDesc": {
"placeholders": {
"count": {
"description": "The number of codes imported",
"type": "int",
"example": "1"
}
}
},
"sorry": "죄송합니다",
"importFailureDesc": "선택하신 파일을 분석할 수 없습니다.\n도움이 필요하시다면 support@ente.io로 문의해주세요.",
"pendingSyncs": "경고",
"pendingSyncsWarningBody": "코드 몇 개가 백업되어있지 않습니다.\n\n로그아웃하기 전 해당 코드들을 백업하셨는지 확인해주세요.",
"checkInboxAndSpamFolder": "검증을 위해 메일 보관함 (또는 스팸함) 을 확인해주세요",
"tapToEnterCode": "눌러서 코드 입력하기",
"resendEmail": "이메일 다시 보내기",
"weHaveSendEmailTo": "<green>{email}</green>으로 메일을 보냈습니다",
"@weHaveSendEmailTo": {
"description": "Text to indicate that we have sent a mail to the user",
"placeholders": {
"email": {
"description": "The email address of the user",
"type": "String",
"example": "example@ente.io"
}
}
},
"activeSessions": "활성화된 세션",
"somethingWentWrongPleaseTryAgain": "뭔가 잘못됐습니다, 다시 시도해주세요",
"thisWillLogYouOutOfThisDevice": "이 작업을 하시면 기기에서 로그아웃하게 됩니다!",
"thisWillLogYouOutOfTheFollowingDevice": "이 작업을 하시면 다음 기기에서 로그아웃하게 됩니다:",
"terminateSession": "세션을 종결하시겠습니까?",
"terminate": "종결",
"thisDevice": "이 기기",
"toResetVerifyEmail": "암호를 재설정하시려면, 먼저 이메일을 인증해주세요.",
"thisEmailIsAlreadyInUse": "이 이메일은 이미 사용 중입니다",
"verificationFailedPleaseTryAgain": "검증 실패, 다시 시도해주세요",
"yourVerificationCodeHasExpired": "검증 코드의 유효시간이 경과하였습니다",
"incorrectCode": "잘못된 코드",
"sorryTheCodeYouveEnteredIsIncorrect": "죄송합니다, 입력하신 코드가 맞지 않습니다",
"emailChangedTo": "{newEmail}로 메일이 변경되었습니다",
"authenticationFailedPleaseTryAgain": "인증절차 실패, 다시 시도해주세요",
"authenticationSuccessful": "인증 성공!",
"twofactorAuthenticationSuccessfullyReset": "2FA가 성공적으로 초기화되었습니다",
"incorrectRecoveryKey": "잘못 된 복구 키",
"theRecoveryKeyYouEnteredIsIncorrect": "입력하신 복구 키가 맞지 않습니다",
"enterPassword": "암호 입력",
"selectExportFormat": "내보낼 포맷 선택",
"exportDialogDesc": "내보낸 파일은 선택하신 암호로 암호화됩니다.",
"encrypted": "암호화됨",
"plainText": "평문",
"passwordToEncryptExport": "암호화된 내보내기를 위한 암호",
"export": "내보내기",
"useOffline": "백업 없이 사용",
"signInToBackup": "코드를 백업하시려면 로그인해주세요",
"singIn": "로그인",
"sigInBackupReminder": "복구 가능한 방법을 남겨두기 위해 코드를 내보내세요.",
"offlineModeWarning": "백업 없이 진행하는 것을 선택하셨습니다. 코드의 안전성을 위해 별도의 백업 대책을 마련해주세요.",
"showLargeIcons": "큰 아이콘 보기",
"compactMode": "조밀하게 보기",
"shouldHideCode": "코드 숨기기",
"doubleTapToViewHiddenCode": "코드를 보시려면 해당 란을 더블탭해주세요",
"focusOnSearchBar": "앱 구동시 곧바로 검색하기",
"confirmUpdatingkey": "비밀 키를 업데이트하시겠어요?",
"minimizeAppOnCopy": "복사 후 어플을 최소화하기",
"editCodeAuthMessage": "코드 수정을 위해 인증절차를 거쳐주세요",
"deleteCodeAuthMessage": "코드 삭제를 위해 인증절차를 거쳐주세요",
"showQRAuthMessage": "QR 코드를 보기 위해 인증절차를 거쳐주세요",
"confirmAccountDeleteTitle": "계정 삭제 확인",
"confirmAccountDeleteMessage": "다른 Ente의 서비스를 이용하고 계시다면, 해당 계정은 모두 연결이 되어있습니다.\n\n모든 Ente 서비스에 업로드 된 당신의 데이터는 삭제 수순에 들어가며, 계정은 불가역적으로 삭제됩니다.",
"androidBiometricHint": "신원 확인",
"@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
},
"androidBiometricNotRecognized": "식별할 수 없습니다. 다시 시도해주세요.",
"@androidBiometricNotRecognized": {
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
},
"androidBiometricSuccess": "성공",
"@androidBiometricSuccess": {
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
},
"androidCancelButton": "취소",
"@androidCancelButton": {
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
},
"androidSignInTitle": "인증 필요",
"@androidSignInTitle": {
"description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
},
"androidBiometricRequiredTitle": "생체인증 필요",
"@androidBiometricRequiredTitle": {
"description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
},
"androidDeviceCredentialsRequiredTitle": "장치 자격 증명 필요",
"@androidDeviceCredentialsRequiredTitle": {
"description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters."
},
"androidDeviceCredentialsSetupDescription": "장치 자격 증명 필요",
"@androidDeviceCredentialsSetupDescription": {
"description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side."
},
"goToSettings": "설정으로 가기",
"@goToSettings": {
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
},
"androidGoToSettingsDescription": "기기에 생체인증이 설정되어있지 않습니다. '설정 > 보안'으로 가셔서 생체인증을 설정해주세요.",
"@androidGoToSettingsDescription": {
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
},
"iOSLockOut": "생체인증에 문제가 있습니다. 활성화하시려면 기기를 잠궜다가 다시 풀어주세요.",
"@iOSLockOut": {
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
},
"iOSGoToSettingsDescription": "기기에 생체인증이 설정되어있지 않습니다. 핸드폰에서 Touch ID나 Face ID를 설정해주세요.",
"@iOSGoToSettingsDescription": {
"description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side."
},
"iOSOkButton": "확인",
"@iOSOkButton": {
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
},
"noInternetConnection": "인터넷 연결 없음",
"pleaseCheckYourInternetConnectionAndTryAgain": "인터넷 연결을 확인하시고 다시 시도해주세요.",
"signOutFromOtherDevices": "다른 기기들에서 로그아웃하기",
"signOutOtherBody": "다른 사람이 내 암호를 알 수도 있을 거란 의심이 드신다면, 당신의 계정을 사용 중인 다른 모든 기기에서 로그아웃할 수 있습니다.",
"signOutOtherDevices": "다른 기기들을 로그아웃시키기",
"doNotSignOut": "로그아웃 하지 않기",
"hearUsWhereTitle": "Ente에 대해 어떻게 알게 되셨나요? (선택사항)",
"hearUsExplanation": "저희는 어플 설치 과정을 관찰하지 않습니다. 어디에서 저희를 발견하셨는지 알려주신다면 도움이 될 겁니다!",
"recoveryKeySaved": "다운로드 폴더에 복구 키가 저장되었습니다!",
"waitingForBrowserRequest": "브라우저 요청 대기 중...",
"waitingForVerification": "검증 대기 중...",
"passkey": "패스키",
"passKeyPendingVerification": "검증 절차가 마무리되지 않았습니다",
"loginSessionExpired": "세션 만료됨",
"loginSessionExpiredDetails": "세션이 만료되었습니다. 다시 로그인해주세요.",
"developerSettingsWarning": "정말로 개발자 설정을 수정하시겠습니까?",
"developerSettings": "개발자 설정",
"serverEndpoint": "서버 엔드포인트",
"invalidEndpoint": "유효하지 않은 엔드포인트",
"invalidEndpointMessage": "죄송합니다, 입력하신 엔드포인트가 유효하지 않습니다. 유효한 엔드포인트를 입력하시고 다시 시도해주세요.",
"endpointUpdatedMessage": "엔드포인트가 성공적으로 업데이트됨",
"customEndpoint": "{endpoint}에 접속됨",
"pinText": "핀",
"unpinText": "핀 해제",
"pinnedCodeMessage": "{code}가 핀 되었습니다.",
"unpinnedCodeMessage": "{code}의 핀이 해제되었습니다.",
"tags": "태그",
"createNewTag": "새 태그 만들기",
"tag": "태그",
"create": "만들기",
"editTag": "태그 수정하기",
"deleteTagTitle": "태그를 지우시겠습니까?",
"deleteTagMessage": "태그를 지우시겠습니까? 되돌리실 수 없습니다.",
"somethingWentWrongParsingCode": "{x} 코드를 분석할 수 없습니다.",
"updateNotAvailable": "업데이트 없음",
"viewRawCodes": "원시 코드 보기",
"rawCodes": "원시 코드",
"rawCodeData": "원시 코드 데이터",
"appLock": "어플 잠금",
"noSystemLockFound": "시스템 잠금 찾을 수 없음",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "어플 잠금을 활성화하시려면, 기기의 암호를 만들거나 시스템 설정에서 화면 잠금을 설정해주세요.",
"autoLock": "자동 잠금",
"immediately": "즉시",
"reEnterPassword": "암호 재입력",
"reEnterPin": "핀 재입력",
"next": "다음",
"tooManyIncorrectAttempts": "잘못된 시도 횟수가 너무 많습니다",
"tapToUnlock": "잠금을 해제하려면 누르세요",
"setNewPassword": "새 비밀번호 설정",
"deviceLock": "기기 잠금",
"hideContent": "내용 숨기기",
"hideContentDescriptionAndroid": "어플 전환 화면에서 어플의 내용을 숨기고 스크린샷 촬영을 막습니다",
"hideContentDescriptioniOS": "어플 전환 화면에서 어플의 내용을 숨깁니다",
"autoLockFeatureDescription": "어플이 백그라운드로 넘어가고 잠금 처리되기까지의 시간",
"appLockDescription": "기본 잠금 화면이나, PIN 번호나 암호를 사용한 사용자 설정 잠금 화면 중에 선택하세요.",
"pinLock": "PIN 잠금",
"enterPin": "PIN 번호 입력",
"setNewPin": "새 PIN 번호 설정",
"importFailureDescNew": "선택하신 파일을 분석할 수 없습니다.",
"appLockNotEnabled": "어플 잠금 설정되지 않음",
"appLockNotEnabledDescription": "설정 > 어플 잠금에서 어플 잠금을 활성화해주세요",
"authToViewPasskey": "패스키를 보려면 인증절차를 거쳐주세요",
"appLockOfflineModeWarning": "백업 없이 진행하는 것을 선택하셨습니다. 어플 잠금 방법을 잊어버리신 경우, 데이터에 접근하실 수 없게 됩니다."
}

View File

@@ -6,7 +6,7 @@
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
},
"onBoardingBody": "Saugiai kurkite atsargines 2FA kodų kopijas",
"onBoardingBody": "Saugiai kurkite savo atsargines 2FA kodų kopijas",
"onBoardingGetStarted": "Pradėti",
"setupFirstAccount": "Nustatykite savo pirmąją paskyrą",
"importScanQrCode": "Skenuoti QR kodą",
@@ -154,8 +154,9 @@
"enterCodeHint": "Įveskite 6 skaitmenų kodą\niš autentifikatoriaus programos",
"lostDeviceTitle": "Prarastas įrenginys?",
"twoFactorAuthTitle": "Dvigubas tapatybės nustatymas",
"passkeyAuthTitle": "Slaptarakto patvirtinimas",
"verifyPasskey": "Patvirtinti slaptaraktą",
"passkeyAuthTitle": "Slaptarakčio patvirtinimas",
"verifyPasskey": "Patvirtinti slaptaraktį",
"loginWithTOTP": "Prisijungti su TOTP",
"recoverAccount": "Atkurti paskyrą",
"enterRecoveryKeyHint": "Įveskite atkūrimo raktą",
"recover": "Atkurti",
@@ -299,7 +300,7 @@
},
"authToExportCodes": "Nustatykite tapatybę, kad eksportuotumėte savo kodus",
"importSuccessTitle": "Valio!",
"importSuccessDesc": "Importavote {count} kodų.",
"importSuccessDesc": "Importavote {count} kodus (-ų).",
"@importSuccessDesc": {
"placeholders": {
"count": {
@@ -313,10 +314,10 @@
"importFailureDesc": "Nepavyko išanalizuoti pasirinkto failo.\nJei reikia pagalbos, rašykite adresu support@ente.io.",
"pendingSyncs": "Įspėjimas",
"pendingSyncsWarningBody": "Kai kurių jūsų kodų atsarginės kopijos nebuvo sukurtos.\n\nPrieš atsijungdami įsitikinkite, kad turite atsarginę šių kodų kopiją.",
"checkInboxAndSpamFolder": "Patikrinkite savo gautieją (ir šlamštą), kad užbaigtumėte patvirtinimą",
"checkInboxAndSpamFolder": "Patikrinkite savo gautieją (ir šlamštą), kad užbaigtumėte patvirtinimą.",
"tapToEnterCode": "Palieskite, kad įvestumėte kodą",
"resendEmail": "Iš naujo siųsti el. laišką",
"weHaveSendEmailTo": "Išsiuntėme laišką adresu <green>{email}</green>",
"weHaveSendEmailTo": "Išsiuntėme laišką adresu <green>{email}</green>.",
"@weHaveSendEmailTo": {
"description": "Text to indicate that we have sent a mail to the user",
"placeholders": {
@@ -337,12 +338,12 @@
"toResetVerifyEmail": "Kad iš naujo nustatytumėte slaptažodį, pirmiausia patvirtinkite savo el. paštą.",
"thisEmailIsAlreadyInUse": "Šis el. paštas jau naudojamas.",
"verificationFailedPleaseTryAgain": "Patvirtinimas nepavyko. Bandykite dar kartą.",
"yourVerificationCodeHasExpired": "Jūsų patvirtinimo kodo laikas nebegaliojantis.",
"yourVerificationCodeHasExpired": "Jūsų patvirtinimo kodas nebegaliojantis.",
"incorrectCode": "Neteisingas kodas",
"sorryTheCodeYouveEnteredIsIncorrect": "Atsiprašome, įvestas kodas yra neteisingas.",
"emailChangedTo": "El. paštas pakeistas į {newEmail}",
"authenticationFailedPleaseTryAgain": "Tapatybės nustatymas nepavyko. Bandykite dar kartą.",
"authenticationSuccessful": "Tapatybės nustatymas sėkmingas!",
"authenticationSuccessful": "Tapatybės nustatymas sėkmingas.",
"twofactorAuthenticationSuccessfullyReset": "Dvigubas tapatybės nustatymas sėkmingai iš naujo nustatytas.",
"incorrectRecoveryKey": "Neteisingas atkūrimo raktas",
"theRecoveryKeyYouEnteredIsIncorrect": "Įvestas atkūrimo raktas yra neteisingas.",
@@ -433,7 +434,7 @@
"recoveryKeySaved": "Atkūrimo raktas išsaugotas atsisiuntimų aplanke.",
"waitingForBrowserRequest": "Laukiama naršyklės užklausos...",
"waitingForVerification": "Laukiama patvirtinimo...",
"passkey": "Slaptaraktas",
"passkey": "Slaptaraktis",
"passKeyPendingVerification": "Vis dar laukiama patvirtinimo",
"loginSessionExpired": "Seansas baigėsi",
"loginSessionExpiredDetails": "Jūsų seansas baigėsi. Prisijunkite iš naujo.",
@@ -473,9 +474,9 @@
"setNewPassword": "Nustatykite naują slaptažodį",
"deviceLock": "Įrenginio užraktas",
"hideContent": "Slėpti turinį",
"hideContentDescriptionAndroid": "Paslepia programų turinį programų perjungiklyje ir išjungia ekrano kopijas",
"hideContentDescriptioniOS": "Paslepia programos turinį programos perjungiklyje",
"autoLockFeatureDescription": "Laikas, po kurio programa užrakinama perkėlus ją į foną",
"hideContentDescriptionAndroid": "Paslepia programų turinį programų perjungiklyje ir išjungia ekrano kopijas.",
"hideContentDescriptioniOS": "Paslepia programos turinį programos perjungiklyje.",
"autoLockFeatureDescription": "Laikas, po kurio programa užrakinama perkėlus ją į foną.",
"appLockDescription": "Pasirinkite tarp numatytojo įrenginio užrakinimo ekrano ir pasirinktinio užrakinimo ekrano su PIN kodu arba slaptažodžiu.",
"pinLock": "PIN užraktas",
"enterPin": "Įveskite PIN",
@@ -483,6 +484,6 @@
"importFailureDescNew": "Nepavyko išanalizuoti pasirinkto failo.",
"appLockNotEnabled": "Programos užraktas neįjungtas",
"appLockNotEnabledDescription": "Įjunkite programos užraktą iš Saugumas > Programos užraktas",
"authToViewPasskey": "Nustatykite tapatybę, kad peržiūrėtumėte slaptaraktą",
"authToViewPasskey": "Nustatykite tapatybę, kad peržiūrėtumėte slaptaraktį",
"appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų."
}

View File

@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Uwierzytelnianie dwustopniowe",
"passkeyAuthTitle": "Weryfikacja kluczem dostępu",
"verifyPasskey": "Zweryfikuj klucz dostępu",
"loginWithTOTP": "Zaloguj się za pomocą TOTP",
"recoverAccount": "Odzyskaj konto",
"enterRecoveryKeyHint": "Wprowadź swój klucz odzyskiwania",
"recover": "Odzyskaj",

View File

@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Autenticação de dois fatores",
"passkeyAuthTitle": "Verificação de chave de acesso",
"verifyPasskey": "Verificar chave de acesso",
"loginWithTOTP": "Registrar com TOTP",
"recoverAccount": "Recuperar conta",
"enterRecoveryKeyHint": "Digite a chave de recuperação",
"recover": "Recuperar",
@@ -173,7 +174,7 @@
"invalidEmailTitle": "Endereço de e-mail inválido",
"invalidEmailMessage": "Insira um endereço de e-mail válido.",
"deleteAccount": "Excluir conta",
"deleteAccountQuery": "Estamos tristes com sua decisão. Você encontrou algum problema?",
"deleteAccountQuery": "Estamos tristes por vê-lo sair. Você enfrentou algum problema?",
"yesSendFeedbackAction": "Sim, enviar feedback",
"noDeleteAccountAction": "Não, excluir conta",
"initiateAccountDeleteTitle": "Autentique para iniciar a exclusão de conta",

View File

@@ -19,6 +19,20 @@
"pleaseVerifyDetails": "Prosím, skontrolujte svoje údaje a skúste to znova",
"codeIssuerHint": "Vydavateľ",
"codeSecretKeyHint": "Tajný kľúč",
"secret": "Tajný kľúč",
"all": "Všetko",
"notes": "Poznámky",
"notesLengthLimit": "Maximálna dĺžka poznámky je {count} znakov",
"@notesLengthLimit": {
"description": "Text to indicate the maximum number of characters allowed for notes",
"placeholders": {
"count": {
"description": "The maximum number of characters allowed for notes",
"type": "int",
"example": "100"
}
}
},
"codeAccountHint": "Účet (ty@domena.sk)",
"codeTagHint": "Tag",
"accountKeyType": "Typ kľúča",
@@ -34,6 +48,9 @@
"nextTotpTitle": "ďalej",
"deleteCodeTitle": "Odstrániť položku?",
"deleteCodeMessage": "Naozaj chcete odstrániť položku? Táto akcia je nezvratná.",
"trashCode": "Odstrániť kód?",
"trashCodeMessage": "Ste si istý, že chcete odstrániť kód pre účet {account}?",
"trash": "Odstrániť",
"viewLogsAction": "Zobraziť logy",
"sendLogsDescription": "Toto odošle logy, ktoré nám pomôžu vyriešiť váš problém. Aj keď prijímame preventívne opatrenia, aby sme zabezpečili, že sa citlivé informácie neukladajú do logov, odporúčame vám, aby ste si ich pred zdieľaním pozreli.",
"preparingLogsTitle": "Príprava logov...",
@@ -100,6 +117,7 @@
"emailVerificationToggle": "Overenie pomocou e-mailovej adresy",
"emailVerificationEnableWarning": "Aby ste predišli vymknutiu sa z vášho účtu, nezabudnite pred povolením overenia emailom uložiť kópiu svojho 2FA emailu mimo Ente Auth.",
"authToChangeEmailVerificationSetting": "Pre zmenu overenia pomocou emailu sa musíte overiť",
"authenticateGeneric": "Prosím, overte svoju identitu",
"authToViewYourRecoveryKey": "Pre zobrazenie vášho kľúča na obnovenie sa musíte overiť",
"authToChangeYourEmail": "Pre zmenu vášho emailu sa musíte overiť",
"authToChangeYourPassword": "Pre zmenu vášho hesla sa musíte overiť",
@@ -127,6 +145,8 @@
"leaveFamily": "Opustiť rodinku",
"leaveFamilyMessage": "Ste si istý, že chcete opustiť rodinku?",
"inFamilyPlanMessage": "Ste prihlásený k rodinke!",
"hintForMobile": "Pre úpravu alebo odstránenie kódu podržte.",
"hintForDesktop": "Pre úpravu alebo odstránenie kódu kliknite pravým tlačidlom myši.",
"scan": "Skenovať",
"scanACode": "Skenovať kód",
"verify": "Overiť",
@@ -171,6 +191,7 @@
"security": "Zabezpečenie",
"lockscreen": "Uzamknutie obrazovky",
"authToChangeLockscreenSetting": "Pre zmenu nastavenia uzamknutia obrazovky sa musíte overiť",
"deviceLockEnablePreSteps": "Pre povolenie zámku zariadenia, nastavte prístupový kód zariadenia alebo zámok obrazovky v nastaveniach systému.",
"viewActiveSessions": "Zobraziť aktívne relácie",
"authToViewYourActiveSessions": "Pre zobrazenie vašich aktívnych relácii sa musíte overiť",
"searchHint": "Hľadať...",
@@ -181,6 +202,10 @@
"scanAQrCode": "Naskenovať QR kód",
"enterDetailsManually": "Zadajte údaje manuálne",
"edit": "Upraviť",
"share": "Zdielať",
"shareCodes": "Zdieľať kódy",
"shareCodesDuration": "Zvoľte dobu, počas ktorej chcete zdieľať kódy.",
"restore": "Obnoviť",
"copiedToClipboard": "Skopírované do schránky",
"copiedNextToClipboard": "Skopírovaný následujúci kód do schránky",
"error": "Chyba",
@@ -334,6 +359,7 @@
"sigInBackupReminder": "Exportujte svoje kódy, aby ste sa uistili, že máte zálohu, ktorú môžete neskôr obnoviť.",
"offlineModeWarning": "Rozhodli ste sa pokračovať bez zálohovania. Prosím, vykonávajte pravidelné manuálne zálohy aby ste mali istotu, že kódy nestratíte.",
"showLargeIcons": "Zobraziť veľké ikony",
"compactMode": "Kompaktný mód",
"shouldHideCode": "Skryť kódy",
"doubleTapToViewHiddenCode": "Dvakrát klepnite na položku aby ste zobrazili kód",
"focusOnSearchBar": "Využívať pole vyhľadávania pri spustení aplikácie",
@@ -454,5 +480,9 @@
"pinLock": "Zámok PIN",
"enterPin": "Zadajte PIN",
"setNewPin": "Nastaviť nový PIN",
"importFailureDescNew": "Vybraný súbor nie je možné spracovať."
"importFailureDescNew": "Vybraný súbor nie je možné spracovať.",
"appLockNotEnabled": "Zámok aplikácie nie je povolený",
"appLockNotEnabledDescription": "Prosím, povoľte zámok aplikácie v časti Zabezpečenie > Zámok aplikácie",
"authToViewPasskey": "Pre zobrazenie vášho passkey sa musíte overiť",
"appLockOfflineModeWarning": "Rozhodli ste sa pokračovať bez záloh. Ak zabudnete svoj zámok aplikácie, prístup k údajom bude nemožný."
}

View File

@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Двофакторна аутентифікація",
"passkeyAuthTitle": "Перевірка секретного ключа",
"verifyPasskey": "Підтвердження секретного ключа",
"loginWithTOTP": "Увійти за допомогою TOTP",
"recoverAccount": "Відновити обліковий запис",
"enterRecoveryKeyHint": "Введіть ваш ключ відновлення",
"recover": "Відновлення",

View File

@@ -139,7 +139,7 @@
"enterYourPasswordHint": "Nhập mật khẩu của bạn",
"forgotPassword": "Quên mật khẩu",
"oops": "Rất tiếc",
"suggestFeatures": "Tính năng đề nghị",
"suggestFeatures": "Gợi ý tính năng",
"faq": "Câu hỏi thường gặp",
"somethingWentWrongMessage": "Đã xảy ra lỗi, xin thử lại",
"leaveFamily": "Rời khỏi gia đình",
@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Xác thực hai yếu tố",
"passkeyAuthTitle": "Xác minh mã khóa",
"verifyPasskey": "Xác minh mã khóa",
"loginWithTOTP": "Đăng nhập bằng TOTP",
"recoverAccount": "Khôi phục tài khoản",
"enterRecoveryKeyHint": "Nhập khóa khôi phục của bạn",
"recover": "Khôi phục",
@@ -331,8 +332,8 @@
"somethingWentWrongPleaseTryAgain": "Phát hiện có lỗi, xin thử lại",
"thisWillLogYouOutOfThisDevice": "Thao tác này sẽ đăng xuất bạn khỏi thiết bị này!",
"thisWillLogYouOutOfTheFollowingDevice": "Thao tác này sẽ đăng xuất bạn khỏi thiết bị sau:",
"terminateSession": "Chấm dứt phiên?",
"terminate": "Dừng lại",
"terminateSession": "Kết thúc phiên?",
"terminate": "Kết thúc",
"thisDevice": "Thiết bị này",
"toResetVerifyEmail": "Để đặt lại mật khẩu, vui lòng xác minh email của bạn trước.",
"thisEmailIsAlreadyInUse": "Email này đã được sử dụng",

View File

@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "两步验证",
"passkeyAuthTitle": "通行密钥验证",
"verifyPasskey": "验证通行密钥",
"loginWithTOTP": "使用 TOTP 登录",
"recoverAccount": "恢复账户",
"enterRecoveryKeyHint": "输入您的恢复密钥",
"recover": "恢复",
@@ -181,7 +182,7 @@
"createNewAccount": "创建新账号",
"weakStrength": "弱",
"strongStrength": "强",
"moderateStrength": "中",
"moderateStrength": "中",
"confirmPassword": "请确认密码",
"close": "关闭",
"oopsSomethingWentWrong": "哎呀,出了点问题。",
@@ -263,7 +264,7 @@
"invalidRecoveryKey": "您输入的恢复密钥无效。请确保它包含24个单词并检查每个单词的拼写。\n\n如果您输入了旧的恢复码请确保它长度为64个字符并检查其中每个字符。",
"recreatePasswordTitle": "重新创建密码",
"recreatePasswordBody": "当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码如果您愿意可以再次使用相同的密码。",
"invalidKey": "无效的密钥",
"invalidKey": "密钥无效",
"tryAgain": "请再试一次",
"viewRecoveryKey": "查看恢复密钥",
"confirmRecoveryKey": "确认恢复密钥",
@@ -293,7 +294,7 @@
"youAreOnTheLatestVersion": "当前为最新版本",
"warning": "警告",
"exportWarningDesc": "导出的文件包含敏感信息。请安全存储。",
"iUnderStand": "我明白了",
"iUnderStand": "了",
"@iUnderStand": {
"description": "Text for the button to confirm the user understands the warning"
},
@@ -374,7 +375,7 @@
"@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
},
"androidBiometricNotRecognized": "未能识别。再试一次。",
"androidBiometricNotRecognized": "未能识别,请重试。",
"@androidBiometricNotRecognized": {
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
},
@@ -418,7 +419,7 @@
"@iOSGoToSettingsDescription": {
"description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side."
},
"iOSOkButton": "好",
"iOSOkButton": "好",
"@iOSOkButton": {
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
},
@@ -434,7 +435,7 @@
"waitingForBrowserRequest": "正在等待浏览器请求...",
"waitingForVerification": "等待验证...",
"passkey": "通行密钥",
"passKeyPendingVerification": "仍需进行验证",
"passKeyPendingVerification": "仍需验证",
"loginSessionExpired": "会话已过期",
"loginSessionExpiredDetails": "您的会话已过期。请重新登录。",
"developerSettingsWarning": "您确定要修改开发者设置吗?",

View File

@@ -59,21 +59,22 @@ class TagChip extends StatelessWidget {
),
child: Row(
children: [
if (iconData != null) ...[
if (iconData != null)
Icon(
iconData,
size: 16,
size: label.isNotEmpty ? 16 : 20,
color: color,
),
if (iconData != null && label.isNotEmpty)
const SizedBox(width: 8),
],
Text(
label,
style: TextStyle(
color: color,
fontSize: 14,
if (label.isNotEmpty)
Text(
label,
style: TextStyle(
color: color,
fontSize: 14,
),
),
),
],
),
),

View File

@@ -2,6 +2,14 @@ import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/icons_changed_event.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum CodeSortKey {
issuerName,
accountName,
mostFrequentlyUsed,
recentlyUsed,
manual,
}
class PreferenceService {
PreferenceService._privateConstructor();
static final PreferenceService instance =
@@ -28,6 +36,15 @@ class PreferenceService {
}
}
CodeSortKey codeSortKey() {
return CodeSortKey
.values[_prefs.getInt("codeSortKey") ?? CodeSortKey.manual.index];
}
Future<void> setCodeSortKey(CodeSortKey key) async {
await _prefs.setInt("codeSortKey", key.index);
}
Future<void> setHasShownCoachMark(bool value) {
return _prefs.setBool(kHasShownCoachMarkKey, value);
}

View File

@@ -380,7 +380,8 @@ class UserService {
Widget page;
final String passkeySessionID = response.data["passkeySessionID"];
String twoFASessionID = response.data["twoFactorSessionID"];
if (twoFASessionID.isEmpty) {
if (twoFASessionID.isEmpty &&
response.data["twoFactorSessionIDV2"] != null) {
twoFASessionID = response.data["twoFactorSessionIDV2"];
}
if (passkeySessionID.isNotEmpty) {
@@ -692,7 +693,8 @@ class UserService {
Widget? page;
final String passkeySessionID = response.data["passkeySessionID"];
String twoFASessionID = response.data["twoFactorSessionID"];
if (twoFASessionID.isEmpty) {
if (twoFASessionID.isEmpty &&
response.data["twoFactorSessionIDV2"] != null) {
twoFASessionID = response.data["twoFactorSessionIDV2"];
}
Configuration.instance.setVolatilePassword(userPassword);

View File

@@ -25,22 +25,42 @@ class CodeStore {
}
Future<bool> saveUpadedIndexes(List<Code> codes) async {
int changedCount = 0;
final existingAllCodes = await getAllCodes();
final existingPosition = {};
for (final code in existingAllCodes) {
if (code.hasError || code.isTrashed) {
continue;
}
existingPosition[code.generatedID] = code.display.position;
}
for (final code in codes) {
if (code.hasError || code.isTrashed) {
continue;
}
Code? c = _cacheCodes[code.generatedID];
if (c == null) {
int? oldIndex = existingPosition[code.generatedID];
if (oldIndex == null) {
continue;
}
int oldIndex = c.display.position;
int newIndex = codes.indexOf(code);
if (oldIndex != newIndex) {
Code updatedCode =
c.copyWith(display: c.display.copyWith(position: newIndex));
await addCode(updatedCode);
code.copyWith(display: code.display.copyWith(position: newIndex));
await addCode(
updatedCode,
shouldSync: false,
existingAllCodes: existingAllCodes,
);
changedCount++;
}
}
_logger.info("changedCount index for $changedCount codes");
if (changedCount > 0 &&
_authenticatorService.getAccountMode() == AccountMode.online) {
_authenticatorService.onlineSync().ignore();
}
return true;
}
@@ -115,9 +135,10 @@ class CodeStore {
Code code, {
bool shouldSync = true,
AccountMode? accountMode,
List<Code>? existingAllCodes,
}) async {
final mode = accountMode ?? _authenticatorService.getAccountMode();
final allCodes = await getAllCodes(accountMode: mode);
final allCodes = existingAllCodes ?? (await getAllCodes(accountMode: mode));
bool isExistingCode = false;
bool hasSameCode = false;
for (final existingCode in allCodes) {

View File

@@ -25,18 +25,19 @@ import 'package:ente_auth/utils/totp_util.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:logging/logging.dart';
import 'package:move_to_background/move_to_background.dart';
class CodeWidget extends StatefulWidget {
final Code code;
final bool isCompactMode;
final CodeSortKey? sortKey;
const CodeWidget(
this.code, {
super.key,
required this.isCompactMode,
this.sortKey,
});
@override
@@ -113,20 +114,27 @@ class _CodeWidgetState extends State<CodeWidget> {
final l10n = context.l10n;
Widget getCardContents(AppLocalizations l10n) {
// final bool isFavorite = widget.code.isPinned;
return Stack(
children: [
if (widget.code.isPinned)
Align(
alignment: Alignment.topRight,
child: CustomPaint(
painter: PinBgPainter(
color: colorScheme.pinnedBgColor,
),
size: widget.isCompactMode
? const Size(24, 24)
: const Size(39, 39),
),
),
// if (isFavorite)
// Align(
// alignment: Alignment.topRight,
// child: Icon(
// Icons.star,
// color: colorScheme.pinnedBgColor,
// size: widget.isCompactMode ? 16 : 24,
// ),
// //
// // child: CustomPaint(
// // painter: PinBgPainter(
// // color: colorScheme.pinnedBgColor,
// // ),
// // size: widget.isCompactMode
// // ? const Size(24, 24)
// // : const Size(39, 39),
// // ),
// ),
if (widget.code.isTrashed && kDebugMode)
Align(
alignment: Alignment.topLeft,
@@ -173,13 +181,11 @@ class _CodeWidgetState extends State<CodeWidget> {
Align(
alignment: Alignment.topRight,
child: Padding(
padding: widget.isCompactMode
? const EdgeInsets.only(right: 4, top: 4)
: const EdgeInsets.only(right: 6, top: 6),
child: SvgPicture.asset(
"assets/svg/pin-card.svg",
width: widget.isCompactMode ? 8 : null,
height: widget.isCompactMode ? 8 : null,
padding: const EdgeInsets.only(top: 2, right: 2),
child: Icon(
Icons.star,
color: colorScheme.primary700.withOpacity(0.4),
size: widget.isCompactMode ? 12 : 20,
),
),
),
@@ -272,11 +278,10 @@ class _CodeWidgetState extends State<CodeWidget> {
),
if (!widget.code.isTrashed)
MenuItem(
label:
widget.code.isPinned ? l10n.unpinText : l10n.pinText,
label: widget.code.isPinned ? l10n.unfav : l10n.fav,
icon: widget.code.isPinned
? Icons.push_pin
: Icons.push_pin_outlined,
? Icons.star_border
: Icons.star_border_outlined,
onSelected: () => _onPinPressed(null),
),
if (!widget.code.isTrashed)
@@ -454,11 +459,12 @@ class _CodeWidgetState extends State<CodeWidget> {
);
}
void _copyCurrentOTPToClipboard() async {
void _copyCurrentOTPToClipboard() {
_copyToClipboard(
_getCurrentOTP(),
confirmationMessage: context.l10n.copiedToClipboard,
);
_udateCodeMetadata().ignore();
}
void _copyNextToClipboard() {
@@ -466,6 +472,26 @@ class _CodeWidgetState extends State<CodeWidget> {
_getNextTotp(),
confirmationMessage: context.l10n.copiedNextToClipboard,
);
_udateCodeMetadata().ignore();
}
Future<void> _udateCodeMetadata() async {
if (widget.sortKey == null) return;
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
if (widget.sortKey == CodeSortKey.mostFrequentlyUsed ||
widget.sortKey == CodeSortKey.recentlyUsed) {
final display = widget.code.display;
final Code code = widget.code.copyWith(
display: display.copyWith(
tapCount: display.tapCount + 1,
lastUsedAt: DateTime.now().microsecondsSinceEpoch,
),
);
unawaited(CodeStore.instance.addCode(code));
}
}
});
}
void _copyToClipboard(
@@ -579,8 +605,8 @@ class _CodeWidgetState extends State<CodeWidget> {
(value) => showToast(
context,
!currentlyPinned
? context.l10n.pinnedCodeMessage(widget.code.issuer)
: context.l10n.unpinnedCodeMessage(widget.code.issuer),
? context.l10n.favoritedCodeMessage(widget.code.issuer)
: context.l10n.unfavoritedCodeMessage(widget.code.issuer),
),
),
);

View File

@@ -16,7 +16,7 @@ class IconButtonWidget extends StatefulWidget {
final Color? defaultColor;
final Color? pressedColor;
final Color? iconColor;
const IconButtonWidget({
const IconButtonWidget({
super.key,
required this.icon,
required this.iconButtonType,

View File

@@ -58,9 +58,9 @@ class _CodeSelectionActionsWidgetState
items.add(
SelectionActionButton(
labelText: widget.code.isPinned
? context.l10n.unpinText
: context.l10n.pinText,
icon: widget.code.isPinned ? Icons.push_pin : Icons.push_pin_outlined,
? context.l10n.unfav
: context.l10n.fav,
icon: widget.code.isPinned ? Icons.star_outline : Icons.star,
onTap: widget.onPin,
),
);

View File

@@ -31,6 +31,7 @@ import 'package:ente_auth/ui/home/speed_dial_label_widget.dart';
import 'package:ente_auth/ui/reorder_codes_page.dart';
import 'package:ente_auth/ui/scanner_page.dart';
import 'package:ente_auth/ui/settings_page.dart';
import 'package:ente_auth/ui/sort_option_menu.dart';
import 'package:ente_auth/ui/tools/app_lock.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
@@ -79,11 +80,16 @@ class _HomePageState extends State<HomePage> {
bool hasTrashedCodes = false;
bool hasNonTrashedCodes = false;
bool isCompactMode = false;
bool _isFavouriteOpen = false;
bool hasFavouriteCodes = false;
bool hasNonFavouriteCodes = false;
late CodeSortKey _codeSortKey;
@override
void initState() {
super.initState();
_textController.addListener(_applyFilteringAndRefresh);
_codeSortKey = PreferenceService.instance.codeSortKey();
_loadCodes();
_streamSubscription = Bus.instance.on<CodesUpdatedEvent>().listen((event) {
_loadCodes();
@@ -92,6 +98,7 @@ class _HomePageState extends State<HomePage> {
Bus.instance.on<TriggerLogoutEvent>().listen((event) async {
await autoLogoutAlert(context);
});
_initDeepLinks();
Future.delayed(
const Duration(seconds: 1),
@@ -110,13 +117,26 @@ class _HomePageState extends State<HomePage> {
_allCodes = codes;
hasTrashedCodes = false;
hasNonTrashedCodes = false;
hasNonFavouriteCodes = false;
hasFavouriteCodes = false;
for (final c in _allCodes ?? []) {
if (c.isTrashed) {
hasTrashedCodes = true;
} else {
hasNonTrashedCodes = true;
}
if (hasNonTrashedCodes && hasTrashedCodes) {
if (!c.isTrashed) {
if (c.isPinned) {
hasFavouriteCodes = true;
} else {
hasNonFavouriteCodes = true;
}
}
if (hasTrashedCodes &&
hasNonTrashedCodes &&
hasFavouriteCodes &&
hasNonFavouriteCodes) {
break;
}
}
@@ -126,6 +146,12 @@ class _HomePageState extends State<HomePage> {
if (!hasNonTrashedCodes && hasTrashedCodes) {
_isTrashOpen = true;
}
if (!hasFavouriteCodes) {
_isFavouriteOpen = false;
}
if (!hasNonFavouriteCodes && hasFavouriteCodes) {
_isFavouriteOpen = true;
}
CodeDisplayStore.instance.getAllTags(allCodes: _allCodes).then((value) {
tags = value;
@@ -156,7 +182,8 @@ class _HomePageState extends State<HomePage> {
if (codeState.hasError ||
selectedTag != "" &&
!codeState.display.tags.contains(selectedTag) ||
(codeState.isTrashed != _isTrashOpen)) {
(codeState.isTrashed != _isTrashOpen) ||
(codeState.isPinned != _isFavouriteOpen && val.isEmpty)) {
continue;
}
@@ -175,6 +202,14 @@ class _HomePageState extends State<HomePage> {
)
.toList() ??
[];
} else if (_isFavouriteOpen) {
_filteredCodes = _allCodes
?.where(
(element) =>
!element.hasError && !element.isTrashed && element.isPinned,
)
.toList() ??
[];
} else {
_filteredCodes = _allCodes
?.where(
@@ -188,8 +223,7 @@ class _HomePageState extends State<HomePage> {
[];
}
_filteredCodes
.sort((a, b) => a.display.position.compareTo(b.display.position));
sortFilteredCodes(_filteredCodes, _codeSortKey);
if (mounted) {
setState(() {});
@@ -208,6 +242,40 @@ class _HomePageState extends State<HomePage> {
super.dispose();
}
void sortFilteredCodes(List<Code> codes, CodeSortKey sortKey) {
switch (sortKey) {
case CodeSortKey.issuerName:
codes.sort((a, b) => a.issuer.compareTo(b.issuer));
break;
case CodeSortKey.accountName:
codes.sort((a, b) => a.account.compareTo(b.account));
break;
case CodeSortKey.mostFrequentlyUsed:
codes.sort((a, b) => b.display.tapCount.compareTo(a.display.tapCount));
break;
case CodeSortKey.recentlyUsed:
codes.sort(
(a, b) => b.display.lastUsedAt.compareTo(a.display.lastUsedAt),
);
break;
case CodeSortKey.manual:
default:
codes.sort((a, b) => a.display.position.compareTo(b.display.position));
break;
}
if (sortKey != CodeSortKey.manual) {
// move pinned codes to the using
int insertIndex = 0;
for (int i = 0; i < codes.length; i++) {
if (codes[i].isPinned) {
final code = codes.removeAt(i);
codes.insert(insertIndex, code);
insertIndex++;
}
}
}
}
Future<void> _redirectToScannerPage() async {
final Code? code = await Navigator.of(context).push(
MaterialPageRoute(
@@ -261,10 +329,15 @@ class _HomePageState extends State<HomePage> {
}
Future<void> navigateToReorderPage(List<Code> allCodes) async {
List<Code> sortCandidate = allCodes
.where((element) => !element.hasError && !element.isTrashed)
.toList();
sortCandidate
.sort((a, b) => a.display.position.compareTo(b.display.position));
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return ReorderCodesPage(codes: _filteredCodes);
return ReorderCodesPage(codes: sortCandidate);
},
),
).then((value) {
@@ -327,11 +400,19 @@ class _HomePageState extends State<HomePage> {
),
centerTitle: PlatformUtil.isDesktop() ? false : true,
actions: <Widget>[
IconButton(
icon: const Icon(Icons.edit),
tooltip: l10n.edit,
onPressed: () {
navigateToReorderPage(_allCodes!);
SortCodeMenuWidget(
currentKey: PreferenceService.instance.codeSortKey(),
onSelected: (newOrder) async {
await PreferenceService.instance.setCodeSortKey(newOrder);
if (newOrder == CodeSortKey.manual && newOrder == _codeSortKey) {
await navigateToReorderPage(_allCodes!);
}
setState(() {
_codeSortKey = newOrder;
});
if (mounted) {
_applyFilteringAndRefresh();
}
},
),
PlatformUtil.isDesktop()
@@ -362,10 +443,7 @@ class _HomePageState extends State<HomePage> {
_searchText = _textController.text;
// Request focus on the search box
// For Windows and macOS only for now. This if statement can be removed if other platforms has been tested.
if (Platform.isWindows || Platform.isMacOS) {
searchBoxFocusNode.requestFocus();
}
searchBoxFocusNode.requestFocus();
}
_applyFilteringAndRefresh();
},
@@ -396,7 +474,8 @@ class _HomePageState extends State<HomePage> {
_allCodes?.firstWhereOrNull((element) => element.hasError) != null;
final indexOffset = anyCodeHasError ? 1 : 0;
final itemCount = (hasNonTrashedCodes ? tags.length + 1 : 0) +
(hasTrashedCodes ? 1 : 0);
(hasTrashedCodes ? 1 : 0) +
(hasFavouriteCodes ? 1 : 0);
final list = Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -415,17 +494,36 @@ class _HomePageState extends State<HomePage> {
if (index == 0 && hasNonTrashedCodes) {
return TagChip(
label: l10n.all,
state: selectedTag == "" && _isTrashOpen == false
state: selectedTag == "" &&
_isTrashOpen == false &&
_isFavouriteOpen == false
? TagChipState.selected
: TagChipState.unselected,
onTap: () {
selectedTag = "";
_isTrashOpen = false;
_isFavouriteOpen = false;
setState(() {});
_applyFilteringAndRefresh();
},
);
}
if (index == 1 && hasFavouriteCodes) {
return TagChip(
label: context.l10n.favorites,
state: _isFavouriteOpen
? TagChipState.selected
: TagChipState.unselected,
onTap: () {
selectedTag = "";
_isTrashOpen = false;
_isFavouriteOpen = !_isFavouriteOpen;
setState(() {});
_applyFilteringAndRefresh();
},
// iconData: Icons.star,
);
}
if (index == itemCount - 1 && hasTrashedCodes) {
return TagChip(
label: l10n.trash,
@@ -435,31 +533,38 @@ class _HomePageState extends State<HomePage> {
onTap: () {
selectedTag = "";
_isTrashOpen = !_isTrashOpen;
_isFavouriteOpen = false;
setState(() {});
_applyFilteringAndRefresh();
},
iconData: Icons.delete,
);
}
return TagChip(
label: tags[index - 1],
action: TagChipAction.menu,
state: selectedTag == tags[index - 1]
? TagChipState.selected
: TagChipState.unselected,
onTap: () {
_isTrashOpen = false;
if (selectedTag == tags[index - 1]) {
selectedTag = "";
final customTagIndex =
hasFavouriteCodes ? index - 2 : index - 1;
if (customTagIndex >= 0 && customTagIndex < tags.length) {
return TagChip(
label: tags[customTagIndex],
action: TagChipAction.menu,
state: selectedTag == tags[customTagIndex]
? TagChipState.selected
: TagChipState.unselected,
onTap: () {
_isTrashOpen = false;
_isFavouriteOpen = false;
if (selectedTag == tags[customTagIndex]) {
selectedTag = "";
setState(() {});
_applyFilteringAndRefresh();
return;
}
selectedTag = tags[customTagIndex];
setState(() {});
_applyFilteringAndRefresh();
return;
}
selectedTag = tags[index - 1];
setState(() {});
_applyFilteringAndRefresh();
},
);
},
);
}
return const SizedBox.shrink();
},
),
),
@@ -488,6 +593,7 @@ class _HomePageState extends State<HomePage> {
key: ValueKey('${code.hashCode}_$newIndex'),
code,
isCompactMode: isCompactMode,
sortKey: _codeSortKey,
),
);
}),

View File

@@ -1,4 +1,5 @@
import 'dart:ui';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/store/code_store.dart';
@@ -34,7 +35,7 @@ class _ReorderCodesPageState extends State<ReorderCodesPage> {
},
child: Scaffold(
appBar: AppBar(
title: const Text("Edit Codes"),
title: Text(context.l10n.editOrder),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () async {
@@ -44,66 +45,6 @@ class _ReorderCodesPageState extends State<ReorderCodesPage> {
}
},
),
actions: [
PopupMenuButton(
icon: const Icon(Icons.sort),
onSelected: (int value) {
selectedSortOption = value;
switch (value) {
case 0:
sortByIssuer();
break;
case 1:
sortByAccount();
break;
case 2:
setState(() {});
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
selectedSortOption == 0
? const Icon(Icons.check)
: const SizedBox.square(dimension: 24),
const SizedBox(width: 10),
const Text("Issuer"),
],
),
),
PopupMenuItem(
value: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
selectedSortOption == 1
? const Icon(Icons.check)
: const SizedBox.square(dimension: 24),
const SizedBox(width: 10),
const Text("Account"),
],
),
),
PopupMenuItem(
value: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
selectedSortOption == 2
? const Icon(Icons.check)
: const SizedBox.square(dimension: 24),
const SizedBox(width: 10),
const Text("Manual"),
],
),
),
],
),
],
),
body: ReorderableListView(
buildDefaultDragHandles: false,
@@ -158,14 +99,4 @@ class _ReorderCodesPageState extends State<ReorderCodesPage> {
widget.codes.insert(newIndex, code);
});
}
void sortByIssuer() {
widget.codes.sort((a, b) => a.issuer.compareTo(b.issuer));
setState(() {});
}
void sortByAccount() {
widget.codes.sort((a, b) => a.account.compareTo(b.account));
setState(() {});
}
}

View File

@@ -0,0 +1,80 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/preference_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class SortCodeMenuWidget extends StatelessWidget {
final CodeSortKey currentKey;
final void Function(CodeSortKey) onSelected;
const SortCodeMenuWidget({
super.key,
required this.currentKey,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
Text sortOptionText(CodeSortKey key) {
String text = key.toString();
switch (key) {
case CodeSortKey.issuerName:
text = context.l10n.codeIssuerHint;
break;
case CodeSortKey.accountName:
text = context.l10n.account;
break;
case CodeSortKey.mostFrequentlyUsed:
text = context.l10n.mostFrequentlyUsed;
break;
case CodeSortKey.recentlyUsed:
text = context.l10n.mostRecentlyUsed;
break;
case CodeSortKey.manual:
text = context.l10n.manualSort;
}
return Text(
text,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
color: Theme.of(context).iconTheme.color!.withOpacity(0.7),
),
);
}
return GestureDetector(
onTapDown: (TapDownDetails details) async {
final int? selectedValue = await showMenu<int>(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy + 300,
),
items: List.generate(CodeSortKey.values.length, (index) {
return PopupMenuItem(
value: index,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
sortOptionText(CodeSortKey.values[index]),
if (CodeSortKey.values[index] == currentKey)
Icon(
CodeSortKey.values[index] == CodeSortKey.manual
? Icons.mode_edit
: Icons.check,
color: Theme.of(context).iconTheme.color,
),
],
),
);
}),
);
if (selectedValue != null) {
onSelected(CodeSortKey.values[selectedValue]);
}
},
child: const Icon(Icons.sort_outlined),
);
}
}

View File

@@ -64,16 +64,22 @@ class DirectoryUtils {
),
);
Directory oldDataDir;
Directory newDataDir;
Directory? tempDir;
Directory newDataDir = await getApplicationSupportDirectory();
await newDataDir.create(recursive: true);
if (Platform.isLinux) {
oldDataDir = Directory(
p.join(dataHome.path, "ente_auth"),
);
tempDir = Directory(
Directory tempDir = Directory(
p.join(dataHome.path, "enteauth"),
);
if (tempDir.existsSync()) {
oldDataDir = tempDir;
}
if (await oldDataDir.exists()) {
await copyPath(oldDataDir.path, newDataDir.path);
}
} else if (Platform.isWindows) {
oldDataDir = Directory(
p.join(
@@ -81,12 +87,27 @@ class DirectoryUtils {
"ente",
),
);
tempDir = Directory(
Directory tempDir = Directory(
p.join(
(await getApplicationDocumentsDirectory()).path,
"enteauth",
),
);
if (tempDir.existsSync()) {
oldDataDir = tempDir;
databaseFile = File(
p.join(
tempDir.path,
".ente.authenticator.db",
),
);
offlineDatabaseFile = File(
p.join(
tempDir.path,
".ente.offline_authenticator.db",
),
);
}
} else {
oldDataDir = await getApplicationDocumentsDirectory();
databaseFile = File(
@@ -103,12 +124,6 @@ class DirectoryUtils {
);
}
if (tempDir?.existsSync() ?? false) {
oldDataDir = tempDir!;
}
newDataDir = await getApplicationSupportDirectory();
await newDataDir.create(recursive: true);
final prefix = Platform.isMacOS ? "" : ".";
File newDatabaseFile =
File(p.join(newDataDir.path, "${prefix}ente.authenticator.db"));
@@ -124,10 +139,6 @@ class DirectoryUtils {
await offlineDatabaseFile.copy(newOfflineDatabaseFile.path);
}
if (Platform.isLinux && await oldDataDir.exists()) {
await copyPath(oldDataDir.path, newDataDir.path);
}
sharedPrefs.setBool(migratedNamingChanges, true).ignore();
} catch (e, st) {
logger.warning("Migrating Database failed!", e, st);

View File

@@ -4,8 +4,6 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- desktop_webview_window (0.0.1):
- FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- file_saver (0.0.1):
@@ -74,7 +72,6 @@ PODS:
DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
- desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
@@ -107,8 +104,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
connectivity_plus:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
desktop_webview_window:
:path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_saver:
@@ -153,7 +148,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b

View File

@@ -1,6 +1,6 @@
name: ente_auth
description: ente two-factor authenticator
version: 4.1.1+411
version: 4.1.4+414
publish_to: none
environment:

View File

@@ -1,72 +0,0 @@
package cmd
import (
"fmt"
"os"
"net/url"
"strings"
"github.com/spf13/viper"
"github.com/spf13/cobra"
"github.com/ente-io/cli/pkg"
)
var setupCmd = &cobra.Command {
Use: "setup",
Short: "Manage setup/configuration settings",
}
// Command to set the public albums url in configurations/<environment>.yaml file
// Reads value from environment variable "ENTE_MUSEUM_DIR"
var addPublicAlbumsUrl = &cobra.Command {
Use: "public-albums-url account add-public-url [url]",
Short: "Set the public-albums URL in Museum YAML configuraiton file",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Get environment to make changes to valid file
environment := os.Getenv("ENVIRONMENT")
if environment == "" {
environment = "local"
}
dir := pkg.ConfigureServerDir()
// Adding path to the museum config file
viper.AddConfigPath(dir)
viper.SetConfigName(environment)
viper.SetConfigType("yaml")
err := viper.ReadInConfig()
if err != nil {
fmt.Errorf("%v", err)
return
}
albumsUrl := strings.TrimSuffix(args[0], "/")
_, err = url.ParseRequestURI(albumsUrl)
if err != nil {
// Report Error and exit
fmt.Printf("Invalid URL: %s\nPlease enter a valid endpoint for public albums", err)
os.Exit(1)
} else {
viper.Set("apps.public-albums", albumsUrl)
}
// Overwrite the public album url in Configuration File
err = viper.WriteConfig()
if err != nil {
fmt.Println("Error saving config: %v\n", err)
} else {
fmt.Println("Public Albums URL set to", albumsUrl)
}
},
}
func init() {
// `ente setup` will be the initial root command
rootCmd.AddCommand(setupCmd)
rootCmd.Flags().String("public-albums-url", "", "Sets the public-albums url in your environments configuration file")
setupCmd.AddCommand(addPublicAlbumsUrl)
}

View File

@@ -41,17 +41,3 @@ func GetCLITempPath() (string) {
}
return os.TempDir()
}
// Configure Museum Server Directory to find proper <environment.yaml> file.
// Made for and used in command `ente account public-albums-url [url]`
func ConfigureServerDir() (string) {
serverEnv := os.Getenv("ENTE_SERVER_DIR")
if serverEnv != "" {
fmt.Errorf(`ENTE_SERVER_DIR environment is not set, please setup it with\n
export ENTE_SERVER_DIR=/path/to/ente/
`)
}
configDir := filepath.Join(serverEnv, "server", "configurations")
return configDir
}

View File

@@ -64,15 +64,6 @@ videos that you imported. The modifications (e.g. date changes) you make within
Ente will be written into a separate metadata JSON file during export so as to
not modify the original.
> [!WARNING]
>
> There used to be one exception to this - for JPEG files, the Exif
> DateTimeOriginal was changed during export from web or desktop apps. This was
> done on a customer request, but in hindsight this was an incorrect change.
>
> We have deprecated this behaviour, and the desktop version 1.7.6 is going to
> be the last version with this exception.
As an example: suppose you have `flower.png`. When you export your library, you
will end up with:

View File

@@ -41,3 +41,7 @@ The first user you create on your instance is treated as an admin.
If you want, you can modify this behaviour by providing an explicit list of
admins in the [configuration](/self-hosting/guides/admin#becoming-an-admin).
### Can I disable registration of new accounts on my self hosted instance?
Yes. See `internal.disable-registration` in local.yaml.

View File

@@ -72,6 +72,7 @@ services:
- ./.credentials.env
volumes:
- custom-logs:/var/logs
- museum.yaml:/museum.yaml:ro
networks:
- internal
@@ -199,22 +200,33 @@ ENTE_KEY_ENCRYPTION=
ENTE_KEY_HASH=
ENTE_JWT_SECRET=
ENTE_S3_B2-EU-CEN_KEY=YOUR_S3_KEY
ENTE_S3_B2-EU-CEN_SECRET=YOUR_S3_SECRET
ENTE_S3_B2-EU-CEN_ENDPOINT=YOUR_S3_ENDPOINT
ENTE_S3_B2-EU-CEN_REGION=YOUR_S3_REGION
ENTE_S3_B2-EU-CEN_BUCKET=YOUR_S3_BUCKET
ENTE_S3_ARE_LOCAL_BUCKETS=false
ENTE_INTERNAL_HARDCODED-OTT_LOCAL-DOMAIN-SUFFIX="@example.com"
ENTE_INTERNAL_HARDCODED-OTT_LOCAL-DOMAIN-VALUE=123456
# if you deploy it on a server under a domain, you need to set the correct value of the following variables
# it can be changed later
# The backend server URL (Museum) to be used by the webapp
ENDPOINT=http://localhost:8080
# The URL of the public albums webapp (also need to be updated in museum.yml so the correct links are generated)
ALBUMS_ENDPOINT=http://localhost:8082
# This is used to generate sharable URLs
ENTE_APPS_PUBLIC-ALBUMS=http://localhost:8082
```
Create the `museum.yaml` with additional configuration, this will be mounted
(read-only) into the container:
```yaml
s3:
are_local_buckets: false
# For some self-hosted S3 deployments you (e.g. Minio) you might need to disable bucket subdomains
use_path_style_urls: true
# The key must be named like so
b2-eu-cen:
key: $YOUR_S3_KEY
secret: $YOUR_S3_SECRET
endpoint: $YOUR_S3_ENDPOINT
region: $YOUR_S3_REGION
bucket: $YOUR_S3_BUCKET_NAME
# The same value as the one specified in ALBUMS_ENDPOINT
apps:
public-albums: http://localhost:8082
```
## 3. Run `docker-compose up`

View File

@@ -1,59 +0,0 @@
const List<String> connectWords = [
'a', 'an', 'the', // Articles
'about', 'above', 'across', 'after', 'against', 'along', 'amid', 'among',
'around', 'as', 'at', 'before', 'behind', 'below', 'beneath', 'beside',
'between', 'beyond', 'by', 'concerning', 'considering', 'despite', 'down',
'during', 'except', 'for', 'from', 'in', 'inside', 'into', 'like', 'near',
'of', 'off', 'on', 'onto', 'out', 'outside', 'over', 'past', 'regarding',
'round', 'since', 'through', 'to', 'toward', 'under', 'underneath', 'until',
'unto', 'up', 'upon', 'with', 'within', 'without', // Prepositions
'and', 'as', 'because', 'but', 'for', 'if', 'nor', 'or', 'since', 'so',
'that', 'though', 'unless', 'until', 'when', 'whenever', 'where', 'whereas',
'wherever', 'while', 'yet', // Conjunctions
'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them',
'my', 'your', 'his', 'its', 'our', 'their', 'mine', 'yours', 'hers', 'ours',
'theirs', 'who', 'whom', 'whose', 'which', 'what', // Pronouns
'am', 'is', 'are', 'was', 'were', 'be', 'being', 'been', 'have', 'has', 'had',
'do', 'does', 'did', 'will', 'would', 'shall', 'should', 'can', 'could',
'may', 'might', 'must', // Auxiliary Verbs
];
extension StringExtensionsNullSafe on String? {
int get sumAsciiValues {
if (this == null) {
return -1;
}
int sum = 0;
for (int i = 0; i < this!.length; i++) {
sum += this!.codeUnitAt(i);
}
return sum;
}
}
extension DescriptionString on String? {
bool get isAllConnectWords {
if (this == null) {
throw AssertionError("String cannot be null");
}
final subDescWords = this!.split(" ");
return subDescWords.every(
(subDescWord) => connectWords.any(
(connectWord) => subDescWord.toLowerCase() == connectWord,
),
);
}
bool get isLastWordConnectWord {
if (this == null) {
throw AssertionError("String cannot be null");
}
final subDescWords = this!.split(" ");
return connectWords
.any((element) => element == subDescWords.last.toLowerCase());
}
}

View File

@@ -688,7 +688,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Geteiltes Album löschen?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"Dieses Album wird für alle gelöscht\n\nDu wirst den Zugriff auf geteilte Fotos in diesem Album, die anderen gehören, verlieren"),
"descriptions": MessageLookupByLibrary.simpleMessage("Beschreibungen"),
"deselectAll": MessageLookupByLibrary.simpleMessage("Alle abwählen"),
"designedToOutlive":
MessageLookupByLibrary.simpleMessage("Entwickelt um zu bewahren"),

View File

@@ -671,7 +671,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Delete shared album?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"The album will be deleted for everyone\n\nYou will lose access to shared photos in this album that are owned by others"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll": MessageLookupByLibrary.simpleMessage("Deselect all"),
"designedToOutlive":
MessageLookupByLibrary.simpleMessage("Designed to outlive"),
@@ -1091,7 +1090,9 @@ class MessageLookup extends MessageLookupByLibrary {
"Magic search allows to search photos by their contents, e.g. \'flower\', \'red car\', \'identity documents\'"),
"manage": MessageLookupByLibrary.simpleMessage("Manage"),
"manageDeviceStorage":
MessageLookupByLibrary.simpleMessage("Manage device storage"),
MessageLookupByLibrary.simpleMessage("Manage device Cache"),
"manageDeviceStorageDesc": MessageLookupByLibrary.simpleMessage(
"Review and clear local cache storage."),
"manageFamily": MessageLookupByLibrary.simpleMessage("Manage Family"),
"manageLink": MessageLookupByLibrary.simpleMessage("Manage link"),
"manageParticipants": MessageLookupByLibrary.simpleMessage("Manage"),

View File

@@ -635,7 +635,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("¿Borrar álbum compartido?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"El álbum se eliminará para todos\n\nPerderás el acceso a las fotos compartidas en este álbum que son propiedad de otros"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descripciones"),
"deselectAll":
MessageLookupByLibrary.simpleMessage("Deseleccionar todo"),
"designedToOutlive":

View File

@@ -707,7 +707,6 @@ class MessageLookup extends MessageLookupByLibrary {
"Supprimer l\'album partagé ?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"L\'album sera supprimé pour tout le monde\n\nVous perdrez l\'accès aux photos partagées dans cet album qui sont détenues par d\'autres personnes"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll":
MessageLookupByLibrary.simpleMessage("Tout déselectionner"),
"designedToOutlive":

View File

@@ -540,7 +540,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Hapus album bersama?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"Album ini akan di hapus untuk semua\n\nKamu akan kehilangan akses ke foto yang di bagikan dalam album ini yang di miliki oleh pengguna lain"),
"descriptions": MessageLookupByLibrary.simpleMessage("Keterangan"),
"designedToOutlive":
MessageLookupByLibrary.simpleMessage("Dibuat untuk melestarikan"),
"details": MessageLookupByLibrary.simpleMessage("Rincian"),

View File

@@ -654,7 +654,6 @@ class MessageLookup extends MessageLookupByLibrary {
"Eliminare l\'album condiviso?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"L\'album verrà eliminato per tutti\n\nPerderai l\'accesso alle foto condivise in questo album che sono di proprietà di altri"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descrizioni"),
"deselectAll":
MessageLookupByLibrary.simpleMessage("Deseleziona tutti"),
"designedToOutlive":

View File

@@ -557,7 +557,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("共有アルバムを削除しますか?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"このアルバムは他の人からも削除されます\n\n他の人が共有してくれた写真も、あなたからは見れなくなります"),
"descriptions": MessageLookupByLibrary.simpleMessage("説明文"),
"deselectAll": MessageLookupByLibrary.simpleMessage("選択解除"),
"designedToOutlive":
MessageLookupByLibrary.simpleMessage("生き延びるためのデザイン"),

View File

@@ -387,7 +387,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Mano priežastis nenurodyta"),
"deleteRequestSLAText": MessageLookupByLibrary.simpleMessage(
"Jūsų prašymas bus apdorotas per 72 valandas."),
"descriptions": MessageLookupByLibrary.simpleMessage("Aprašymai"),
"designedToOutlive":
MessageLookupByLibrary.simpleMessage("Sukurta išgyventi"),
"developerSettings":

View File

@@ -692,7 +692,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Gedeeld album verwijderen?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"Het album wordt verwijderd voor iedereen\n\nJe verliest de toegang tot gedeelde foto\'s in dit album die eigendom zijn van anderen"),
"descriptions": MessageLookupByLibrary.simpleMessage("Beschrijvingen"),
"deselectAll":
MessageLookupByLibrary.simpleMessage("Alles deselecteren"),
"designedToOutlive": MessageLookupByLibrary.simpleMessage(

View File

@@ -651,7 +651,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Usunąć udostępniony album?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"Album zostanie usunięty dla wszystkich\n\nUtracisz dostęp do udostępnionych zdjęć w tym albumie, które są własnością innych osób"),
"descriptions": MessageLookupByLibrary.simpleMessage("Opisy"),
"deselectAll": MessageLookupByLibrary.simpleMessage("Odznacz wszystko"),
"designedToOutlive": MessageLookupByLibrary.simpleMessage(
"Zaprojektowane do przetrwania"),

View File

@@ -2896,16 +2896,26 @@ class S {
);
}
/// `Manage device storage`
/// `Manage device Cache`
String get manageDeviceStorage {
return Intl.message(
'Manage device storage',
'Manage device Cache',
name: 'manageDeviceStorage',
desc: '',
args: [],
);
}
/// `Review and clear local cache storage.`
String get manageDeviceStorageDesc {
return Intl.message(
'Review and clear local cache storage.',
name: 'manageDeviceStorageDesc',
desc: '',
args: [],
);
}
/// `Machine learning`
String get machineLearning {
return Intl.message(
@@ -8839,16 +8849,6 @@ class S {
);
}
/// `Descriptions`
String get descriptions {
return Intl.message(
'Descriptions',
name: 'descriptions',
desc: '',
args: [],
);
}
/// `Add a name`
String get addAName {
return Intl.message(

View File

@@ -412,7 +412,8 @@
"description": "The text to display in the advanced settings section"
},
"photoGridSize": "Photo grid size",
"manageDeviceStorage": "Manage device storage",
"manageDeviceStorage": "Manage device Cache",
"manageDeviceStorageDesc": "Review and clear local cache storage.",
"machineLearning": "Machine learning",
"mlConsent": "Enable machine learning",
"mlConsentTitle": "Enable machine learning?",
@@ -1245,7 +1246,6 @@
"deviceCodeHint": "Enter the code",
"joinDiscord": "Join Discord",
"locations": "Locations",
"descriptions": "Descriptions",
"addAName": "Add a name",
"findThemQuickly": "Find them quickly",
"@findThemQuickly": {

View File

@@ -46,7 +46,6 @@ enum SectionType {
moment,
album,
// People section shows the files shared by other persons
fileCaption,
contacts,
fileTypesAndExtension,
}
@@ -69,8 +68,6 @@ extension SectionTypeExtensions on SectionType {
return S.of(context).albums;
case SectionType.fileTypesAndExtension:
return S.of(context).fileTypes;
case SectionType.fileCaption:
return S.of(context).descriptions;
}
}
@@ -90,8 +87,6 @@ extension SectionTypeExtensions on SectionType {
return S.of(context).searchAlbumsEmptySection;
case SectionType.fileTypesAndExtension:
return S.of(context).searchFileTypesAndNamesEmptySection;
case SectionType.fileCaption:
return S.of(context).searchCaptionEmptySection;
}
}
@@ -113,8 +108,6 @@ extension SectionTypeExtensions on SectionType {
return true;
case SectionType.fileTypesAndExtension:
return false;
case SectionType.fileCaption:
return false;
}
}
@@ -136,8 +129,6 @@ extension SectionTypeExtensions on SectionType {
return true;
case SectionType.fileTypesAndExtension:
return false;
case SectionType.fileCaption:
return false;
}
}
@@ -159,8 +150,6 @@ extension SectionTypeExtensions on SectionType {
return S.of(context).addNew;
case SectionType.fileTypesAndExtension:
return "";
case SectionType.fileCaption:
return S.of(context).addNew;
}
}
@@ -180,8 +169,6 @@ extension SectionTypeExtensions on SectionType {
return Icons.add;
case SectionType.fileTypesAndExtension:
return null;
case SectionType.fileCaption:
return null;
}
}
@@ -267,9 +254,6 @@ extension SectionTypeExtensions on SectionType {
case SectionType.fileTypesAndExtension:
return SearchService.instance
.getAllFileTypesAndExtensionsResults(context, limit);
case SectionType.fileCaption:
return SearchService.instance.getAllDescriptionSearchResults(limit);
}
}

View File

@@ -12,7 +12,6 @@ import 'package:photos/data/years.dart';
import 'package:photos/db/files_db.dart';
import "package:photos/db/ml/db.dart";
import 'package:photos/events/local_photos_updated_event.dart';
import "package:photos/extensions/string_ext.dart";
import "package:photos/models/api/collection/user.dart";
import 'package:photos/models/collection/collection.dart';
import 'package:photos/models/collection/collection_items.dart';
@@ -536,165 +535,6 @@ class SearchService {
}
}
///Todo: Optimise + make this function more readable
//This can be furthur optimized by not just limiting keys to 0 and 1. Use key
//0 for single word, 1 for 2 word, 2 for 3 ..... and only check the substrings
//in higher key if there are matches in the lower key.
Future<List<GenericSearchResult>> getAllDescriptionSearchResults(
//todo: use limit
int? limit,
) async {
try {
final List<GenericSearchResult> searchResults = [];
final List<EnteFile> allFiles = await getAllFilesForSearch();
//each list element will be substrings from a description mapped by
//word count = 1 and word count > 1
//New items will be added to [orderedSubDescriptions] list for every
//distinct description.
//[orderedSubDescriptions[x]] has two keys, 0 & 1. Value of key 0 will be single
//word substrings. Value of key 1 will be multi word subStrings. When
//iterating through [allFiles], we check for matching substrings from
//[orderedSubDescriptions[x]] with the file's description. Starts from value
//of key 0 (x=0). If there are no substring matches from key 0, there will
//be none from key 1 as well. So these two keys are for avoiding unnecessary
//checking of all subDescriptions with file description.
final orderedSubDescs = <Map<int, List<String>>>[];
final descAndMatchingFiles = <String, Set<EnteFile>>{};
int distinctFullDescCount = 0;
final allDistinctFullDescs = <String>[];
for (EnteFile file in allFiles) {
if (file.caption != null && file.caption!.isNotEmpty) {
//This limit doesn't necessarily have to be the limit parameter of the
//method. Using the same variable to avoid unwanted iterations when
//iterating over [orderedSubDescriptions] in case there is a limit
//passed. Using the limit passed here so that there will be almost
//always be more than 7 descriptionAndMatchingFiles and can shuffle
//and choose only limited elements from it. Without shuffling,
//result will be ["hello", "world", "hello world"] for the string
//"hello world"
if (limit == null || distinctFullDescCount < limit) {
final descAlreadyRecorded = allDistinctFullDescs
.any((element) => element.contains(file.caption!.trim()));
if (!descAlreadyRecorded) {
distinctFullDescCount++;
allDistinctFullDescs.add(file.caption!.trim());
final words = file.caption!.trim().split(" ");
orderedSubDescs.add({0: <String>[], 1: <String>[]});
for (int i = 1; i <= words.length; i++) {
for (int j = 0; j <= words.length - i; j++) {
final subList = words.sublist(j, j + i);
final substring = subList.join(" ").toLowerCase();
if (i == 1) {
orderedSubDescs.last[0]!.add(substring);
} else {
orderedSubDescs.last[1]!.add(substring);
}
}
}
}
}
for (Map<int, List<String>> orderedSubDescription
in orderedSubDescs) {
bool matchesSingleWordSubString = false;
for (String subDescription in orderedSubDescription[0]!) {
if (file.caption!.toLowerCase().contains(subDescription)) {
matchesSingleWordSubString = true;
//continue only after setting [matchesSingleWordSubString] to true
if (subDescription.isAllConnectWords ||
subDescription.isLastWordConnectWord) continue;
if (descAndMatchingFiles.containsKey(subDescription)) {
descAndMatchingFiles[subDescription]!.add(file);
} else {
descAndMatchingFiles[subDescription] = {file};
}
}
}
if (matchesSingleWordSubString) {
for (String subDescription in orderedSubDescription[1]!) {
if (subDescription.isAllConnectWords ||
subDescription.isLastWordConnectWord) continue;
if (file.caption!.toLowerCase().contains(subDescription)) {
if (descAndMatchingFiles.containsKey(subDescription)) {
descAndMatchingFiles[subDescription]!.add(file);
} else {
descAndMatchingFiles[subDescription] = {file};
}
}
}
}
}
}
}
///[relevantDescAndFiles] will be a filterd version of [descriptionAndMatchingFiles]
///In [descriptionAndMatchingFiles], there will be descriptions with the same
///set of matching files. These descriptions will be substrings of a full
///description. [relevantDescAndFiles] will keep only the entry which has the
///longest description among enties with matching set of files.
final relevantDescAndFiles = <String, Set<EnteFile>>{};
while (descAndMatchingFiles.isNotEmpty) {
final baseEntry = descAndMatchingFiles.entries.first;
final descsWithSameFiles = <String, Set<EnteFile>>{};
final baseUploadedFileIDs =
baseEntry.value.map((e) => e.uploadedFileID).toSet();
descAndMatchingFiles.forEach((desc, files) {
final uploadedFileIDs = files.map((e) => e.uploadedFileID).toSet();
final hasSameFiles =
uploadedFileIDs.containsAll(baseUploadedFileIDs) &&
baseUploadedFileIDs.containsAll(uploadedFileIDs);
if (hasSameFiles) {
descsWithSameFiles.addAll({desc: files});
}
});
descAndMatchingFiles
.removeWhere((desc, files) => descsWithSameFiles.containsKey(desc));
final longestDescription = descsWithSameFiles.keys.reduce(
(desc1, desc2) => desc1.length > desc2.length ? desc1 : desc2,
);
relevantDescAndFiles.addAll(
{longestDescription: descsWithSameFiles[longestDescription]!},
);
}
relevantDescAndFiles.forEach((key, value) {
final listOfFiles = value.toList();
searchResults.add(
GenericSearchResult(
ResultType.fileCaption,
key,
listOfFiles,
hierarchicalSearchFilter: TopLevelGenericFilter(
filterName: key,
occurrence: kMostRelevantFilter,
filterResultType: ResultType.fileCaption,
matchedUploadedIDs: filesToUploadedFileIDs(listOfFiles),
filterIcon: Icons.description_outlined,
),
),
);
});
if (limit != null) {
return searchResults.sublist(0, min(limit, searchResults.length));
} else {
return searchResults;
}
} catch (e) {
_logger.severe("Error in getAllDescriptionSearchResults", e);
return [];
}
}
Future<List<GenericSearchResult>> getCaptionAndNameResults(
String query,
) async {

View File

@@ -189,25 +189,17 @@ class _TextInputWidgetState extends State<TextInputWidget> {
),
borderRadius: BorderRadius.circular(8),
),
suffixIcon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 175),
switchInCurve: Curves.easeInExpo,
switchOutCurve: Curves.easeOutExpo,
child: SuffixIconWidget(
key: ValueKey(executionState),
executionState: executionState,
shouldSurfaceExecutionStates:
widget.shouldSurfaceExecutionStates,
obscureTextNotifier: _obscureTextNotifier,
isPasswordInput: widget.isPasswordInput,
textController: _textController,
isClearable: widget.isClearable,
shouldUnfocusOnClearOrSubmit:
widget.shouldUnfocusOnClearOrSubmit,
),
),
suffixIcon: SuffixIconWidget(
key: ValueKey(executionState),
executionState: executionState,
shouldSurfaceExecutionStates:
widget.shouldSurfaceExecutionStates,
obscureTextNotifier: _obscureTextNotifier,
isPasswordInput: widget.isPasswordInput,
textController: _textController,
isClearable: widget.isClearable,
shouldUnfocusOnClearOrSubmit:
widget.shouldUnfocusOnClearOrSubmit,
),
prefixIconConstraints: const BoxConstraints(
maxHeight: 44,
@@ -218,8 +210,8 @@ class _TextInputWidgetState extends State<TextInputWidget> {
suffixIconConstraints: const BoxConstraints(
maxHeight: 24,
maxWidth: 48,
minHeight: 24,
minWidth: 48,
minHeight: 0,
minWidth: 0,
),
prefixIcon: widget.prefixIcon != null
? Icon(
@@ -442,7 +434,7 @@ class SuffixIconWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final Widget trailingWidget;
final Widget? trailingWidget;
final colorScheme = getEnteColorScheme(context);
if (executionState == ExecutionState.idle ||
!shouldSurfaceExecutionStates) {
@@ -473,7 +465,7 @@ class SuffixIconWidget extends StatelessWidget {
),
);
} else {
trailingWidget = const SizedBox.shrink();
trailingWidget = null;
}
} else if (executionState == ExecutionState.inProgress) {
trailingWidget = EnteLoadingWidget(
@@ -486,8 +478,19 @@ class SuffixIconWidget extends StatelessWidget {
color: colorScheme.primary500,
);
} else {
trailingWidget = const SizedBox.shrink();
trailingWidget = null;
}
return trailingWidget;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 175),
switchInCurve: Curves.easeInExpo,
switchOutCurve: Curves.easeOutExpo,
child: trailingWidget != null
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: trailingWidget,
)
: const SizedBox.shrink(),
);
}
}

View File

@@ -268,7 +268,6 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
isFromMemories: true,
),
);
},

View File

@@ -14,7 +14,6 @@ import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/components/title_bar_widget.dart';
import "package:photos/ui/components/toggle_switch_widget.dart";
import "package:photos/ui/settings/ml/machine_learning_settings_page.dart";
import 'package:photos/ui/tools/debug/app_storage_viewer.dart';
import 'package:photos/ui/viewer/gallery/photo_grid_size_picker_page.dart';
import 'package:photos/utils/navigation_util.dart';
@@ -124,25 +123,6 @@ class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
const SizedBox(
height: 24,
),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).manageDeviceStorage,
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 8,
alignCaptionedTextToLeft: true,
onTap: () async {
// ignore: unawaited_futures
routeToPage(context, const AppStorageViewer());
},
),
const SizedBox(
height: 24,
),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).showMemories,

View File

@@ -18,6 +18,7 @@ import "package:photos/ui/components/menu_section_description_widget.dart";
import "package:photos/ui/components/models/button_type.dart";
import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/components/title_bar_widget.dart';
import "package:photos/ui/tools/debug/app_storage_viewer.dart";
import "package:photos/ui/tools/deduplicate_page.dart";
import "package:photos/ui/tools/free_space_page.dart";
import "package:photos/ui/viewer/gallery/large_files_page.dart";
@@ -71,6 +72,7 @@ class _FreeUpSpaceOptionsScreenState extends State<FreeUpSpaceOptionsScreen> {
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
@@ -208,6 +210,35 @@ class _FreeUpSpaceOptionsScreenState extends State<FreeUpSpaceOptionsScreen> {
MenuSectionDescriptionWidget(
content: S.of(context).viewLargeFilesDesc,
),
const SizedBox(
height: 24,
),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).manageDeviceStorage,
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 8,
alignCaptionedTextToLeft: true,
onTap: () async {
// ignore: unawaited_futures
routeToPage(
context,
const AppStorageViewer(),
);
},
),
Align(
alignment: Alignment.centerLeft,
child: MenuSectionDescriptionWidget(
content:
S.of(context).manageDeviceStorageDesc,
),
),
],
),
],

View File

@@ -175,7 +175,7 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
context,
{'enableDownload': !isDownloadEnabled},
);
if (!isDownloadEnabled) {
if (isDownloadEnabled) {
// ignore: unawaited_futures
showErrorDialog(
context,

View File

@@ -276,7 +276,6 @@ class _DetailPageState extends State<DetailPage> {
});
},
backgroundDecoration: const BoxDecoration(color: Colors.black),
isFromMemories: false,
);
return GestureDetector(
onTap: () {

View File

@@ -12,7 +12,6 @@ class FileWidget extends StatelessWidget {
final Function(bool)? playbackCallback;
final BoxDecoration? backgroundDecoration;
final bool? autoPlay;
final bool isFromMemories;
const FileWidget(
this.file, {
@@ -21,7 +20,6 @@ class FileWidget extends StatelessWidget {
this.playbackCallback,
this.tagPrefix,
this.backgroundDecoration,
required this.isFromMemories,
super.key,
});

View File

@@ -200,12 +200,12 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: widget.playbackCallback != null
? () {
_showControls.value = !_showControls.value;
widget.playbackCallback!(!_showControls.value);
}
: null,
onTap: () {
_showControls.value = !_showControls.value;
if (widget.playbackCallback != null) {
widget.playbackCallback!(!_showControls.value);
}
},
child: Container(
constraints: const BoxConstraints.expand(),
),
@@ -310,7 +310,9 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
return;
}
_showControls.value = false;
widget.playbackCallback!(true);
if (widget.playbackCallback != null) {
widget.playbackCallback!(true);
}
}
});
}
@@ -321,7 +323,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
return;
}
if (_controller!.playbackInfo?.status == PlaybackStatus.playing) {
if (widget.playbackCallback != null && mounted) {
if (mounted) {
_debouncer.run(() async {
if (mounted) {
if (_isSeeking.value ||
@@ -329,7 +331,9 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
return;
}
_showControls.value = false;
widget.playbackCallback!(true);
if (widget.playbackCallback != null) {
widget.playbackCallback!(true);
}
}
});
}
@@ -379,7 +383,11 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
_setFilePathForNativePlayer(file.path);
}
}).onError((error, stackTrace) {
showErrorDialog(context, S.of(context).error, S.of(context).failedToDownloadVideo);
showErrorDialog(
context,
S.of(context).error,
S.of(context).failedToDownloadVideo,
);
});
}

View File

@@ -88,208 +88,213 @@ class _SaveOrEditPersonState extends State<SaveOrEditPerson> {
),
),
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(
bottom: 32.0,
left: 16.0,
right: 16.0,
),
child: Column(
children: [
const SizedBox(height: 48),
if (person != null)
FutureBuilder<(String, EnteFile)>(
future: _getRecentFileWithClusterID(person!),
builder: (context, snapshot) {
if (snapshot.hasData) {
final String personClusterID = snapshot.data!.$1;
final personFile = snapshot.data!.$2;
return Stack(
children: [
SizedBox(
height: 110,
width: 110,
child: ClipPath(
clipper: ShapeBorderClipper(
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(80),
body: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(
bottom: 32.0,
left: 16.0,
right: 16.0,
),
child: Column(
children: [
const SizedBox(height: 48),
if (person != null)
FutureBuilder<(String, EnteFile)>(
future: _getRecentFileWithClusterID(person!),
builder: (context, snapshot) {
if (snapshot.hasData) {
final String personClusterID = snapshot.data!.$1;
final personFile = snapshot.data!.$2;
return Stack(
children: [
SizedBox(
height: 110,
width: 110,
child: ClipPath(
clipper: ShapeBorderClipper(
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(80),
),
),
),
child: snapshot.hasData
? PersonFaceWidget(
key: ValueKey(
person?.data.avatarFaceID ?? "",
child: snapshot.hasData
? PersonFaceWidget(
key: ValueKey(
person?.data.avatarFaceID ?? "",
),
personFile,
clusterID: personClusterID,
personId: person!.remoteID,
)
: const NoThumbnailWidget(
addBorder: false,
),
personFile,
clusterID: personClusterID,
personId: person!.remoteID,
)
: const NoThumbnailWidget(
addBorder: false,
),
),
),
if (person != null)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(8.0),
boxShadow: Theme.of(context)
.colorScheme
.enteTheme
.shadowMenu,
color: getEnteColorScheme(context)
.backgroundElevated2,
),
child: IconButton(
icon: const Icon(Icons.edit),
iconSize:
16, // specify the size of the icon
onPressed: () async {
final result =
await showPersonAvatarPhotoSheet(
context,
person!,
);
if (result != null) {
_logger
.info('Person avatar updated');
setState(() {
person = result;
});
Bus.instance.fire(
PeopleChangedEvent(),
);
}
},
),
),
),
],
);
if (person != null)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(8.0),
boxShadow: Theme.of(context)
.colorScheme
.enteTheme
.shadowMenu,
color: getEnteColorScheme(context)
.backgroundElevated2,
),
child: IconButton(
icon: const Icon(Icons.edit),
iconSize:
16, // specify the size of the icon
onPressed: () async {
final result =
await showPersonAvatarPhotoSheet(
context,
person!,
);
if (result != null) {
_logger
.info('Person avatar updated');
setState(() {
person = result;
});
Bus.instance.fire(
PeopleChangedEvent(),
);
}
},
),
),
),
],
);
} else {
return const SizedBox.shrink();
}
},
),
if (person == null)
SizedBox(
height: 110,
width: 110,
child: ClipPath(
clipper: ShapeBorderClipper(
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(80),
),
),
child: widget.file != null
? PersonFaceWidget(
widget.file!,
clusterID: widget.clusterID,
)
: const NoThumbnailWidget(
addBorder: false,
),
),
),
const SizedBox(height: 36),
TextFormField(
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.words,
autocorrect: false,
onChanged: (value) {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce =
Timer(const Duration(milliseconds: 300), () {
setState(() {
_inputName = value;
});
});
},
initialValue: _inputName,
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderRadius:
const BorderRadius.all(Radius.circular(8.0)),
borderSide: BorderSide(
color: getEnteColorScheme(context).strokeMuted,
),
),
fillColor: getEnteColorScheme(context).fillFaint,
filled: true,
hintText: context.l10n.enterName,
hintStyle: getEnteTextTheme(context).bodyFaint,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(8),
),
),
),
const SizedBox(height: 16),
DatePickerField(
hintText: context.l10n.enterDateOfBirth,
firstDate: DateTime(100),
lastDate: DateTime.now(),
initialValue: _selectedDate,
isRequired: false,
onChanged: (date) {
setState(() {
// format date to yyyy-MM-dd
_selectedDate =
date?.toIso8601String().split("T").first;
});
},
),
const SizedBox(height: 32),
ButtonWidget(
buttonType: ButtonType.primary,
labelText: context.l10n.save,
isDisabled: !changed,
onTap: () async {
if (widget.isEditing) {
await updatePerson(context);
} else {
return const SizedBox.shrink();
await addNewPerson(
context,
text: _inputName,
clusterID: widget.clusterID!,
);
}
},
),
if (person == null)
SizedBox(
height: 110,
width: 110,
child: ClipPath(
clipper: ShapeBorderClipper(
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(80),
),
),
child: widget.file != null
? PersonFaceWidget(
widget.file!,
clusterID: widget.clusterID,
)
: const NoThumbnailWidget(
addBorder: false,
),
),
),
const SizedBox(height: 36),
TextFormField(
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.words,
autocorrect: false,
onChanged: (value) {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce =
Timer(const Duration(milliseconds: 300), () {
setState(() {
_inputName = value;
});
});
},
initialValue: _inputName,
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderRadius:
const BorderRadius.all(Radius.circular(8.0)),
borderSide: BorderSide(
color: getEnteColorScheme(context).strokeMuted,
const SizedBox(height: 32),
if (!widget.isEditing) _getPersonItems(),
if (widget.isEditing)
Align(
alignment: Alignment.centerLeft,
child: Text(
context.l10n.mergedPhotos,
style: getEnteTextTheme(context).body,
),
),
fillColor: getEnteColorScheme(context).fillFaint,
filled: true,
hintText: context.l10n.enterName,
hintStyle: getEnteTextTheme(context).bodyFaint,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
if (widget.isEditing)
Padding(
padding: const EdgeInsets.only(bottom: 12.0, top: 24.0),
child: PersonClustersWidget(person!),
),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(8),
),
),
),
const SizedBox(height: 16),
DatePickerField(
hintText: context.l10n.enterDateOfBirth,
firstDate: DateTime(100),
lastDate: DateTime.now(),
initialValue: _selectedDate,
isRequired: false,
onChanged: (date) {
setState(() {
// format date to yyyy-MM-dd
_selectedDate =
date?.toIso8601String().split("T").first;
});
},
),
const SizedBox(height: 32),
ButtonWidget(
buttonType: ButtonType.primary,
labelText: context.l10n.save,
isDisabled: !changed,
onTap: () async {
if (widget.isEditing) {
await updatePerson(context);
} else {
await addNewPerson(
context,
text: _inputName,
clusterID: widget.clusterID!,
);
}
},
),
const SizedBox(height: 32),
if (!widget.isEditing) _getPersonItems(),
if (widget.isEditing)
Align(
alignment: Alignment.centerLeft,
child: Text(
context.l10n.mergedPhotos,
style: getEnteTextTheme(context).body,
),
),
if (widget.isEditing)
Padding(
padding: const EdgeInsets.only(bottom: 12.0, top: 24.0),
child: PersonClustersWidget(person!),
),
],
],
),
),
),
),
],
],
),
),
),
);
@@ -357,6 +362,7 @@ class _SaveOrEditPersonState extends State<SaveOrEditPerson> {
itemBuilder: (context, index) {
final person = searchResults[index];
return PersonGridItem(
key: ValueKey(person.$1.remoteID),
person: person.$1,
personFile: person.$2,
onTap: () async {

View File

@@ -1,239 +0,0 @@
import "dart:async";
import "package:figma_squircle/figma_squircle.dart";
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/events/event.dart";
import "package:photos/models/search/generic_search_result.dart";
import "package:photos/models/search/recent_searches.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/ui/viewer/search/result/search_result_page.dart";
import "package:photos/ui/viewer/search/search_section_cta.dart";
import "package:photos/ui/viewer/search_tab/section_header.dart";
import "package:photos/utils/navigation_util.dart";
class DescriptionsSection extends StatefulWidget {
final List<GenericSearchResult> descriptionsSearchResults;
const DescriptionsSection(this.descriptionsSearchResults, {super.key});
@override
State<DescriptionsSection> createState() => _DescriptionsSectionState();
}
class _DescriptionsSectionState extends State<DescriptionsSection> {
late List<GenericSearchResult> _descriptionsSearchResults;
final streamSubscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
_descriptionsSearchResults = widget.descriptionsSearchResults;
final streamsToListenTo = SectionType.fileCaption.sectionUpdateEvents();
for (Stream<Event> stream in streamsToListenTo) {
streamSubscriptions.add(
stream.listen((event) async {
_descriptionsSearchResults = (await SectionType.fileCaption.getData(
context,
limit: kSearchSectionLimit,
)) as List<GenericSearchResult>;
setState(() {});
}),
);
}
}
@override
void dispose() {
for (var subscriptions in streamSubscriptions) {
subscriptions.cancel();
}
super.dispose();
}
@override
void didUpdateWidget(covariant DescriptionsSection oldWidget) {
super.didUpdateWidget(oldWidget);
_descriptionsSearchResults = widget.descriptionsSearchResults;
}
@override
Widget build(BuildContext context) {
if (_descriptionsSearchResults.isEmpty) {
final textTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.only(left: 12, right: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
SectionType.fileCaption.sectionTitle(context),
style: textTheme.largeBold,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
SectionType.fileCaption.getEmptyStateText(context),
style: textTheme.smallMuted,
),
),
],
),
),
const SizedBox(width: 8),
const SearchSectionEmptyCTAIcon(SectionType.fileCaption),
],
),
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
SectionType.fileCaption,
hasMore: (_descriptionsSearchResults.length >=
kSearchSectionLimit - 1),
),
const SizedBox(height: 2),
SizedBox(
child: SingleChildScrollView(
clipBehavior: Clip.none,
padding: const EdgeInsets.symmetric(horizontal: 4.5),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _descriptionsSearchResults
.map(
(descriptionSearchResult) =>
DescriptionRecommendation(descriptionSearchResult),
)
.toList(),
),
),
),
],
),
);
}
}
}
class DescriptionRecommendation extends StatelessWidget {
final GenericSearchResult descriptionSearchResult;
const DescriptionRecommendation(this.descriptionSearchResult, {super.key});
@override
Widget build(BuildContext context) {
final heroTag = descriptionSearchResult.heroTag() +
(descriptionSearchResult.previewThumbnail()?.tag ?? "");
final enteTextTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.5),
child: GestureDetector(
onTap: () {
RecentSearches().add(descriptionSearchResult.name());
if (descriptionSearchResult.onResultTap != null) {
descriptionSearchResult.onResultTap!(context);
} else {
routeToPage(
context,
SearchResultPage(descriptionSearchResult),
);
}
},
child: SizedBox(
width: 100,
height: 150,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Container(
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(
blurRadius: 15,
offset: Offset(0, 7.5),
color: Color.fromRGBO(68, 68, 68, 0.1),
),
],
),
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: 7.5,
cornerSmoothing: 1,
),
child: Container(
color: Theme.of(context).colorScheme.brightness ==
Brightness.light
? const Color(0xFFFFFFFF)
: const Color(0xFF181818),
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(9, 9, 9, 0),
child: SizedBox(
width: 82,
height: 82,
child: descriptionSearchResult.previewThumbnail() !=
null
? Hero(
tag: heroTag,
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: 7.5,
cornerSmoothing: 1,
),
child: ThumbnailWidget(
descriptionSearchResult
.previewThumbnail()!,
shouldShowArchiveStatus: false,
shouldShowSyncStatus: false,
),
),
)
: const NoThumbnailWidget(),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 10,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
descriptionSearchResult.name(),
style: enteTextTheme.smallMuted,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
),
],
),
),
),
),
],
),
),
),
);
}
}

View File

@@ -18,7 +18,6 @@ import "package:photos/ui/viewer/search/search_suggestions.dart";
import "package:photos/ui/viewer/search/tab_empty_state.dart";
import 'package:photos/ui/viewer/search_tab/albums_section.dart';
import "package:photos/ui/viewer/search_tab/contacts_section.dart";
import "package:photos/ui/viewer/search_tab/descriptions_section.dart";
import "package:photos/ui/viewer/search_tab/file_type_section.dart";
import "package:photos/ui/viewer/search_tab/locations_section.dart";
import "package:photos/ui/viewer/search_tab/magic_section.dart";
@@ -140,11 +139,6 @@ class _AllSearchSectionsState extends State<AllSearchSections> {
snapshot.data!.elementAt(index)
as List<GenericSearchResult>,
);
case SectionType.fileCaption:
return DescriptionsSection(
snapshot.data!.elementAt(index)
as List<GenericSearchResult>,
);
case SectionType.location:
return LocationsSection(
snapshot.data!.elementAt(index)

View File

@@ -321,6 +321,13 @@ internal:
# the "admin" instead of "admins". This can be useful e.g. when wishing to
# pass the admin as an environment variable.
admin:
# If set to true, users will not be able to register a new account.
#
# Account creation will fail when entering the email ott. This doesn't
# affect the clients, users can still go through the registration flow, but
# when entering the ott they received via mail, they will get an
# unauthorized error and thus cannot create new accounts.
disable-registration: false
# Replication config
#

View File

@@ -141,7 +141,7 @@ func (c *MailingListsController) Unsubscribe(email string) error {
// should be skipped due to missing credentials.
func (c *MailingListsController) shouldSkipZoho() bool {
if c.zohoCredentials.RefreshToken == "" {
log.Info("Skipping Zoho mailing list update because credentials are not configured")
// Skip
return true
}
return false
@@ -157,7 +157,7 @@ func (c *MailingListsController) shouldSkipZoho() bool {
func (c *MailingListsController) shouldSkipListmonk() bool {
if c.listmonkCredentials.BaseURL == "" || c.listmonkCredentials.Username == "" ||
c.listmonkCredentials.Password == "" || len(c.listmonkListIDs) == 0 {
log.Info("Skipping Listmonk mailing list because credentials are not configured")
// Skip
return true
}
return false

View File

@@ -249,15 +249,11 @@ func (c *ReplicationController3) tryReplicate() error {
done := func(err error) error {
if err != nil {
logger.Error(err)
}
if strings.Contains(err.Error(), "size of the uploaded file") {
delayErr := c.ObjectCopiesRepo.DelayNextAttemptByDays(context.Background(), objectKey, replicationDelayForStaleObjects)
if delayErr != nil {
logger.WithError(delayErr).Error("Failed to delay next attempt")
} else {
discordAlert := fmt.Sprintf("🔥 Size mismatch for object %s, deferred next attemp for %d days", objectKey, replicationDelayForStaleObjects)
c.notifyDiscord(discordAlert)
if strings.Contains(err.Error(), "size of the uploaded file") {
delayErr := c.ObjectCopiesRepo.DelayNextAttemptByDays(context.Background(), objectKey, replicationDelayForStaleObjects)
if delayErr != nil {
logger.WithError(delayErr).Error("Failed to delay next attempt")
}
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/ente-io/museum/ente"
"github.com/ente-io/museum/pkg/utils/auth"
"github.com/ente-io/stacktrace"
@@ -14,7 +15,11 @@ import (
"net/http"
)
const Srp4096Params = 4096
const (
Srp4096Params = 4096
// MaxUnverifiedSessionInAnHour is the number of unverified sessions in the last hour
MaxUnverifiedSessionInAnHour = 10
)
func (c *UserController) SetupSRP(context *gin.Context, userID int64, req ente.SetupSRPRequest) (*ente.SetupSRPResponse, error) {
srpB, sessionID, err := c.createAndInsertSRPSession(context, req.SrpUserID, req.SRPVerifier, req.SRPA)
@@ -160,6 +165,19 @@ func (c *UserController) createAndInsertSRPSession(
srpA string,
) (*string, *uuid.UUID, error) {
unverifiedSessions, err := c.UserAuthRepo.GetUnverifiedSessionsInLastHour(srpUserID)
if err != nil {
return nil, nil, stacktrace.Propagate(err, "")
}
if unverifiedSessions >= MaxUnverifiedSessionInAnHour {
go c.DiscordController.NotifyPotentialAbuse(fmt.Sprintf("Too many unverified sessions for user %s", srpUserID.String()))
return nil, nil, stacktrace.Propagate(&ente.ApiError{
Code: "TOO_MANY_UNVERIFIED_SESSIONS",
HttpStatusCode: http.StatusTooManyRequests,
}, "")
}
serverSecret := srp.GenKey()
srpParams := srp.GetParams(Srp4096Params)
srpServer := srp.NewServer(srpParams, convertStringToBytes(srpVerifier), serverSecret)

View File

@@ -362,9 +362,13 @@ func (c *UserController) onVerificationSuccess(context *gin.Context, email strin
userID, err := c.UserRepo.GetUserIDWithEmail(email)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
userID, _, err = c.createUser(email, source)
if err != nil {
return ente.EmailAuthorizationResponse{}, stacktrace.Propagate(err, "")
if viper.GetBool("internal.disable-registration") {
return ente.EmailAuthorizationResponse{}, stacktrace.Propagate(ente.ErrPermissionDenied, "")
} else {
userID, _, err = c.createUser(email, source)
if err != nil {
return ente.EmailAuthorizationResponse{}, stacktrace.Propagate(err, "")
}
}
} else {
return ente.EmailAuthorizationResponse{}, stacktrace.Propagate(err, "")

View File

@@ -152,6 +152,7 @@ func (r *RateLimitMiddleware) getLimiter(reqPath string, reqMethod string) *limi
reqPath == "/users/srp/attributes" ||
(reqPath == "/cast/device-info" && reqMethod == "POST") ||
(reqPath == "/cast/device-info/" && reqMethod == "POST") ||
reqPath == "/users/srp/create-session" ||
reqPath == "/users/srp/verify-session" ||
reqPath == "/family/invite-info/:token" ||
reqPath == "/family/add-member" ||

View File

@@ -19,6 +19,12 @@ func (repo *UserAuthRepository) AddSRPSession(srpUserID uuid.UUID, serverKey str
return id, stacktrace.Propagate(err, "")
}
func (repo *UserAuthRepository) GetUnverifiedSessionsInLastHour(srpUserID uuid.UUID) (int64, error) {
var count int64
err := repo.DB.QueryRow(`SELECT COUNT(*) FROM srp_sessions WHERE srp_user_id = $1 AND has_verified = false AND created_at > (now_utc_micro_seconds() - (60::BIGINT * 60 * 1000 * 1000))`, srpUserID).Scan(&count)
return count, stacktrace.Propagate(err, "")
}
func (repo *UserAuthRepository) GetSRPAuthEntity(ctx context.Context, userID int64) (*ente.SRPAuthEntity, error) {
result := ente.SRPAuthEntity{}
row := repo.DB.QueryRowContext(ctx, `SELECT user_id, srp_user_id, salt, verifier FROM srp_auth WHERE user_id = $1`, userID)

View File

@@ -1,9 +1,9 @@
/*
Cache-Control: no-store, must-revalidate
Strict-Transport-Security: max-age=63072000
Strict-Transport-Security: max-age=63072000
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: deny
X-XSS-Protection: 1; mode=block
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' https://assets.ente.io 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/ ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob:; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;

View File

@@ -16,7 +16,6 @@ import {
} from "@ente/shared/components/OverflowMenu";
import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages";
import LogoutOutlined from "@mui/icons-material/LogoutOutlined";
import MoreHoriz from "@mui/icons-material/MoreHoriz";
import {
Button,
ButtonBase,
@@ -155,10 +154,7 @@ const AuthNavbar: React.FC = () => {
<EnteLogo />
</HorizontalFlex>
<HorizontalFlex position={"absolute"} right="24px">
<OverflowMenu
ariaControls={"auth-options"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenu ariaID={"auth-options"}>
<OverflowMenuOption
color="critical"
startIcon={<LogoutOutlined />}

View File

@@ -1,6 +1,8 @@
import log from "@/base/log";
import { nullToUndefined } from "@/utils/transform";
import { HOTP, TOTP } from "otpauth";
import { z } from "zod";
import { Steam } from "./steam";
/**
* A parsed representation of an *OTP code URI.
*
@@ -44,10 +46,36 @@ export interface Code {
* {@link type}-specific manner).
*/
secret: string;
/**
* Optional metadata.
*
* This is metadata of the shape {@link CodeDisplay} containing Ente
* specific metadata (e.g. if it is pinned) for this code.
*/
codeDisplay: CodeDisplay | undefined;
/** The original string from which this code was generated. */
uriString: string;
}
export interface CodeDisplay {
/**
* `true` if this code is in the Trash (i.e. it has been deleted by the
* user and will be automatically permanenently deleted after some time).
*/
trashed?: boolean;
/**
* `true` if this code has been pinned by the user.
*
* Pinned codes show at the top of the list of codes.
*/
pinned?: boolean;
}
const CodeDisplay = z.object({
trashed: z.boolean().nullish().transform(nullToUndefined),
pinned: z.boolean().nullish().transform(nullToUndefined),
});
/**
* Convert a OTP code URI into its parse representation, a {@link Code}.
*
@@ -95,6 +123,7 @@ const _codeFromURIString = (id: string, uriString: string): Code => {
algorithm: parseAlgorithm(url),
counter: parseCounter(url),
secret: parseSecret(url),
codeDisplay: parseCodeDisplay(url),
uriString,
};
};
@@ -207,6 +236,21 @@ const parseCounter = (url: URL): number | undefined => {
const parseSecret = (url: URL): string =>
url.searchParams.get("secret")!.replaceAll(" ", "").toUpperCase();
/**
* Parse a JSON string containing Ente specific metadata attached to the code.
*/
const parseCodeDisplay = (url: URL): CodeDisplay | undefined => {
const s = url.searchParams.get("codeDisplay");
if (!s) return undefined;
try {
return CodeDisplay.parse(JSON.parse(s));
} catch (e) {
log.error(`Ignoring unparseable code display ${s}`, e);
return undefined;
}
};
/**
* Generate a pair of OTPs (one time passwords) from the given {@link code}.
*

View File

@@ -17,30 +17,38 @@ export const getAuthCodes = async (masterKey: Uint8Array): Promise<Code[]> => {
authenticatorEntityKey,
masterKey,
);
const authEntities = await authenticatorEntityDiff(authenticatorKey);
const authCodes = authEntities.map((entity) => {
try {
return codeFromURIString(entity.id, ensureString(entity.data));
} catch (e) {
log.error(`Failed to parse codeID ${entity.id}`, e);
return undefined;
}
});
return (await authenticatorEntityDiff(authenticatorKey))
.map((entity) => {
try {
return codeFromURIString(entity.id, ensureString(entity.data));
} catch (e) {
log.error(`Failed to parse codeID ${entity.id}`, e);
return undefined;
}
})
.filter((f) => f !== undefined)
.filter((f) => {
// Do not show trashed entries in the web inteface.
return !f.codeDisplay?.trashed;
})
.sort((a, b) => {
// If only one of them is pinned, prefer it.
if (a.codeDisplay?.pinned && !b.codeDisplay?.pinned) return -1;
if (!a.codeDisplay?.pinned && b.codeDisplay?.pinned) return 1;
// If we get here, either both are pinned, or none are...
const filteredAuthCodes = authCodes.filter((f) => f !== undefined);
filteredAuthCodes.sort((a, b) => {
if (a.issuer && b.issuer) {
return a.issuer.localeCompare(b.issuer);
}
if (a.issuer) {
return -1;
}
if (b.issuer) {
return 1;
}
return 0;
});
return filteredAuthCodes;
// Sort by issuer, alphabetically.
if (a.issuer && b.issuer) {
return a.issuer.localeCompare(b.issuer);
}
if (a.issuer) {
return -1;
}
if (b.issuer) {
return 1;
}
return 0;
});
};
/**
@@ -68,7 +76,17 @@ const RemoteAuthenticatorEntityChange = z.object({
* Will be `null` when isDeleted is true.
*/
header: z.string().nullable(),
/**
* `true` if the corresponding entity was deleted.
*/
isDeleted: z.boolean(),
/**
* Epoch milliseconds when this entity was last updated.
*
* This value is suitable for being passed as the `sinceTime` in the diff
* requests to implement pagination.
*/
updatedAt: z.number(),
});
/**
@@ -86,26 +104,48 @@ export const authenticatorEntityDiff = async (
authenticatorKey,
);
// Always fetch all data from server for now.
const params = new URLSearchParams({
sinceTime: "0",
limit: "2500",
});
const url = await apiURL("/authenticator/entity/diff");
const res = await fetch(`${url}?${params.toString()}`, {
headers: await authenticatedRequestHeaders(),
});
ensureOk(res);
const diff = z
.object({ diff: z.array(RemoteAuthenticatorEntityChange) })
.parse(await res.json()).diff;
// Fetch all the entities, paginating the requests.
const entities = new Map<
string,
{ id: string; encryptedData: string; header: string }
>();
let sinceTime = 0;
const batchSize = 2500;
while (true) {
const params = new URLSearchParams({
sinceTime: `${sinceTime}`,
limit: `${batchSize}`,
});
const url = await apiURL("/authenticator/entity/diff");
const res = await fetch(`${url}?${params.toString()}`, {
headers: await authenticatedRequestHeaders(),
});
ensureOk(res);
const diff = z
.object({ diff: z.array(RemoteAuthenticatorEntityChange) })
.parse(await res.json()).diff;
if (diff.length == 0) break;
for (const change of diff) {
sinceTime = Math.max(sinceTime, change.updatedAt);
if (change.isDeleted) {
entities.delete(change.id);
} else {
entities.set(change.id, {
id: change.id,
encryptedData: change.encryptedData!,
header: change.header!,
});
}
}
}
return Promise.all(
diff
.filter((entity) => !entity.isDeleted)
.map(async ({ id, encryptedData, header }) => ({
id,
data: await decrypt(encryptedData!, header!),
})),
[...entities.values()].map(async ({ id, encryptedData, header }) => ({
id,
data: await decrypt(encryptedData, header),
})),
);
};

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