Compare commits

...

487 Commits

Author SHA1 Message Date
Vishnu Mohandas
8a222d9dc4 [auth] Fix icons (#4524)
## Description
- Update contract for Simple Icons:
https://github.com/simple-icons/simple-icons/pull/12415

## Tests
- Tested manually on Simulator
2024-12-27 19:42:48 +05:30
vishnukvmd
cf4c20f2d3 [auth] v4.2.2 2024-12-27 19:41:23 +05:30
vishnukvmd
28c29a0c3a Update parser for simple-icons 2024-12-27 19:40:58 +05:30
Neeraj Gupta
3de1c8011a [server] Fix canDowngrade storage check (#4520)
## Description
Previously, we were only checking if the usage is less than newStorage +
Paid Add Ons.
If the user also have referral bonus, we also need to calculate the new
usable bonus based on the newStorage.

## Tests
2024-12-27 15:21:50 +05:30
Neeraj Gupta
7adb166fad [server] Fix canDowngrade storage check 2024-12-27 15:18:55 +05:30
Neeraj Gupta
6447ba6ec0 [docs] Fix ACCOUNTS ENDPOINT variable. (#4511) 2024-12-27 13:21:21 +05:30
Neeraj Gupta
258cdca69b [auth] Reduce progress bar refresh rate to lower CPU usage (#4517)
## Description
Related
https://github.com/ente-io/ente/issues/2003#issuecomment-2563380828
## Tests
2024-12-27 13:20:32 +05:30
Neeraj Gupta
2a19c30d0e Lint fix 2024-12-27 13:12:42 +05:30
Neeraj Gupta
d23c22762b [auth] Bump version 4.2.1 2024-12-27 13:12:29 +05:30
Neeraj Gupta
bf9d0e3d6b [auth] Reduce refresh rate for progress bar to lower CPU usage 2024-12-27 12:18:35 +05:30
Manav Rathi
a818f062b1 [web] New translations (#4515)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2024-12-27 12:10:13 +05:30
Crowdin Bot
f57f1a8636 New Crowdin translations by GitHub Action 2024-12-27 06:39:11 +00:00
Manav Rathi
da78c45b1d [web] Minor translation keys improvements (#4514) 2024-12-27 12:08:30 +05:30
Manav Rathi
3429a9f3d6 Fixes 2024-12-27 12:03:35 +05:30
Manav Rathi
cdbd86d63c Rename 2024-12-27 11:55:58 +05:30
Manav Rathi
69e1aa18e6 fix 2024-12-27 11:54:44 +05:30
Manav Rathi
6f2079c7c6 Ren 2024-12-27 11:50:48 +05:30
Manav Rathi
a14358416a [web] New translations (#4513)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2024-12-27 11:22:35 +05:30
Crowdin Bot
57c353a443 New Crowdin translations by GitHub Action 2024-12-27 05:48:15 +00:00
Manav Rathi
dd7cea1f96 [web] Switch to new dup implementation (same as mobile) (#4512) 2024-12-27 11:17:25 +05:30
Manav Rathi
6d2a223acf Fixes 2024-12-27 11:12:58 +05:30
Manav Rathi
eca0137426 Replace old impl 2024-12-27 11:10:05 +05:30
mngshm
e1e2c528c4 fix: accounts env variable 2024-12-27 10:02:17 +05:30
Manav Rathi
28ab3c321c Loc 2024-12-27 09:34:13 +05:30
Manav Rathi
5c2c6b2a84 Set 2024-12-27 09:13:37 +05:30
Manav Rathi
4ae0683c62 ids are enough and less confusinng 2024-12-27 08:59:28 +05:30
Manav Rathi
0a6740bb2e Prune 2024-12-27 08:46:40 +05:30
Manav Rathi
b026020485 [web] New dedup, same as mobile - Almost completed (#4508)
The changes are done, haven't swapped with the existing implementation
yet pending another scan.
2024-12-26 19:44:27 +05:30
Manav Rathi
b71fa478b9 Remote expects uniques 2024-12-26 19:31:15 +05:30
Manav Rathi
b9c992cae0 Dedup 2024-12-26 19:19:27 +05:30
Manav Rathi
2845d7bfeb lf 2024-12-26 18:49:34 +05:30
Manav Rathi
b09d6ab2a6 Sync after dedup 2024-12-26 18:46:28 +05:30
Manav Rathi
94ce77c07b Move 2024-12-26 18:45:18 +05:30
Manav Rathi
a292f01187 Move 2024-12-26 18:42:57 +05:30
Manav Rathi
aae2632b19 See: [Note: strict mode migration] 2024-12-26 18:28:54 +05:30
Manav Rathi
746c85bc9f Move 2024-12-26 18:23:06 +05:30
Manav Rathi
adeab53d3b Move 2024-12-26 18:03:32 +05:30
Manav Rathi
3e23ff9c9b Move 2024-12-26 17:54:58 +05:30
Manav Rathi
dca6e02286 Move 2024-12-26 17:52:26 +05:30
Manav Rathi
daf3fd2a75 Sketch 2024-12-26 17:42:41 +05:30
Manav Rathi
f5a3b8a3fb Del 2024-12-26 17:29:13 +05:30
Manav Rathi
7cd1ce0a99 Avoid the same name 2024-12-26 17:13:59 +05:30
Manav Rathi
1b863005ea Ref 2024-12-26 17:11:48 +05:30
Ashil
b580756e6b [mob][photos] On freeing up space, retry deleting local files in batches with smaller max batch sizes if no files are deleted with bigger max batch sizes (#4505) 2024-12-26 15:53:24 +05:30
Manav Rathi
26fb47c165 Invert the processing 2024-12-26 15:50:35 +05:30
Manav Rathi
3f21011392 Retain files so that we can reuse trashFiles code 2024-12-26 15:19:15 +05:30
Manav Rathi
7348170a36 Tweak progress 2024-12-26 14:57:46 +05:30
Manav Rathi
3919fb0db2 Progress is not tied to specific groups 2024-12-26 14:42:05 +05:30
Manav Rathi
ad6a0e9c31 linprog 2024-12-26 13:38:02 +05:30
Manav Rathi
be3896826d [web] Refactor some APIs we'll use for dedup (#4506) 2024-12-26 13:00:15 +05:30
Manav Rathi
268550f292 lf 2024-12-26 12:56:47 +05:30
Manav Rathi
68e557124c Up 2024-12-26 12:55:33 +05:30
Manav Rathi
96863923d1 Tweak 2024-12-26 12:21:57 +05:30
Manav Rathi
a22b0aec58 Tweak 2024-12-26 12:11:35 +05:30
ashilkn
93e26c6caf [mob][photos] Retry deleting local files in batches with smaller max batch sizes if no files are deleted with a bigger max batch size 2024-12-26 12:01:18 +05:30
Manav Rathi
8a8f5c20c6 Tweak 2024-12-26 11:56:26 +05:30
Manav Rathi
3c7b6694e9 Ren 2024-12-26 11:53:21 +05:30
Manav Rathi
c37d85f6c5 Move 2024-12-26 11:49:28 +05:30
Neeraj Gupta
e0abb2de9c [auth] Add dependency on super_text_layout 2024-12-26 11:44:37 +05:30
Neeraj Gupta
b73ba4a22f [auth][mac] build changes 2024-12-26 11:42:29 +05:30
Manav Rathi
912279e3cf Dedup 2024-12-26 11:35:32 +05:30
Manav Rathi
5a0bab9304 Merge 2024-12-26 11:33:58 +05:30
Manav Rathi
a9cd56c4ce Ren 2024-12-26 11:28:18 +05:30
Manav Rathi
5b4028378b Retain all collections associated 2024-12-26 11:20:45 +05:30
Ashil
57bd5b9d17 [mob][photos] In-app public link fixes (#4495)
## Description

Fixed these issues:
- Unrelated files coming up (these are local device files) in public
link when opened in-app.
- Max number of files in link capped to 2000.
- Sort order not working.
2024-12-24 21:07:41 +05:30
Manav Rathi
2bd074bd79 [web] New dedup - WIP - Part (n-1)/n (#4496) 2024-12-24 18:41:03 +05:30
Manav Rathi
a8d831364d Progress 2 2024-12-24 18:25:45 +05:30
ashilkn
21f0602161 [mob][photos] Fix sort order setting not reflecting on public link opened in-app 2024-12-24 18:13:28 +05:30
Manav Rathi
1ac2d60c7b Progress 2024-12-24 18:06:13 +05:30
Manav Rathi
a098481b98 tryctch 2024-12-24 18:03:02 +05:30
Manav Rathi
472339cafb Reduce potential for aliasing 2024-12-24 17:56:03 +05:30
Manav Rathi
d24f5bcee7 ftr 2024-12-24 17:46:36 +05:30
ashilkn
bd0e8e6fe6 [mob][photos] Fix in-app public links only showing upto 2000 files 2024-12-24 17:25:33 +05:30
Neeraj Gupta
c9d2a0a4ca [mob] Remove hardcoded app urls for passkey & cast (#4494)
## Description

## Tests
Tested locally
2024-12-24 16:57:47 +05:30
Neeraj Gupta
1a6eb26f2b [mob] Remove hardcoded url for cast 2024-12-24 16:51:30 +05:30
Manav Rathi
7c2fce2ebe Fin local part 2024-12-24 16:49:42 +05:30
Neeraj Gupta
e90871ea6b Merge remote-tracking branch 'origin/main' into remove_hardcoded_apps_url 2024-12-24 16:34:31 +05:30
Neeraj Gupta
3ca78cac35 [mob] Remove hardcoded url for accounts/passkey 2024-12-24 16:34:09 +05:30
Neeraj Gupta
49ddfdfde5 [mobile] New translations (#4493)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-app)
2024-12-24 16:33:29 +05:30
Crowdin Bot
bc76864587 New Crowdin translations by GitHub Action 2024-12-24 11:03:00 +00:00
Manav Rathi
4f1d3c23f5 Fix and imp 2024-12-24 16:31:39 +05:30
ashilkn
cc674183cd [mob][photos] Use genId + uploadedId instead of just genId for value key to make sure all keys are unique when opening a public link in-app (genID is null for all files in public link) 2024-12-24 16:30:38 +05:30
Manav Rathi
86bd098406 Sort 2024-12-24 16:29:55 +05:30
Manav Rathi
e5fe3a7255 size 2024-12-24 16:28:16 +05:30
ashilkn
df68d3f005 [mob][photos] Fix local files coming up when public links are opened in-app 2024-12-24 16:25:55 +05:30
ashilkn
9a6a46fd0d [mob][photos] Chore 2024-12-24 16:23:57 +05:30
Neeraj Gupta
14c9929451 [server] Rename accountUrl -> accountsUrl (#4492)
## Description
Related https://github.com/ente-io/ente/pull/4491
## Tests
2024-12-24 15:54:24 +05:30
Manav Rathi
54f9bd880a Funnel 2024-12-24 15:43:14 +05:30
Neeraj Gupta
aaa636345c [mob] Remove hardcoded url for accounts/passkey 2024-12-24 15:32:41 +05:30
Neeraj Gupta
6f3e02888e [auth] Remove hardcoded accounts url (#4491)
## Description

## Tests
2024-12-24 15:26:16 +05:30
Neeraj Gupta
42a8b5c826 [server] Rename accountUrl -> accountsUrl 2024-12-24 15:25:04 +05:30
Manav Rathi
24674f6da6 Notes and changes based on discussion
The hash change now matches mobile
2024-12-24 15:25:03 +05:30
Neeraj Gupta
90b45665f5 [auth] Remove hardcoded accounts url 2024-12-24 15:23:58 +05:30
Manav Rathi
8a217a292b Comments 2024-12-24 14:18:31 +05:30
Neeraj Gupta
047fede844 [mobile] New translations (#4471)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-app)
2024-12-24 14:02:41 +05:30
Crowdin Bot
e77b557990 New Crowdin translations by GitHub Action 2024-12-24 07:11:57 +00:00
Manav Rathi
c2bfcf23c0 larger 2024-12-24 12:36:35 +05:30
Vishnu Mohandas
646a012734 [mob] Fix typos (#4481) 2024-12-24 12:36:20 +05:30
Manav Rathi
21332c6b92 Placeholder activity indicator 2024-12-24 12:26:16 +05:30
Manav Rathi
f9c101241e Sketch 2024-12-24 12:10:40 +05:30
Manav Rathi
bc6f147f5e Fix 2024-12-24 11:50:51 +05:30
Manav Rathi
fbaa360741 do both transitions simultaneously 2024-12-24 10:12:09 +05:30
Manav Rathi
4029398351 Reduce noise 2024-12-24 10:07:43 +05:30
Manav Rathi
7602d48bd9 Refresh list on width and sort order changes 2024-12-24 09:54:18 +05:30
Manav Rathi
f28d50ace6 div 2024-12-24 09:24:03 +05:30
Manav Rathi
b026b30172 layout 2024-12-24 09:03:07 +05:30
Manav Rathi
977f5c21a6 [web] New dedup - WIP - Part x/x (#4482) 2024-12-23 20:41:12 +05:30
Manav Rathi
dafbf23d67 2 line 2024-12-23 19:39:31 +05:30
vishnukvmd
d9bc6597c0 Fix typo 2024-12-23 19:28:46 +05:30
vishnukvmd
913a80591b Fix typo 2024-12-23 19:26:51 +05:30
Manav Rathi
26c0a8e1d5 Title 2024-12-23 19:25:37 +05:30
Manav Rathi
2e2c972a84 Tiles 2024-12-23 19:04:17 +05:30
Manav Rathi
748dd2b0e2 [web] New dedup - WIP (#4478) 2024-12-23 17:20:50 +05:30
Manav Rathi
dbb376056d Fix type 2024-12-23 17:13:56 +05:30
Manav Rathi
b31fc5cbe9 Pad 2024-12-23 17:02:08 +05:30
Manav Rathi
cb76ba7560 Var 2024-12-23 16:33:44 +05:30
Manav Rathi
e915ded2de fit in 2024-12-23 16:18:12 +05:30
mangesh
eedc538283 [docs] minor fixes in Dockerfile and some others (#4473) 2024-12-23 16:11:25 +05:30
Neeraj Gupta
294b333d0e [auth] New translations (#4472)
New translations from
[Crowdin](https://crowdin.com/project/ente-authenticator-app)
2024-12-23 16:10:54 +05:30
Vishnu Mohandas
8f705f2f72 [auth] Make "Ente Auth" at the top of app similar to marketing images/Photos app (#4469)
## Description

Currently the "Ente Auth" text at the top of the mobile/desktop app is
different in style to the one in the marketing images and the equivalent
in the Photos app. So I just copied the style from the Photos app.

Marketing image:


![auth-home-screen-dark](https://github.com/user-attachments/assets/0e90b524-391c-4de5-b5d4-da4688149aea)

Currently: (not the latest version of the app but the text part is the
same except for the capitalization)


![screenshots](https://github.com/user-attachments/assets/cd39fdf1-d518-4b24-8f60-e0960f2c9985)

## Tests

I haven't tested this.
2024-12-23 16:10:44 +05:30
Neeraj Gupta
c5c0ee5ddf [auth] Update simple-icons (#4477)
## Description
Fixes https://github.com/ente-io/ente/issues/4476
## Tests
2024-12-23 16:05:30 +05:30
Neeraj Gupta
5f43f03a65 Update simple-icons 2024-12-23 16:02:50 +05:30
Manav Rathi
aa62f4003c Dup the layout algo 2024-12-23 15:56:51 +05:30
Manav Rathi
6c5dbc3696 Grid 2024-12-23 15:14:52 +05:30
Neeraj Gupta
1bef409552 [server] Return various app urls as part of relevant API response. (#4458)
## Description

For the cast dialog, that we show on the app, before making the API call
to pair. Have parked that change for now.

## Tests
2024-12-23 15:08:00 +05:30
Neeraj Gupta
62155040da Review comment + return castUrl as part of featureFlag 2024-12-23 14:19:33 +05:30
Manav Rathi
592dc26d8b Optimize unnecessary rerenders 2024-12-23 14:11:04 +05:30
Manav Rathi
1af1c3f196 Probe 2024-12-23 14:09:24 +05:30
Manav Rathi
6f077310c1 [web] New translations (#4470)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2024-12-23 10:27:57 +05:30
Neeraj Gupta
905fc2ad78 [server] Disable 2fa on legacy account recovery (#4463)
## Description

## Tests
2024-12-23 10:26:56 +05:30
mngshm
e6e4f313de minor: add log level for caddy and change build sequence 2024-12-23 09:43:55 +05:30
mngshm
1ae8caa917 fix:remove serve for non-existent albums app and add note 2024-12-23 09:42:41 +05:30
Crowdin Bot
209228326d New Crowdin translations by GitHub Action 2024-12-23 01:17:31 +00:00
Crowdin Bot
0533f99313 New Crowdin translations by GitHub Action 2024-12-23 00:35:18 +00:00
dnred
545cf40710 Update home_page.dart 2024-12-22 22:20:59 +01:00
dnred
8ae8ed20fe Make the "Ente Auth" text similar to marketing images 2024-12-22 21:55:43 +01:00
Neeraj Gupta
0df0126af4 [server] Disable 2fa on legacy account recovery 2024-12-21 17:15:50 +05:30
Neeraj Gupta
60ad6ef713 [auth] Potential fix for desktop build (#4462)
## Description

## Tests
2024-12-21 17:03:01 +05:30
Neeraj Gupta
fc626c1287 Update pod for macos 2024-12-21 17:00:04 +05:30
Neeraj Gupta
4035e364df [auth] Update github workflow for auth-release 2024-12-21 16:56:47 +05:30
Neeraj Gupta
791ec10a0f [auth] Improve icon path matching (#4461)
## Description

## Tests
2024-12-21 16:40:28 +05:30
Neeraj Gupta
87ab805cf7 Lint fix 2024-12-21 16:37:24 +05:30
Neeraj Gupta
447bb72556 Fix altname icons when slug is missing 2024-12-21 16:32:46 +05:30
Neeraj Gupta
c07e2f1387 Improve simple-icon path creation 2024-12-21 16:24:50 +05:30
Neeraj Gupta
151a0b3ab7 Pull simple-icons 2024-12-21 16:04:29 +05:30
Neeraj Gupta
cf15d68bd2 Merge remote-tracking branch 'origin/main' into auth_minor_1 2024-12-21 15:57:41 +05:30
Neeraj Gupta
e4a05edc12 iOS build changes 2024-12-21 15:57:33 +05:30
Neeraj Gupta
879d6571bc Show scroll bar 2024-12-21 15:57:04 +05:30
Neeraj Gupta
2496350fad [auth] fix formatting of hex value in custom-icons.json (#4460)
## Description

Error:  **Invalid radix-16 number (at character 3) 0xFF#4687FF**


![image](https://github.com/user-attachments/assets/60649484-51a8-46e4-a06c-6c30880b20b7)

![image](https://github.com/user-attachments/assets/51236a07-2d70-4433-a450-3fa32fe9bf7e)
2024-12-21 15:52:30 +05:30
Aman Raj
653a7f22ef [auth] Bump version 2024-12-21 15:33:34 +05:30
Aman Raj
6071df2083 [auth] fix formatting of hex value in custom-icons.json 2024-12-21 15:27:49 +05:30
Neeraj Gupta
f6e93ab060 [auth] Rename 2024-12-21 15:07:14 +05:30
Neeraj Gupta
ddc9dfe552 [auth] Fix icons not showing on Choose Icon page (#4459) 2024-12-21 15:06:23 +05:30
Aman Raj
b9b87c1570 [auth] Bump version 2024-12-21 15:03:08 +05:30
Aman Raj
3a413524f8 [auth] fix icons not showing 2024-12-21 14:58:28 +05:30
Neeraj Gupta
4830451d4e [docs] minor fixes in web-apps hosting doc (#4446)
Did not move the [!IMPORTANT] section at the bottom like discussed,
because I don't feel that would change the situation a lot.

Also fixed the CMD command, the flags and serve url were wrong in the
previous commit (wonder how it worked on local system, probably loaded
wrong image in the compose.yaml)
2024-12-21 12:02:26 +05:30
Neeraj Gupta
4cb11f4b06 [server] Return accounts & familyUrl 2024-12-21 11:56:43 +05:30
Neeraj Gupta
150faa5d84 [server] Config for accounts,cast, & family apps 2024-12-21 11:46:12 +05:30
Neeraj Gupta
9b15102058 [mob] Update permission dialog (#4456)
## Description




## Tests
2024-12-21 11:20:06 +05:30
Neeraj Gupta
25bb175ff7 Remove redundant import 2024-12-21 10:21:41 +05:30
Neeraj Gupta
ec7b61c36a [mob] Update permission dialog 2024-12-21 10:21:06 +05:30
Manav Rathi
85735b4ff0 [web] New dedup (same as mobile) - WIP (#4454) 2024-12-20 18:44:12 +05:30
Manav Rathi
8029829d9b Retain sel on sort 2024-12-20 18:30:31 +05:30
Manav Rathi
87c5f05f84 ds all 2024-12-20 18:24:12 +05:30
Manav Rathi
356b2542c9 Cherry 2024-12-20 18:14:05 +05:30
Manav Rathi
dc3329368e Sel 2024-12-20 18:12:27 +05:30
Manav Rathi
92868dccb4 sz/count 2024-12-20 17:49:50 +05:30
Manav Rathi
49942909b0 Pass data 2024-12-20 17:35:55 +05:30
Manav Rathi
b36c8de417 List 2024-12-20 17:26:34 +05:30
Manav Rathi
a0335b82c6 Extr 2024-12-20 17:20:57 +05:30
Manav Rathi
01cbf29217 Button 2024-12-20 16:56:32 +05:30
Manav Rathi
7c464a0d60 Sort 2024-12-20 16:41:33 +05:30
Laurens Priem
8c20f5f660 Ml string (#4453)
## Description

## Tests
2024-12-20 11:54:55 +01:00
Manav Rathi
671199e286 Redirect 2024-12-20 16:19:04 +05:30
laurenspriem
6805ee1a2a [mob][photos] Suggestions less filtering on large size 2024-12-20 11:36:17 +01:00
Ashil
fd8246705c [mob][photos] Memories widget UI changes (#4448)
### Description 

Change seen and unseen memory states.

(New state on the right)
<img width="824" alt="Screenshot 2024-12-20 at 1 12 47 PM"
src="https://github.com/user-attachments/assets/f8d176f2-08d0-49fc-9758-4f8f75ef4479"
/>
2024-12-20 15:45:58 +05:30
Neeraj Gupta
65e1745aa0 [auth] HTML export fixes (#4449) 2024-12-20 15:45:45 +05:30
Neeraj Gupta
c6c3b1f9bd [auth] Remove Contact Support option when no duplicate code found (#4450)
## Description

## Tests
2024-12-20 15:45:30 +05:30
Manav Rathi
175467267a [desktop] Flush file writes (#4452)
A customer reported a partial export_status.json being written to an
external drive. Forcing a flush to attempt to reduce chances of this
happening. Since this particular code path is only used for writing JSON
files (export status and metadata), we unconditionally enable this for
all writes.
2024-12-20 15:17:27 +05:30
Manav Rathi
9756c178bf [desktop] Flush file writes
A customer reported a partial export_status.json being written to an external
drive. Forcing a flush to attempt to reduce chances of this happening. Since
this particular code path is only used for writing JSON files (export status and
metadata), we unconditionally enable this for all writes.
2024-12-20 15:13:46 +05:30
Manav Rathi
af420a8fc3 [web] New dedupe WIP + overflow menu cleanup (#4451) 2024-12-20 15:07:41 +05:30
Manav Rathi
9a5d977419 lf 2024-12-20 15:01:06 +05:30
Manav Rathi
d4ae5c118b Swap and fin 2024-12-20 15:00:35 +05:30
Manav Rathi
605fda2710 Swap 2024-12-20 14:59:45 +05:30
Manav Rathi
0181693736 Swap 2024-12-20 14:57:29 +05:30
Manav Rathi
5c92d093ca Swap 2024-12-20 14:56:33 +05:30
Manav Rathi
a6c9a153e7 Fix watch folder opening 2024-12-20 14:24:20 +05:30
Manav Rathi
613f7294e1 Swap 2024-12-20 14:20:19 +05:30
Manav Rathi
facd05bd89 Swap 2024-12-20 14:15:42 +05:30
Manav Rathi
4bbe71e135 Use 2024-12-20 14:12:05 +05:30
Manav Rathi
5583902433 tt 2024-12-20 14:02:12 +05:30
Manav Rathi
664c723c78 Tweak 2024-12-20 13:47:14 +05:30
Aman Raj
9b35fe04b9 [auth] remove Contact Support option when no duplicate code found 2024-12-20 13:42:48 +05:30
Manav Rathi
34068d09ba Use regular menu 2024-12-20 13:35:52 +05:30
Aman Raj
c23b22cc5b [auth] Extract String 2024-12-20 13:34:23 +05:30
Aman Raj
065382ddd2 [auth] Use better names 2024-12-20 13:34:11 +05:30
Aman Raj
5a72686e53 [auth] use table to display exported code contents 2024-12-20 13:29:38 +05:30
Manav Rathi
8c5b77cd52 Extr 2024-12-20 13:08:12 +05:30
Manav Rathi
d30dce0896 Menu 2024-12-20 13:05:30 +05:30
ashilkn
c453827cc8 [mob][photos] Make text alignment perfect 2024-12-20 12:57:21 +05:30
Manav Rathi
1068b6811f Dup 2024-12-20 12:51:45 +05:30
ashilkn
09fe2c6f7e [mob][photos] Minor perf improvement 2024-12-20 12:50:26 +05:30
Manav Rathi
a31803e3f5 Appear 2024-12-20 12:41:39 +05:30
ashilkn
2ec8ae34b8 [mob][photos] Remove unnecessary widget + minor UI tweak on memory widget 2024-12-20 12:33:17 +05:30
Manav Rathi
3263542f5e fin 1 2024-12-20 12:26:55 +05:30
Manav Rathi
a302f986d7 Mirror files 2024-12-20 12:04:32 +05:30
Manav Rathi
69ccf7d3c9 Shorten 2024-12-20 12:00:16 +05:30
Manav Rathi
76308cc9d0 Name 2024-12-20 11:56:18 +05:30
Manav Rathi
1d02732719 Impl 2024-12-20 11:43:07 +05:30
ashilkn
55fa86a6c8 [mob][photos] Change seen and unseen memories UI state 2024-12-20 11:36:25 +05:30
Manav Rathi
39fad29bc8 [web] Automatically update search results on deletes (#4445)
...and other actions which the search results are being shown.
2024-12-19 16:00:24 +05:30
mngshm
2ebe8712e8 fix[web-docs]:add env vars for albums and accounts endpoints 2024-12-19 15:59:41 +05:30
Manav Rathi
1f7176cea2 Update search results on delete etc 2024-12-19 15:47:35 +05:30
Manav Rathi
bede7559be Move to reducer 2024-12-19 14:41:32 +05:30
Manav Rathi
77563a7483 Reduce scope 2024-12-19 12:53:24 +05:30
Manav Rathi
37df79314a Use count from search results 2024-12-19 12:39:13 +05:30
Manav Rathi
fc5d1f931c Move to reducer 2024-12-19 12:18:32 +05:30
Manav Rathi
243948f182 Revert "[web] Reflect deletes in search result"
This reverts commit ddc953045b.

Because count doesn't get updated this way.
2024-12-19 12:04:04 +05:30
Manav Rathi
ddc953045b [web] Reflect deletes in search result 2024-12-19 11:51:17 +05:30
Neeraj Gupta
f74f285b7f [docs] improvements to external s3 guide (#4398) 2024-12-19 10:46:53 +05:30
Neeraj Gupta
2a4a886fca [server] Support for storing preview files (#4226)
## Description
- This change introduced the concept of associated object for a file.
- Added additional columns for object_id, object_nonce, and object_size.

Depending upon data_type, the values of certain columns will be nil.
The original size column will reflect total size for that particular
type. In case of vid_preview, it's size of the playlist + size of the
preview video.



## Tests

- [x] Replication
- [x] Test Deletion post replication
2024-12-19 10:06:15 +05:30
Ashil
43e3e44e5c [mob][photos] Move delete option in file selection actions to make it easily accessible (no scrolling needed) (#4440) 2024-12-19 09:26:39 +05:30
ashilkn
09cc226511 [mob][photos] Move delete option in file selection actions to make it easily accessible (no scrolling needed) 2024-12-18 20:43:44 +05:30
laurenspriem
d9f62b8956 [mob][photos] Log empty person 2024-12-18 15:08:42 +01:00
Manav Rathi
a691745ef7 [web] Retry on failures in fetching already indexed items (#4439)
https://github.com/ente-io/ente/issues/4087#issuecomment-2525073128
2024-12-18 19:18:29 +05:30
Manav Rathi
108e984f29 [web] Retry on failures in fetching already indexed items
https://github.com/ente-io/ente/issues/4087#issuecomment-2525073128
2024-12-18 19:00:38 +05:30
laurenspriem
fbbb8edce1 [mob][photos] Copy people empty state 2024-12-18 14:26:31 +01:00
Manav Rathi
72dd4949ce [web] Remove node buffer dependency from base 58 conversion and cleanup (#4438) 2024-12-18 18:54:27 +05:30
Manav Rathi
948b869bea Move and update 2024-12-18 18:46:15 +05:30
Manav Rathi
3cde395f42 Fix the buffer warning 2024-12-18 18:42:19 +05:30
Manav Rathi
05165728f1 Use 2024-12-18 18:38:36 +05:30
Manav Rathi
42f2bb819b Move 2024-12-18 18:38:05 +05:30
Manav Rathi
6b28aa1652 Use 2024-12-18 18:22:26 +05:30
Manav Rathi
d167da02d5 both 2024-12-18 18:19:59 +05:30
Manav Rathi
34fe3bee7f Move 2024-12-18 18:02:55 +05:30
Manav Rathi
4bfa398312 [desktop] Fix export when selecting root folder (#4437)
This fixes the issue where files would be exported twice on pressing
resync if the user selected "C://" (or some other root drive) as the
export destination.
2024-12-18 17:14:53 +05:30
Manav Rathi
5920999bf4 Use 2024-12-18 17:08:35 +05:30
Neeraj Gupta
7559ab4236 [auth] Align icon to show on top center of Code Edit Screen & Bump Version (#4436)
## Description
Light Mode


![image](https://github.com/user-attachments/assets/9db0c583-d798-47fa-b17a-9b1c847ff704)

Dark Mode


![image](https://github.com/user-attachments/assets/5c14616b-6ee6-4fa6-8a7a-c4dfb9b7b3d4)

## Tests
2024-12-18 16:58:10 +05:30
Manav Rathi
44d2f66260 pathJoin 2024-12-18 16:45:01 +05:30
Aman Raj
ae3b4604e9 [auth] Bump version v4.1.8 2024-12-18 16:36:57 +05:30
Manav Rathi
7b32ace2d9 Revert "[desktop] Add some temporary debug traces"
This reverts commit 2bc1c90637.
2024-12-18 16:33:05 +05:30
Ashil
29052e2888 [mob][photos] Use media_kit player for videos not supported by native player (#4429)
## Description

On iOS, if a video is unsupported by the native player, the app will
switch automatically to use media_kit player.
On android, user will have to manually switch to the media_kit player.  

I used white with 20% opacity for the icon that switches to the MediaKit
player, making it most noticeable against the darkest background (black,
when the video isn't playable) and less prominent against lighter
backgrounds.



https://github.com/user-attachments/assets/b4316c6e-1691-4328-8984-4d8240179873



Have fixed a UI/UX issue on the video player seen when the video has a
caption/description.
2024-12-18 16:32:41 +05:30
Aman Raj
b18d8bb5e6 [auth] UI improvements 2024-12-18 16:21:17 +05:30
Aman Raj
91aea808f5 [auth] show icon on the top center 2024-12-18 16:20:09 +05:30
Aman Raj
a6590c29d5 [auth] added figma_squircle package to clip icon on edit page 2024-12-18 16:19:22 +05:30
ashilkn
b139dea7ff [mob][photos] Make code cleaner 2024-12-18 15:51:50 +05:30
ashilkn
80857d5441 [mob][photos] Use better icon for switching to media_kit on video player 2024-12-18 15:40:09 +05:30
Manav Rathi
1fb9a7e6c4 [desktop] Add some temporary debug traces (#4435) 2024-12-18 14:32:46 +05:30
Manav Rathi
2bc1c90637 [desktop] Add some temporary debug traces 2024-12-18 14:31:55 +05:30
Neeraj Gupta
23a33610ee [auth] Update feature request url (#4434)
## Description

## Tests
2024-12-18 14:18:28 +05:30
Neeraj Gupta
a6cd937347 [auth] Update feature request url 2024-12-18 14:02:11 +05:30
Neeraj Gupta
7f3d0a5328 Add comment 2024-12-18 13:19:32 +05:30
mangesh
40d938b6a3 [docs][web-app]: rewrite web-app hosting guide (#4424)
rewrites the web-app hosting guide on help.ente.io/self-hosting.
Includes steps for building the Dockerfile and also additionally
includes the pm2 setup, perhaps Docker is marked "Recommended" to avoid
unnecessary variables which "might" (likely won't) show up in the pm2
setup.
2024-12-18 13:12:47 +05:30
Neeraj Gupta
423a669cff [sever] Bump db migration query 2024-12-18 13:12:16 +05:30
Neeraj Gupta
4546d60e61 Merge remote-tracking branch 'origin/main' into video_file_preview 2024-12-18 13:09:19 +05:30
Aman Raj
735f5e3d3c [auth] Change title of custom_icon page 2024-12-18 12:07:32 +05:30
Aman Raj Singh Mourya
7cf7443177 [auth] Export code as html (#4426) 2024-12-18 11:55:24 +05:30
mngshm
d66aa25ee7 fix: minor typos and some additions 2024-12-18 10:21:25 +05:30
ashilkn
0d920a35e7 [mob][photos] Remove unused code 2024-12-17 21:37:00 +05:30
ashilkn
38860d91d3 [mob][photos] Make '?' icon for switching to media_kit player more noticeable 2024-12-17 21:09:39 +05:30
Aman Raj
941b326328 [auth] add notes & secret to html export file 2024-12-17 21:08:14 +05:30
ashilkn
6851d0fae5 [mob][photos] Extract strings 2024-12-17 21:05:46 +05:30
ashilkn
42dc8a451b [mob][photos] bump up to v0.9.72 2024-12-17 20:56:35 +05:30
ashilkn
8e2f052ac3 Merge branch 'main' into use_media_kit_for_unsupported_videos 2024-12-17 20:55:55 +05:30
Manav Rathi
087f34304b [web] Dedup like mobile - WIP, Part x/x (#4427) 2024-12-17 19:21:24 +05:30
Manav Rathi
427c5b4d7d lf 2024-12-17 19:18:24 +05:30
Manav Rathi
bf3a47826b Button wip 2024-12-17 19:17:00 +05:30
Manav Rathi
423ebc6588 Autosizer 2024-12-17 19:11:13 +05:30
ashilkn
9a89153563 [mob][photos] Fix UX/UX issue when a video is opened that has a caption/description 2024-12-17 19:02:25 +05:30
Manav Rathi
606e3013f7 States 2024-12-17 18:34:28 +05:30
Manav Rathi
653ae485a9 Reducer 2024-12-17 18:18:52 +05:30
ashilkn
f681c956ea [mob][photos] UX improvement on switching to media kit player from native video player 2024-12-17 18:02:37 +05:30
ashilkn
4422b4a7b0 [mob][photos] Automatically change to media kit if video not playable on ios + let the user manually switch to media kit 2024-12-17 17:58:05 +05:30
Manav Rathi
926b5de6cd Reduce 2024-12-17 17:35:16 +05:30
Manav Rathi
41de48c454 State wip 2024-12-17 17:21:43 +05:30
mngshm
2f3f48f4db remove: unintended copy-pasted text 2024-12-17 17:08:54 +05:30
Manav Rathi
71ea266f9a Dup 2024-12-17 17:01:31 +05:30
Aman Raj
af187a3c0c [auth] improve UI of HTML file + code refractor 2024-12-17 16:49:29 +05:30
Manav Rathi
11d32752d4 back 2024-12-17 16:29:42 +05:30
Manav Rathi
8a3b0d956e Banner 2024-12-17 16:03:26 +05:30
Manav Rathi
183321fa18 [web] New translations (#4422)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2024-12-17 15:59:40 +05:30
Neeraj Gupta
96d9c6d5ee [auth] Bump version v4.1.7 (#4425)
## Description

## Tests
2024-12-17 15:55:32 +05:30
Neeraj Gupta
728cd31210 [auth] Bump version v4.1.7 2024-12-17 15:54:44 +05:30
Neeraj Gupta
9491310bfd [auth] Add Support for Custom Icons (#4395)
## Description
This PR introduces a new feature allowing users to select and assign
custom icons to their codes.
2024-12-17 15:53:39 +05:30
Neeraj Gupta
1978226fce [auth] Sort by natural order (#4423)
## Description
Resolves https://github.com/ente-io/ente/issues/4420 &
https://github.com/ente-io/ente/issues/4371

## Tests
2024-12-17 15:52:54 +05:30
mngshm
956e74533e fix: remove commented guide 2024-12-17 15:14:58 +05:30
mngshm
1b5e6174f1 [docs][web-app]: rewrite web-app hosting guide 2024-12-17 15:13:53 +05:30
Aman Raj
8fc9ff0d9f [auth] minor fix 2024-12-17 15:06:31 +05:30
Neeraj Gupta
8e6330dfdb Fix sorting 2024-12-17 14:46:13 +05:30
Neeraj Gupta
eec4bbde98 Lint fix 2024-12-17 14:08:57 +05:30
Neeraj Gupta
b12371437f [auth] Extract strings 2024-12-17 14:02:29 +05:30
Neeraj Gupta
8219177c1c [auth] Sort by natural order 2024-12-17 14:01:52 +05:30
Manav Rathi
56be41c38f Up 2024-12-17 13:52:01 +05:30
Manav Rathi
5b371380fd Conv raw h1 2024-12-17 13:44:59 +05:30
Neeraj Gupta
eec060ae71 Fixed typos 2024-12-17 13:37:34 +05:30
Neeraj Gupta
70ec18462b Add icon back 2024-12-17 13:36:06 +05:30
Neeraj Gupta
b3cf07f232 Merge remote-tracking branch 'origin/main' into video_file_preview 2024-12-17 13:34:24 +05:30
Neeraj Gupta
2f3639fbbc Clean up git 2024-12-17 13:33:34 +05:30
Manav Rathi
c1ac9d22ba wip 2024-12-17 13:30:37 +05:30
Manav Rathi
3fdfa10402 new 2024-12-17 13:16:57 +05:30
Crowdin Bot
6b49a889da New Crowdin translations by GitHub Action 2024-12-17 06:07:44 +00:00
Manav Rathi
431ad61ca2 [web] Use purpose to distinguish signup / login (#4421) 2024-12-17 11:36:47 +05:30
Manav Rathi
75456c1b34 lf 2024-12-17 11:32:36 +05:30
Manav Rathi
45b490cb43 Specific 2024-12-17 11:24:42 +05:30
Manav Rathi
1f4aebf20f tr 2024-12-17 11:19:55 +05:30
Manav Rathi
e639aa9306 Don't pass a purpose during recover since we're not handling the error 2024-12-17 11:09:39 +05:30
Manav Rathi
5ce96fde3e Signup 2024-12-17 11:04:55 +05:30
Manav Rathi
a91027c335 Generic 2024-12-17 10:59:09 +05:30
Manav Rathi
1ad7ba82c2 Tweak 2024-12-17 10:48:01 +05:30
Manav Rathi
b5d274f7ae Save state only if we can proceed 2024-12-17 10:46:52 +05:30
Manav Rathi
35f710439f Split errors 2024-12-17 10:41:42 +05:30
Manav Rathi
bb2072aafe Don't show arbitrarily long lower level errors in the UI 2024-12-17 10:31:09 +05:30
Manav Rathi
479f172e4d Limit box width 2024-12-17 10:29:01 +05:30
Manav Rathi
4383841ef1 Use 2024-12-17 09:47:10 +05:30
Manav Rathi
57a00c1703 purp 2024-12-17 09:37:35 +05:30
Manav Rathi
9b292bbd80 X-Client-Package determines this 2024-12-17 09:33:59 +05:30
Manav Rathi
8ffb52dd7e Upd wip 2024-12-17 09:32:59 +05:30
Vishnu Mohandas
a9545c3bef [docs] Add a tip about enabling ML before doing imports (#4412) 2024-12-16 07:52:46 -08:00
Manav Rathi
b0f1cea9ef [desktop] Include the disk file count in the export logs (#4417) 2024-12-16 20:34:18 +05:30
Manav Rathi
2c9cff040d [desktop] Include the disk file count in the export logs 2024-12-16 20:18:55 +05:30
Manav Rathi
9aaee77004 [desktop] Add workaround for back button on Stripe checkout (#4416)
Fixes: https://github.com/ente-io/ente/issues/4358
2024-12-16 19:41:00 +05:30
Manav Rathi
9ac61d063a [desktop] Add workaround for back button on Stripe checkout
Fixes: https://github.com/ente-io/ente/issues/4358
2024-12-16 19:37:01 +05:30
Ashil
3e205ac275 [mob][photos] Log device brand and model (#4413) 2024-12-16 17:00:29 +05:30
Aman Raj
277d7fa0cd [auth] fixed height & width to display icon 2024-12-16 16:26:49 +05:30
Aman Raj
51734d96d5 [auth] minor fixes 2024-12-16 16:25:54 +05:30
ashilkn
c4880fd07e [mob][photos] Log device brand and model 2024-12-16 16:11:57 +05:30
Aman Raj
75ae277334 [auth] minor fixes 2024-12-16 16:00:40 +05:30
Aman Raj
726cfc8bf2 [auth] rearrange widgets 2024-12-16 16:00:17 +05:30
Aman Raj
6b6db069b0 [auth] fix: showing duplicate icons on custom_icon screen 2024-12-16 15:58:18 +05:30
Manav Rathi
1459678d70 [docs] Add a tip about enabling ML before doing imports 2024-12-16 14:58:48 +05:30
Manav Rathi
ce3e8bf315 [web] New translations (#4353)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2024-12-16 14:41:59 +05:30
Neeraj Gupta
adaf70695b [server] Upgrade go version to 1.23 (#4409)
## Description

## Tests
2024-12-16 13:44:28 +05:30
Neeraj Gupta
f1e17948c4 [server] Lint fix 2024-12-16 13:42:11 +05:30
Neeraj Gupta
abeac7aa49 [server] Upgrade go version to 1.23 2024-12-16 13:38:53 +05:30
Ivan Lepekha
d46b7a8189 [auth] Fixing taskbar overlapping window panel (#4384)
## Description

Pull request was made because of some user percentage has taskbar
connected on top, so it overlaps auth window for WinApp at start.
After some measuring, I found that taskbar occupies:

- 50px on FHD with 125% scaling (standard values on some notebooks)
- 40px on FHD with 100% scaling
- 30px on FHD with 100% scaling and small taskbar icons
2024-12-16 13:21:02 +05:30
Vishnu Mohandas
d312761166 Update README.md (#4408)
## Description

## Tests
2024-12-16 11:23:45 +05:30
Vishnu Mohandas
c29d857b83 Update README.md 2024-12-16 11:23:15 +05:30
Neeraj Gupta
7e79c9d847 Update README.md 2024-12-16 11:09:31 +05:30
Neeraj Gupta
55d0a1a0b5 [mobile] New translations (#4406)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-app)
2024-12-16 10:55:09 +05:30
Neeraj Gupta
6e14a3af09 [auth] New translations (#4407)
New translations from
[Crowdin](https://crowdin.com/project/ente-authenticator-app)
2024-12-16 10:49:14 +05:30
mngshm
7ce5306f53 minor edits 2024-12-16 10:11:18 +05:30
Vishnu Mohandas
34e6e71b34 Return empty list (#4402) 2024-12-16 09:37:48 +05:30
Crowdin Bot
3fd4717ec4 New Crowdin translations by GitHub Action 2024-12-16 01:18:00 +00:00
Crowdin Bot
20acf7c0bf New Crowdin translations by GitHub Action 2024-12-16 01:05:37 +00:00
Crowdin Bot
46e764d3db New Crowdin translations by GitHub Action 2024-12-16 00:38:41 +00:00
vishnukvmd
c6faaf8aa9 Return empty list 2024-12-14 10:27:44 -08:00
Aman Raj Singh Mourya
f5e77d8b23 [auth] fix missing [ ] in altNames in custom-icons.json (#4401)
## Description
Debug build failing due missing [ ] in altNames in the custom-icon.json
from PR [https://github.com/ente-io/ente/pull/4348/files]()
Code Ref:
https://github.com/ente-io/ente/blob/main/auth/assets/custom-icons/_data/custom-icons.json#L639
2024-12-14 22:15:54 +05:30
Aman Raj
449c966342 [auth] fix missing [ ] in altNames in custom-icons.json 2024-12-14 22:02:04 +05:30
Aman Raj Singh Mourya
ca02c20d09 [auth] Remove duplicate code (#4383) 2024-12-14 08:32:39 +05:30
Aman Raj
8870a8ec4a [auth] minor fix 2024-12-13 20:44:10 +05:30
mngshm
4150e607eb minor:add improvements to external s3 guide 2024-12-13 16:58:20 +05:30
Aman Raj
0a5d31da18 [auth] fix: move class AllIconData to models 2024-12-13 14:49:43 +05:30
Neeraj Gupta
e3d3f3b1f4 [auth] New translations (#4355)
New translations from
[Crowdin](https://crowdin.com/project/ente-authenticator-app)
2024-12-13 14:46:26 +05:30
Ashil
062a7fe257 [mob][photos] Minor UI fix (#4396) 2024-12-13 14:40:47 +05:30
ashilkn
442cf6583b [mob][photos] Minor UI fix 2024-12-13 14:39:47 +05:30
k3kk07
7bc38a061d Updated randstad icon (#4363)
## Description
Updated randstad icon
## Tests
2024-12-13 14:21:20 +05:30
Neeraj Gupta
fd04cd0a98 Fix #4296 + download UI improvement + fixes (#4369)
## Description

### Background 
Proposal to fix #4296 (@ua741). High level, there is a race condition
which is generating wrong entries in the FilesDB (creating orphan
files). This issue is easily replicable if you select multiple files and
try to download them (the more files, the more likely wrong entries will
be created). This issue is also messing up with the date of the files we
see in the timeline and is creating "orphan" (not linked to uploaded
files) files in the device collections.

To my understanding, this is quite a critical bug and any user is very
likely to encounter it in the current implementation.

There is some self healing already in place that will reupload all those
files and this should correct the dates, but this will only do it 10
files at a time, and based on potential candidates, without fixing the
orphan entries. I was also able to produce files which were never
corrected.

### This pull request
After extensive tests and lots of debugging, I was able to fully fix
this issue (meaning I am not able to reproduce it, at all, and I tried a
lot..). I also do not see any orphan in the device collection
("Pictures" folder for Android), while i could see 20+ (even more) each
time I was trying to replicate the issue. To fix it, I have:

- created a new class `PhotoManagerSafe` which is safely making sure
that the app will not react to file changes when it has been asked not
to do so (previous implementation allowed scenarios where a caller was
executing code thinking no change notification would be sent).
- created a Lock in the `LocalSyncService` that is used to make sure no
local synchronisation can be performed at the same time a download is
performed (this was the main culprit). The main side effect is that when
you download multiple files, they will be processed sequentially. I
think this can be improved but the main "side" advantage is that
whenever there is a failure, not a lot will be lost and it is more
robust in that way if you want to download a big collection. It would be
better to have at least 2 threads downloading/decrypting at the same
time, maybe (maybe not needed as well)?
- improved the "Downloading files..." dialog, which is now showing the
progress (for example "Downloading files... (235/494)"), so that the
user knows if something is actually happening and what is the current
progress.
- Added a missing call on the Video Editor page where the change notify
was stopped but never started again at the end when task was performed.

Feel free to close this PR if not suitable according to you, I can also
take feedback and try to implement them as best as I can.

## Tests

Tested on my Pixel 6a

Built with :

Flutter 3.24.3
JDK 17.0.2
Gradle 7.2
2024-12-13 14:17:34 +05:30
Neeraj Gupta
1a80c40f78 [auth] Add icons for Nelnet, US Mobile (#4389)
## Description

Custom icons for Nelnet and US Mobile
2024-12-13 14:16:28 +05:30
Neeraj Gupta
19441d9fee [auth] Lint rule to validate file size (#4394)
## Description

## Tests
2024-12-13 14:14:39 +05:30
Neeraj Gupta
826b2f997e [auth] Reduce icon sizes 2024-12-13 14:11:50 +05:30
Neeraj Gupta
747bf88515 [auth] Lint on file size 2024-12-13 14:11:26 +05:30
Aman Raj
5175f24402 [auth] fix: change the selectedItemCount 2024-12-13 13:37:32 +05:30
Simon Dubrulle
32ed84f48d Moved getFile() outside lock + Implemented simple Download queue 2024-12-13 08:34:52 +01:00
Vishnu Mohandas
ca40492d99 Remove bold (#4393) 2024-12-13 12:44:18 +05:30
vishnukvmd
c220e0385a Update 2024-12-12 23:13:13 -08:00
vishnukvmd
df9c08cd7f Remove bold 2024-12-12 23:08:08 -08:00
Neeraj Gupta
a7cd7030f1 [server] Fix recovery email (#4392)
## Description

## Tests
2024-12-13 12:37:04 +05:30
Neeraj Gupta
113fbef0d9 [server] Fix recovery email 2024-12-13 12:31:12 +05:30
Neeraj Gupta
af23fd37b2 [docs] Adding help article for Legacy (#4390)
## Description

## Tests
2024-12-13 12:24:31 +05:30
setalp
6dc2f6139a Update sidebar.ts 2024-12-13 12:23:09 +05:30
setalp
665ba9e634 Update initiate_account_recovery.png 2024-12-13 12:07:51 +05:30
Neeraj Gupta
acd2f63a87 [server] Send emails for legacy (#4391)
## Description

## Tests
2024-12-13 12:05:38 +05:30
Neeraj Gupta
58ae5eee32 Update from address 2024-12-13 12:01:35 +05:30
Neeraj Gupta
eaee515e17 Update templates 2024-12-13 12:01:06 +05:30
Neeraj Gupta
3be7c4a60f Update templates 2024-12-13 11:49:26 +05:30
setalp
84e39c43d6 Adding help article for Legacy 2024-12-13 11:45:35 +05:30
Neeraj Gupta
7f0d07db3f [auth] Add custom icon for DreamHost (#4381)
This is my first PR for this repo; please let me know if I've done
anything incorrectly. I've got a couple other icons to submit if this
one looks good.
2024-12-13 11:41:44 +05:30
Aaron Sherber
0fd6eceda6 Add icons for Nelnet, US Mobile 2024-12-12 22:18:47 -05:00
Aman Raj
f439d805fc [auth] extract strings 2024-12-12 20:27:13 +05:30
Aman Raj Singh Mourya
0e98ef43df [auth] Save button on reorder screen (#4382) 2024-12-12 15:36:56 +05:30
Neeraj Gupta
38d679f574 [server] Clean up emergency contacts on account deletion 2024-12-12 15:32:10 +05:30
Aman Raj
be92c30bb1 Merge branch 'main' into save_button_reorder_screen 2024-12-12 15:06:55 +05:30
Neeraj Gupta
c648127ff8 [server] Reject/Stop active recovery when contact is removed 2024-12-12 14:53:08 +05:30
Aman Raj
209bdf3f0b [auth] remove unused variables 2024-12-12 14:46:38 +05:30
Aman Raj
257344f2e5 [auth] Added save button on appbar to save the updated code order 2024-12-12 14:45:14 +05:30
Neeraj Gupta
cbe105020b Update emails 2024-12-12 14:21:32 +05:30
Aman Raj
0e33013cec [auth] UI fix 2024-12-12 12:46:06 +05:30
Neeraj Gupta
9c0426d716 [server] Update contact emails 2024-12-12 11:27:58 +05:30
Neeraj Gupta
35de887624 [server] Update contact emails 2024-12-12 11:27:52 +05:30
Aaron Sherber
d81c545423 Add custom icon for DreamHost 2024-12-11 19:51:43 -05:00
Neeraj Gupta
c499df4212 [mob] Fix copy (#4380)
## Description

## Tests
2024-12-11 21:11:44 +05:30
Neeraj Gupta
e0d462ec75 [mob] Fix copy 2024-12-11 21:11:19 +05:30
ashilkn
e81986a27c [mob][photos] Fix NaN exception 2024-12-11 20:13:23 +05:30
ashilkn
27c66ce2f7 [mob][photos] Chore 2024-12-11 19:57:46 +05:30
Aman Raj
c80f64943d [auth] UI implementation 2024-12-11 18:35:27 +05:30
Aman Raj
5326c7452b [auth] impelmented logic to find duplicate code 2024-12-11 18:35:03 +05:30
Aman Raj
38023d0ab1 [auth] add new tile to navigate to duplicate code screen 2024-12-11 18:34:06 +05:30
ashilkn
401ed5bf9c [mob][photos] Improve media_kit player's video control UX (3) 2024-12-11 17:35:18 +05:30
Neeraj Gupta
6cc6d2521a [mob] Bump version v0.9.70 (#4376)
## Description

## Tests
2024-12-11 17:25:11 +05:30
Neeraj Gupta
553276828c [mob] Bump version v0.9.70 2024-12-11 17:24:29 +05:30
ashilkn
e5743fceed [mob][photos] Improve media_kit player's video control UX (2) 2024-12-11 17:21:51 +05:30
Neeraj Gupta
b6d9527f1d [server] Update template 2024-12-11 16:23:40 +05:30
Neeraj Gupta
64594c5077 Merge remote-tracking branch 'origin/main' into s_emails 2024-12-11 16:12:31 +05:30
Neeraj Gupta
56e3f23a56 [mob] Copy and UX improvements (#4375)
## Description

## Tests
2024-12-11 16:12:20 +05:30
Neeraj Gupta
12927b6f82 [server] Send recovery emails 2024-12-11 16:08:27 +05:30
Neeraj Gupta
92208b7d21 [server] Send legacy invites 2024-12-11 15:25:54 +05:30
Neeraj Gupta
e4c35b404e [server] Support for sending mail with base template 2024-12-11 15:24:00 +05:30
Simon Dubrulle
e9a8449a64 Implemented first PR feedbacks: removed PhotoManagerSafe not needded with LocalSyncService lock 2024-12-11 10:14:52 +01:00
Neeraj Gupta
575d220b31 Lint fix 2024-12-11 13:35:58 +05:30
Neeraj Gupta
4d280fd14b Merge remote-tracking branch 'origin/main' into ev_2 2024-12-11 13:13:14 +05:30
ashilkn
22e4f6bc94 [mob][photos] Improve media_kit player's video control UX 2024-12-11 11:45:16 +05:30
ashilkn
ca95ea8de2 [mob][photos] Make seekbar work for new media_kit controls 2024-12-11 10:58:15 +05:30
Vishnu Mohandas
d1625361d7 Copy changes (#4370) 2024-12-10 22:29:09 +05:30
vishnukvmd
7aedfb7e9b Copy changes 2024-12-10 08:58:27 -08:00
Neeraj Gupta
00b722a0a5 [mob] Bump version 2024-12-10 21:21:09 +05:30
Neeraj Gupta
9da5f6c99d [mob] Add confirmation while adding trusted contact 2024-12-10 21:20:43 +05:30
Neeraj Gupta
fbf2a2bb23 [mob] Copy changes 2024-12-10 20:59:28 +05:30
Neeraj Gupta
e6a2cb0e57 [mob] Copy change 2024-12-10 20:53:17 +05:30
ashilkn
61e1ea4f42 [mob][photos] Create new seekbar, duration and time elapsed for new media_kit controls 2024-12-10 20:45:58 +05:30
Neeraj Gupta
1547b04ddf [mob] UX improvement 2024-12-10 20:37:44 +05:30
Simon Dubrulle
9658cde381 Fixed incoherent message format in download dialog 2024-12-10 14:34:56 +01:00
ashilkn
dec2ee7202 [mob][photos] Start creating new controls (play pause button is done), similar to controls of native player. 2024-12-10 18:01:55 +05:30
Simon Dubrulle
0cb79102fd Creation of PhotoManagerSafe + lock in LocalSyncService + improved download UI + missing notify call 2024-12-10 13:25:00 +01:00
ashilkn
2e2e381100 [mob][photos] Create a new widget that uses native_video_player for video playback by default and toggles to use media_kit for playback when UseMediaKitForVideo event is fired 2024-12-10 16:09:59 +05:30
Neeraj Gupta
815d9e8972 [mob] Recovery copy changes 2024-12-10 15:52:38 +05:30
ashilkn
5893c927c2 [mob][photos] Rename old video player 2024-12-10 15:48:36 +05:30
Neeraj Gupta
da89c02505 Copy changes 2024-12-10 15:47:30 +05:30
Neeraj Gupta
3d58a8cf5a [mob] Bump version 2024-12-10 15:35:59 +05:30
Neeraj Gupta
33c497e101 [mob] Copy change 2024-12-10 15:13:36 +05:30
Neeraj Gupta
0ec493836c Merge remote-tracking branch 'origin/main' into ev_2 2024-12-10 15:00:55 +05:30
Neeraj Gupta
e5ccf494c5 [mob] Bump version 2024-12-10 14:47:10 +05:30
Neeraj Gupta
8692421b9a [mob] Recovery contact (#4366)
## Description

## Tests
2024-12-10 14:45:06 +05:30
Neeraj Gupta
7b85d216dd [mob] Legacy UX fixes 2024-12-10 14:34:17 +05:30
Neeraj Gupta
ebf92dba94 [mob] Allow internal user to approve recovery for testing 2024-12-10 13:43:25 +05:30
Neeraj Gupta
c4799a719b [server] Fix next reminder time 2024-12-10 13:38:39 +05:30
Neeraj Gupta
1222a063e8 Add support for approving recovery 2024-12-10 13:33:18 +05:30
Neeraj Gupta
c5c77ab706 Lint fixes 2024-12-10 13:29:04 +05:30
Neeraj Gupta
7952257a89 Fix server status 2024-12-10 11:45:24 +05:30
Neeraj Gupta
8db40c5c58 extract strings 2024-12-10 11:24:27 +05:30
Neeraj Gupta
051b197180 [mob] Legacy UX Changes 2024-12-10 10:54:47 +05:30
Neeraj Gupta
a46dd1f447 [mob] Legacy UX changes 2024-12-10 10:26:10 +05:30
Tanguy
394d98ca46 [auth] Optimize Kotas icon (#4364)
## Description

- Optimize `kotas.svg` size.
- Add `mix-blend-mode:difference` property for light/dark theme.
- Remove redundant slug in `custom-icons.json`.
2024-12-10 06:47:31 +05:30
Neeraj Gupta
1cf5875a6f Merge remote-tracking branch 'origin/main' into emergency_contact 2024-12-10 06:45:55 +05:30
Neeraj Gupta
77856f2b6d [mob] Copy update 2024-12-10 00:33:43 +05:30
Sven
7b689f4197 Add Kotas icon (#4361)
## Description

Adds the icon for Kotas ([kotas.com.br](https://kotas.com.br)) to Auth
2024-12-09 23:26:26 +05:30
ashilkn
25a3ce2909 Merge branch 'main' into native_video_player_pkg_debug 2024-12-09 18:16:33 +05:30
ashilkn
5d474350c2 [mob][photos] Reference fork of native_video_player package to get notified if video isn't playable on iOS 2024-12-09 18:04:05 +05:30
Neeraj Gupta
5aa2527021 [server] Change default emergency notice to 30 days 2024-12-09 13:08:30 +05:30
Crowdin Bot
62469dec0b New Crowdin translations by GitHub Action 2024-12-09 01:17:50 +00:00
Aman Raj
ff41f1c7f8 [auth] changes to display new icon on code_widget 2024-12-07 17:41:59 +05:30
Aman Raj
2fc2107bca [auth] implemented custom icon screen 2024-12-07 17:41:15 +05:30
Aman Raj
0e80508f62 [auth] Implemented custom icon widget 2024-12-07 17:40:53 +05:30
Aman Raj
ee7c7a447d [auth] UI to select option for custom icons 2024-12-07 17:40:34 +05:30
Aman Raj
16e8aa3803 [auth] add parameter to store details for custom icons 2024-12-07 17:40:02 +05:30
Neeraj Gupta
e6fa7d4e21 [server] Migrate server changes for emergency contact 2024-12-06 21:41:28 +05:30
Neeraj Gupta
0e677f052b [mob] Move trusted contact code from old repo 2024-12-06 21:02:24 +05:30
Neeraj Gupta
5c4c489912 [mob] Add strings 2024-12-06 20:50:58 +05:30
Neeraj Gupta
58a00a8758 [mob] Add model 2024-12-06 20:48:43 +05:30
Neeraj Gupta
9650eb3ff6 [server] Enable replication for vid_preview 2024-12-02 05:49:38 +05:30
Neeraj Gupta
32f8075acf [server] Store preview obj size 2024-11-30 00:02:28 +05:30
Neeraj Gupta
9fb1dbf67e Merge branch 'main' into video_file_preview 2024-11-29 23:50:58 +05:30
Aman Raj
f68f0a5ea8 [auth] minor fixes 2024-11-29 20:24:50 +05:30
Aman Raj
c5996ffc9c [auth] Fix: update html UI 2024-11-29 20:14:02 +05:30
Neeraj Gupta
06cad1b996 [server] fileData: Support for replicating object 2024-11-29 11:01:24 +05:30
Neeraj Gupta
2a4b15ea48 [server] Fix lint 2024-11-29 10:58:38 +05:30
Neeraj Gupta
c0bbad8f88 Store nil as objectNonce for video objet 2024-11-28 15:38:47 +05:30
Neeraj Gupta
b5c2991575 Merge branch 'main' into video_file_preview 2024-11-28 12:46:09 +05:30
Aman Raj
f439f2fcec [auth] Add feature to export code as a HTML file 2024-11-25 12:26:35 +05:30
Neeraj Gupta
8b01129cc9 [server] FileData: Return objectId and nonce 2024-11-08 16:12:41 +05:30
Neeraj Gupta
58486744e1 Merge branch 'main' into video_file_preview 2024-11-08 15:52:36 +05:30
Neeraj Gupta
a0d46ac60e Merge branch 'main' into video_file_preview 2024-11-07 14:09:56 +05:30
Neeraj Gupta
2430473a10 [server] Minor refactor 2024-10-03 21:37:09 +05:30
Neeraj Gupta
4f963f250f [server] More fixes for video preview API 2024-10-01 22:34:11 +05:30
Neeraj Gupta
ca70c36ae0 [server] Fix minor bugs in reporting video preview 2024-10-01 22:01:37 +05:30
Neeraj Gupta
1bf8f2749e [server] Remove objectNonce from video preview 2024-10-01 21:32:43 +05:30
Neeraj Gupta
56b8728e79 Merge branch 'main' into video_file_preview 2024-10-01 21:23:41 +05:30
Neeraj Gupta
df28a8bf50 [server] Hook API to insert video preview 2024-09-30 16:02:13 +05:30
Neeraj Gupta
0a446a6629 [server] Fix deletion for preview objects 2024-09-30 15:57:15 +05:30
Neeraj Gupta
b8f1bce341 [server] Initial support for storing video preview data 2024-09-30 14:58:10 +05:30
Neeraj Gupta
be615197fd [server] Fix error in getting preview url 2024-09-25 15:50:33 +05:30
Neeraj Gupta
d4a68069ba [server] Add columns to store preview objects 2024-09-25 15:40:52 +05:30
Neeraj Gupta
8fc14c72e2 Merge branch 'main' into video_file_preview 2024-09-25 15:26:29 +05:30
Neeraj Gupta
69d75644d0 Merge remote-tracking branch 'origin/main' into video_file_preview 2024-09-06 12:02:34 +05:30
Neeraj Gupta
1fabaf9aaa [server] Request model for putting video preview 2024-09-06 12:02:29 +05:30
Neeraj Gupta
aa482ea227 [server] Return both objectID and url for previewUrl 2024-09-06 12:01:16 +05:30
Neeraj Gupta
41c242a0ee [server] Ignore __debug_bin 2024-09-04 16:05:50 +05:30
Neeraj Gupta
c2e53c6ec9 [server] Clean up 2024-09-04 14:39:36 +05:30
383 changed files with 20567 additions and 8542 deletions

View File

@@ -30,6 +30,18 @@ jobs:
exit 1
fi
done
- name: Verify all icons are less than 20KB
run: |
find assets/custom-icons -type f -name "*.svg" | while read -r file; do
if [[ "$file" == "assets/custom-icons/icons/bbs_nga.svg" ]]; then
continue
fi
if [[ "$(stat --printf="%s" "$file")" -gt 20480 ]]; then
echo "File size is greater than 20KB: $file ($file_size bytes)"
exit 1
fi
done
- name: Verify custom icon JSON
run: cat assets/custom-icons/_data/custom-icons.json | jq empty

View File

@@ -151,7 +151,7 @@ jobs:
- name: Install dependencies for desktop build
run: |
sudo apt-get update -y
sudo apt-get install -y libsecret-1-dev libsodium-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff5 xz-utils libarchive-tools
sudo apt-get install -y libsecret-1-dev libsodium-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff5 xz-utils libarchive-tools libcurl4-openssl-dev
sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu'
- name: Install appimagetool

View File

@@ -30,10 +30,10 @@ Learn more at [ente.io](https://ente.io).
![Screenshots of Ente Photos](.github/assets/photos.png)
Our flagship product. 3x data replication. On device machine learning. Cross
platform. Private sharing. Collaborative albums. Family plans. Easy import,
easier export. Background uploads. The list goes on. And of course, all of this,
while being fully end-to-end encrypted.
Our flagship product. 3x data replication. Face detection. Semantic search.
Private sharing. Collaborative albums. Family plans. Easy import, easier export.
Background uploads. The list goes on. And of course, all of this, while being
fully end-to-end encrypted across platforms.
Ente Photos is a paid service, but we offer 5GB of free storage.
You can also clone this repository and choose to self-host.

View File

@@ -269,6 +269,10 @@
{
"title": "Dropbox"
},
{
"title": "DreamHost Panel",
"slug": "dreamhost_panel"
},
{
"title": "dus.net",
"slug": "dusnet"
@@ -444,6 +448,9 @@
{
"title": "Kite"
},
{
"title": "Kotas"
},
{
"title": "KnownHost",
"altNames": [
@@ -526,7 +533,9 @@
},
{
"title": "matlab",
"altNames": ["mathworks"]
"altNames": [
"mathworks"
]
},
{
"title": "Mercado Livre",
@@ -613,6 +622,9 @@
"title": "ngrok",
"hex": "858585"
},
{
"title": "Nelnet"
},
{
"title": "nintendo",
"altNames": [
@@ -625,8 +637,10 @@
{
"title": "nordvpn",
"slug": "nordaccount",
"hex": "#4687FF",
"altNames": "Nord Account"
"hex": "4687FF",
"altNames": [
"Nord Account"
]
},
{
"title": "Notesnook"
@@ -732,7 +746,8 @@
]
},
{
"title": "randstad"
"title": "randstad",
"hex": "2175D9"
},
{
"title": "Real-Debrid",
@@ -964,6 +979,10 @@
{
"title": "Upstox"
},
{
"title": "US Mobile",
"slug": "us_mobile"
},
{
"title": "Vikunja"
},
@@ -1027,4 +1046,4 @@
]
}
]
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="160.5px" height="160.5px" viewBox="0 0 160.5 160.5" enable-background="new 0 0 160.5 160.5" xml:space="preserve">
<g>
<path d="M145.794,113.854c-11.111,5.932-23.811,9.227-37.417,9.008c-40.366-0.879-72.114-32.293-71.434-70.515
c0.227-12.961,4.309-25.042,11.112-35.148C26.512,28.841,11.771,50.589,11.318,75.851c-0.907,38.002,31.068,69.636,71.208,70.515
C109.284,146.805,132.642,133.844,145.794,113.854z"/>
<path fill="#0073EC" d="M86.833,14.123c-10.885,0-21.09,2.636-30.161,7.469c-5.669,9.007-8.844,19.331-9.297,29.875
c-0.68,33.171,27.212,60.63,61.909,61.289c10.432,0,22.224-2.417,31.294-7.249c5.443-9.007,8.617-19.551,8.617-30.754
C149.648,41.362,121.53,14.123,86.833,14.123z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,5 @@
<svg width="500" height="500" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<g style="mix-blend-mode:difference">
<path d="M105.08 0.61C90.5 2.16 71.47 8.21 57.3 15.82C28.49 31.36 10.26 55.31 3.69 86.29C0.94 99.26 1.00 94.06 1.00 249.17C1.00 386.74 1.06 395.6 2.03 401.78C5.40 423.61 13.35 442.47 25.46 457.5C30.32 463.56 40.49 473.27 46.27 477.39C65.24 490.93 88.90 498.99 112.16 499.91C139.48 500.99 165.60 491.62 188.29 472.53C194.29 467.50 222.01 440.24 233.90 427.72C241.45 419.78 244.13 418.06 249.33 418.12C255.22 418.12 258.88 420.86 269.62 433.15C278.20 442.93 306.54 470.07 314.09 475.67C324.49 483.45 332.66 488.02 343.92 492.36C388.79 509.62 438.68 497.05 470.23 460.47C504.47 420.86 507.90 364.91 478.80 320.90C472.18 310.79 466.52 304.56 441.48 279.58C424.57 262.72 417.59 255.35 416.79 253.58C414.05 247.57 414.96 246.20 435.43 226.14C456.63 205.34 473.20 187.91 477.55 181.85C488.18 166.99 494.35 152.47 497.21 135.67C498.64 127.04 498.64 108.52 497.15 99.72C491.38 64.23 475.20 38.05 448.00 19.93C428.68 7.07 407.19 0.61 383.82 0.56C356.27 0.56 332.66 8.84 311.34 26.10C305.57 30.79 276.77 58.97 263.56 72.91C256.36 80.51 254.02 82.17 249.85 82.69C248.19 82.91 246.88 82.63 244.48 81.49C241.96 80.23 239.39 77.83 231.10 68.97C212.18 48.68 193.15 30.56 182.75 22.85C158.74 5.19 132.74 -2.30 105.08 0.61ZM397.19 49.37C405.71 51.19 413.65 54.51 420.79 59.14C426.85 63.03 437.43 73.88 441.26 80.17C453.43 99.83 454.91 122.69 445.31 142.87C440.17 153.67 439.20 154.81 397.59 196.88C356.95 237.97 315.80 279.01 289.86 304.33C277.74 316.22 274.37 319.82 273.17 322.27C271.28 326.16 271.17 329.36 272.94 332.50C274.71 335.76 292.66 353.94 295.51 355.42C298.66 357.02 302.49 356.97 305.80 355.31C307.46 354.51 310.43 351.82 314.15 347.76C317.29 344.28 331.46 329.88 345.64 315.70C363.07 298.27 372.10 289.58 373.64 288.90C376.44 287.64 380.04 287.64 382.44 288.90C385.19 290.33 433.02 338.73 437.37 344.51C448.28 359.02 453.31 377.08 451.03 393.54C446.23 428.35 411.42 457.04 378.67 453.33C365.24 451.78 354.38 447.16 342.55 437.90C336.66 433.32 161.55 258.78 159.03 255.00C156.74 251.58 156.74 248.60 158.97 245.12C159.83 243.75 175.20 228.09 193.09 210.31C213.10 190.48 226.01 177.22 226.64 175.90C228.81 171.33 227.33 167.56 221.21 162.19C218.98 160.19 214.58 156.24 211.50 153.44C198.64 141.67 197.72 141.15 194.01 143.15C191.61 144.41 104.79 231.80 101.42 236.32C95.65 244.09 94.56 250.49 97.82 257.52C98.79 259.63 101.08 263.18 102.91 265.41C104.68 267.64 129.25 292.55 157.49 320.79C188.58 351.94 209.21 373.03 209.90 374.34C211.38 377.26 211.38 381.20 209.84 384.23C209.15 385.60 206.01 389.09 202.52 392.29C199.21 395.43 187.61 406.92 176.75 417.84C155.49 439.27 151.20 442.87 141.60 447.56C127.14 454.59 111.88 455.44 95.88 450.13C73.59 442.64 56.78 427.09 50.95 408.63C47.69 398.29 47.92 409.66 47.75 253.46C47.58 126.12 47.69 109.32 48.44 103.78C50.61 88.86 56.61 77.08 67.07 67.14C76.56 58.11 88.96 51.65 101.13 49.37C108.68 47.99 122.51 48.45 130.00 50.45C141.08 53.37 150.23 57.94 158.92 64.91C161.37 66.85 198.18 103.43 240.87 146.18C301.86 207.39 319.00 224.26 321.29 225.40C325.35 227.46 329.23 227.40 333.06 225.23C336.09 223.51 341.24 218.37 347.98 210.31C355.12 201.79 356.21 198.19 353.07 192.82C351.07 189.39 322.09 159.67 297.11 135.44C285.74 124.41 284.54 121.27 289.11 114.52C290.20 112.86 292.71 110.18 294.66 108.52C296.60 106.86 307.57 96.06 319.06 84.57C337.58 65.94 340.61 63.14 346.09 59.54C354.67 53.94 364.50 50.11 374.21 48.57C379.01 47.82 392.27 48.28 397.19 49.37Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="167.5px" height="167.5px" viewBox="0 0 167.5 167.5" enable-background="new 0 0 167.5 167.5" xml:space="preserve">
<g>
<defs>
<rect id="SVGID_1_" x="3.75" y="3.75" width="160" height="160"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" overflow="visible"/>
</clipPath>
<g id="icon-circle-n" clip-path="url(#SVGID_2_)">
<rect x="3.75" y="3.75" fill="#FFFFFF" width="160" height="160"/>
<g id="Group_1" transform="translate(-283.001 134)">
<path id="Path_926" fill="#A2AAAD" d="M371.633-89.707c-8.596,0-14.667,3.859-19.81,8.422l-0.283-5.307
c-0.027-0.655-0.562-1.173-1.217-1.178h-16.4c-0.68,0.004-1.229,0.56-1.224,1.24c0,0.001,0,0.003,0,0.004v74.489
c-0.003,0.678,0.542,1.23,1.221,1.236h18.258c0.68-0.004,1.228-0.557,1.225-1.236v-51.36c6.662-7.678,9.619-8.926,14.025-8.926
c6.132,0,12.679,4.229,12.679,16.1v44.186c-0.004,0.678,0.543,1.231,1.221,1.236c0,0,0,0,0.001,0h18.259
c0.678-0.006,1.225-0.559,1.221-1.236v-47.985C400.81-79.507,386.134-89.707,371.633-89.707"/>
<linearGradient id="Path_929_1_" gradientUnits="userSpaceOnUse" x1="59.4341" y1="340.5049" x2="59.4341" y2="339.5049" gradientTransform="matrix(160 0 0 -160 -9142.6602 54350.5)">
<stop offset="0" style="stop-color:#AFD135"/>
<stop offset="1" style="stop-color:#70BA44"/>
</linearGradient>
<path id="Path_929" fill="url(#Path_929_1_)" d="M366.751-130.25c-44.162,0.05-79.95,35.838-80,80l0,0
c0.051,44.162,35.838,79.949,80,80l0,0c44.162-0.051,79.949-35.838,80-80l0,0C446.701-94.412,410.913-130.2,366.751-130.25z
M303.223-50.25c0.04-35.069,28.459-63.487,63.528-63.527l0,0c35.065,0.043,63.479,28.459,63.521,63.525l0,0
c-0.042,35.065-28.456,63.481-63.521,63.525l0,0c-35.066-0.043-63.483-28.459-63.524-63.525"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,19 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.0" width="990.35431" height="207.28346" id="svg2">
<defs id="defs4"/>
<g transform="translate(116.92129,-338.61963)" id="layer1">
<g transform="translate(35.40372,35.392161)" id="g31225">
<path d="M -28.795615,415.09001 C -28.795615,415.09001 -59.115451,384.77025 -59.125465,384.77025 C -60.927824,382.91774 -63.370971,381.90646 -66.024482,381.90646 L -116.92129,381.90646 L -116.92129,401.27187 L -61.19811,401.27187 C -56.952604,401.27187 -52.957357,402.92402 -49.953371,405.92801 C -46.949465,408.93191 -45.297309,412.91715 -45.297309,417.17275 L -45.297309,472.88583 L -25.941913,472.88583 L -25.941913,421.98902 C -25.941913,419.34561 -26.953202,416.89237 -28.795615,415.09001" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path38"/>
<path d="M 25.195234,381.90646 C 22.551768,381.90646 20.09856,382.91774 18.296201,384.77025 L -12.023635,415.09001 C -13.866049,416.89237 -14.877338,419.34561 -14.877338,421.98902 L -14.877338,472.88583 L 4.4780586,472.88583 L 4.4780586,417.17275 C 4.4780586,408.40119 11.607388,401.27187 20.378939,401.27187 L 76.09204,401.27187 L 76.09204,381.90646 L 25.195234,381.90646" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path42"/>
<path d="M 209.20675,379.69354 C 199.83445,379.69354 190.17176,386.31226 185.7059,395.80471 L 185.5557,396.11513 L 184.29404,396.11513 L 184.29404,381.90646 L 169.60477,381.90646 L 169.60477,472.88583 L 185.36545,472.88583 L 185.36545,431.53162 C 185.36545,408.36114 195.43867,393.39152 211.00911,393.39152 C 214.37352,393.39152 217.30737,393.91222 219.85071,394.8434 L 219.85071,380.85511 C 216.47628,380.17419 213.99302,379.69354 209.20675,379.69354" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path46"/>
<path d="M 694.79406,388.47511 C 688.87629,382.54732 679.95458,379.67351 667.50825,379.67351 C 657.2648,379.67351 647.27168,382.50727 637.79925,388.04452 L 638.61032,401.2218 C 645.55944,395.64449 655.47244,392.18997 664.8047,392.18997 C 672.78517,392.18997 678.48265,394.01236 682.23757,397.76728 C 686.21279,401.73249 688.06522,407.81047 688.06522,416.88235 L 688.06522,417.47315 L 687.48446,417.4331 C 680.77566,417.07261 676.03944,417.07261 669.49085,417.08262 C 658.30617,417.07261 643.26643,420.16664 634.56501,428.86809 C 629.74868,433.68437 627.30548,439.74233 627.30548,446.85171 C 627.30548,455.20262 629.70863,461.83135 634.44485,466.57761 C 640.04219,472.16493 649.04401,475.11877 660.47902,475.11877 C 676.93061,475.11877 685.55192,465.50616 688.49579,461.37073 L 688.656,461.14041 L 689.85757,461.14041 L 689.85757,472.8658 L 703.82591,472.8658 L 703.82591,417.17275 C 703.82591,403.93539 700.88205,394.55301 694.79406,388.47511 z M 688.06522,428.93819 L 688.06522,437.61954 C 688.06522,444.83907 685.75219,450.99717 681.39648,455.43293 C 676.7704,460.12912 670.04157,462.60239 661.92091,462.61241 C 656.97442,462.61241 651.83768,460.53968 648.5033,457.21526 C 645.72966,454.43165 644.24772,450.88702 644.24772,446.95185 C 644.24772,430.37005 666.9375,428.5777 676.70031,428.5777 C 678.51268,428.5777 680.35511,428.66782 682.12743,428.75795 C 683.89976,428.84807 685.72215,428.93819 687.5145,428.93819 L 688.06522,428.93819" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path50"/>
<path d="M 285.2165,388.47511 C 279.28873,382.54732 270.36701,379.67351 257.92068,379.67351 C 247.67724,379.67351 237.68412,382.50727 228.21168,388.04452 L 229.02275,401.2218 C 235.97188,395.64449 245.88488,392.18997 255.21713,392.18997 C 263.1976,392.18997 268.9051,394.01236 272.65001,397.76728 C 276.62523,401.73249 278.47766,407.81047 278.47766,416.88235 L 278.47766,417.47315 L 277.8969,417.4331 C 271.1981,417.07261 266.46189,417.07261 259.90328,417.08262 C 248.72861,417.07261 233.67886,420.16664 224.98746,428.86809 C 220.16112,433.68437 217.71791,439.74233 217.71791,446.85171 C 217.71791,455.20262 220.12107,461.83135 224.86729,466.57761 C 230.45463,472.16493 239.45645,475.11877 250.89145,475.11877 C 267.34305,475.11877 275.96436,465.50616 278.90823,461.37073 L 279.07845,461.14041 L 280.27001,461.14041 L 280.27001,472.8658 L 294.24836,472.8658 L 294.24836,417.17275 C 294.24836,403.93539 291.28447,394.55301 285.2165,388.47511 z M 278.47766,428.93819 L 278.47766,437.61954 C 278.47766,444.83907 276.16463,450.99717 271.80891,455.43293 C 267.19285,460.12912 260.454,462.60239 252.33335,462.61241 C 247.38685,462.61241 242.25011,460.53968 238.92575,457.21526 C 236.1421,454.43165 234.67016,450.88702 234.67016,446.95185 C 234.67016,430.37005 257.35994,428.5777 267.11274,428.5777 C 268.92512,428.5777 270.76754,428.66782 272.54988,428.75795 C 274.31219,428.84807 276.13459,428.93819 277.92694,428.93819 L 278.47766,428.93819" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path54"/>
<path d="M 379.59037,388.13464 C 373.973,382.51728 365.87236,379.67351 355.50876,379.67351 C 342.50169,379.67351 330.55601,386.25218 325.79976,396.05504 L 325.64957,396.37548 L 324.38792,396.37548 L 324.38792,381.92648 L 309.16794,381.92648 L 309.16794,472.88583 L 324.93864,472.88583 L 324.93864,430.84068 C 324.93864,419.03519 327.24166,409.67292 331.78763,403.02416 C 336.70408,395.83475 344.02369,392.18997 353.54618,392.18997 C 367.97512,393.0811 373.6025,402.4634 373.6025,425.67393 L 373.6025,472.88583 L 389.37321,472.88583 L 389.37321,418.61462 C 389.37321,404.87658 386.07888,394.62311 379.59037,388.13464" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path58"/>
<path d="M 533.222,420.44702 C 524.52058,416.4718 516.29979,412.72689 516.29979,403.99547 C 516.29979,396.81599 522.87843,392.18997 533.06179,392.18997 C 536.77666,392.18997 546.07887,394.27271 550.45461,396.5957 L 551.76634,383.16808 C 544.79719,381.06532 538.58904,379.67351 531.25943,379.67351 C 520.74563,379.67351 512.20441,382.65747 506.56701,388.29486 C 501.78074,393.0811 499.35755,399.51957 499.35755,407.43997 C 499.35755,421.45839 511.46344,427.37616 522.14746,432.60299 C 530.91898,436.87862 539.18983,440.93395 539.18983,448.81429 C 539.18983,452.37896 538.01829,455.36283 535.70526,457.67588 C 531.25943,462.12174 524.18013,462.60239 521.35643,462.60239 C 516.85052,462.60239 507.67848,460.66986 500.73937,456.45431 L 499.91829,470.32252 C 508.10904,474.55809 517.39122,475.11877 524.59067,475.11877 C 533.09183,475.11877 540.86203,472.70559 546.44936,468.29986 C 552.78767,463.30326 556.14208,455.94362 556.14208,446.9919 C 556.14208,430.90077 543.9661,425.3535 533.222,420.44702" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path62"/>
<path d="M 611.11423,462.61241 C 607.20911,462.61241 603.6244,461.16044 601.01097,458.55708 C 597.92692,455.47298 596.3048,450.97714 596.3048,445.55997 L 596.3048,394.42292 L 620.66677,394.42292 L 620.66677,381.90646 L 596.3048,381.90646 L 596.3048,355.4017 L 580.52408,360.46832 L 580.52408,381.90646 L 559.76684,381.90646 L 559.76684,394.42292 L 580.52408,394.42292 L 580.52408,449.00455 C 580.52408,457.29537 582.747,463.71382 587.13276,468.08958 C 591.78887,472.75565 598.88818,475.11877 608.22043,475.11877 C 613.54742,475.11877 618.79431,473.68691 622.10866,472.74564 L 622.10866,459.71857 C 618.8644,461.61105 615.17957,462.61241 611.11423,462.61241" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path66"/>
<path d="M 786.85488,338.61963 L 786.85488,394.38286 L 785.63328,394.38286 L 785.47307,394.12251 C 779.53527,384.52993 769.44203,379.67351 755.47369,379.67351 C 744.8097,379.67351 735.94807,383.11801 729.14915,389.91698 C 720.8883,398.17784 716.34234,410.88448 716.34234,425.68395 C 716.34234,442.11546 721.31887,456.51439 730.00027,465.18573 C 736.58891,471.78442 745.10008,475.11877 755.29346,475.11877 C 770.87391,475.11877 781.47782,467.46874 786.20403,460.29936 L 786.36424,460.04902 L 787.56581,460.04902 L 787.56581,472.8658 L 802.62558,472.8658 L 802.62558,338.61963 L 786.85488,338.61963 z M 786.85488,427.57643 C 786.85488,438.71102 783.04989,449.15475 776.67152,455.53306 C 771.98537,460.22926 766.12768,462.61241 759.25866,462.61241 C 753.08055,462.61241 747.71351,460.3294 743.32775,455.83347 C 735.5876,447.89304 733.08431,435.35664 733.28457,427.04571 C 733.03424,415.32032 736.34859,405.06684 742.36649,398.90875 C 746.73222,394.45296 752.35961,392.18997 759.07843,392.18997 C 766.01753,392.18997 771.95533,394.60308 776.71157,399.35936 C 783.16003,405.80784 786.85488,416.09128 786.85488,427.57643" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path70"/>
<path d="M 472.51232,338.61963 L 472.51232,394.38286 L 471.29072,394.38286 L 471.13051,394.12251 C 465.1827,384.52993 455.08945,379.67351 441.13114,379.67351 C 430.45712,379.67351 421.5955,383.11801 414.80658,389.91698 C 406.54575,398.17784 401.98976,410.88448 401.98976,425.68395 C 401.98976,442.11546 406.97631,456.51439 415.64769,465.18573 C 422.23633,471.78442 430.74751,475.11877 440.95089,475.11877 C 456.52134,475.11877 467.12525,467.46874 471.85145,460.29936 L 472.01166,460.04902 L 473.22325,460.04902 L 473.22325,472.8658 L 488.27301,472.8658 L 488.27301,338.61963 L 472.51232,338.61963 z M 472.51232,427.57643 C 472.51232,438.71102 468.69731,449.15475 462.32896,455.53306 C 457.63279,460.22926 451.7751,462.61241 444.9161,462.61241 C 438.72798,462.61241 433.37095,460.3294 428.9852,455.83347 C 421.24503,447.89304 418.73173,435.35664 418.94201,427.04571 C 418.69168,415.32032 421.99602,405.06684 428.02393,398.90875 C 432.38966,394.45296 438.00704,392.18997 444.73587,392.18997 C 451.67497,392.18997 457.60275,394.60308 462.359,399.35936 C 468.80746,405.80784 472.51232,416.09128 472.51232,427.57643" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path74"/>
</g>
</g>
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70.5 33.5" style="enable-background:new 0 0 70.5 33.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#007DC5;}
</style>
<metadata>
<sfw xmlns="ns_sfw;">
<slices>
</slices>
<sliceSourceBounds bottomLeftOrigin="true" height="33.5" width="70.5" x="-228.2" y="-213.4">
</sliceSourceBounds>
</sfw>
</metadata>
<g>
<path class="st0" d="M26,33.5h7.4V14.8c0-1-0.4-2-1.1-2.6l-11-11C20.7,0.4,19.7,0,18.7,0H0v7.4h20.4c3.1,0,5.6,2.5,5.6,5.6V33.5z">
</path>
<path class="st0" d="M44.5,33.5h-7.4V14.8c0-1,0.4-2,1.1-2.6l11-11C49.9,0.4,50.8,0,51.8,0h18.7v7.4H50.1c-3.1,0-5.6,2.5-5.6,5.6
V33.5z">
</path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 846 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="400px" height="400px" viewBox="1754.66 549.858 400 400" enable-background="new 1754.66 549.858 400 400"
xml:space="preserve">
<g>
<g id="deeditor_bgCarrier">
<rect id="dee_c_e" x="1754.66" y="549.858" fill="#1043B8" width="400" height="400"/>
</g>
<g>
<path fill="#E6E7E8" d="M2137.105,669.97l-45.542,18.7c0,0,45.358-18.52,45.358-18.701h-131.485v2.541
c-0.184,4.357-0.918,8.534-2.204,12.347c-1.652,5.446-4.224,10.531-7.713,14.888c-4.591,6.173-10.65,11.439-17.813,14.888
c-0.184,0-0.367,0.182-0.367,0.182c-0.367,0.18-0.734,0.362-1.286,0.544h-0.184c-0.367,0.182-0.367,0.182-0.551,0.182h-0.184
V669.97h-50.134c-0.918,0-1.836,0.726-1.836,1.634v7.262c1.469-1.999,3.856-3.269,6.611-3.269c1.469,0,2.938,0.364,4.04,1.09
c0.184,0,0.367,0.182,0.551,0.182c0.551,0.362,0.918,0.726,1.285,0.906c1.47,1.091,2.755,2.362,3.856,3.813l2.571,3.995
l5.142,8.17l-13.957,21.786c-0.734-0.362-1.285-0.542-2.203-1.088c-6.945-3.615-13.023-8.695-17.813-14.888
c-3.319-4.537-5.921-9.559-7.713-14.888c-1.12-3.853-1.858-7.806-2.203-11.803c0-1.088-0.184-2.177-0.184-3.085h-130.751
l44.991,18.701l-45.176-18.519c1.47,5.083,3.306,9.985,5.143,14.888l35.259,14.888h-28.465c2.571,5.083,5.326,10.167,8.447,14.888
l60.785,14.888h-50.501c4.04,5.266,8.264,10.167,12.671,14.888l78.414,15.252h-62.621c6.428,5.446,13.223,10.349,20.385,14.888
l82.638,14.889h-54.357c20.935,8.896,43.522,13.98,67.396,14.889h14.324c23.873-0.908,46.645-6.174,67.396-14.889h-54.357
l82.821-14.889c7.162-4.541,13.957-9.622,20.384-14.888h-62.621l78.414-14.888c4.407-4.722,8.631-9.805,12.671-14.889h-50.5
l60.784-14.89c2.938-4.901,5.877-9.803,8.447-14.888h-28.647l35.259-14.888C2133.984,679.955,2135.637,675.054,2137.105,669.97"/>
<path fill="#99999B" d="M1951.63,685.402c3.854,0,6.979-3.088,6.979-6.899c0-3.812-3.124-6.898-6.979-6.898
s-6.979,3.089-6.979,6.898C1944.651,682.314,1947.777,685.402,1951.63,685.402"/>
<path fill="#E7E7E6" d="M2005.62,669.97h-0.184v2.541c-0.184,4.357-0.918,8.534-2.204,12.347
c-1.652,5.446-4.224,10.531-7.713,14.888c-4.591,6.173-10.65,11.439-17.813,14.888c-0.184,0-0.367,0.182-0.367,0.182
c-0.367,0.181-0.734,0.363-1.286,0.544h-0.184c-0.367,0.182-0.367,0.182-0.551,0.182h-0.184V669.97h-50.134
c-0.918,0-1.836,0.726-1.836,1.634v7.262c1.469-1.999,3.856-3.269,6.611-3.269c1.469,0,2.938,0.364,4.04,1.09
c0.184,0,0.367,0.182,0.551,0.182c0.551,0.362,0.918,0.726,1.285,0.906c1.47,1.091,2.755,2.362,3.856,3.813l2.571,3.995
l5.142,8.17l-13.957,21.786c-0.734-0.362-1.285-0.542-2.203-1.088c-6.945-3.615-13.023-8.695-17.813-14.888
c-3.319-4.537-5.921-9.559-7.713-14.888c-1.12-3.853-1.858-7.806-2.203-11.803c0-1.088-0.184-2.177-0.184-3.085h-130.751
l44.991,18.701l1.469,0.544l1.286,0.908l134.424,99.316l134.424-99.134l1.285-0.908l1.47-0.544c0,0,45.358-18.519,45.358-18.701
H2005.62z M1951.813,673.057c2.388,0,4.407,1.997,4.407,4.357c0,2.361-2.02,4.357-4.407,4.357s-4.407-1.997-4.407-4.357
C1947.406,675.054,1949.426,673.057,1951.813,673.057"/>
<path fill="#CED4D4" d="M2137.105,669.97l-45.542,18.7l-1.47,0.544l-1.285,0.908l-134.24,99.134l-134.607-99.132l-1.286-0.908
l-1.469-0.544l-44.992-18.701c1.47,5.083,3.306,9.985,5.143,14.888l35.259,14.888h-28.465c2.571,5.083,5.326,10.167,8.447,14.888
l60.785,14.888h-50.501c4.04,5.266,8.264,10.167,12.671,14.888l78.414,15.252h-62.621c6.428,5.446,13.223,10.349,20.385,14.888
l82.638,14.889h-54.357c20.935,8.896,43.522,13.98,67.396,14.889h14.324c23.873-0.908,46.645-6.174,67.396-14.889h-54.357
l82.821-14.889c7.162-4.541,13.957-9.622,20.384-14.888h-62.621l78.414-14.888c4.407-4.722,8.631-9.805,12.671-14.889h-50.5
l60.784-14.89c2.938-4.901,5.877-9.803,8.447-14.888h-28.647l35.259-14.888C2133.984,679.955,2135.637,675.054,2137.105,669.97"/>
<path fill="#0055A2" d="M1970.359,726.256h-74.741l17.263,29.412h86.678l17.262-29.412H1970.359z"/>
<path fill="#CE2832" d="M1999.559,755.668h-29.199v49.93l16.16-27.596L1999.559,755.668z"/>
<path fill="#F2F2F2" d="M1942.08,755.668v49.93l14.141,24.331l14.141-24.331v-49.93H1942.08z"/>
<path fill="#CE2832" d="M1940.426,755.668h-27.545L1925.919,778l16.16,27.598v-49.93H1940.426z"/>
<path fill="#FFFFFF" d="M1956.219,732.426l2.388,4.903l5.325,0.727l-3.855,3.813l0.918,5.266l-4.774-2.542l-4.959,2.542
l1.103-5.266l-4.041-3.813l5.51-0.727L1956.219,732.426z M1984.684,732.426l2.387,4.903l5.51,0.727l-4.04,3.813l0.918,5.266
l-4.774-2.542l-4.774,2.542l0.918-5.266l-3.856-3.813l5.325-0.727L1984.684,732.426z M1927.571,732.426l2.571,4.903l5.325,0.727
l-3.856,3.813l0.919,5.266l-4.959-2.542l-4.774,2.542l0.918-5.266l-3.856-3.813l5.326-0.727L1927.571,732.426z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -82,9 +82,9 @@ PODS:
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- SDWebImage (5.19.7):
- SDWebImage/Core (= 5.19.7)
- SDWebImage/Core (5.19.7)
- SDWebImage (5.20.0):
- SDWebImage/Core (= 5.20.0)
- SDWebImage/Core (5.20.0)
- Sentry/HybridSDK (8.36.0)
- sentry_flutter (8.9.0):
- Flutter
@@ -245,7 +245,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57
sentry_flutter: 0eb93e5279eb41e2392212afe1ccd2fecb4f8cbe
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad

View File

@@ -7,8 +7,11 @@ const String sentryDSN =
"https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9";
const String sentryTunnel = "https://sentry-reporter.ente.io";
const String roadmapURL = "https://roadmap.ente.io";
const String githubIssuesUrl =
"https://github.com/ente-io/ente/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc";
const String kAccountsUrl = "https://accounts.ente.io";
const String githubFeatureRequestUrl =
"https://github.com/ente-io/ente/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+requests%22+label%3A%22-+auth%22+sort%3Atop";
const int microSecondsInDay = 86400000000;
const int android11SDKINT = 30;
const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748

View File

@@ -435,8 +435,6 @@
"customEndpoint": "متصل بـ{endpoint}",
"pinText": "ثبت",
"unpinText": "ألغِ التثبيت",
"pinnedCodeMessage": "ثُبِّت {code}",
"unpinnedCodeMessage": "أُلغِي تثبيت {code}",
"tags": "الأوسمة",
"createNewTag": "أنشيء وسم جديد",
"tag": "وسم",

View File

@@ -148,7 +148,7 @@
"hintForMobile": "Натиснете продължително код, за да го редактирате или премахнете.",
"hintForDesktop": "Натиснете десен бутон върху код, за да го редактирате или премахнете.",
"scan": "Сканиране",
"scanACode": "Скениране на код",
"scanACode": "Сканиране на код",
"verify": "Потвърждаване",
"verifyEmail": "Потвърдете имейла",
"enterCodeHint": "Въведете 6-цифрения код от\nВашето приложение за удостоверяване",
@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Двуфакторно удостоверяване",
"passkeyAuthTitle": "Удостоверяване с ключ за парола",
"verifyPasskey": "Потвърдете ключ за парола",
"loginWithTOTP": "Влизане с еднократен код",
"recoverAccount": "Възстановяване на акаунт",
"enterRecoveryKeyHint": "Въведете Вашия ключ за възстановяване",
"recover": "Възстановяване",
@@ -199,7 +200,7 @@
"sorryUnableToGenCode": "За съжаление не може да се генерира код за {issuerName}",
"noResult": "Няма резултати",
"addCode": "Добавяне на код",
"scanAQrCode": "Скениране на QR код",
"scanAQrCode": "Сканиране на QR код",
"enterDetailsManually": "Въведете подробности ръчно",
"edit": "Редактиране",
"share": "Споделяне",
@@ -327,6 +328,10 @@
}
}
},
"manualSort": "Персонализирано",
"editOrder": "Промяна на подредбата",
"mostFrequentlyUsed": "Често използвани",
"mostRecentlyUsed": "Последно използвани",
"activeSessions": "Активни сесии",
"somethingWentWrongPleaseTryAgain": "Нещо се обърка, моля опитайте отново",
"thisWillLogYouOutOfThisDevice": "Това ще Ви изкара от профила на това устройство!",
@@ -444,10 +449,11 @@
"invalidEndpointMessage": "За съжаление въведената от Вас крайна точка е невалидна. Моля, въведете валидна крайна точка и опитайте отново.",
"endpointUpdatedMessage": "Крайната точка е актуализирана успешно",
"customEndpoint": "Свързан към {endpoint}",
"pinText": "ПИН код",
"pinText": "Закачане",
"unpinText": "Откачане",
"pinnedCodeMessage": "{code} е закачен",
"unpinnedCodeMessage": "{code} е откачен",
"pinned": "Закачен",
"tags": "Етикети",
"createNewTag": "Създаване на етикет",
"tag": "Етикет",

View File

@@ -446,8 +446,6 @@
"customEndpoint": "Connectat a {endpoint}",
"pinText": "Fixa",
"unpinText": "Desfixa",
"pinnedCodeMessage": "{code} fixat",
"unpinnedCodeMessage": "{code} deixat de fixar",
"tags": "Etiquetes",
"createNewTag": "Crea una nova etiqueta",
"tag": "Etiqueta",

View File

@@ -446,8 +446,6 @@
"customEndpoint": "Forbindelse oprettet til {endpoint}",
"pinText": "Fastgør",
"unpinText": "Frigør",
"pinnedCodeMessage": "{code} er blevet fastgjort",
"unpinnedCodeMessage": "{code} er blevet frigjort",
"tags": "Tags",
"createNewTag": "Opret nyt tag",
"tag": "Tag",

View File

@@ -444,8 +444,6 @@
"customEndpoint": "Mit {endpoint} verbunden",
"pinText": "Anpinnen",
"unpinText": "Lösen",
"pinnedCodeMessage": "{code} wurde angepinnt",
"unpinnedCodeMessage": "{code} wurde Losgelöst",
"tags": "Tags",
"createNewTag": "Neuen Tag erstellen",
"tag": "Tag",

View File

@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Αυθεντικοποίηση δύο παραγόντων",
"passkeyAuthTitle": "Επιβεβαίωση κλειδιού πρόσβασης",
"verifyPasskey": "Επιβεβαίωση κλειδιού πρόσβασης",
"loginWithTOTP": "Είσοδος με TOTP",
"recoverAccount": "Ανάκτηση λογαριασμού",
"enterRecoveryKeyHint": "Εισάγετε το κλειδί ανάκτησης σας",
"recover": "Ανάκτηση",
@@ -327,6 +328,10 @@
}
}
},
"manualSort": "Προσαρμοσμένο",
"editOrder": "Επεξεργασία σειράς",
"mostFrequentlyUsed": "Συχνά χρησιμοποιούμενο",
"mostRecentlyUsed": "Πρόσφατα χρησιμοποιούμενο",
"activeSessions": "Ενεργές συνεδρίες",
"somethingWentWrongPleaseTryAgain": "Κάτι πήγε στραβά, παρακαλώ προσπαθήστε ξανά",
"thisWillLogYouOutOfThisDevice": "Αυτό θα σας αποσυνδέσει από αυτή τη συσκευή!",
@@ -446,8 +451,9 @@
"customEndpoint": "Συνδεδεμένο στο {endpoint}",
"pinText": "Καρφίτσωμα",
"unpinText": "Ξεκαρφίτσωμα",
"pinnedCodeMessage": "Το {code} καρφιτσώθηκε",
"unpinnedCodeMessage": "Το {code} ξεκαρφιτσώθηκε",
"pinnedCodeMessage": "{code} έχει καρφιτσωθεί",
"unpinnedCodeMessage": "Το {code} έχει ξεκαρφιτσωθεί",
"pinned": "Καρφιτσωμένο",
"tags": "Ετικέτες",
"createNewTag": "Δημιουργία Νέας Ετικέτας",
"tag": "Ετικέτα",

View File

@@ -258,6 +258,10 @@
"areYouSureYouWantToLogout": "Are you sure you want to logout?",
"yesLogout": "Yes, logout",
"exit": "Exit",
"theme": "Theme",
"lightTheme": "Light",
"darkTheme": "Dark",
"systemTheme": "System",
"verifyingRecoveryKey": "Verifying recovery key...",
"recoveryKeyVerified": "Recovery key verified",
"recoveryKeySuccessBody": "Great! Your recovery key is valid. Thank you for verifying.\n\nPlease remember to keep your recovery key safely backed up.",
@@ -454,7 +458,6 @@
"pinnedCodeMessage": "{code} has been pinned",
"unpinnedCodeMessage": "{code} has been unpinned",
"pinned": "Pinned",
"tags": "Tags",
"createNewTag": "Create New Tag",
"tag": "Tag",
@@ -491,5 +494,13 @@
"appLockNotEnabled": "App lock not enabled",
"appLockNotEnabledDescription": "Please enable app lock from Security > App Lock",
"authToViewPasskey": "Please authenticate to view passkey",
"appLockOfflineModeWarning": "You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data."
"appLockOfflineModeWarning": "You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data.",
"duplicateCodes": "Duplicate codes",
"noDuplicates": "✨ No duplicates",
"youveNoDuplicateCodesThatCanBeCleared": "You've no duplicate codes that can be cleared",
"deduplicateCodes": "Deduplicate codes",
"deselectAll": "Deselect all",
"selectAll": "Select all",
"deleteDuplicates": "Delete duplicates",
"plainHTML": "Plain HTML"
}

View File

@@ -54,7 +54,7 @@
"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...",
"emailLogsTitle": "Enviar registros por email",
"emailLogsTitle": "Enviar registros por correo electrónico",
"emailLogsMessage": "Por favor, envíe los registros a {email}",
"@emailLogsMessage": {
"placeholders": {
@@ -115,7 +115,7 @@
"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": "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.",
"emailVerificationEnableWarning": "Para evitar quedarte bloqueado fuera de tu cuenta, asegúrate de guardar una copia de tu 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",
@@ -160,7 +160,7 @@
"recoverAccount": "Recuperar cuenta",
"enterRecoveryKeyHint": "Introduce tu clave de recuperación",
"recover": "Recuperar",
"contactSupportViaEmailMessage": "Por favor, envía un email a {email} desde la dirección de correo electrónico que usó durante el registro",
"contactSupportViaEmailMessage": "Por favor, envía un correo electrónico a {email} desde la dirección de correo electrónico que usó durante el registro",
"@contactSupportViaEmailMessage": {
"placeholders": {
"email": {
@@ -258,6 +258,10 @@
"areYouSureYouWantToLogout": "¿Seguro que quieres cerrar la sesión?",
"yesLogout": "Sí, cerrar la sesión",
"exit": "Salir",
"theme": "Tema",
"lightTheme": "Claro",
"darkTheme": "Oscuro",
"systemTheme": "Sistema",
"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.",
@@ -317,7 +321,7 @@
"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",
"weHaveSendEmailTo": "Hemos enviado un correo a <green>{email}</green>",
"weHaveSendEmailTo": "Hemos enviado un correo electrónico a <green>{email}</green>",
"@weHaveSendEmailTo": {
"description": "Text to indicate that we have sent a mail to the user",
"placeholders": {
@@ -328,6 +332,10 @@
}
}
},
"manualSort": "Personalizado",
"editOrder": "Editar orden",
"mostFrequentlyUsed": "Usados frecuentemente",
"mostRecentlyUsed": "Usados recientemente",
"activeSessions": "Sesiones activas",
"somethingWentWrongPleaseTryAgain": "Algo ha ido mal, por favor, inténtelo de nuevo",
"thisWillLogYouOutOfThisDevice": "¡Esto cerrará la sesión de este dispositivo!",
@@ -449,6 +457,7 @@
"unpinText": "Desanclar",
"pinnedCodeMessage": "{code} ha sido anclado",
"unpinnedCodeMessage": "{code} ha sido desanclado",
"pinned": "Anclado",
"tags": "Etiquetas",
"createNewTag": "Crear Nueva Etiqueta",
"tag": "Etiqueta",
@@ -485,5 +494,12 @@
"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."
"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.",
"duplicateCodes": "Duplicar códigos",
"noDuplicates": "✨ No hay duplicados",
"youveNoDuplicateCodesThatCanBeCleared": "No tienes códigos duplicados que se puedan borrar",
"deduplicateCodes": "Desduplicar códigos",
"deselectAll": "Deseleccionar todo",
"selectAll": "Seleccionar todo",
"deleteDuplicates": "Eliminar duplicados"
}

View File

@@ -401,8 +401,6 @@
"customEndpoint": "متصل شده به {endpoint}",
"pinText": "پین",
"unpinText": "حذف پین",
"pinnedCodeMessage": "{code} پین شد",
"unpinnedCodeMessage": "{code} از پین حذف شد",
"tags": "برچسب‌ها",
"createNewTag": "ایجاد برچسب جدید",
"tag": "برچسب",

View File

@@ -445,8 +445,6 @@
"customEndpoint": "Connecté à {endpoint}",
"pinText": "Épingler",
"unpinText": "Désépingler",
"pinnedCodeMessage": "{code} a été épinglé",
"unpinnedCodeMessage": "{code} a été désépinglé",
"tags": "Tags",
"createNewTag": "Créer un nouveau tag",
"tag": "Tag",

View File

@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Autentikasi dua langkah",
"passkeyAuthTitle": "Verifikasi passkey",
"verifyPasskey": "Verifikasi passkey",
"loginWithTOTP": "Login menggunakan TOTP",
"recoverAccount": "Pulihkan akun",
"enterRecoveryKeyHint": "Masukkan kunci pemulihanmu",
"recover": "Pulihkan",
@@ -327,6 +328,10 @@
}
}
},
"manualSort": "Kustom",
"editOrder": "Ubah pesanan",
"mostFrequentlyUsed": "Sering digunakan",
"mostRecentlyUsed": "Baru digunakan",
"activeSessions": "Sesi aktif",
"somethingWentWrongPleaseTryAgain": "Ada yang salah. Mohon coba kembali",
"thisWillLogYouOutOfThisDevice": "Langkah ini akan mengeluarkan Anda dari gawai ini!",
@@ -446,8 +451,6 @@
"customEndpoint": "Terkoneksi ke {endpoint}",
"pinText": "Sematkan",
"unpinText": "Awasematkan",
"pinnedCodeMessage": "{code} telah disematkan",
"unpinnedCodeMessage": "{code} telah diawasematkan",
"tags": "Tanda",
"createNewTag": "Buat Tanda Baru",
"tag": "Tanda",

View File

@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Autenticazione a due fattori",
"passkeyAuthTitle": "Verifica della passkey",
"verifyPasskey": "Verifica passkey",
"loginWithTOTP": "Login con TOTP",
"recoverAccount": "Recupera account",
"enterRecoveryKeyHint": "Inserisci la tua chiave di recupero",
"recover": "Recupera",
@@ -327,6 +328,10 @@
}
}
},
"manualSort": "Personalizzato",
"editOrder": "Modifica ordine",
"mostFrequentlyUsed": "Utilizzato di frequente",
"mostRecentlyUsed": "Utilizzato di recente",
"activeSessions": "Sessioni attive",
"somethingWentWrongPleaseTryAgain": "Qualcosa è andato storto, per favore riprova",
"thisWillLogYouOutOfThisDevice": "Questo ti disconnetterà da questo dispositivo!",
@@ -448,6 +453,7 @@
"unpinText": "Sgancia",
"pinnedCodeMessage": "{code} è stato fissato",
"unpinnedCodeMessage": "{code} è stato sganciato",
"pinned": "Fissato",
"tags": "Tag",
"createNewTag": "Crea un nuovo tag",
"tag": "Tag",
@@ -484,5 +490,12 @@
"appLockNotEnabled": "Blocco app non abilitato",
"appLockNotEnabledDescription": "Si prega di abilitare il blocco dell'app da Sicurezza > Blocco App",
"authToViewPasskey": "Autenticati per visualizzare le tue passkey",
"appLockOfflineModeWarning": "Hai scelto di procedere senza backup. Se dimentichi il tuo codice di blocco dell'app, non potrai più accedere ai tuoi dati."
"appLockOfflineModeWarning": "Hai scelto di procedere senza backup. Se dimentichi il tuo codice di blocco dell'app, non potrai più accedere ai tuoi dati.",
"duplicateCodes": "Codici duplicati",
"noDuplicates": "✨ Nessun doppione",
"youveNoDuplicateCodesThatCanBeCleared": "Non ci sono codici duplicati che possono essere cancellati",
"deduplicateCodes": "Codici deduplicati",
"deselectAll": "Deselezionare tutti",
"selectAll": "Seleziona tutti",
"deleteDuplicates": "Elimina i duplicati"
}

View File

@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "2 要素認証",
"passkeyAuthTitle": "パスキー認証",
"verifyPasskey": "パスキーの認証",
"loginWithTOTP": "TOTPでログイン",
"recoverAccount": "アカウントを回復",
"enterRecoveryKeyHint": "回復キーを入力",
"recover": "回復",
@@ -327,6 +328,10 @@
}
}
},
"manualSort": "カスタム",
"editOrder": "並べ替え",
"mostFrequentlyUsed": "よく使う",
"mostRecentlyUsed": "最近使った",
"activeSessions": "アクティブセッション",
"somethingWentWrongPleaseTryAgain": "問題が発生しました、再試行してください",
"thisWillLogYouOutOfThisDevice": "このデバイスからログアウトします!",
@@ -446,8 +451,9 @@
"customEndpoint": "{endpoint} に接続しました",
"pinText": "固定",
"unpinText": "固定を解除",
"pinnedCodeMessage": "{code} を固定しました",
"unpinnedCodeMessage": "{code} の固定が解除されました",
"pinnedCodeMessage": "{code}がピン留めされました",
"unpinnedCodeMessage": "{code}のピン留めが解除されました",
"pinned": "ピン留め",
"tags": "タグ",
"createNewTag": "新しいタグの作成",
"tag": "タグ",

View File

@@ -328,6 +328,10 @@
}
}
},
"manualSort": "사용자 정의",
"editOrder": "순서 변경",
"mostFrequentlyUsed": "자주 사용됨",
"mostRecentlyUsed": "최근에 사용됨",
"activeSessions": "활성화된 세션",
"somethingWentWrongPleaseTryAgain": "뭔가 잘못됐습니다, 다시 시도해주세요",
"thisWillLogYouOutOfThisDevice": "이 작업을 하시면 기기에서 로그아웃하게 됩니다!",
@@ -449,6 +453,7 @@
"unpinText": "핀 해제",
"pinnedCodeMessage": "{code}가 핀 되었습니다.",
"unpinnedCodeMessage": "{code}의 핀이 해제되었습니다.",
"pinned": "고정됨",
"tags": "태그",
"createNewTag": "새 태그 만들기",
"tag": "태그",

View File

@@ -258,6 +258,10 @@
"areYouSureYouWantToLogout": "Ar tikrai norite atsijungti?",
"yesLogout": "Taip, atsijungti",
"exit": "Išeiti",
"theme": "Tema",
"lightTheme": "Šviesi",
"darkTheme": "Tamsi",
"systemTheme": "Sistemos",
"verifyingRecoveryKey": "Patvirtinima atkūrimo raktą...",
"recoveryKeyVerified": "Patvirtintas atkūrimo raktas",
"recoveryKeySuccessBody": "Puiku! Jūsų atkūrimo raktas tinkamas. Dėkojame už patvirtinimą.\n\nNepamirškite sukurti saugią atkūrimo rakto atsarginę kopiją.",
@@ -328,6 +332,10 @@
}
}
},
"manualSort": "Pasirinktinis",
"editOrder": "Redaguoti tvarką",
"mostFrequentlyUsed": "Dažniausiai naudojamą",
"mostRecentlyUsed": "Neseniai naudotą",
"activeSessions": "Aktyvūs seansai",
"somethingWentWrongPleaseTryAgain": "Kažkas nutiko ne taip. Bandykite dar kartą.",
"thisWillLogYouOutOfThisDevice": "Tai jus atjungs nuo šio įrenginio.",
@@ -449,6 +457,7 @@
"unpinText": "Atsegti",
"pinnedCodeMessage": "{code} buvo prisegtas",
"unpinnedCodeMessage": "{code} buvo atsegtas",
"pinned": "Prisegta",
"tags": "Žymės",
"createNewTag": "Kurti naują žymę",
"tag": "Žymė",
@@ -485,5 +494,13 @@
"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į",
"appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų."
"appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų.",
"duplicateCodes": "Dubliuoti kodus",
"noDuplicates": "✨ Dublikatų nėra",
"youveNoDuplicateCodesThatCanBeCleared": "Neturite dubliuotų kodų, kuriuos būtų galima išvalyti.",
"deduplicateCodes": "Atdubliuoti kodus",
"deselectAll": "Naikinti visų pasirinkimą",
"selectAll": "Pasirinkti viską",
"deleteDuplicates": "Ištrinti dublikatus",
"plainHTML": "Grynasis HTML"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Tweestapsverificatie",
"passkeyAuthTitle": "Passkey verificatie",
"verifyPasskey": "Bevestig passkey",
"loginWithTOTP": "Inloggen met TOTP",
"recoverAccount": "Account herstellen",
"enterRecoveryKeyHint": "Voer je herstelsleutel in",
"recover": "Herstellen",
@@ -257,6 +258,10 @@
"areYouSureYouWantToLogout": "Weet je zeker dat je wilt uitloggen?",
"yesLogout": "Ja, uitloggen",
"exit": "Afsluiten",
"theme": "Thema",
"lightTheme": "Licht",
"darkTheme": "Donker",
"systemTheme": "Systeem",
"verifyingRecoveryKey": "Herstelsleutel verifiëren...",
"recoveryKeyVerified": "Herstelsleutel geverifieerd",
"recoveryKeySuccessBody": "Super! Je herstelsleutel is geldig. Bedankt voor het verifiëren.\n\nVergeet niet om je herstelsleutel veilig te bewaren.",
@@ -327,6 +332,10 @@
}
}
},
"manualSort": "Aangepast",
"editOrder": "Volgorde wijzigen",
"mostFrequentlyUsed": "Vaak gebruikt",
"mostRecentlyUsed": "Recent gebruikt",
"activeSessions": "Actieve sessies",
"somethingWentWrongPleaseTryAgain": "Er is iets fout gegaan, probeer het opnieuw",
"thisWillLogYouOutOfThisDevice": "Dit zal je uitloggen van dit apparaat!",
@@ -448,6 +457,7 @@
"unpinText": "Losmaken",
"pinnedCodeMessage": "{code} is vastgezet",
"unpinnedCodeMessage": "{code} is losgemaakt",
"pinned": "Vastgezet",
"tags": "Labels",
"createNewTag": "Nieuw label maken",
"tag": "Label",
@@ -484,5 +494,12 @@
"appLockNotEnabled": "App-vergrendeling niet ingeschakeld",
"appLockNotEnabledDescription": "Schakel app vergrendeling in vanuit Beveiliging > App vergrendeling",
"authToViewPasskey": "Verifieer uzelf om uw passkey te bekijken",
"appLockOfflineModeWarning": "Je hebt ervoor gekozen om verder te gaan zonder backups. Als je jouw applock vergeet, wordt je uitgesloten van toegang tot je gegevens."
"appLockOfflineModeWarning": "Je hebt ervoor gekozen om verder te gaan zonder backups. Als je jouw applock vergeet, wordt je uitgesloten van toegang tot je gegevens.",
"duplicateCodes": "Dubbele codes",
"noDuplicates": "✨ Geen dubbele",
"youveNoDuplicateCodesThatCanBeCleared": "Je hebt geen dubbele codes die kunnen worden gewist",
"deduplicateCodes": "Dubbele codes",
"deselectAll": "Alles deselecteren",
"selectAll": "Alles selecteren",
"deleteDuplicates": "Dubbelen verwijderen"
}

View File

@@ -258,6 +258,10 @@
"areYouSureYouWantToLogout": "Czy na pewno chcesz się wylogować?",
"yesLogout": "Tak, wyloguj",
"exit": "Wyjdź",
"theme": "Motyw",
"lightTheme": "Jasny",
"darkTheme": "Ciemny",
"systemTheme": "Systemowy",
"verifyingRecoveryKey": "Weryfikowanie klucza odzyskiwania...",
"recoveryKeyVerified": "Klucz odzyskiwania zweryfikowany",
"recoveryKeySuccessBody": "Znakomicie! Klucz odzyskiwania jest prawidłowy. Dziękujemy za weryfikację.\n\nPamiętaj, aby bezpiecznie przechowywać kopię zapasową klucza odzyskiwania.",
@@ -328,6 +332,10 @@
}
}
},
"manualSort": "Niestandardowe",
"editOrder": "Zmień kolejność",
"mostFrequentlyUsed": "Często używane",
"mostRecentlyUsed": "Ostatnio używane",
"activeSessions": "Aktywne sesje",
"somethingWentWrongPleaseTryAgain": "Coś poszło nie tak, spróbuj ponownie",
"thisWillLogYouOutOfThisDevice": "To wyloguje Cię z tego urządzenia!",
@@ -449,6 +457,7 @@
"unpinText": "Odepnij",
"pinnedCodeMessage": "Przypięto {code}",
"unpinnedCodeMessage": "Odpięto {code}",
"pinned": "Przypięte",
"tags": "Etykiety",
"createNewTag": "Utwórz nową etykietę",
"tag": "Etykieta",
@@ -485,5 +494,13 @@
"appLockNotEnabled": "Blokada aplikacji nie jest włączona",
"appLockNotEnabledDescription": "Prosimy włączyć blokadę aplikacji z Zabezpieczenia > Blokada aplikacji",
"authToViewPasskey": "Prosimy uwierzytelnić się, aby wyświetlić klucz dostępu",
"appLockOfflineModeWarning": "Wybrano kontynuowanie bez kopii zapasowych. Jeśli zapomnisz blokady aplikacji, utracisz dostęp do swoich danych."
"appLockOfflineModeWarning": "Wybrano kontynuowanie bez kopii zapasowych. Jeśli zapomnisz blokady aplikacji, utracisz dostęp do swoich danych.",
"duplicateCodes": "Duplikuj kody",
"noDuplicates": "✨ Brak duplikatów",
"youveNoDuplicateCodesThatCanBeCleared": "Nie masz duplikatów kodów, które mogą być wyczyszczone",
"deduplicateCodes": "Deduplikuj kody",
"deselectAll": "Odznacz wszystko",
"selectAll": "Zaznacz wszystko",
"deleteDuplicates": "Usuń duplikaty",
"plainHTML": "Zwykły HTML"
}

View File

@@ -132,7 +132,7 @@
"general": "Geral",
"settings": "Ajustes",
"copied": "Copiado",
"pleaseTryAgain": "Tente de novo",
"pleaseTryAgain": "Tente novamente",
"existingUser": "Usuário existente",
"newUser": "Novo no Ente",
"delete": "Excluir",
@@ -142,7 +142,7 @@
"suggestFeatures": "Sugerir recursos",
"faq": "Perguntas frequentes",
"somethingWentWrongMessage": "Algo deu errado. Tente outra vez",
"leaveFamily": "Sair da família",
"leaveFamily": "Sair do plano familiar",
"leaveFamilyMessage": "Deseja mesmo sair do plano familiar?",
"inFamilyPlanMessage": "Você está em um plano familiar!",
"hintForMobile": "Pressione em um código para editar ou excluir.",
@@ -258,6 +258,10 @@
"areYouSureYouWantToLogout": "Deseja mesmo sair?",
"yesLogout": "Sim, quero sair",
"exit": "Sair",
"theme": "Tema",
"lightTheme": "Claro",
"darkTheme": "Escuro",
"systemTheme": "Sistema",
"verifyingRecoveryKey": "Verificando chave de recuperação...",
"recoveryKeyVerified": "Chave de recuperação verificada",
"recoveryKeySuccessBody": "Ótimo! Sua chave de recuperação é válida. Obrigada por verificar.\n\nLembre-se de manter sua chave de recuperação copiada com segurança.",
@@ -271,7 +275,7 @@
"recoveryKeyVerifyReason": "Sua chave de recuperação é a única maneira de recuperar suas fotos se você esqueceu sua senha. Você pode encontrar sua chave de recuperação em Opções > Conta.\n\nInsira sua chave de recuperação aqui para verificar se você a salvou corretamente.",
"confirmYourRecoveryKey": "Confirme sua chave de recuperação",
"confirm": "Confirmar",
"emailYourLogs": "Enviar logs por e-mail",
"emailYourLogs": "Enviar registros por e-mail",
"pleaseSendTheLogsTo": "Envie os logs para \n{toEmail}",
"copyEmailAddress": "Copiar endereço de e-mail",
"exportLogs": "Exportar logs",
@@ -328,6 +332,10 @@
}
}
},
"manualSort": "Personalizado",
"editOrder": "Editar ordem",
"mostFrequentlyUsed": "Usado com frequência",
"mostRecentlyUsed": "Usado recentemente",
"activeSessions": "Sessões ativas",
"somethingWentWrongPleaseTryAgain": "Algo deu errado. Tente outra vez",
"thisWillLogYouOutOfThisDevice": "Isso fará com que você saia deste dispositivo!",
@@ -337,7 +345,7 @@
"thisDevice": "Esse dispositivo",
"toResetVerifyEmail": "Para redefinir sua senha, verifique seu e-mail primeiramente.",
"thisEmailIsAlreadyInUse": "Este e-mail já está em uso",
"verificationFailedPleaseTryAgain": "Falha na verificação. Tente novamente",
"verificationFailedPleaseTryAgain": "Falhou na verificação. Tente novamente",
"yourVerificationCodeHasExpired": "Seu código de verificação expirou",
"incorrectCode": "Código incorreto",
"sorryTheCodeYouveEnteredIsIncorrect": "O código inserido está incorreto",
@@ -354,7 +362,7 @@
"plainText": "Texto simples",
"passwordToEncryptExport": "Senha para criptografar a exportação",
"export": "Exportar",
"useOffline": "Usar sem backups",
"useOffline": "Usar sem cópia de segurança",
"signInToBackup": "Entre para fazer backup de seus códigos",
"singIn": "Entrar",
"sigInBackupReminder": "Exporte seus códigos para garantir que você tenha uma cópia para restaurar.",
@@ -449,6 +457,7 @@
"unpinText": "Desafixar",
"pinnedCodeMessage": "{code} foi fixado",
"unpinnedCodeMessage": "{code} foi desafixado",
"pinned": "Fixado",
"tags": "Etiquetas",
"createNewTag": "Criar nova etiqueta",
"tag": "Etiqueta",
@@ -485,5 +494,13 @@
"appLockNotEnabled": "Bloqueio de aplicativo não ativado",
"appLockNotEnabledDescription": "Ative o bloqueio de aplicativo em Segurança > Bloqueio de aplicativo",
"authToViewPasskey": "Autentique para ver a sua chave de acesso",
"appLockOfflineModeWarning": "Você prosseguiu sem cópias de segurança. Caso, se esqueça de seu aplicativo de bloqueio, você não poderá mais acessar seus dados."
"appLockOfflineModeWarning": "Você prosseguiu sem cópias de segurança. Caso, se esqueça de seu aplicativo de bloqueio, você não poderá mais acessar seus dados.",
"duplicateCodes": "Duplicar códigos",
"noDuplicates": "✨ Sem duplicados",
"youveNoDuplicateCodesThatCanBeCleared": "Você não possui códigos duplicados para limpar",
"deduplicateCodes": "Desduplicar códigos",
"deselectAll": "Deselecionar tudo",
"selectAll": "Selecionar tudo",
"deleteDuplicates": "Excluir duplicados",
"plainHTML": "HTML simples"
}

View File

@@ -446,8 +446,6 @@
"customEndpoint": "Подключено к {endpoint}",
"pinText": "Прикрепить",
"unpinText": "Открепить",
"pinnedCodeMessage": "{code} прикреплен",
"unpinnedCodeMessage": "{code} откреплен",
"tags": "Метки",
"createNewTag": "Создать новую метку",
"tag": "Метка",

View File

@@ -45,7 +45,7 @@
"timeBasedKeyType": "Na základe času (TOTP)",
"counterBasedKeyType": "Na základe počítadla (HOTP)",
"saveAction": "Uložiť",
"nextTotpTitle": "ďalej",
"nextTotpTitle": "ďalší",
"deleteCodeTitle": "Odstrániť položku?",
"deleteCodeMessage": "Naozaj chcete odstrániť položku? Táto akcia je nezvratná.",
"trashCode": "Odstrániť kód?",
@@ -156,6 +156,7 @@
"twoFactorAuthTitle": "Dvojfaktorové overovanie",
"passkeyAuthTitle": "Overenie pomocou passkey",
"verifyPasskey": "Overiť passkey",
"loginWithTOTP": "Prihlásenie pomocou TOTP",
"recoverAccount": "Obnoviť účet",
"enterRecoveryKeyHint": "Vložte váš kód pre obnovenie",
"recover": "Obnoviť",
@@ -446,8 +447,6 @@
"customEndpoint": "Pripojený k endpointu {endpoint}",
"pinText": "Pripnúť",
"unpinText": "Odopnúť",
"pinnedCodeMessage": "{code} bol pripnutý",
"unpinnedCodeMessage": "{code} bol odopnutý",
"tags": "Tagy",
"createNewTag": "Vytvoriť nový tag",
"tag": "Tag",

View File

@@ -327,6 +327,8 @@
}
}
},
"mostFrequentlyUsed": "Pogosto uporabljeni",
"mostRecentlyUsed": "Nedavno uporabljeno",
"activeSessions": "Aktivne seje",
"somethingWentWrongPleaseTryAgain": "Nekaj je šlo narobe, prosimo poizkusite znova.",
"thisWillLogYouOutOfThisDevice": "To vas bo odjavilo iz te naprave!",
@@ -446,8 +448,7 @@
"customEndpoint": "Povezano na {endpoint}",
"pinText": "Pripni",
"unpinText": "Odpni",
"pinnedCodeMessage": "{code} je bila pripeta",
"unpinnedCodeMessage": "{code} je bila odpeta",
"pinned": "Pripeto",
"tags": "Oznake",
"createNewTag": "Ustvari novo oznako",
"tag": "Oznaka",

View File

@@ -2,6 +2,10 @@
"account": "Konto",
"unlock": "Lås upp",
"recoveryKey": "Återställningsnyckel",
"counterAppBarTitle": "Räknare",
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
},
"onBoardingBody": "Säkerhetskopiera dina 2FA-koder",
"onBoardingGetStarted": "Kom igång",
"setupFirstAccount": "Konfigurera ditt första konto",
@@ -15,22 +19,41 @@
"pleaseVerifyDetails": "Kontrollera dina detaljer och försök igen",
"codeIssuerHint": "Utfärdare",
"codeSecretKeyHint": "Secret Key",
"secret": "Säkerhets nyckel",
"all": "Alla",
"notes": "Anteckningar",
"notesLengthLimit": "Anteckningar kan vara högst {count} tecken långa",
"@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": "Konto (du@domän.com)",
"codeTagHint": "Tagg",
"accountKeyType": "Typ av nyckel",
"sessionExpired": "Sessionen har gått ut",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
},
"pleaseLoginAgain": "Logga in igen",
"loggingOut": "Loggar ut...",
"timeBasedKeyType": "Tidsbaserad (TOTP)",
"counterBasedKeyType": "Räknarbaserad (HOTP)",
"saveAction": "Spara",
"nextTotpTitle": "nästa",
"deleteCodeTitle": "Radera kod?",
"deleteCodeMessage": "Vill du ta bort den här koden? Det går inte att ångra den här åtgärden.",
"trashCode": "Ta bort kod?",
"trashCodeMessage": "Är du säker på att du vill ta bort koden för {account}?",
"trash": "Papperskorg",
"viewLogsAction": "Visa loggar",
"sendLogsDescription": "Detta kommer att skicka över loggar för att hjälpa oss felsöka ditt problem. Även om vi vidtar försiktighetsåtgärder för att säkerställa att känslig information inte loggas, uppmuntrar vi dig att se dessa loggar innan du delar dem.",
"preparingLogsTitle": "Förbereder loggar...",
"emailLogsTitle": "E-posta loggar",
"emailLogsMessage": "Skicka loggarna till {email}",
"@emailLogsMessage": {
@@ -61,55 +84,111 @@
"pleaseWait": "Vänligen vänta...",
"generatingEncryptionKeysTitle": "Skapar krypteringsnycklar...",
"recreatePassword": "Återskapa lösenord",
"recreatePasswordMessage": "Denna enhet är inte tillräckligt kraftfull för att verifiera ditt lösenord, men vi kan återskapa det på ett sätt som fungerar med alla enheter.\n\nLogga in med din återställningsnyckel och återskapa ditt lösenord (du kan använda samma igen om du vill).",
"useRecoveryKey": "Använd återställningsnyckel",
"incorrectPasswordTitle": "Felaktigt lösenord",
"welcomeBack": "Välkommen tillbaka!",
"madeWithLoveAtPrefix": "gjord med ❤️ av ",
"supportDevs": "Prenumerera på <bold-green>ente</bold-green> för att stödja oss",
"supportDiscount": "Använd kupongkoden \"AUTH\" för att få 10% rabatt första året",
"changeEmail": "Ändra e-postadress",
"changePassword": "Ändra lösenord",
"data": "Data",
"importCodes": "Importera koder",
"importTypePlainText": "Enkel text",
"importTypeEnteEncrypted": "Ente krypterad export",
"passwordForDecryptingExport": "Lösenord för att dekryptera export",
"passwordEmptyError": "Lösenordet får inte vara tomt",
"importFromApp": "Importera koder från {appName}",
"importGoogleAuthGuide": "Exportera dina konton från Google Authenticator till en QR-kod med alternativet \"Överföra konton\". Använd sedan en annan enhet och skanna QR-koden.\n\nTips: Du kan använda din bärbara dators webbkamera för att ta en bild av QR-koden.",
"importSelectJsonFile": "Välj JSON-fil",
"importSelectAppExport": "Välj {appName} exportfil",
"importEnteEncGuide": "Välj den krypterade JSON-filen som exporteras från Ente",
"importRaivoGuide": "Använd alternativet \"Exportera OTPs till zip-arkiv\" i Raivos inställningar.\n\nExtrahera zip-filen och importera JSON-filen.",
"importBitwardenGuide": "Använd alternativet \"Exportera valv\" inom Bitwarden Tools och importera den okrypterade JSON-filen.",
"exportCodes": "Exportera koder",
"importLabel": "Importera",
"importInstruction": "Vänligen välj en fil som innehåller en lista över dina koder i följande format",
"importCodeDelimiterInfo": "Koderna kan separeras med kommatecken eller en ny rad",
"selectFile": "Välj fil",
"emailVerificationToggle": "E-postverifiering",
"emailVerificationEnableWarning": "För att undvika att bli låst från ditt konto, se till att spara en kopia av din e-post 2FA utanför Ente Auth innan du aktiverar e-postverifiering.",
"authToChangeEmailVerificationSetting": "Autentisera för att ändra din e-postadress",
"authenticateGeneric": "Var god autentisera",
"authToViewYourRecoveryKey": "Autentisera för att visa din återställningsnyckel",
"authToChangeYourEmail": "Autentisera för att ändra din e-postadress",
"authToChangeYourPassword": "Autentisera för att ändra ditt lösenord",
"authToViewSecrets": "Autentisera för att visa din återställningsnyckel",
"authToInitiateSignIn": "Vänligen autentisera för att initiera inloggning för säkerhetskopiering.",
"ok": "OK",
"cancel": "Avbryt",
"yes": "Ja",
"no": "Nej",
"email": "E-post",
"support": "Support",
"general": "Allmänt",
"settings": "Inställningar",
"copied": "Kopierat",
"pleaseTryAgain": "Försök igen",
"existingUser": "Befintlig användare",
"newUser": "Ny hos Ente",
"delete": "Radera",
"enterYourPasswordHint": "Ange ditt lösenord",
"forgotPassword": "Glömt lösenord",
"oops": "Hoppsan",
"suggestFeatures": "Föreslå funktionalitet",
"faq": "FAQ",
"somethingWentWrongMessage": "Något gick fel, vänligen försök igen",
"leaveFamily": "Lämna familjen",
"leaveFamilyMessage": "Är du säker på att du vill lämna familjeplanen?",
"inFamilyPlanMessage": "Du är på en familjeplan!",
"hintForMobile": "Håll i på en kod för att redigera eller ta bort.",
"hintForDesktop": "Högerklicka på en kod för att redigera eller ta bort.",
"scan": "Skanna",
"scanACode": "Skanna kod",
"verify": "Verifiera",
"verifyEmail": "Verifiera e-postadress",
"enterCodeHint": "Ange den 6-siffriga koden från din autentiseringsapp",
"lostDeviceTitle": "Förlorad enhet?",
"twoFactorAuthTitle": "Tvåfaktorsautentisering",
"passkeyAuthTitle": "Lösenordsverifiering",
"verifyPasskey": "Verifiera nyckel",
"loginWithTOTP": "Logga in med TOTP",
"recoverAccount": "Återställ konto",
"enterRecoveryKeyHint": "Ange din återställningsnyckel",
"recover": "Återställ",
"contactSupportViaEmailMessage": "Vänligen skicka ett e-postmeddelande till {email} från din registrerade e-postadress",
"@contactSupportViaEmailMessage": {
"placeholders": {
"email": {
"type": "String"
}
}
},
"invalidQRCode": "Ogiltig QR-kod",
"noRecoveryKeyTitle": "Ingen återställningsnyckel?",
"enterEmailHint": "Ange din e-postadress",
"invalidEmailTitle": "Ogiltig e-postadress",
"invalidEmailMessage": "Ange en giltig e-postadress.",
"deleteAccount": "Radera konto",
"deleteAccountQuery": "Vi kommer att vara ledsna över att se dig gå. Har du något problem?",
"yesSendFeedbackAction": "Ja, skicka feedback",
"noDeleteAccountAction": "Nej, radera konto",
"initiateAccountDeleteTitle": "Vänligen autentisera för att initiera borttagning av konto",
"sendEmail": "Skicka e-post",
"createNewAccount": "Skapa nytt konto",
"weakStrength": "Svag",
"strongStrength": "Stark",
"moderateStrength": "Måttligt",
"confirmPassword": "Bekräfta lösenord",
"close": "Stäng",
"oopsSomethingWentWrong": "Hoppsan! Något gick fel.",
"selectLanguage": "Välj språk",
"language": "Språk",
"social": "Social",
"security": "Säkerhet",
"lockscreen": "Låsskärm",
"authToChangeLockscreenSetting": "Vänligen autentisera för att ändra låsskärms inställningar",
"viewActiveSessions": "Visa aktiva sessioner",
"authToViewYourActiveSessions": "Autentisera för att visa dina aktiva sessioner",
"searchHint": "Sök...",
@@ -128,9 +207,13 @@
"error": "Fel",
"recoveryKeyCopiedToClipboard": "Återställningsnyckel kopierad till urklipp",
"recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel.",
"recoveryKeySaveDescription": "Vi lagrar inte och har därför inte åtkomst till denna nyckel, vänligen spara denna 24 ords nyckel på en säker plats.",
"doThisLater": "Gör detta senare",
"saveKey": "Spara nyckel",
"save": "Spara",
"send": "Skicka",
"saveOrSendDescription": "Vill du spara detta till din lagringsmapp (Nedladdningsmappen som standard) eller skicka den till andra appar?",
"saveOnlyDescription": "Vill du spara detta till din lagringsmapp (Nedladdningsmappen som standard)?",
"back": "Tillbaka",
"createAccount": "Skapa konto",
"passwordStrength": "Lösenordsstyrka: {passwordStrengthValue}",
@@ -146,6 +229,7 @@
"message": "Password Strength: {passwordStrengthText}"
},
"password": "Lösenord",
"signUpTerms": "Jag samtycker till <u-terms>användarvillkoren</u-terms> och <u-policy>integritetspolicyn</u-policy>",
"privacyPolicyTitle": "Integritetspolicy",
"termsOfServicesTitle": "Villkor",
"encryption": "Kryptering",
@@ -153,24 +237,58 @@
"changePasswordTitle": "Ändra lösenord",
"resetPasswordTitle": "Återställ lösenord",
"encryptionKeys": "Krypteringsnycklar",
"passwordWarning": "Vi lagrar inte detta lösenord, så om du glömmer bort det, <underline>kan vi inte dekryptera dina data</underline>",
"enterPasswordToEncrypt": "Ange ett lösenord som vi kan använda för att kryptera din data",
"enterNewPasswordToEncrypt": "Ange ett nytt lösenord som vi kan använda för att kryptera din data",
"passwordChangedSuccessfully": "Lösenordet har ändrats",
"generatingEncryptionKeys": "Skapar krypteringsnycklar...",
"continueLabel": "Fortsätt",
"insecureDevice": "Osäker enhet",
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Tyvärr, kunde vi inte generera säkra nycklar på den här enheten.\n\nvänligen registrera dig från en annan enhet.",
"howItWorks": "Så här fungerar det",
"ackPasswordLostWarning": "Jag förstår att om jag förlorar mitt lösenord kan jag förlora mina data eftersom min data är <underline>end-to-end-krypterad</underline>.",
"loginTerms": "Jag samtycker till <u-terms>användarvillkoren</u-terms> och <u-policy>integritetspolicyn</u-policy>",
"logInLabel": "Logga in",
"logout": "Logga ut",
"areYouSureYouWantToLogout": "Är du säker på att du vill logga ut?",
"yesLogout": "Ja, logga ut",
"exit": "Avsluta",
"theme": "Tema",
"lightTheme": "Ljust",
"darkTheme": "Mörkt",
"systemTheme": "System",
"verifyingRecoveryKey": "Verifierar återställningsnyckel...",
"recoveryKeyVerified": "Återställningsnyckel verifierad",
"recoveryKeySuccessBody": "Grymt! Din återställningsnyckel är giltig. Tack för att du verifierade.\n\nKom ihåg att hålla din återställningsnyckel säker med backups.",
"recreatePasswordTitle": "Återskapa lösenord",
"invalidKey": "Ogiltig nyckel",
"tryAgain": "Försök igen",
"viewRecoveryKey": "Visa återställningsnyckel",
"confirmRecoveryKey": "Bekräfta återställningsnyckel",
"confirmYourRecoveryKey": "Bekräfta din återställningsnyckel",
"confirm": "Bekräfta",
"emailYourLogs": "Maila dina loggar",
"copyEmailAddress": "Kopiera e-postadress",
"exportLogs": "Exportera loggar",
"enterYourRecoveryKey": "Ange din återställningsnyckel",
"about": "Om",
"weAreOpenSource": "Vi är öppen källkod!",
"privacy": "Sekretess",
"terms": "Villkor",
"checkForUpdates": "Sök efter uppdateringar",
"checkStatus": "Kontrollera status",
"downloadUpdate": "Ladda ner",
"criticalUpdateAvailable": "Kritisk uppdatering tillgänglig",
"update": "Uppdatera",
"checking": "Kontrollerar ...",
"youAreOnTheLatestVersion": "Du är på den senaste versionen",
"warning": "Varning",
"iUnderStand": "Jag förstår",
"@iUnderStand": {
"description": "Text for the button to confirm the user understands the warning"
},
"authToExportCodes": "Autentisera för att exportera dina koder",
"importSuccessTitle": "Jippi!",
"importSuccessDesc": "Du har importerat {count} koder!",
"@importSuccessDesc": {
"placeholders": {
@@ -181,40 +299,108 @@
}
}
},
"sorry": "Tyvärr",
"pendingSyncs": "Varning",
"activeSessions": "Aktiva sessioner",
"incorrectCode": "Felaktig kod",
"authenticationSuccessful": "Autentisering lyckades!",
"twofactorAuthenticationSuccessfullyReset": "Tvåfaktorsautentisering återställd",
"incorrectRecoveryKey": "Felaktig återställningsnyckel",
"enterPassword": "Ange lösenord",
"selectExportFormat": "Välj exportformat",
"encrypted": "Krypterad",
"plainText": "Enkel text",
"passwordToEncryptExport": "Lösenord för att kryptera export",
"export": "Exportera",
"useOffline": "Använd utan säkerhetskopior",
"signInToBackup": "Logga in för att säkerhetskopiera dina koder",
"singIn": "Logga in",
"showLargeIcons": "Visa stora ikoner",
"shouldHideCode": "Dölj koder",
"minimizeAppOnCopy": "Minimera appen vid kopiering",
"editCodeAuthMessage": "Autentisera för att redigera kod",
"deleteCodeAuthMessage": "Autentisera för att radera kod",
"showQRAuthMessage": "Autentisera för att visa QR-kod",
"confirmAccountDeleteTitle": "Bekräfta radering av kontot",
"androidBiometricHint": "Verifiera identitet",
"@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
},
"androidBiometricNotRecognized": "Ej godkänd. Försök igen.",
"@androidBiometricNotRecognized": {
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
},
"androidBiometricSuccess": "Slutförd",
"@androidBiometricSuccess": {
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
},
"androidCancelButton": "Avbryt",
"@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": "Obligatorisk autentisering",
"@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": "Biometriska uppgifter krävs",
"@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": "Enhetsuppgifter krävs",
"@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": "Enhetsuppgifter krävs",
"@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": "Gå till inställningar",
"@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."
},
"iOSOkButton": "OK",
"@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": "Ingen internetanslutning",
"pleaseCheckYourInternetConnectionAndTryAgain": "Kontrollera din internetanslutning och försök igen.",
"signOutFromOtherDevices": "Logga ut från andra enheter",
"signOutOtherDevices": "Logga ut andra enheter",
"doNotSignOut": "Logga inte ut",
"hearUsWhereTitle": "Hur hörde du talas om Ente? (valfritt)",
"hearUsExplanation": "Vi spårar inte appinstallationer, Det skulle hjälpa oss om du berättade var du hittade oss!",
"recoveryKeySaved": "Återställningsnyckel sparad i nedladdningsmappen!",
"waitingForBrowserRequest": "Väntar på webbläsarbegäran...",
"waitingForVerification": "Väntar på verifiering...",
"passkey": "Nyckel",
"passKeyPendingVerification": "Verifiering pågår fortfarande",
"loginSessionExpired": "Sessionen har gått ut",
"loginSessionExpiredDetails": "Din session har upphört. Logga in igen.",
"developerSettingsWarning": "Är du säker på att du vill ändra på utvecklarinställningar?",
"developerSettings": "Utvecklarinställningar",
"serverEndpoint": "Serverns slutpunkt",
"invalidEndpoint": "Ogiltig slutpunkt",
"invalidEndpointMessage": "Tyvärr, slutpunkten du angav är ogiltig. Ange en giltig slutpunkt och försök igen.",
"endpointUpdatedMessage": "Slutpunkten har uppdaterats",
"customEndpoint": "Ansluten till {endpoint}",
"pinText": "Fäst",
"unpinText": "Ångra fäst",
"pinnedCodeMessage": "{code} har fästs",
"pinned": "Fastmarkerad",
"tags": "Taggar",
"createNewTag": "Skapa ny tagg",
"tag": "Tagg",
"create": "Skapa",
"editTag": "Redigera tagg",
"deleteTagTitle": "Radera tagg?",
"updateNotAvailable": "Uppdateringen är inte tillgänglig",
"viewRawCodes": "Visa råa koder",
"rawCodes": "Råa koder",
"rawCodeData": "Rå koddata",
"appLock": "Applås",
"noSystemLockFound": "Inget systemlås hittades",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "För att aktivera applås, vänligen ställ in enhetens lösenord eller skärmlås i systeminställningarna.",
"autoLock": "Automatisk låsning",
"immediately": "Omedelbart",
"reEnterPassword": "Ange lösenord igen",
"reEnterPin": "Ange PIN-kod igen",
@@ -224,6 +410,8 @@
"setNewPassword": "Ställ in nytt lösenord",
"deviceLock": "Enhetslås",
"hideContent": "Dölj innehåll",
"hideContentDescriptionAndroid": "Döljer appinnehåll i app-växlaren och inaktiverar skärmdumpar",
"hideContentDescriptioniOS": "Döljer appinnehåll i app-växlaren",
"enterPin": "Ange PIN-kod",
"setNewPin": "Ställ in ny PIN-kod",
"authToViewPasskey": "Autentisera för att visa nyckel"

View File

@@ -446,8 +446,6 @@
"customEndpoint": "Bağlandı: {endpoint}",
"pinText": "Sabitle",
"unpinText": "Sabitlemeyi kaldır",
"pinnedCodeMessage": "{code} sabitlendi",
"unpinnedCodeMessage": "{code} sabitlemesi kaldırıldı",
"tags": "Etiketler",
"createNewTag": "Yeni etiket oluştur",
"tag": "Etiket",

View File

@@ -115,14 +115,14 @@
"importCodeDelimiterInfo": "Коди можуть бути розділені комою або новим рядком",
"selectFile": "Вибрати файл",
"emailVerificationToggle": "Підтвердження адреси електронної пошти",
"emailVerificationEnableWarning": "Щоб уникнути блокування доступу до свого облікового запису, обов’язково збережіть копію двофакторної аутентифікації до своєї електронної пошти за межами Ente Auth, перш ніж увімкнути перевірку електронної пошти.",
"authToChangeEmailVerificationSetting": "Будь ласка, пройдіть аутентифікацію, щоб змінити перевірку адреси електронної пошти",
"emailVerificationEnableWarning": "Щоб уникнути блокування доступу до свого облікового запису, обов’язково збережіть копію двоетапної автентифікації до своєї електронної пошти за межами Ente Auth, перш ніж увімкнути перевірку електронної пошти.",
"authToChangeEmailVerificationSetting": "Будь ласка, пройдіть автентифікацію, щоб змінити перевірку адреси електронної пошти",
"authenticateGeneric": "Будь ласка, авторизуйтеся",
"authToViewYourRecoveryKey": "Будь ласка, пройдіть аутентифікацію, щоб переглянути ваш ключ відновлення",
"authToChangeYourEmail": "Будь ласка, пройдіть аутентифікацію, щоб змінити адресу електронної пошти",
"authToChangeYourPassword": "Будь ласка, пройдіть аутентифікацію, щоб змінити ваш пароль",
"authToViewSecrets": "Будь ласка, пройдіть аутентифікацію, щоб переглянути ваші секретні коди",
"authToInitiateSignIn": "Будь ласка, пройдіть аутентифікацію, щоб розпочати вхід для резервного копіювання.",
"authToViewYourRecoveryKey": "Будь ласка, пройдіть автентифікацію, щоб переглянути ваш ключ відновлення",
"authToChangeYourEmail": "Будь ласка, пройдіть автентифікацію, щоб змінити адресу електронної пошти",
"authToChangeYourPassword": "Будь ласка, пройдіть автентифікацію, щоб змінити ваш пароль",
"authToViewSecrets": "Будь ласка, пройдіть автентифікацію, щоб переглянути ваші секретні коди",
"authToInitiateSignIn": "Будь ласка, пройдіть автентифікацію, щоб розпочати вхід для резервного копіювання.",
"ok": "Ок",
"cancel": "Скасувати",
"yes": "Так",
@@ -153,7 +153,7 @@
"verifyEmail": "Підтвердити електронну адресу",
"enterCodeHint": "Введіть нижче шестизначний код із застосунку для автентифікації",
"lostDeviceTitle": "Загубили пристрій?",
"twoFactorAuthTitle": "Двофакторна аутентифікація",
"twoFactorAuthTitle": "Двоетапна автентифікація",
"passkeyAuthTitle": "Перевірка секретного ключа",
"verifyPasskey": "Підтвердження секретного ключа",
"loginWithTOTP": "Увійти за допомогою TOTP",
@@ -194,7 +194,7 @@
"authToChangeLockscreenSetting": "Будь ласка, авторизуйтесь для зміни налаштувань екрану блокування",
"deviceLockEnablePreSteps": "Для увімкнення блокування програми, будь ласка, налаштуйте пароль пристрою або блокування екрана в системних налаштуваннях.",
"viewActiveSessions": "Показати активні сеанси",
"authToViewYourActiveSessions": "Будь ласка, пройдіть аутентифікацію, щоб переглянути ваші активні сеанси",
"authToViewYourActiveSessions": "Будь ласка, пройдіть автентифікацію, щоб переглянути ваші активні сеанси",
"searchHint": "Пошук...",
"search": "Пошук",
"sorryUnableToGenCode": "Вибачте, не вдалося створити код для {issuerName}",
@@ -258,6 +258,10 @@
"areYouSureYouWantToLogout": "Ви впевнені, що хочете вийти з системи?",
"yesLogout": "Так, вийти з системи",
"exit": "Вийти",
"theme": "Тема",
"lightTheme": "Світла",
"darkTheme": "Темна",
"systemTheme": "Як в системі",
"verifyingRecoveryKey": "Перевірка ключа відновлення...",
"recoveryKeyVerified": "Ключ відновлення перевірено",
"recoveryKeySuccessBody": "Чудово! Ваш ключ відновлення дійсний. Дякуємо за перевірку.\n\nБудь ласка, не забувайте зберігати надійну резервну копію ключа відновлення.",
@@ -328,6 +332,10 @@
}
}
},
"manualSort": "Власні",
"editOrder": "Змінити порядок",
"mostFrequentlyUsed": "Часто використовувані",
"mostRecentlyUsed": "Нещодавно використані",
"activeSessions": "Активні сеанси",
"somethingWentWrongPleaseTryAgain": "Щось пішло не так, спробуйте, будь ласка, знову",
"thisWillLogYouOutOfThisDevice": "Це призведе до виходу на цьому пристрої!",
@@ -342,9 +350,9 @@
"incorrectCode": "Невірний код",
"sorryTheCodeYouveEnteredIsIncorrect": "Вибачте, але введений вами код є невірним",
"emailChangedTo": "Адресу електронної пошти змінено на {newEmail}",
"authenticationFailedPleaseTryAgain": "Аутентифікація не пройдена. Будь ласка, спробуйте ще раз",
"authenticationFailedPleaseTryAgain": "Автентифікація не пройдена. Будь ласка, спробуйте ще раз",
"authenticationSuccessful": "Автентифікацію виконано!",
"twofactorAuthenticationSuccessfullyReset": "Двофакторна аутентифікація успішно скинута",
"twofactorAuthenticationSuccessfullyReset": "Двоетапна автентифікація успішно скинута",
"incorrectRecoveryKey": "Неправильний ключ відновлення",
"theRecoveryKeyYouEnteredIsIncorrect": "Ви ввели неправильний ключ відновлення",
"enterPassword": "Введіть пароль",
@@ -366,9 +374,9 @@
"focusOnSearchBar": "Сфокусуватися на пошуку після запуску програми",
"confirmUpdatingkey": "Ви впевнені у тому, що бажаєте змінити секретний ключ?",
"minimizeAppOnCopy": "Згорнути програму після копіювання",
"editCodeAuthMessage": "Аутентифікуйтесь, щоб змінити код",
"deleteCodeAuthMessage": "Аутентифікуйтесь, щоб видалити код",
"showQRAuthMessage": "Аутентифікуйтесь, щоб показати QR-код",
"editCodeAuthMessage": "Авторизуйтесь, щоб змінити код",
"deleteCodeAuthMessage": "Авторизуйтесь, щоб видалити код",
"showQRAuthMessage": "Авторизуйтесь, щоб показати QR-код",
"confirmAccountDeleteTitle": "Підтвердіть видалення облікового запису",
"confirmAccountDeleteMessage": "Цей обліковий запис є зв'язаним з іншими програмами Ente, якщо ви використовуєте якісь з них.\n\nВаші завантажені дані у всіх програмах Ente будуть заплановані до видалення, а обліковий запис буде видалено назавжди.",
"androidBiometricHint": "Підтвердити ідентифікацію",
@@ -387,11 +395,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": "Необхідна аутентифікація",
"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": "Потрібна біометрична автентифікація",
"@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."
},
@@ -407,7 +415,7 @@
"@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": "Біометрична автентифікація не налаштована на вашому пристрої. Перейдіть в «Налаштування > Безпека», щоб додати біометричну автентифікацію.",
"@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."
},
@@ -415,7 +423,7 @@
"@iOSLockOut": {
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
},
"iOSGoToSettingsDescription": "Біометрична аутентифікація не налаштована на вашому пристрої. Увімкніть TouchID або FaceID на вашому телефоні.",
"iOSGoToSettingsDescription": "Біометрична автентифікація не налаштована на вашому пристрої. Увімкніть TouchID або FaceID на вашому телефоні.",
"@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."
},
@@ -449,6 +457,7 @@
"unpinText": "Відкріпити",
"pinnedCodeMessage": "{code} закріплено",
"unpinnedCodeMessage": "{code} відкріплено",
"pinned": "Закріплено",
"tags": "Мітки",
"createNewTag": "Створити нову мітку",
"tag": "Мітка",
@@ -485,5 +494,13 @@
"appLockNotEnabled": "Блокування програм не увімкнено",
"appLockNotEnabledDescription": "Увімкніть блокування програм від безпеки > Блокування програм",
"authToViewPasskey": "Будь ласка, авторизуйтеся для перегляду ключа доступу",
"appLockOfflineModeWarning": "Ви обрали продовжити без резервних копій. Якщо ви забудете свій пароль, доступ до ваших даних буде заблоковано."
"appLockOfflineModeWarning": "Ви обрали продовжити без резервних копій. Якщо ви забудете свій пароль, доступ до ваших даних буде заблоковано.",
"duplicateCodes": "Дублювати коди",
"noDuplicates": "✨ Немає дублікатів",
"youveNoDuplicateCodesThatCanBeCleared": "У вас немає дублікатів кодів, які можна очистити",
"deduplicateCodes": "Дедуплікувати коди",
"deselectAll": "Зняти виділення",
"selectAll": "Вибрати все",
"deleteDuplicates": "Видалити дублікати",
"plainHTML": "Звичайний HTML"
}

View File

@@ -258,6 +258,10 @@
"areYouSureYouWantToLogout": "Bạn có chắc chắn muốn đăng xuất?",
"yesLogout": "Có, đăng xuất",
"exit": "Thoát",
"theme": "Chủ đề",
"lightTheme": "Sáng",
"darkTheme": "Tối",
"systemTheme": "Hệ thống",
"verifyingRecoveryKey": "Đang xác minh khóa khôi phục...",
"recoveryKeyVerified": "Khóa khôi phục đã được xác thực",
"recoveryKeySuccessBody": "Tuyệt vời! Khóa khôi phục của bạn hợp lệ. Cảm ơn bạn đã xác minh.\n\nHãy nhớ sao lưu khóa khôi phục của bạn một cách an toàn.",
@@ -328,6 +332,10 @@
}
}
},
"manualSort": "Tùy chỉnh",
"editOrder": "Chỉnh sửa đơn hàng",
"mostFrequentlyUsed": "Thường dùng",
"mostRecentlyUsed": "Dùng gần đây",
"activeSessions": "Các phiên làm việc hiện tại",
"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!",
@@ -449,6 +457,7 @@
"unpinText": "Bỏ ghim",
"pinnedCodeMessage": "{code} đã được ghim",
"unpinnedCodeMessage": "{code} đã được bỏ ghim",
"pinned": "Đã ghim",
"tags": "Thẻ",
"createNewTag": "Tạo thẻ mới",
"tag": "Thẻ",
@@ -485,5 +494,12 @@
"appLockNotEnabled": "Khóa ứng dụng chưa được bật",
"appLockNotEnabledDescription": "Vui lòng bật khóa ứng dụng từ Bảo mật > Khóa ứng dụng",
"authToViewPasskey": "Vui lòng xác thực để xem mã khóa",
"appLockOfflineModeWarning": "Bạn đã chọn tiếp tục mà không có bản sao lưu. Nếu bạn quên khóa ứng dụng, bạn sẽ bị khóa khỏi việc truy cập dữ liệu của mình."
"appLockOfflineModeWarning": "Bạn đã chọn tiếp tục mà không có bản sao lưu. Nếu bạn quên khóa ứng dụng, bạn sẽ bị khóa khỏi việc truy cập dữ liệu của mình.",
"duplicateCodes": "Mã trùng lặp",
"noDuplicates": "✨ Không có trùng lặp",
"youveNoDuplicateCodesThatCanBeCleared": "Bạn không có mã trùng lặp nào có thể được xóa",
"deduplicateCodes": "Loại bỏ mã trùng lặp",
"deselectAll": "Bỏ chọn tất cả",
"selectAll": "Chọn tất cả",
"deleteDuplicates": "Xóa trùng lặp"
}

View File

@@ -258,6 +258,10 @@
"areYouSureYouWantToLogout": "您确定要登出吗?",
"yesLogout": "是的,登出",
"exit": "退出",
"theme": "主题",
"lightTheme": "浅色",
"darkTheme": "深色",
"systemTheme": "系统",
"verifyingRecoveryKey": "正在验证恢复密钥...",
"recoveryKeyVerified": "恢复密钥已验证",
"recoveryKeySuccessBody": "太棒了! 您的恢复密钥是有效的。 感谢您的验证。\n\n请记住要安全备份您的恢复密钥。",
@@ -328,6 +332,10 @@
}
}
},
"manualSort": "自定义",
"editOrder": "编辑顺序",
"mostFrequentlyUsed": "经常使用",
"mostRecentlyUsed": "最近使用",
"activeSessions": "已登录的设备",
"somethingWentWrongPleaseTryAgain": "出了点问题,请重试",
"thisWillLogYouOutOfThisDevice": "这将使您登出该设备!",
@@ -449,6 +457,7 @@
"unpinText": "取消置顶",
"pinnedCodeMessage": "{code} 已被置顶",
"unpinnedCodeMessage": "{code} 已被取消置顶",
"pinned": "已置顶",
"tags": "标签",
"createNewTag": "创建新标签",
"tag": "标签",
@@ -485,5 +494,13 @@
"appLockNotEnabled": "应用锁未启用",
"appLockNotEnabledDescription": "请从“安全”>“应用锁”启用应用锁",
"authToViewPasskey": "请验证身份以查看通行密钥",
"appLockOfflineModeWarning": "您已选择继续而不备份。如果您忘记了应用锁,您将无法访问数据。"
"appLockOfflineModeWarning": "您已选择继续而不备份。如果您忘记了应用锁,您将无法访问数据。",
"duplicateCodes": "重复代码",
"noDuplicates": "✨ 没有重复",
"youveNoDuplicateCodesThatCanBeCleared": "您没有可清除的重复代码",
"deduplicateCodes": "删除重复代码",
"deselectAll": "取消全选",
"selectAll": "全选",
"deleteDuplicates": "删除重复项",
"plainHTML": "Plain HTML"
}

View File

@@ -0,0 +1,15 @@
enum IconType { simpleIcon, customIcon }
class AllIconData {
final String title;
final IconType type;
final String? color;
final String? slug;
AllIconData({
required this.title,
required this.type,
required this.color,
this.slug,
});
}

View File

@@ -12,6 +12,8 @@ class CodeDisplay {
String note;
final List<String> tags;
int position;
String iconSrc;
String iconID;
CodeDisplay({
this.pinned = false,
@@ -21,8 +23,12 @@ class CodeDisplay {
this.tags = const [],
this.note = '',
this.position = 0,
this.iconSrc = '',
this.iconID = '',
});
bool get isCustomIcon => (iconSrc != '' && iconID != '');
// copyWith
CodeDisplay copyWith({
bool? pinned,
@@ -32,6 +38,8 @@ class CodeDisplay {
List<String>? tags,
String? note,
int? position,
String? iconSrc,
String? iconID,
}) {
final bool updatedPinned = pinned ?? this.pinned;
final bool updatedTrashed = trashed ?? this.trashed;
@@ -40,6 +48,8 @@ class CodeDisplay {
final List<String> updatedTags = tags ?? this.tags;
final String updatedNote = note ?? this.note;
final int updatedPosition = position ?? this.position;
final String updatedIconSrc = iconSrc ?? this.iconSrc;
final String updatedIconID = iconID ?? this.iconID;
return CodeDisplay(
pinned: updatedPinned,
@@ -49,6 +59,8 @@ class CodeDisplay {
tags: updatedTags,
note: updatedNote,
position: updatedPosition,
iconSrc: updatedIconSrc,
iconID: updatedIconID,
);
}
@@ -64,6 +76,8 @@ class CodeDisplay {
tags: List<String>.from(json['tags'] ?? []),
note: json['note'] ?? '',
position: json['position'] ?? 0,
iconSrc: json['iconSrc'] ?? 'ente',
iconID: json['iconID'] ?? '',
);
}
@@ -106,6 +120,8 @@ class CodeDisplay {
'tags': tags,
'note': note,
'position': position,
'iconSrc': iconSrc,
'iconID': iconID,
};
}

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/codes_updated_event.dart';
import "package:ente_auth/l10n/l10n.dart";
import 'package:ente_auth/models/all_icon_data.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/models/code_display.dart';
import 'package:ente_auth/onboarding/model/tag_enums.dart';
@@ -13,7 +14,10 @@ import 'package:ente_auth/onboarding/view/common/tag_chip.dart';
import 'package:ente_auth/store/code_display_store.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/custom_icon_widget.dart';
import 'package:ente_auth/ui/components/models/button_result.dart';
import 'package:ente_auth/ui/custom_icon_page.dart';
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_auth/utils/totp_util.dart';
@@ -42,6 +46,9 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
late List<String> selectedTags = [...?widget.code?.display.tags];
List<String> allTags = [];
StreamSubscription<CodesUpdatedEvent>? _streamSubscription;
bool isCustomIcon = false;
String _customIconID = "";
late IconType _iconSrc;
@override
void initState() {
@@ -81,6 +88,19 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
_limitTextLength(_accountController, _otherTextLimit);
_limitTextLength(_secretController, _otherTextLimit);
}
isCustomIcon = widget.code?.display.isCustomIcon ?? false;
if (isCustomIcon) {
_customIconID = widget.code?.display.iconID ?? "ente";
} else {
if (widget.code != null) {
_customIconID = widget.code!.issuer;
}
}
_iconSrc = widget.code?.display.iconSrc == "simpleIcon"
? IconType.simpleIcon
: IconType.customIcon;
super.initState();
}
@@ -120,191 +140,208 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
if (widget.code != null)
GestureDetector(
onTap: () async {
await navigateToCustomIconPage();
},
child: CustomIconWidget(iconData: _customIconID),
),
const SizedBox(height: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FieldLabel(l10n.codeIssuerHint),
Expanded(
child: TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter some text";
}
return null;
},
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
),
style: getEnteTextTheme(context).small,
controller: _issuerController,
autofocus: true,
),
),
],
),
Row(
children: [
FieldLabel(l10n.secret),
Expanded(
child: TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter some text";
}
return null;
},
style: getEnteTextTheme(context).small,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 12.0),
suffixIcon: GestureDetector(
// padding: EdgeInsets.zero,
onTap: () {
setState(() {
_secretKeyObscured = !_secretKeyObscured;
});
Row(
children: [
FieldLabel(l10n.codeIssuerHint),
Expanded(
child: TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter some text";
}
return null;
},
child: _secretKeyObscured
? const Icon(
Icons.visibility_off_rounded,
size: 18,
)
: const Icon(
Icons.visibility_rounded,
size: 18,
),
decoration: const InputDecoration(
contentPadding:
EdgeInsets.symmetric(vertical: 12.0),
),
style: getEnteTextTheme(context).small,
controller: _issuerController,
autofocus: true,
),
),
obscureText: _secretKeyObscured,
controller: _secretController,
),
],
),
],
),
Row(
children: [
FieldLabel(l10n.account),
Expanded(
child: TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter some text";
}
return null;
},
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
Row(
children: [
FieldLabel(l10n.secret),
Expanded(
child: TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter some text";
}
return null;
},
style: getEnteTextTheme(context).small,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 12.0),
suffixIcon: GestureDetector(
// padding: EdgeInsets.zero,
onTap: () {
setState(() {
_secretKeyObscured = !_secretKeyObscured;
});
},
child: _secretKeyObscured
? const Icon(
Icons.visibility_off_rounded,
size: 18,
)
: const Icon(
Icons.visibility_rounded,
size: 18,
),
),
),
obscureText: _secretKeyObscured,
controller: _secretController,
),
),
style: getEnteTextTheme(context).small,
controller: _accountController,
),
],
),
],
),
const SizedBox(height: 12),
Row(
children: [
FieldLabel(l10n.notes),
Expanded(
child: TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter some text";
}
if (value.length > _notesLimit) {
return "Notes can't be more than 1000 characters";
}
return null;
},
maxLength: _notesLimit,
minLines: 1,
maxLines: 5,
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
Row(
children: [
FieldLabel(l10n.account),
Expanded(
child: TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter some text";
}
return null;
},
decoration: const InputDecoration(
contentPadding:
EdgeInsets.symmetric(vertical: 12.0),
),
style: getEnteTextTheme(context).small,
controller: _accountController,
),
),
style: getEnteTextTheme(context).small,
controller: _notesController,
),
],
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
alignment: WrapAlignment.start,
children: [
...allTags.map(
(e) => TagChip(
label: e,
action: TagChipAction.check,
state: selectedTags.contains(e)
? TagChipState.selected
: TagChipState.unselected,
onTap: () {
if (selectedTags.contains(e)) {
selectedTags.remove(e);
} else {
selectedTags.add(e);
}
setState(() {});
},
),
const SizedBox(height: 12),
Row(
children: [
FieldLabel(l10n.notes),
Expanded(
child: TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter some text";
}
if (value.length > _notesLimit) {
return "Notes can't be more than 1000 characters";
}
return null;
},
maxLength: _notesLimit,
minLines: 1,
maxLines: 5,
decoration: const InputDecoration(
contentPadding:
EdgeInsets.symmetric(vertical: 12.0),
),
style: getEnteTextTheme(context).small,
controller: _notesController,
),
),
],
),
AddChip(
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AddTagDialog(
onTap: (tag) {
final exist = allTags.contains(tag);
if (exist && selectedTags.contains(tag)) {
return Navigator.pop(context);
}
if (!exist) allTags.add(tag);
selectedTags.add(tag);
setState(() {});
Navigator.pop(context);
const SizedBox(height: 12),
Wrap(
spacing: 12,
alignment: WrapAlignment.start,
children: [
...allTags.map(
(e) => TagChip(
label: e,
action: TagChipAction.check,
state: selectedTags.contains(e)
? TagChipState.selected
: TagChipState.unselected,
onTap: () {
if (selectedTags.contains(e)) {
selectedTags.remove(e);
} else {
selectedTags.add(e);
}
setState(() {});
},
),
),
AddChip(
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AddTagDialog(
onTap: (tag) {
final exist = allTags.contains(tag);
if (exist && selectedTags.contains(tag)) {
return Navigator.pop(context);
}
if (!exist) allTags.add(tag);
selectedTags.add(tag);
setState(() {});
Navigator.pop(context);
},
);
},
barrierColor: Colors.black.withOpacity(0.85),
barrierDismissible: false,
);
},
barrierColor: Colors.black.withOpacity(0.85),
barrierDismissible: false,
);
},
),
],
),
const SizedBox(height: 40),
SizedBox(
width: 400,
child: OutlinedButton(
onPressed: () async {
if ((_accountController.text.trim().isEmpty &&
_issuerController.text.trim().isEmpty) ||
_secretController.text.trim().isEmpty) {
String message;
if (_secretController.text.trim().isEmpty) {
message = context.l10n.secretCanNotBeEmpty;
} else {
message =
context.l10n.bothIssuerAndAccountCanNotBeEmpty;
}
_showIncorrectDetailsDialog(
context,
message: message,
);
return;
}
await _saveCode();
},
child: Text(l10n.saveAction),
),
),
],
),
const SizedBox(
height: 40,
),
SizedBox(
width: 400,
child: OutlinedButton(
onPressed: () async {
if ((_accountController.text.trim().isEmpty &&
_issuerController.text.trim().isEmpty) ||
_secretController.text.trim().isEmpty) {
String message;
if (_secretController.text.trim().isEmpty) {
message = context.l10n.secretCanNotBeEmpty;
} else {
message =
context.l10n.bothIssuerAndAccountCanNotBeEmpty;
}
_showIncorrectDetailsDialog(context, message: message);
return;
}
await _saveCode();
},
child: Text(l10n.saveAction),
),
),
],
),
),
@@ -324,6 +361,11 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
widget.code?.display.copyWith(tags: selectedTags) ??
CodeDisplay(tags: selectedTags);
display.note = notes;
display.iconID = _customIconID.toLowerCase();
display.iconSrc =
_iconSrc == IconType.simpleIcon ? 'simpleIcon' : 'customIcon';
if (widget.code != null && widget.code!.secret != secret) {
ButtonResult? result = await showChoiceActionSheet(
context,
@@ -373,4 +415,28 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
message ?? context.l10n.pleaseVerifyDetails,
);
}
Future<void> navigateToCustomIconPage() async {
final allIcons = IconUtils.instance.getAllIcons();
String currentIcon;
if (widget.code!.display.isCustomIcon) {
currentIcon = widget.code!.display.iconID;
} else {
currentIcon = widget.code!.issuer;
}
final AllIconData newCustomIcon = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return CustomIconPage(
currentIcon: currentIcon,
allIcons: allIcons,
);
},
),
);
setState(() {
_customIconID = newCustomIcon.title;
_iconSrc = newCustomIcon.type;
});
}
}

View File

@@ -0,0 +1,56 @@
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:logging/logging.dart';
class DeduplicationService {
final _logger = Logger("DeduplicationService");
DeduplicationService._privateConstructor();
static final DeduplicationService instance =
DeduplicationService._privateConstructor();
Future<List<DuplicateCodes>> getDuplicateCodes() async {
try {
final List<DuplicateCodes> result = await _getDuplicateCodes();
return result;
} catch (e, s) {
_logger.severe("failed to get dedupeCode", e, s);
rethrow;
}
}
Future<List<DuplicateCodes>> _getDuplicateCodes() async {
final codes = await CodeStore.instance.getAllCodes();
final List<DuplicateCodes> duplicateCodes = [];
Map<String, List<Code>> uniqueCodes = {};
for (final code in codes) {
if (code.hasError || code.isTrashed) continue;
final uniqueKey = "${code.secret}_${code.issuer}_${code.account}";
if (uniqueCodes.containsKey(uniqueKey)) {
uniqueCodes[uniqueKey]!.add(code);
} else {
uniqueCodes[uniqueKey] = [code];
}
}
for (final key in uniqueCodes.keys) {
if (uniqueCodes[key]!.length > 1) {
duplicateCodes.add(DuplicateCodes(key, uniqueCodes[key]!));
}
}
return duplicateCodes;
}
}
class DuplicateCodes {
String hash;
final List<Code> codes;
DuplicateCodes(
this.hash,
this.codes,
);
}

View File

@@ -1,6 +1,6 @@
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -11,11 +11,13 @@ class PasskeyService {
final _enteDio = Network.instance.enteDio;
Future<String> getJwtToken() async {
Future<String> getAccountsUrl() async {
final response = await _enteDio.get(
"/users/accounts-token",
);
return response.data!["accountsToken"] as String;
final accountsUrl = response.data!["accountsUrl"] ?? kAccountsUrl;
final jwtToken = response.data!["accountsToken"] as String;
return "$accountsUrl/passkeys?token=$jwtToken";
}
Future<bool> isPasskeyRecoveryEnabled() async {
@@ -25,10 +27,6 @@ class PasskeyService {
return response.data!["isPasskeyRecoveryEnabled"] as bool;
}
String get accountsUrl {
return kDebugMode ? "http://localhost:3001" : "https://accounts.ente.io";
}
Future<void> configurePasskeyRecovery(
String secret,
String userEncryptedSecret,
@@ -46,8 +44,7 @@ class PasskeyService {
Future<void> openPasskeyPage(BuildContext context) async {
try {
final jwtToken = await getJwtToken();
final url = "$accountsUrl/passkeys?token=$jwtToken";
final url = await getAccountsUrl();
await launchUrlString(
url,
mode: LaunchMode.externalApplication,

View File

@@ -379,6 +379,7 @@ class UserService {
if (response.statusCode == 200) {
Widget page;
final String passkeySessionID = response.data["passkeySessionID"];
final String accountsUrl = response.data["accountsUrl"] ?? kAccountsUrl;
String twoFASessionID = response.data["twoFactorSessionID"];
if (twoFASessionID.isEmpty &&
response.data["twoFactorSessionIDV2"] != null) {
@@ -388,6 +389,7 @@ class UserService {
page = PasskeyPage(
passkeySessionID,
totp2FASessionID: twoFASessionID,
accountsUrl: accountsUrl,
);
} else if (twoFASessionID.isNotEmpty) {
page = TwoFactorAuthenticationPage(twoFASessionID);
@@ -692,6 +694,7 @@ class UserService {
if (response.statusCode == 200) {
Widget? page;
final String passkeySessionID = response.data["passkeySessionID"];
final String accountsUrl = response.data["accountsUrl"] ?? kAccountsUrl;
String twoFASessionID = response.data["twoFactorSessionID"];
if (twoFASessionID.isEmpty &&
response.data["twoFactorSessionIDV2"] != null) {
@@ -702,6 +705,7 @@ class UserService {
page = PasskeyPage(
passkeySessionID,
totp2FASessionID: twoFASessionID,
accountsUrl: accountsUrl,
);
} else if (twoFASessionID.isNotEmpty) {
page = TwoFactorAuthenticationPage(twoFASessionID);

View File

@@ -1,58 +1,58 @@
import 'package:ente_auth/services/preference_service.dart';
import 'dart:async';
import 'dart:io';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class CodeTimerProgressCache {
static final Map<int, CodeTimerProgress> _cache = {};
static CodeTimerProgress getCachedWidget(int period) {
if (!_cache.containsKey(period)) {
_cache[period] = CodeTimerProgress(period: period);
}
return _cache[period]!;
}
}
class CodeTimerProgress extends StatefulWidget {
final int period;
final bool isCompactMode;
const CodeTimerProgress({
super.key,
required this.period,
this.isCompactMode = false,
});
@override
State<CodeTimerProgress> createState() => _CodeTimerProgressState();
}
class _CodeTimerProgressState extends State<CodeTimerProgress>
with SingleTickerProviderStateMixin {
late final Ticker _ticker;
class _CodeTimerProgressState extends State<CodeTimerProgress> {
late final Timer _timer;
late final ValueNotifier<double> _progress;
late final int _microSecondsInPeriod;
late bool _isCompactMode=false;
late final int _periodInMicros;
// Cache the start time to avoid repeated system calls
late final int _startMicros;
// Reduce update frequency
final int _updateIntervalMs =
(Platform.isAndroid || Platform.isIOS) ? 16 : 500; // approximately 60 FPS
@override
void initState() {
super.initState();
_microSecondsInPeriod = widget.period * 1000000;
_periodInMicros = widget.period * 1000000;
_progress = ValueNotifier<double>(0.0);
_ticker = createTicker(_updateTimeRemaining);
_ticker.start();
_isCompactMode = PreferenceService.instance.isCompactMode();
_updateTimeRemaining(Duration.zero);
_startMicros = DateTime.now().microsecondsSinceEpoch;
// Use a Timer instead of a Ticker
_timer = Timer.periodic(Duration(milliseconds: _updateIntervalMs), (timer) {
final now = DateTime.now().microsecondsSinceEpoch;
_updateTimeRemaining(now);
});
}
void _updateTimeRemaining(Duration elapsed) {
int timeRemaining = _microSecondsInPeriod -
(DateTime.now().microsecondsSinceEpoch % _microSecondsInPeriod);
_progress.value = timeRemaining / _microSecondsInPeriod;
void _updateTimeRemaining(int currentMicros) {
// More efficient time calculation using modulo
final elapsed = (currentMicros - _startMicros) % _periodInMicros;
final timeRemaining = _periodInMicros - elapsed;
_progress.value = timeRemaining / _periodInMicros;
}
@override
void dispose() {
_ticker.dispose();
_timer.cancel();
_progress.dispose();
super.dispose();
}
@@ -60,18 +60,19 @@ class _CodeTimerProgressState extends State<CodeTimerProgress>
@override
Widget build(BuildContext context) {
return SizedBox(
height: _isCompactMode ?1:3,
height: widget.isCompactMode ? 1 : 3,
child: ValueListenableBuilder<double>(
valueListenable: _progress,
builder: (context, progress, _) {
return CustomPaint(
key: Key(progress.toString()), // Add key here
painter: _ProgressPainter(
progress: progress,
color: progress > 0.4
? getEnteColorScheme(context).primary700
: Colors.orange,
),
size: Size.infinite,
size: const Size.fromHeight(double.infinity),
);
},
),
@@ -83,7 +84,10 @@ class _ProgressPainter extends CustomPainter {
final double progress;
final Color color;
_ProgressPainter({required this.progress, required this.color});
const _ProgressPainter({
required this.progress,
required this.color,
});
@override
void paint(Canvas canvas, Size size) {

View File

@@ -146,8 +146,10 @@ class _CodeWidgetState extends State<CodeWidget> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.code.type.isTOTPCompatible)
CodeTimerProgressCache.getCachedWidget(
widget.code.period,
CodeTimerProgress(
key: ValueKey('period_${widget.code.period}'),
period: widget.code.period,
isCompactMode: widget.isCompactMode,
),
widget.isCompactMode
? const SizedBox(height: 4)
@@ -445,13 +447,19 @@ class _CodeWidgetState extends State<CodeWidget> {
}
Widget _getIcon() {
final String iconData;
if (widget.code.display.isCustomIcon) {
iconData = widget.code.display.iconID;
} else {
iconData = widget.code.issuer;
}
return Padding(
padding: _shouldShowLargeIcon
? EdgeInsets.only(left: widget.isCompactMode ? 12 : 16)
: const EdgeInsets.all(0),
child: IconUtils.instance.getIcon(
context,
safeDecode(widget.code.issuer).trim(),
safeDecode(iconData).trim(),
width: widget.isCompactMode
? (_shouldShowLargeIcon ? 32 : 24)
: (_shouldShowLargeIcon ? 42 : 24),

View File

@@ -0,0 +1,111 @@
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:figma_squircle/figma_squircle.dart';
import 'package:flutter/material.dart';
class CustomIconWidget extends StatelessWidget {
final String iconData;
CustomIconWidget({
super.key,
required this.iconData,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 90,
width: 90,
child: Stack(
children: [
Container(
width: 80,
height: 80,
decoration: ShapeDecoration(
shape: SmoothRectangleBorder(
side: BorderSide(
width: 1.5,
color: getEnteColorScheme(context)
.tagChipSelectedColor
.withOpacity(0.5),
),
borderRadius: SmoothBorderRadius(
cornerRadius: 15.5,
cornerSmoothing: 1.0,
),
),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
child: FittedBox(
fit: BoxFit.contain,
child: IconUtils.instance.getIcon(
context,
safeDecode(iconData).trim(),
width: 50,
),
),
),
_getEditIcon(context),
],
),
);
}
Widget _getEditIcon(BuildContext context) {
return Positioned(
left: 60,
top: 60,
child: Center(
child: Container(
height: 28,
width: 28,
decoration: ShapeDecoration(
color: Colors.white,
shadows: const [
BoxShadow(
offset: Offset(0, 0),
blurRadius: 0.84,
color: Color.fromRGBO(0, 0, 0, 0.11),
),
BoxShadow(
offset: Offset(0.84, 0.84),
blurRadius: 1.68,
color: Color.fromRGBO(0, 0, 0, 0.09),
),
BoxShadow(
offset: Offset(2.53, 2.53),
blurRadius: 2.53,
color: Color.fromRGBO(0, 0, 0, 0.05),
),
BoxShadow(
offset: Offset(5.05, 4.21),
blurRadius: 2.53,
color: Color.fromRGBO(0, 0, 0, 0.02),
),
BoxShadow(
offset: Offset(7.58, 6.74),
blurRadius: 2.53,
color: Color.fromRGBO(0, 0, 0, 0.0),
),
],
shape: SmoothRectangleBorder(
borderRadius: SmoothBorderRadius(
cornerRadius: 8,
cornerSmoothing: 1.0,
),
),
),
child: Icon(
Icons.edit,
size: 16,
color: Colors.black.withOpacity(0.9),
),
),
),
);
}
}

View File

@@ -0,0 +1,220 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/all_icon_data.dart';
import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:flutter/material.dart';
class CustomIconPage extends StatefulWidget {
final Map<String, AllIconData> allIcons;
final String currentIcon;
const CustomIconPage({
super.key,
required this.allIcons,
required this.currentIcon,
});
@override
State<CustomIconPage> createState() => _CustomIconPageState();
}
class _CustomIconPageState extends State<CustomIconPage> {
Map<String, AllIconData> _filteredIcons = {};
bool _showSearchBox = false;
final bool _autoFocusSearch =
PreferenceService.instance.shouldAutoFocusOnSearchBar();
final TextEditingController _textController = TextEditingController();
String _searchText = "";
// Used to request focus on the search box when clicked the search icon
late FocusNode searchBoxFocusNode;
@override
void initState() {
_filteredIcons = widget.allIcons;
_showSearchBox = _autoFocusSearch;
searchBoxFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
_textController.dispose();
searchBoxFocusNode.dispose();
super.dispose();
}
void _applyFilteringAndRefresh() {
if (_searchText.isEmpty) {
setState(() {
_filteredIcons = widget.allIcons;
});
return;
}
final filteredIcons = <String, AllIconData>{};
widget.allIcons.forEach((title, iconData) {
if (title.toLowerCase().contains(_searchText.toLowerCase())) {
filteredIcons[title] = iconData;
}
});
setState(() {
_filteredIcons = filteredIcons;
});
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: !_showSearchBox
? const Text('Choose icon')
: TextField(
autocorrect: false,
enableSuggestions: false,
autofocus: _autoFocusSearch,
controller: _textController,
onChanged: (value) {
_searchText = value;
_applyFilteringAndRefresh();
},
decoration: InputDecoration(
hintText: l10n.searchHint,
border: InputBorder.none,
focusedBorder: InputBorder.none,
),
focusNode: searchBoxFocusNode,
),
actions: [
IconButton(
icon: _showSearchBox
? const Icon(Icons.clear)
: const Icon(Icons.search),
tooltip: l10n.search,
onPressed: () {
setState(
() {
_showSearchBox = !_showSearchBox;
if (!_showSearchBox) {
_textController.clear();
_searchText = "";
} else {
_searchText = _textController.text;
// Request focus on the search box
searchBoxFocusNode.requestFocus();
}
_applyFilteringAndRefresh();
},
);
},
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Expanded(
child: Scrollbar(
thumbVisibility: true,
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 90)
.clamp(1, double.infinity)
.toInt(),
crossAxisSpacing: 14,
mainAxisSpacing: 14,
childAspectRatio: 1,
),
itemCount: _filteredIcons.length,
itemBuilder: (context, index) {
final title = _filteredIcons.keys.elementAt(index);
final iconData = _filteredIcons[title]!;
IconType iconType = iconData.type;
String? color = iconData.color;
String? slug = iconData.slug;
Widget iconWidget;
if (iconType == IconType.simpleIcon) {
final simpleIconPath = normalizeSimpleIconName(title);
iconWidget = IconUtils.instance.getSVGIcon(
"assets/simple-icons/icons/$simpleIconPath.svg",
title,
color,
40,
context,
);
} else {
iconWidget = IconUtils.instance.getSVGIcon(
"assets/custom-icons/icons/${slug ?? title}.svg",
title,
color,
40,
context,
);
}
return GestureDetector(
key: ValueKey(title),
onTap: () {
final newIcon = AllIconData(
title: title,
type: iconType,
color: color,
slug: slug,
);
Navigator.of(context).pop(newIcon);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 1.5,
color: title.toLowerCase() ==
widget.currentIcon.toLowerCase()
? getEnteColorScheme(context)
.tagChipSelectedColor
: Colors.transparent,
),
borderRadius: const BorderRadius.all(
Radius.circular(12.0),
),
),
child: Column(
children: [
const SizedBox(height: 8),
Expanded(
child: iconWidget,
),
const SizedBox(height: 12),
Padding(
padding: title.toLowerCase() ==
widget.currentIcon.toLowerCase()
? const EdgeInsets.only(left: 2, right: 2)
: const EdgeInsets.all(0.0),
child: Text(
'${title[0].toUpperCase()}${title.substring(1)}',
style: getEnteTextTheme(context).mini,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const SizedBox(height: 4),
],
),
),
);
},
),
),
),
],
),
),
),
);
}
}

View File

@@ -18,6 +18,7 @@ import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/store/code_display_store.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/theme/text_style.dart';
import 'package:ente_auth/ui/account/logout_dialog.dart';
import 'package:ente_auth/ui/code_error_widget.dart';
import 'package:ente_auth/ui/code_widget.dart';
@@ -217,10 +218,11 @@ class _HomePageState extends State<HomePage> {
void sortFilteredCodes(List<Code> codes, CodeSortKey sortKey) {
switch (sortKey) {
case CodeSortKey.issuerName:
codes.sort((a, b) => a.issuer.compareTo(b.issuer));
codes.sort((a, b) => compareAsciiLowerCaseNatural(a.issuer, b.issuer));
break;
case CodeSortKey.accountName:
codes.sort((a, b) => a.account.compareTo(b.account));
codes
.sort((a, b) => compareAsciiLowerCaseNatural(a.account, b.account));
break;
case CodeSortKey.mostFrequentlyUsed:
codes.sort((a, b) => b.display.tapCount.compareTo(a.display.tapCount));
@@ -353,7 +355,7 @@ class _HomePageState extends State<HomePage> {
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: !_showSearchBox
? const Text('Ente Auth')
? const Text('Ente Auth', style: brandStyleMedium)
: TextField(
autocorrect: false,
enableSuggestions: false,

View File

@@ -5,7 +5,6 @@ import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/errors.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/account/two_factor.dart';
import 'package:ente_auth/services/passkey_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
@@ -20,10 +19,12 @@ import 'package:url_launcher/url_launcher_string.dart';
class PasskeyPage extends StatefulWidget {
final String sessionID;
final String totp2FASessionID;
final String accountsUrl;
const PasskeyPage(
this.sessionID, {
required this.totp2FASessionID,
required this.accountsUrl,
super.key,
});
@@ -47,9 +48,8 @@ class _PasskeyPageState extends State<PasskeyPage> {
}
Future<void> launchPasskey() async {
final String accountsUrl = PasskeyService.instance.accountsUrl;
await launchUrlString(
"$accountsUrl/passkeys/verify?"
"${widget.accountsUrl}/passkeys/verify?"
"passkeySessionID=${widget.sessionID}"
"&redirect=enteauth://passkey"
"&clientPackage=io.ente.auth",

View File

@@ -3,6 +3,7 @@ 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';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/code_widget.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
@@ -16,71 +17,71 @@ class ReorderCodesPage extends StatefulWidget {
}
class _ReorderCodesPageState extends State<ReorderCodesPage> {
int selectedSortOption = 2;
bool hasChanged = false;
final logger = Logger('ReorderCodesPage');
@override
Widget build(BuildContext context) {
final bool isCompactMode = PreferenceService.instance.isCompactMode();
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (!didPop) {
final hasSaved = await saveUpadedIndexes();
if (hasSaved) {
return Scaffold(
appBar: AppBar(
title: const Text("Custom order"),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () async {
Navigator.of(context).pop();
}
}
},
child: Scaffold(
appBar: AppBar(
title: Text(context.l10n.editOrder),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () async {
},
),
actions: [
GestureDetector(
onTap: () async {
final hasSaved = await saveUpadedIndexes();
if (hasSaved) {
Navigator.of(context).pop();
}
},
child: Padding(
padding: const EdgeInsets.only(right: 20),
child: Text(
context.l10n.save,
style: TextStyle(
color: hasChanged
? getEnteColorScheme(context).textBase
: getEnteColorScheme(context).strokeMuted,
),
),
),
),
),
body: ReorderableListView(
buildDefaultDragHandles: false,
proxyDecorator:
(Widget child, int index, Animation<double> animation) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, _) {
final animValue = Curves.easeInOut.transform(animation.value);
final scale = lerpDouble(1, 1.05, animValue)!;
return Transform.scale(scale: scale, child: child);
},
);
},
children: [
for (final code in widget.codes)
selectedSortOption == 2
? ReorderableDragStartListener(
key: ValueKey('${code.hashCode}_${code.generatedID}'),
index: widget.codes.indexOf(code),
child: CodeWidget(
key: ValueKey(code.generatedID),
code,
isCompactMode: isCompactMode,
),
)
: CodeWidget(
key: ValueKey('${code.hashCode}_${code.generatedID}'),
code,
isCompactMode: isCompactMode,
),
],
onReorder: (oldIndex, newIndex) {
if (selectedSortOption == 2) updateCodeIndex(oldIndex, newIndex);
},
),
],
),
body: ReorderableListView(
buildDefaultDragHandles: false,
proxyDecorator: (Widget child, int index, Animation<double> animation) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, _) {
final animValue = Curves.easeInOut.transform(animation.value);
final scale = lerpDouble(1, 1.05, animValue)!;
return Transform.scale(scale: scale, child: child);
},
);
},
children: [
for (final code in widget.codes)
ReorderableDragStartListener(
key: ValueKey('${code.hashCode}_${code.generatedID}'),
index: widget.codes.indexOf(code),
child: CodeWidget(
key: ValueKey(code.generatedID),
code,
isCompactMode: isCompactMode,
),
),
],
onReorder: (oldIndex, newIndex) {
updateCodeIndex(oldIndex, newIndex);
},
),
);
}
@@ -97,6 +98,7 @@ class _ReorderCodesPageState extends State<ReorderCodesPage> {
if (oldIndex < newIndex) newIndex -= 1;
final Code code = widget.codes.removeAt(oldIndex);
widget.codes.insert(newIndex, code);
hasChanged = true;
});
}
}

View File

@@ -1,11 +1,16 @@
import 'dart:async';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/deduplication_service.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/ui/settings/data/duplicate_code_page.dart';
import 'package:ente_auth/ui/settings/data/export_widget.dart';
import 'package:ente_auth/ui/settings/data/import_page.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:flutter/material.dart';
@@ -53,6 +58,35 @@ class DataSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.duplicateCodes,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final List<DuplicateCodes> duplicateCodes =
await DeduplicationService.instance.getDuplicateCodes();
if (duplicateCodes.isEmpty) {
unawaited(
showChoiceDialog(
context,
title: l10n.noDuplicates,
firstButtonLabel: "OK",
secondButtonLabel: null,
body: l10n.youveNoDuplicateCodesThatCanBeCleared,
),
);
return;
}
await routeToPage(
context,
DuplicateCodePage(duplicateCodes: duplicateCodes),
);
},
),
sectionOptionSpacing,
]);
return Column(
children: children,

View File

@@ -0,0 +1,259 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/services/deduplication_service.dart';
import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/code_widget.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:logging/logging.dart';
class DuplicateCodePage extends StatefulWidget {
final List<DuplicateCodes> duplicateCodes;
const DuplicateCodePage({
super.key,
required this.duplicateCodes,
});
@override
State<DuplicateCodePage> createState() => _DuplicateCodePageState();
}
class _DuplicateCodePageState extends State<DuplicateCodePage> {
final Logger _logger = Logger("DuplicateCodePage");
late List<DuplicateCodes> _duplicateCodes;
final Set<int> selectedGrids = <int>{};
@override
void initState() {
_duplicateCodes = widget.duplicateCodes;
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.deduplicateCodes),
elevation: 0,
),
body: _getBody(),
);
}
Widget _getBody() {
final l10n = context.l10n;
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: () {
if (selectedGrids.length == _duplicateCodes.length) {
_removeAllGrids();
} else {
_selectAllGrids();
}
setState(() {});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
selectedGrids.length == _duplicateCodes.length
? l10n.deselectAll
: l10n.selectAll,
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
color: Theme.of(context)
.iconTheme
.color!
.withOpacity(0.7),
),
),
const Padding(padding: EdgeInsets.only(left: 4)),
selectedGrids.length == _duplicateCodes.length
? const Icon(
Icons.check_circle,
size: 24,
)
: Icon(
Icons.check_circle_outlined,
color: getEnteColorScheme(context).strokeMuted,
size: 24,
),
],
),
),
],
),
),
),
const SizedBox(height: 8),
Expanded(
child: ListView.builder(
itemCount: _duplicateCodes.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final List<Code> codes = _duplicateCodes[index].codes;
return _getGridView(
codes,
index,
);
},
),
),
selectedGrids.isEmpty ? const SizedBox.shrink() : _getDeleteButton(),
],
),
);
}
Widget _getGridView(List<Code> code, int itemIndex) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: GestureDetector(
onTap: () {
if (selectedGrids.contains(itemIndex)) {
selectedGrids.remove(itemIndex);
} else {
selectedGrids.add(itemIndex);
}
setState(() {});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${code[0].issuer}, ${code.length} items",
),
!selectedGrids.contains(itemIndex)
? Icon(
Icons.check_circle_outlined,
color: getEnteColorScheme(context).strokeMuted,
size: 24,
)
: const Icon(
Icons.check_circle,
size: 24,
),
],
),
),
),
AlignedGridView.count(
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400)
.clamp(1, double.infinity)
.toInt(),
padding: const EdgeInsets.only(bottom: 40),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return CodeWidget(
key: ValueKey('${code.hashCode}_$index'),
code[index],
isCompactMode: false,
);
},
itemCount: code.length,
),
],
);
}
Widget _getDeleteButton() {
int selectedItemsCount = 0;
for (int idx = 0; idx < _duplicateCodes.length; idx++) {
if (selectedGrids.contains(idx)) {
selectedItemsCount += _duplicateCodes[idx].codes.length - 1;
}
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 20),
child: SizedBox(
width: 400,
child: OutlinedButton(
onPressed: () async {
await deleteDuplicates(selectedItemsCount);
},
child: Text(
"Delete $selectedItemsCount items",
),
),
),
);
}
void _selectAllGrids() {
selectedGrids.clear();
for (int idx = 0; idx < _duplicateCodes.length; idx++) {
selectedGrids.add(idx);
}
}
void _removeAllGrids() {
selectedGrids.clear();
}
Future<void> deleteDuplicates(int itemCount) async {
bool isAuthSuccessful =
await LocalAuthenticationService.instance.requestLocalAuthentication(
context,
context.l10n.deleteCodeAuthMessage,
);
if (!isAuthSuccessful) {
return;
}
FocusScope.of(context).requestFocus();
final l10n = context.l10n;
final String message = "Are you sure you want to trash $itemCount items?";
await showChoiceActionSheet(
context,
title: l10n.deleteDuplicates,
body: message,
firstButtonLabel: l10n.trash,
isCritical: true,
firstButtonOnTap: () async {
try {
for (int idx = 0; idx < _duplicateCodes.length; idx++) {
if (selectedGrids.contains(idx)) {
final List<Code> codes = _duplicateCodes[idx].codes;
for (int i = 1; i < codes.length; i++) {
final display = codes[i].display;
final Code code = codes[i].copyWith(
display: display.copyWith(trashed: true),
);
await CodeStore.instance.addCode(code);
}
}
}
Navigator.of(context).pop();
} catch (e) {
_logger.severe('Failed to trash duplicate codes: ${e.toString()}');
showGenericErrorDialog(context: context, error: e).ignore();
}
},
);
}
}

View File

@@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/export/ente.dart';
@@ -9,16 +8,15 @@ import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/settings/data/html_export.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/share_utils.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
Future<void> handleExportClick(BuildContext context) async {
@@ -41,13 +39,22 @@ Future<void> handleExportClick(BuildContext context) async {
isInAlert: true,
buttonAction: ButtonAction.second,
),
ButtonWidget(
buttonType: ButtonType.secondary,
labelText: context.l10n.plainHTML,
buttonSize: ButtonSize.large,
isInAlert: true,
buttonAction: ButtonAction.third,
),
],
);
if (result?.action != null && result!.action != ButtonAction.cancel) {
if (result.action == ButtonAction.first) {
await _requestForEncryptionPassword(context);
} else {
await _showExportWarningDialog(context);
} else if (result.action == ButtonAction.second) {
await _showExportWarningDialog(context, "txt");
} else if (result.action == ButtonAction.third) {
await _showExportWarningDialog(context, "html");
}
}
}
@@ -98,9 +105,8 @@ Future<void> _requestForEncryptionPassword(
),
);
// get json value of data
await _exportCodes(context, jsonEncode(data.toJson()));
} catch (e, s) {
Logger("ExportWidget").severe(e, s);
await _exportCodes(context, jsonEncode(data.toJson()), "txt");
} catch (e) {
showToast(context, "Error while exporting codes.");
}
}
@@ -108,26 +114,34 @@ Future<void> _requestForEncryptionPassword(
);
}
Future<void> _showExportWarningDialog(BuildContext context) async {
Future<void> _showExportWarningDialog(BuildContext context, String type) async {
await showChoiceActionSheet(
context,
title: context.l10n.warning,
body: context.l10n.exportWarningDesc,
isCritical: true,
firstButtonOnTap: () async {
final data = await _getAuthDataForExport();
await _exportCodes(context, data);
if (type == "html") {
final data = await generateHtml(context);
await _exportCodes(context, data, type);
} else {
final data = await _getAuthDataForExport();
await _exportCodes(context, data, type);
}
},
secondButtonLabel: context.l10n.cancel,
firstButtonLabel: context.l10n.iUnderStand,
);
}
Future<void> _exportCodes(BuildContext context, String fileContent) async {
Future<void> _exportCodes(
BuildContext context,
String fileContent,
String extension,
) async {
DateTime now = DateTime.now().toUtc();
String formattedDate = DateFormat('yyyy-MM-dd').format(now);
String exportFileName = 'ente-auth-codes-$formattedDate';
String exportFileExtension = 'txt';
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(context, context.l10n.authToExportCodes);
await PlatformUtil.refocusWindows();
@@ -142,14 +156,14 @@ Future<void> _exportCodes(BuildContext context, String fileContent) async {
saveAction: () async {
await PlatformUtil.shareFile(
exportFileName,
exportFileExtension,
extension,
CryptoUtil.strToBin(fileContent),
MimeType.text,
);
},
sendAction: () async {
final codeFile = File(
"${Configuration.instance.getTempDirectory()}$exportFileName.$exportFileExtension",
"${Configuration.instance.getTempDirectory()}$exportFileName.$extension",
);
if (codeFile.existsSync()) {
await codeFile.delete();

View File

@@ -0,0 +1,264 @@
import 'dart:convert';
import 'dart:ui' as ui;
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:qr_flutter/qr_flutter.dart';
Future<String> generateQRImageBase64(String data) async {
final qrPainter = QrPainter(
data: data,
version: QrVersions.auto,
eyeStyle: const QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Colors.black,
),
dataModuleStyle: const QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Colors.black,
),
);
const size = 250.0;
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
qrPainter.paint(canvas, const Size(size, size));
final picture = recorder.endRecording();
final img = await picture.toImage(size.toInt(), size.toInt());
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List();
return base64Encode(pngBytes);
}
Future<String> generateOTPEntryHtml(Code code, BuildContext context) async {
final qrBase64 = await generateQRImageBase64(code.rawData);
String notes = code.display.note;
if (notes.isNotEmpty) {
notes = '<p class="group">Note: <b>$notes</b></p>';
}
return '''
<table class="otp-entry">
<tr>
<td>
<p><b>${code.issuer}</b></p>
<p><b>${code.account}</b></p>
<p class="group">Type: <b>${code.type.name}</b></p>
<p>Algorithm: <b>${code.algorithm.name}</b></p>
<p>Digits: <b>${code.digits}</b></p>
<p>Secret: <b>${code.secret}</b></p>
$notes
</td>
<td class="otp-qr">
<img src="data:image/png;base64,$qrBase64" alt="QR Code">
</td>
</tr>
</table>
<br/>
<hr class="red-separator" />
<br/>
''';
}
Future<String> generateHtml(BuildContext context) async {
DateTime now = DateTime.now().toUtc();
String formattedDate = DateFormat('d MMMM, yyyy').format(now);
final allCodes = await CodeStore.instance.getAllCodes();
final List<String> enteries = [];
for (final code in allCodes) {
if (code.hasError) continue;
final entry = await generateOTPEntryHtml(code, context);
enteries.add(entry);
}
return '''
<!DOCTYPE html>
<html>
<meta content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<style>
body {
background-color: #f0f1f3;
font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif;
font-size: 16px;
line-height: 27px;
margin: 0;
color: #444;
}
pre {
background: #f4f4f4f4;
padding: 2px;
}
table {
width: 100%;
}
table td {
border-color: #ddd;
padding: 5px;
}
.wrap {
background-color: #fff;
padding: 30px;
max-width: 600px;
margin: 0 auto;
border-radius: 5px;
}
.button {
background: #0055d4;
border-radius: 3px;
text-decoration: none !important;
color: #fff !important;
font-weight: bold;
padding: 10px 30px;
display: inline-block;
}
.button:hover {
background: #111;
}
.footer {
text-align: center;
font-size: 12px;
color: #888;
}
.footer a {
color: #888;
margin-right: 5px;
}
.gutter {
padding: 30px;
}
img {
max-width: 100%;
height: auto;
}
a {
color: #0055d4;
}
a:hover {
color: #111;
}
@media screen and (max-width: 700px) {
.otp-entry {
display: block;
}
.otp-entry td {
display: block;
width: 100%;
}
.otp-qr img {
margin-top: 10px;
}
}
.footer-icons {
padding: 4px !important;
width: 24px !important;
}
.otp-entry {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.otp-entry td {
padding: 20px;
margin: 0px;
vertical-align: middle;
}
.otp-entry td:first-child {
width: 70%;
word-wrap: break-word;
overflow-wrap: break-word;
}
.otp-qr img {
max-width: 200px;
height: auto;
display: block;
margin: 0 auto;
}
.otp-entry td.otp-qr {
width: 30%;
text-align: center;
vertical-align: middle;
}
.otp-entry p {
margin: 2px 0;
}
.otp-entry p.group {
margin-top: 15px;
}
hr.red-separator {
border: none;
height: 1px;
background-color: rgb(173, 0, 255);
}
</style>
</head>
<body>
<h1 style="text-align: center;">Ente Auth</h1>
<h4 style="text-align: center; margin-bottom: 5px;">OTP Data Export</h4>
<p style="text-align: center; margin-top: 0px;">$formattedDate</p>
<div class="gutter" style="padding: 4px">&nbsp;</div>
<div class="wrap" style=" background-color: rgb(255, 255, 255); padding: 2px
30px 30px 30px; max-width: 700px; margin: 0 auto; border-radius: 5px;
font-size: 16px; ">
<main>
<p>
${enteries.join('')}
</p>
</main>
</div>
<br />
<div class="footer" style="text-align: center; font-size: 12px; color:
rgb(136, 136, 136)">
<div>
<a href="https://ente.io" target="_blank"><img src="https://email-assets.ente.io/ente-green.png" style="width: 100px;
padding: 24px" title="Ente" alt="Ente" /></a>
</div>
<div>
<a href="https://fosstodon.org/@ente" target="_blank"><img src="https://email-assets.ente.io/mastodon-icon.png"
class="footer-icons" style="width: 24px; padding: 4px" title="Mastodon" alt="Mastodon" /></a>
<a href="https://twitter.com/enteio" target="_blank"><img src="https://email-assets.ente.io/twitter-icon.png"
class="footer-icons" style="width: 24px; padding: 4px" title="Twitter" alt="Twitter" /></a>
<a href="https://discord.ente.io" target="_blank"><img src="https://email-assets.ente.io/discord-icon.png"
class="footer-icons" style="width: 24px; padding: 4px" title="Discord" alt="Discord" /></a>
<a href="https://github.com/ente-io" target="_blank"><img src="https://email-assets.ente.io/github-icon.png"
class="footer-icons" style="width: 24px; padding: 4px" title="GitHub" alt="GitHub" /></a>
</div>
<p>
Ente Technologies, Inc.
<br /> 1111B S Governors Ave 6032 Dover, DE 19904
</p>
<br />
</div>
</body>
</html>
''';
}

View File

@@ -64,7 +64,7 @@ class _SupportSectionWidgetState extends State<SupportSectionWidget> {
onTap: () async {
// ignore: unawaited_futures
launchUrlString(
githubIssuesUrl,
githubFeatureRequestUrl,
mode: LaunchMode.externalApplication,
);
},

View File

@@ -1,14 +1,12 @@
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class ThemeSwitchWidget extends StatefulWidget {
const ThemeSwitchWidget({super.key});
@@ -42,7 +40,7 @@ class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
@override
Widget build(BuildContext context) {
return ExpandableMenuItemWidget(
title: "Theme",
title: context.l10n.theme,
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Theme.of(context).brightness == Brightness.light
? Icons.light_mode_outlined
@@ -64,10 +62,21 @@ class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
);
}
String _name(BuildContext ctx, AdaptiveThemeMode mode) {
switch (mode) {
case AdaptiveThemeMode.light:
return ctx.l10n.lightTheme;
case AdaptiveThemeMode.dark:
return ctx.l10n.darkTheme;
case AdaptiveThemeMode.system:
return ctx.l10n.systemTheme;
}
}
Widget _menuItem(BuildContext context, AdaptiveThemeMode themeMode) {
return MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: toBeginningOfSentenceCase(themeMode.name)!,
title: _name(context, themeMode),
textStyle: Theme.of(context).colorScheme.enteTheme.textTheme.body,
),
pressedColor: getEnteColorScheme(context).fillFaint,

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/models/all_icon_data.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -24,6 +25,80 @@ class IconUtils {
await _loadJson();
}
Map<String, AllIconData> getAllIcons() {
Set<String> processedIconPaths = {};
final allIcons = <String, AllIconData>{};
final simpleIterator = _simpleIcons.entries.iterator;
final customIterator = _customIcons.entries.iterator;
var simpleEntry = simpleIterator.moveNext() ? simpleIterator.current : null;
var customEntry = customIterator.moveNext() ? customIterator.current : null;
String simpleIconPath, customIconPath;
while (simpleEntry != null && customEntry != null) {
if (simpleEntry.key.compareTo(customEntry.key) <= 0) {
simpleIconPath = "assets/simple-icons/icons/${simpleEntry.key}.svg";
if (!processedIconPaths.contains(simpleIconPath)) {
allIcons[simpleEntry.key] = AllIconData(
title: simpleEntry.key,
type: IconType.simpleIcon,
color: simpleEntry.value,
);
processedIconPaths.add(simpleIconPath);
}
simpleEntry = simpleIterator.moveNext() ? simpleIterator.current : null;
} else {
customIconPath =
"assets/custom-icons/icons/${customEntry.value.slug ?? customEntry.key}.svg";
if (!processedIconPaths.contains(customIconPath)) {
allIcons[customEntry.key] = AllIconData(
title: customEntry.key,
type: IconType.customIcon,
color: customEntry.value.color,
slug: customEntry.value.slug,
);
processedIconPaths.add(customIconPath);
}
customEntry = customIterator.moveNext() ? customIterator.current : null;
}
}
while (simpleEntry != null) {
simpleIconPath = "assets/simple-icons/icons/${simpleEntry.key}.svg";
if (!processedIconPaths.contains(simpleIconPath)) {
allIcons[simpleEntry.key] = AllIconData(
title: simpleEntry.key,
type: IconType.simpleIcon,
color: simpleEntry.value,
);
processedIconPaths.add(simpleIconPath);
}
simpleEntry = simpleIterator.moveNext() ? simpleIterator.current : null;
}
while (customEntry != null) {
customIconPath =
"assets/custom-icons/icons/${customEntry.value.slug ?? customEntry.key}.svg";
if (!processedIconPaths.contains(customIconPath)) {
allIcons[customEntry.key] = AllIconData(
title: customEntry.key,
type: IconType.customIcon,
color: customEntry.value.color,
slug: customEntry.value.slug,
);
processedIconPaths.add(customIconPath);
}
customEntry = customIterator.moveNext() ? customIterator.current : null;
}
return allIcons;
}
Widget getIcon(
BuildContext context,
String provider, {
@@ -38,7 +113,7 @@ class IconUtils {
);
for (final title in titlesList) {
if (_customIcons.containsKey(title)) {
return _getSVGIcon(
return getSVGIcon(
"assets/custom-icons/icons/${_customIcons[title]!.slug ?? title}.svg",
title,
_customIcons[title]!.color,
@@ -46,8 +121,9 @@ class IconUtils {
context,
);
} else if (_simpleIcons.containsKey(title)) {
return _getSVGIcon(
"assets/simple-icons/icons/$title.svg",
final simpleIconPath = normalizeSimpleIconName(title);
return getSVGIcon(
"assets/simple-icons/icons/$simpleIconPath.svg",
title,
_simpleIcons[title],
width,
@@ -75,7 +151,7 @@ class IconUtils {
}
}
Widget _getSVGIcon(
Widget getSVGIcon(
String path,
String title,
String? color,
@@ -124,7 +200,7 @@ class IconUtils {
final simpleIconData = await rootBundle
.loadString('assets/simple-icons/_data/simple-icons.json');
final simpleIcons = json.decode(simpleIconData);
for (final icon in simpleIcons["icons"]) {
for (final icon in simpleIcons) {
_simpleIcons[icon["title"]
.toString()
.replaceAll(' ', '')
@@ -145,14 +221,14 @@ class IconUtils {
for (final name in icon["altNames"]) {
_customIcons[name.toString().replaceAll(' ', '').toLowerCase()] =
CustomIconData(
icon["slug"],
icon["slug"] ?? ((icon["title"] as String).toLowerCase()),
icon["hex"],
);
}
}
}
} catch (e) {
Logger("IconUtils").severe("Error loading icons", e);
} catch (e, s) {
Logger("IconUtils").severe("Error loading icons", e, s);
if (kDebugMode) {
rethrow;
}
@@ -170,3 +246,43 @@ class CustomIconData {
CustomIconData(this.slug, this.color);
}
final charMap = {
'á': 'a',
'à': 'a',
'â': 'a',
'ä': 'a',
'é': 'e',
'è': 'e',
'ê': 'e',
'ë': 'e',
'í': 'i',
'ì': 'i',
'î': 'i',
'ï': 'i',
'ó': 'o',
'ò': 'o',
'ô': 'o',
'ö': 'o',
'ú': 'u',
'ù': 'u',
'û': 'u',
'ü': 'u',
'ç': 'c',
'ñ': 'n',
'.': 'dot',
'-': '',
'&': 'and',
'+': 'plus',
':': '',
"'": '',
'/': '',
'!': '',
};
String normalizeSimpleIconName(String input) {
final buffer = StringBuffer();
for (var char in input.characters) {
buffer.write(charMap[char] ?? char);
}
return buffer.toString().trim();
}

View File

@@ -174,4 +174,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: f401c31c8f7c5571f6f565c78915d54338812dab
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2

View File

@@ -429,6 +429,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth.mac;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;

View File

@@ -42,10 +42,10 @@ packages:
dependency: "direct main"
description:
name: app_links
sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99
sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950"
url: "https://pub.dev"
source: hosted
version: "6.3.2"
version: "6.3.3"
app_links_linux:
dependency: transitive
description:
@@ -431,6 +431,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
figma_squircle:
dependency: "direct main"
description:
name: figma_squircle
sha256: "790b91a9505e90d246f6efe2fa065ff7fffe658c7b44fe9b5b20c7b0ad3818c0"
url: "https://pub.dev"
source: hosted
version: "0.5.3"
file:
dependency: transitive
description:
@@ -520,10 +528,10 @@ packages:
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: "93cfcca02bdda4b26cd700cf70d9ddba09d8348e3e8f2857638c23ed23a4fcb4"
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
version: "6.1.5"
flutter_inappwebview_android:
dependency: transitive
description:
@@ -576,10 +584,10 @@ packages:
dependency: transitive
description:
name: flutter_inappwebview_windows
sha256: "95ebc65aecfa63b2084c822aec6ba0545f0a0afaa3899f2c752ec96c09108db5"
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
url: "https://pub.dev"
source: hosted
version: "0.5.0+2"
version: "0.6.0"
flutter_launcher_icons:
dependency: "direct main"
description:
@@ -977,10 +985,10 @@ packages:
dependency: "direct main"
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
macros:
dependency: transitive
description:
@@ -1647,10 +1655,10 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
@@ -1660,7 +1668,7 @@ packages:
source: hosted
version: "6.3.11"
url_launcher_ios:
dependency: transitive
dependency: "direct main"
description:
name: url_launcher_ios
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e

View File

@@ -1,6 +1,6 @@
name: ente_auth
description: ente two-factor authenticator
version: 4.1.6+416
version: 4.2.2+422
publish_to: none
environment:
@@ -8,7 +8,7 @@ environment:
dependencies:
adaptive_theme: ^3.1.0 # done
app_links: ^6.2.1
app_links: ^6.3.3
archive: ^3.3.7
auto_size_text: ^3.0.0
base32: ^2.1.3
@@ -31,6 +31,7 @@ dependencies:
expandable: ^5.0.1
expansion_tile_card: ^3.0.0
ffi: ^2.1.0
figma_squircle: ^0.5.3
file_picker: ^8.1.2
# https://github.com/incrediblezayed/file_saver/issues/86
file_saver: ^0.2.11
@@ -43,7 +44,7 @@ dependencies:
flutter_context_menu: ^0.2.0
flutter_displaymode: ^0.6.0
flutter_email_sender: ^6.0.2
flutter_inappwebview: ^6.0.0
flutter_inappwebview: ^6.1.5
flutter_launcher_icons: ^0.14.1
flutter_local_authentication:
git:
@@ -97,7 +98,8 @@ dependencies:
styled_text: ^8.1.0
tray_manager: ^0.2.1
tuple: ^2.0.0
url_launcher: ^6.1.5
url_launcher: ^6.3.1
url_launcher_ios: ^6.3.1
uuid: ^4.2.2
win32: ^5.1.1
window_manager: ^0.4.2

View File

@@ -72,7 +72,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Point origin(70, 70);
Win32Window::Size size(1280, 720);
if (!window.Create(L"Ente Auth", origin, size))
{

View File

@@ -11,7 +11,14 @@
import { nativeImage, shell } from "electron/common";
import type { WebContents } from "electron/main";
import { BrowserWindow, Menu, Tray, app, protocol } from "electron/main";
import {
BrowserWindow,
Menu,
Tray,
app,
dialog,
protocol,
} from "electron/main";
import serveNextAt from "next-electron-server";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
@@ -131,6 +138,7 @@ const main = () => {
const webContents = mainWindow.webContents;
setDownloadPath(webContents);
allowExternalLinks(webContents);
handleBackOnStripeCheckout(mainWindow);
allowAllCORSOrigins(webContents);
// Start loading the renderer.
@@ -502,6 +510,45 @@ const allowExternalLinks = (webContents: WebContents) =>
}
});
/**
* Handle back button presses on the Stripe checkout page.
*
* For payments, we show the Stripe checkout page to the user in the app's
* window. On this page there is a back button that allows the user to get back
* to the app's contents. Since we're not showing the browser controls, this is
* the only way to get back to the app.
*
* If the user enters something in the text fields on this page (e.g. if they
* start entering their credit card number), and then press back, then the
* browser shows the user a dialog asking them to confirm if they want to
* discard their unsaved changes. However, when running in the context of an
* Electron app, this dialog is not shown, and instead the app just gets stuck
* (the back button stops working, and quitting the app also doesn't work since
* there is an invisible modal dialog).
*
* So we instead intercept these back button presses, and show the same dialog
* that the browser would've shown.
*/
const handleBackOnStripeCheckout = (window: BrowserWindow) =>
window.webContents.on("will-prevent-unload", (event) => {
const url = new URL(window.webContents.getURL());
// Only intercept on Stripe checkout pages.
if (url.host != "checkout.stripe.com") return;
// The dialog copy is similar to what Chrome would've shown.
// https://www.electronjs.org/docs/latest/api/web-contents#event-will-prevent-unload
const choice = dialog.showMessageBoxSync(window, {
type: "question",
buttons: ["Leave", "Stay"],
title: "Leave site?",
message: "Changes that you made may not be saved.",
defaultId: 0,
cancelId: 1,
});
const leave = choice === 0;
if (leave) event.preventDefault();
});
/**
* Allow uploads to arbitrary S3 buckets.
*

View File

@@ -22,7 +22,7 @@ export const fsReadTextFile = async (filePath: string) =>
fs.readFile(filePath, "utf-8");
export const fsWriteFile = (path: string, contents: string) =>
fs.writeFile(path, contents);
fs.writeFile(path, contents, { flush: true });
export const fsIsDir = async (dirPath: string) => {
if (!existsSync(dirPath)) return false;

View File

@@ -39,6 +39,10 @@ export const sidebar = [
link: "/photos/features/free-up-space/",
},
{ text: "Hidden photos", link: "/photos/features/hide" },
{
text: "Legacy",
link: "/photos/features/legacy/",
},
{
text: "Location tags",
link: "/photos/features/location-tags",
@@ -254,7 +258,7 @@ export const sidebar = [
link: "/self-hosting/guides/configuring-s3",
},
{
text: "Using external S3",
text: "Hosting Ente with external S3 (Community)",
link: "/self-hosting/guides/external-s3",
},
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,50 @@
---
title: Legacy
description: Using Legacy to pass on your memories to loved ones
---
# Legacy
Legacy allows trusted contacts to recover your account in your absence. The main usecase here is to pass on your memories after your death. It can also be useful for other cases - for e.g., when you forget your password and recovery key.
Trusted Contacts can initiate a recovery, and if not blocked in 30 days, would be able to change the password to your account and thereby access your memories.
## Adding a trusted contact
You can add a trusted contact for your account using the mobile app for Ente Photos. Go to Settings -> Account -> Legacy, and click on "Add Trusted Contact".
You would be asked to enter the email address of the trusted contact you want to add or choose from a list of contacts on Ente. Please note that the trusted contact must be an Ente user.
<div align="center">
![Add Trusted Contact](add_trusted_contact.png){width=300px}
</div>
The trusted contact must accept your request. They can do so by going to Settings -> Account -> Legacy in the Ente Photos mobile app, and clicking on your email address within the Legacy accounts sections. Ente would also send an email notification to the trusted contact to nudge them to accept the invite.
<div align="center">
![Accept Trusted Contact Invite](accept_trusted_contact_invite.png){width=300px}
</div>
## Recovering an account as a trusted contact
As a trusted contact, you can recover an account by going to Settings -> Account -> Legacy in the Ente photos app, tapping on the email address of the account within the Legacy account sections.
<div align="center">
![Initiate Account Recovery](initiate_account_recovery.png){width=300px}
</div>
Once the recovery is initiated, the account owner would get 30 days to block the recovery. After 30 days, you can go the same page in the app, where you will be prompted to change the password of the account. Once you change the password, you would be able to access the recovered account with the new password.
## Blocking account recovery by a trusted contact
After a trusted contact initiates a recover, you, as the account owner, would get 30 days to block the recovery. To do this, you must go to Settings -> Account -> Legacy, where you will see a message that recovery of the account has been initiated. Tapping on that will allow you to block the recovery.
## Removing a trusted contact
You can remove a trusted contact by going to Settings -> Account -> Legacy, tapping on the trusted contact you want to remove, and choosing "Remove" in the popup.

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -39,6 +39,10 @@ device.
> desktop app first, because it can index your existing photos faster. Once your
> existing photos have been indexed, then you can use either. The mobile app is
> fast enough to index new photos as they are being backed up.
>
> Also, it is beneficial to enable machine learning before importing your
> photos, as this allows the Ente app to index your files as they are getting
> uploaded instead of needing to download them again.
The indexes are synced across all your devices automatically using the same
end-to-end encrypted security that we use for syncing your photos.

View File

@@ -68,3 +68,10 @@ will ignore already backed up files and upload just the rest.
If you run into any issues during this migration, please reach out to
[support@ente.io](mailto:support@ente.io) and we will be happy to help you!
> [!TIP]
>
> In case you wish to use face recognition and other advanced search features
> provided by Ente, we recommend that you enable [machine
> learning](/photos/features/machine-learning) before importing your photos so
> that the Ente app can directly index files as they are getting uploaded.

View File

@@ -7,6 +7,14 @@ description:
# Hosting server and web app using external S3
> [!NOTE]
>
> This is a community contributed guide, and some of these steps might be out of
> sync with the upstream documentation. If something is not working correctly,
> please also see the latest
> [READMEs](https://github.com/ente-io/ente/blob/main/server/README.md) in the
> repository and/or other guides in [self-hosting](/self-hosting/).
This guide is for self hosting the server and the web application of Ente Photos
using docker compose and an external S3 bucket. So we assume that you already
have the keys and secrets for the S3 bucket. The plan is as follows:
@@ -17,14 +25,6 @@ have the keys and secrets for the S3 bucket. The plan is as follows:
4. Create an account and increase storage quota
5. Fix potential CORS issue with your bucket
> [!NOTE]
>
> This is a community contributed guide, and some of these steps might be out of
> sync with the upstream documentation. If something is not working correctly,
> please also see the latest
> [READMEs](https://github.com/ente-io/ente/blob/main/server/README.md) in the
> repository and/or other guides in [self-hosting](/self-hosting/).
## 1. Create a `compose.yaml` file
After cloning the main repository with
@@ -40,7 +40,6 @@ Create a `compose.yaml` file at the root of the project with the following
content (there is nothing to change here):
```yaml
version: "3"
services:
museum:
build:
@@ -236,9 +235,10 @@ background).
## 4. Create an account and increase storage quota
Open `http://localhost:8081` (or the url of your server) in your browser and
create an account. Choose 123456 as the value for the one-time token if your
email has the correct domain as defined in the `.credentials.env` file.
Open `http://localhost:8080` or whatever Endpoint you mentioned for the web app and create an account.
If your SMTP related configurations are all set and right, you will receive an email with
your OTT in it. There are two work arounds to retrieve the OTP,
checkout [this document](https://help.ente.io/self-hosting/faq/otp) for getting your OTT's..
If you successfully log in, select any plan and increase the storage quota with
the following command:
@@ -251,6 +251,9 @@ After few reloads, you should see 1 To of quota.
## 5. Fix potential CORS issue with your bucket
### For AWS S3
If you cannot upload a photo due to a CORS issue, you need to fix the CORS
configuration of your bucket.
@@ -272,14 +275,28 @@ Create a `cors.json` file with the following content:
You may want to change the `AllowedOrigins` to a more restrictive value.
Then run the following command with the aws command to update the CORS
configuration of your bucket:
If you are using AWS for S3, you can execute the below command to get rid of CORS. Make sure to enter the right path for the `cors.json` file.
```bash
aws s3api put-bucket-cors --bucket YOUR_S3_BUCKET --cors-configuration file://cors.json
aws s3api put-bucket-cors --bucket YOUR_S3_BUCKET --cors-configuration /path/to/cors.json
```
Upload should now work.
### For Self-hosted Minio Instance
> Important: MinIO does not take JSON CORS file as the input, instead you will have to build a CORS.xml file or just convert the above `cors.json` to XML.
A minor requirement here is the tool `mc` for managing buckets via command line interface. Checkout the `mc set alias` document to configure alias for your instance and bucket.
After this you will be prompted for your AccessKey and Secret, which is your username and password, go ahead and enter that.
```sh
mc cors set <your-minio>/<your-bucket-name /path/to/cors.xml
```
or, if you just want to just set the `AllowedOrigins` Header, you can use the following command to do so.
```sh
mc admin config set <your-minio>/<your-bucket-name> set "cors_allowed_origin=*"
```
You can create also `.csv` file and dump the list of origins you would like to allow and replace the `*` with `path` to the CSV file.
Now, uploads should be working fine.
## Related

View File

@@ -1,7 +1,7 @@
---
title: Hosting the web app
title: Hosting the web apps
description:
Building and hosting Ente's web app, connecting it to your self-hosted
Building and hosting Ente's web apps, connecting it to your self-hosted
server
---
@@ -17,47 +17,229 @@ yarn install
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos
```
This is fine for trying this out and verifying that your self-hosted server is
working correctly etc. But if you would like to use the web app for a longer
term, then it is recommended that you use a production build.
This is fine for trying the web app and verifying that your self-hosted server is
working as expected etc. But if you would like to use the web app for a longer term,
then it is recommended to follow the Docker approach.
To create a production build, you can run the same process, but instead do a
`yarn build` (which is an alias for `yarn build:photos`). For example,
## With Docker/Docker Compose (Recommended)
```sh
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn build:photos
> [!IMPORTANT]
>
> This docker image is still in testing stage and it might show up with some
> unknown variables in different scenarios. But this image has been tested on a production
> ente site.
>
> Recurring changes might be made by the team or from community if more
> improvements can be made so that we are able to build a full-fledged docker image.
```dockerfile
FROM node:20-bookworm-slim as builder
WORKDIR ./ente
COPY . .
COPY apps/ .
# Will help default to yarn versoin 1.22.22
RUN corepack enable
# Configure Albums and Accounts Endpoints
ENV NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=https://your-domain.com
ENV NEXT_PUBLIC_ENTE_ACCOUNTS_URL=https://your-domain.com
RUN yarn cache clean
RUN yarn install --network-timeout 1000000000
RUN yarn build:photos && yarn build:accounts && yarn build:auth && yarn build:cast
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /ente/apps/photos/out /app/photos
COPY --from=builder /ente/apps/accounts/out /app/accounts
COPY --from=builder /ente/apps/auth/out /app/auth
COPY --from=builder /ente/apps/cast/out /app/cast
RUN npm install -g serve
ENV PHOTOS=3000
EXPOSE ${PHOTOS}
ENV ACCOUNTS=3001
EXPOSE ${ACCOUNTS}
ENV AUTH=3002
EXPOSE ${AUTH}
ENV CAST=3003
EXPOSE ${CAST}
# The albums app does not have navigable pages on it, but the
# port will be exposed in-order to self up the albums endpoint
# `apps.public-albums` in museum.yaml configuration file.
ENV ALBUMS=3004
EXPOSE ${ALBUMS}
CMD ["sh", "-c", "serve /app/photos -l tcp://0.0.0.0:${PHOTOS} & serve /app/accounts -l tcp://0.0.0.0:${ACCOUNTS} & serve /app/auth -l tcp://0.0.0.0:${AUTH} & serve /app/cast -l tcp://0.0.0.0:${CAST}"]
```
This creates a production build, which is a static site consisting of a folder
of HTML/CSS/JS files that can then be deployed on any standard web server.
The above is a multi-stage Dockerfile which creates a production ready static output
of the 4 apps (Photos, Accounts, Auth and Cast) and serves the static content with
Caddy.
Nginx is a common choice for a web server, and you can then put the generated
static site (from the `web/apps/photos/out` folder) to where nginx would serve
them. Note that there is nothing specific to nginx here - you can use any web
server - the basic gist is that yarn build will produce a web/apps/photos/out
folder that you can then serve with any web server of your choice.
Looking at 2 different node base-images doing different tasks in the same Dockerfile
would not make sense, but the Dockerfile is divided into two just to improve the build
efficiency as building this Dockerfile will arguably take more time.
If you're new to web development, you might find the [web app's README], and
some of the documentation it its source code -
[docs/new.md](https://github.com/ente-io/ente/blob/main/web/docs/new.md),
[docs/dev.md](https://github.com/ente-io/ente/blob/main/web/docs/dev.md) -
useful. We've also documented the process we use for our own production
deploypments in
[docs/deploy.md](https://github.com/ente-io/ente/blob/main/web/docs/deploy.md),
though be aware that that is probably overkill for simple cases.
Lets build a Docker image from the above Dockerfile. Copy and paste the above Dockerfile
contents in the root of your web directory which is inside `ente/web`. Execute the
below command to create an image from this Dockerfile.
## Using Docker
We currently don't offer pre-built Docker images for the web app, however it is
quite easy to build and deploy the web app in a Docker container without
installing anything extra on your machine. For example, you can use the
dockerfile from this
[discussion](https://github.com/ente-io/ente/discussions/1183), or use the
Dockerfile mentioned in the
[notes](https://help.ente.io/self-hosting/guides/external-s3) created by a
community member.
```sh
# Build the image
docker build -t <image-name>:<tag> --no-cache --progress plain .
```
## Public sharing
You can always edit the Dockerfile and remove the steps for apps which you do not
intend to install on your system (like auth or cast) and opt out of those.
If you'd also like to enable public sharing on the web app you're running,
please follow the [step here](https://help.ente.io/self-hosting/faq/sharing).
Regarding Albums App, please take a note that they are not web pages with
navigable pages, if accessed on the web-browser they will simply redirect to
ente.web.io.
## compose.yaml
Moving ahead, we need to paste the below contents into the compose.yaml inside
`ente/server/compose.yaml` under the services section.
```yaml
ente-web:
image: <image-name> # name of the image you used while building
ports:
- 3000:3000
- 3001:3001
- 3002:3002
- 3003:3003
- 3004:3004
environment:
- NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=https://your-domain.com
- NEXT_PUBLIC_ENTE_ACCOUNTS_URL=https://your-domain.com
- NODE_ENV=development
restart: always
```
Now, we're good to go. All we are left to do now is start the containers.
```sh
docker compose up -d # --build
# Accessing the logs
docker compose logs <container-name>
```
Next part is to configure a [web server](#web-server-configuration).
## Without Docker / Docker compose
One way to run all the apps together without Docker is by using [PM2](https://pm2.keymetrics.io/)
in this setup. The configuration and usage is very simple and just needs one
configuration file for it. You can run the apps both in dev server mode as
well as static files.
The below configuration will run the apps in dev server mode.
### Install PM2
```sh
npm install pm2@latest
```
Copy the below contents to a file called `ecosystem.config.js` inside the `ente/web`
directory.
```js
module.exports = {
apps: [
{
name: "photos",
script: "yarn workspace photos next dev",
env: {
NODE_ENV: "development",
PORT: "3000"
}
},
{
name: "accounts",
script: "yarn workspace accounts next dev",
env: {
NODE_ENV: "development",
PORT: "3001"
},
{
name: "auth",
script: "yarn workspace auth next dev",
env: {
NODE_ENV: "development",
PORT: "3002"
}
},
{
name: "cast",
script: "yarn workspace cast next dev",
env: {
NODE_ENV: "development",
PORT: "3003"
}
}
]
};
```
Finally, start pm2.
```sh
pm2 start
# for logs
pm2 logs all
```
# Web server configuration
The last step ahead is configuring reverse_proxy for the ports on which the
apps are being served (you will have to make changes, if you have cusotmized the ports).
The web server of choice in this guide is [Caddy](https://caddyserver.com) because
with caddy you don't have to manually configure/setup SSL ceritifcates as caddy
will take care of that.
```sh
photos.yourdomain.com {
reverse_proxy http://localhost:3001
# for logging
log {
level error
}
}
auth.yourdomain.com {
reverse_proxy http://localhost:3002
}
# and so on ...
```
Next, start the caddy server :).
```sh
# If caddy service is not enabled
sudo systemctl enable caddy
sudo systemctl daemon-reload
sudo systemctl start caddy
```
## Contributing
Please start a discussion on the Github Repo if you have any suggestions for the Dockerfile,
You can also share your setups on Github Discussions.

View File

@@ -79,11 +79,6 @@ apps and configure them to use your
## Contributing!
While we would love to provide a completely seamless self-hosting experience,
right now we do not have the engineering bandwidth to answer all queries,
document everything exactly etc. We will try (that's why we're writing this!),
but we also hope that community members will step up to fill any gaps.
One particular way in which you can help is by adding new [guides](guides/) on
this help site. The documentation is written in Markdown and adding new pages is
[easy](https://github.com/ente-io/ente/tree/main/docs#readme). Editing existing

View File

@@ -0,0 +1,20 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_34053_111310)">
<path d="M170 66.7593C170 71.8084 169.042 76.732 167.151 81.3933C165.192 86.2233 162.316 90.5536 158.606 94.265L147.36 105.511L151.657 109.806C152.962 111.112 152.26 113.347 150.441 113.671L128.349 117.598C126.785 117.875 125.423 116.513 125.7 114.949L129.628 92.8595C129.952 91.0413 132.187 90.3393 133.492 91.6442L137.12 95.2717L148.366 84.0261C152.978 79.4144 155.519 73.282 155.519 66.7609C155.519 60.2383 152.978 54.105 148.365 49.4941C143.753 44.8815 137.621 42.3422 131.097 42.3422C124.574 42.3422 118.442 44.8815 113.83 49.4941L107.387 55.936C107.383 55.9064 107.378 55.8784 107.373 55.8488L104.299 38.5628C107.848 35.1847 111.936 32.5447 116.463 30.7097C121.123 28.818 126.048 27.8594 131.097 27.8594C136.146 27.8594 141.07 28.818 145.732 30.7089C150.564 32.667 154.894 35.5421 158.606 39.2528C162.317 42.9643 165.192 47.2946 167.151 52.1246C169.041 56.7859 169.999 61.711 170 66.7593Z" fill="#4AAF3C"/>
<path d="M133.814 119.087L105.12 147.779C103.762 149.137 101.92 149.9 99.9998 149.9C98.0791 149.9 96.2375 149.137 94.879 147.779L70.7775 123.678L67.503 126.953C66.1972 128.259 63.9623 127.556 63.6392 125.737L59.7107 103.646C59.4332 102.083 60.7966 100.72 62.3598 100.997L84.4519 104.925C86.2702 105.249 86.9731 107.483 85.6673 108.789L81.0183 113.438L100.001 132.42L124.507 107.914L123.318 114.601C123.053 116.096 123.535 117.63 124.609 118.703C125.684 119.778 127.217 120.259 128.712 119.994L133.814 119.087H133.814Z" fill="#4AAF3C"/>
<path d="M102.359 58.8607L80.2668 54.9325C78.4485 54.6095 77.7456 52.3748 79.0514 51.0692L83.1799 46.9412C79.0498 43.9533 74.1009 42.3406 68.9034 42.3406C62.3808 42.3406 56.2477 44.88 51.6363 49.4925C47.0232 54.1042 44.4828 60.2359 44.4828 66.7585C44.4828 73.2812 47.0232 79.4128 51.6363 84.0246L66.9435 99.3301L62.7415 98.5834C61.2462 98.3179 59.7125 98.8 58.6386 99.8738C57.5647 100.948 57.0825 102.482 57.348 103.977L58.697 111.564L41.3955 94.2635C37.6836 90.552 34.8089 86.2217 32.8499 81.3917C30.9588 76.7312 30 71.8076 30 66.7593C30 61.711 30.9588 56.7867 32.8491 52.1254C34.8081 47.2946 37.6836 42.9643 41.3947 39.2536C45.1057 35.5421 49.4365 32.6678 54.267 30.7097C58.9296 28.818 63.8537 27.8594 68.9034 27.8594C73.953 27.8594 78.8771 28.818 83.5389 30.7081C87.158 32.1753 90.4956 34.1565 93.503 36.6183L97.2157 32.9061C98.5215 31.6004 100.756 32.3032 101.079 34.1214L105.007 56.211C105.286 57.7741 103.922 59.1381 102.359 58.8607Z" fill="#4AAF3C"/>
</g>
<g filter="url(#filter0_f_34053_111310)">
<ellipse cx="102.576" cy="170.536" rx="27.5758" ry="1.59085" fill="#1CA609" fill-opacity="0.5"/>
</g>
<defs>
<filter id="filter0_f_34053_111310" x="70.5" y="164.445" width="64.1515" height="12.1797" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.25" result="effect1_foregroundBlur_34053_111310"/>
</filter>
<clipPath id="clip0_34053_111310">
<rect width="140" height="122" fill="white" transform="translate(30 27.8828)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,20 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_34053_111286)">
<path d="M170 66.7593C170 71.8084 169.042 76.732 167.151 81.3933C165.192 86.2233 162.316 90.5536 158.606 94.265L147.36 105.511L151.657 109.806C152.962 111.112 152.26 113.347 150.441 113.671L128.349 117.598C126.785 117.875 125.423 116.513 125.7 114.949L129.628 92.8594C129.952 91.0413 132.187 90.3393 133.492 91.6441L137.12 95.2717L148.366 84.0261C152.978 79.4144 155.519 73.2819 155.519 66.7609C155.519 60.2383 152.978 54.105 148.365 49.4941C143.753 44.8815 137.621 42.3422 131.097 42.3422C124.574 42.3422 118.442 44.8815 113.83 49.4941L107.387 55.936C107.383 55.9064 107.378 55.8784 107.373 55.8488L104.299 38.5628C107.848 35.1847 111.936 32.5447 116.463 30.7097C121.123 28.818 126.048 27.8594 131.097 27.8594C136.146 27.8594 141.07 28.818 145.732 30.7089C150.564 32.667 154.894 35.5421 158.606 39.2528C162.317 42.9643 165.192 47.2946 167.151 52.1246C169.041 56.7859 169.999 61.7102 170 66.7585V66.7593Z" fill="#4AAF3C"/>
<path d="M133.814 119.087L105.12 147.779C103.762 149.137 101.92 149.9 99.9998 149.9C98.0791 149.9 96.2375 149.137 94.879 147.779L70.7775 123.678L67.503 126.953C66.1972 128.259 63.9623 127.556 63.6392 125.737L59.7107 103.646C59.4332 102.083 60.7966 100.72 62.3598 100.997L84.4519 104.925C86.2702 105.249 86.9731 107.483 85.6673 108.789L81.0183 113.438L100.001 132.42L124.507 107.914L123.318 114.601C123.053 116.096 123.535 117.63 124.609 118.703C125.684 119.778 127.217 120.259 128.712 119.994L133.814 119.087H133.814Z" fill="#4AAF3C"/>
<path d="M102.359 58.8607L80.2668 54.9325C78.4485 54.6095 77.7456 52.3748 79.0514 51.0692L83.1799 46.9412C79.0498 43.9533 74.1009 42.3406 68.9034 42.3406C62.3808 42.3406 56.2477 44.88 51.6363 49.4925C47.0232 54.1042 44.4828 60.2359 44.4828 66.7585C44.4828 73.2812 47.0232 79.4128 51.6363 84.0246L66.9435 99.3301L62.7415 98.5834C61.2462 98.3179 59.7125 98.8 58.6386 99.8738C57.5647 100.948 57.0825 102.482 57.348 103.977L58.697 111.564L41.3955 94.2635C37.6836 90.552 34.8089 86.2217 32.8499 81.3917C30.9588 76.7312 30 71.8076 30 66.7593C30 61.711 30.9588 56.7867 32.8491 52.1254C34.8081 47.2946 37.6836 42.9643 41.3947 39.2536C45.1057 35.5421 49.4365 32.6678 54.267 30.7097C58.9296 28.818 63.8537 27.8594 68.9034 27.8594C73.953 27.8594 78.8771 28.818 83.5389 30.7081C87.158 32.1753 90.4957 34.1565 93.503 36.6183L97.2157 32.9061C98.5215 31.6004 100.756 32.3032 101.079 34.1214L105.007 56.211C105.286 57.7741 103.922 59.1373 102.359 58.8599V58.8607Z" fill="#4AAF3C"/>
</g>
<g filter="url(#filter0_f_34053_111286)">
<ellipse cx="101.576" cy="170.536" rx="27.5758" ry="1.59085" fill="#E1E1E1" fill-opacity="0.5"/>
</g>
<defs>
<filter id="filter0_f_34053_111286" x="69.5" y="164.445" width="64.1515" height="12.1797" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.25" result="effect1_foregroundBlur_34053_111286"/>
</filter>
<clipPath id="clip0_34053_111286">
<rect width="140" height="122" fill="white" transform="translate(30 27.8828)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

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

@@ -1 +1 @@
ente - stockage de photos chiffré
ente - stockage chiffré des photos

View File

@@ -0,0 +1 @@
എന്റേ - പൂർണമായും എൻക്രിപ്റ്റ് ചെയ്ത ചിത്രസംഭരണി

View File

@@ -1 +1 @@
എന്റ - എൻക്രിപ്റ്റ്ട് ചിത്രസംഭരണി
എന്റ - എൻക്രിപ്റ്റ്ട് ചിത്രസംഭരണി

View File

@@ -1,30 +1,30 @@
Entre est une application simple qui sauvegarde et organisé vos photos et vidéos.
Entre est une application simple qui sauvegarde et organise vos photos et vidéos.
Si vous recherchez une alternative respectueuse de la vie privée pour préserver vos souvenirs, vous êtes au bon endroit. Avec Ente, ils sont stockés chiffrés de bout-en-bout (e2ee). Cela signifie que vous-seul pouvez les voir.
Nous avons des applications sur toutes les plateformes, et vos photos seront synchronisées de manière transparente entre tous vos appareils chiffrée de bout en bout (e2ee).
Nous avons des applications sur toutes les plateformes, et vos photos seront synchronisées de manière transparente entre tous vos appareils avec un chiffrement de bout en bout (e2ee).
Ente vous permet également de partager vos albums avec vos proches. Vous pouvez soit les partager directement avec d'autres utilisateurs Ente, chiffrés de bout en bout ou avec des liens visibles publiquement.
Ente vous permet également de partager vos albums avec vos proches. Vous pouvez soit les partager directement avec d'autres utilisateurs Ente, chiffrés de bout en bout; ou avec des liens visibles publiquement.
Vos données chiffrées sont stockées à travers de multiples endroits, dont un abri antiatomique à Paris. Nous prenons la postérité au sérieux et facilitons la conservation de vos souvenirs.
Nous sommes là pour faire l'application photo la plus sûre de tous les temps, rejoignez-nous !
CARACTÉRISTIQUES
- Sauvegardes de qualité originales, car chaque pixel est important
- Abonnement familiaux, pour que vous puissiez partager l'espace de stockage avec votre famille
- Sauvegardes de qualité originale, car chaque pixel est important
- Abonnements familiaux, pour que vous puissiez partager l'espace de stockage avec votre famille
- Dossiers partagés, si vous voulez que votre partenaire profite de vos clichés
- Liens ves les albums qui peuvent être protégés par un mot de passe et être configurés pour expirer
- Liens vers les albums, qui peuvent être protégés par un mot de passe et être configurés pour expirer
- Possibilité de libérer de l'espace en supprimant les fichiers qui ont été sauvegardés en toute sécurité
- Éditeur d'images, pour ajouter des touches de finition
- Éditeur d'images, pour rajouter des finitions
- Favoriser, cacher et revivre vos souvenirs, car ils sont précieux
- Importation en un clic les principaux fournisseurs de stockage
- Thème sombre, parce que vos photos y sont jolies
- Importation en un clic des principaux fournisseurs de stockage
- Thème sombre, parce que vos photos y sont plus jolies
- 2FA, 3FA, authentification biométrique
- et beaucoup de choses encore !
PRIX
Nous ne proposons pas d'abonnement gratuits pour toujours, car il est important pour nous de rester durables et de résister à l'épreuve du temps. Au lieu de cela, nous vous proposons des abonnements abordables que vous pouvez partager librement avec votre famille. Vous pouvez trouver plus d'informations sur ente.io.
Nous ne proposons pas d'abonnement gratuit à vie, car il est important pour nous de rester durable et de résister à l'épreuve du temps. Au lieu de cela, nous vous proposons des abonnements abordables que vous pouvez partager librement avec votre famille. Vous pouvez trouver plus d'informations sur ente.io.
ASSISTANCE
Nous sommes fiers d'offrir un support humain. Si vous êtes un abonné, vous pouvez contacter team@ente.io et vous recevrez une réponse de notre équipe dans les 24 heures.

View File

@@ -1 +1 @@
Stockage de photos chiffré
Stockage chiffré des photos

View File

@@ -1 +1 @@
എന്റ ചിത്രസംഭരണി
എന്റ ചിത്രസംഭരണി

View File

@@ -1 +1 @@
Stockage de photos chiffrées - sauvegardez, organisez et partagez vos photos et vidéos
Stockage chiffré des photos - sauvegardez, organisez et partagez vos photos et vidéos

View File

@@ -1 +1 @@
എന്റ ചിത്രസംഭരണി
എന്റ ചിത്രസംഭരണി

View File

@@ -1,12 +1,12 @@
Ente é um aplicativo simples para copiar com segurança automaticamente e organizar suas fotos e vídeos.
Ente é um aplicativo básico para salvar e organizar suas fotos e vídeos com segurança.
Se você esteve procurando por uma alternativa amigável à privacidade para preservar suas memórias, você veio no lugar certo. Com Ente, elas são armazenadas com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-las.
Se você esteve procurando uma alternativa de ótima privacidade para preservar suas memórias, você chegou ao lugar certo. Com Ente, suas fotos e vídeos são criptografados de ponta a ponta (e2ee). Significando que somente você pode vê-las.
Nós rmos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos vão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).
Temos sites/aplicativos para Android, iOS e Computador, e suas fotos serão sincronizadas sem parar em todos os dispositivos numa maneira de criptografia de ponta a ponta (e2ee).
Ente também torna simples compartilhar seus álbuns com seus entes queridos. Você pode compartilhá-los diretamente com outros usuários do Ente, criptografados de ponta a ponta; ou com links publicamente visíveis.
Ente facilita o compartilhamento de álbuns entre entes queridos. Você pode compartilhá-los diretamente com outros usuários Ente, criptografados de ponta a ponta; ou links disponíveis publicamente.
Seus dados criptografados são replicados em locais diferentes, incluindo um abrigo avançado em Paris. Nós levamos a sério a nossa postura e fazemos que seja fácil garantir que suas memórias vivam.
Seus dados criptografados são armazenados em vários locais, incluindo até um refúgio avançado em Paris. Nós levamos nossa postura a sério e facilitamos que você se certifique que suas memórias vivam.
Estamos aqui para se tornar o aplicativo de fotos mais seguro de todos, venha e participe de nossa jornada!

View File

@@ -1 +1 @@
Armazenamento de fotos criptografado - copie com segurança, organize e compartilhe suas fotos e vídeos
Armazenamento criptografado de fotos - salve com segurança, organize e compartilhe suas fotos e vídeos

View File

@@ -19,7 +19,7 @@ Suntem aici pentru a crea cea mai sigură aplicație de fotografii, veniți ală
- Editor de imagini, pentru a adăuga ultimele retușuri
- Evidențiați, ascundeți și retrăiți-vă amintirile, căci sunt prețioase
- Import cu o singură apăsare din Google, Apple, hard disk și multe altele
- Temă întunecată, deoarece fotografiile dvs. arată cu ea
- Temă întunecată, deoarece fotografiile dvs. arată bine și cu ea
- 2FA, 3FA, autentificare biometrică
- și MULTE altele!

View File

@@ -45,6 +45,9 @@ const supportEmail = 'support@ente.io';
const multipartPartSize = 20 * 1024 * 1024;
const kDefaultProductionEndpoint = 'https://api.ente.io';
const kAccountsUrl = 'https://accounts.ente.io';
const kCasUrl = 'https://cas.ente.io';
const kFamilyUrl = 'https://family.ente.io';
const int intMaxValue = 9223372036854775807;

View File

@@ -17,6 +17,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:photos/core/error-reporting/tunneled_transport.dart';
import "package:photos/core/errors.dart";
import 'package:photos/models/typedefs.dart';
import "package:photos/utils/device_info.dart";
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -198,6 +199,12 @@ class SuperLogging {
$.info("sentry uploader started");
}
unawaited(
getDeviceName().then((name) {
$.info("Device name: $name");
}),
);
if (appConfig.body == null) return;
if (enable && sentryIsEnabled) {

View File

@@ -0,0 +1,557 @@
import "dart:async";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import "package:flutter_svg/flutter_svg.dart";
import 'package:photos/core/configuration.dart';
import "package:photos/emergency/emergency_service.dart";
import "package:photos/emergency/model.dart";
import "package:photos/emergency/other_contact_page.dart";
import "package:photos/emergency/select_contact_page.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/l10n/l10n.dart";
import "package:photos/theme/colors.dart";
import 'package:photos/theme/ente_theme.dart';
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/components/action_sheet_widget.dart";
import "package:photos/ui/components/buttons/button_widget.dart";
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/divider_widget.dart';
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
import 'package:photos/ui/components/menu_section_title.dart';
import "package:photos/ui/components/models/button_type.dart";
import "package:photos/ui/components/notification_widget.dart";
import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/components/title_bar_widget.dart';
import "package:photos/ui/sharing/user_avator_widget.dart";
import "package:photos/utils/navigation_util.dart";
import "package:photos/utils/toast_util.dart";
class EmergencyPage extends StatefulWidget {
const EmergencyPage({
super.key,
});
@override
State<EmergencyPage> createState() => _EmergencyPageState();
}
class _EmergencyPageState extends State<EmergencyPage> {
late int currentUserID;
EmergencyInfo? info;
bool hasTrustedContact = false;
@override
void initState() {
super.initState();
currentUserID = Configuration.instance.getUserID()!;
// set info to null after 5 second
Future.delayed(
const Duration(seconds: 0),
() async {
unawaited(_fetchData());
},
);
}
Future<void> _fetchData() async {
try {
final result = await EmergencyContactService.instance.getInfo();
if (mounted) {
setState(() {
info = result;
if (info != null) {
hasTrustedContact = info!.contacts.isNotEmpty;
}
});
}
} catch (e) {
showShortToast(
context,
S.of(context).somethingWentWrong,
);
}
}
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final currentUserID = Configuration.instance.getUserID()!;
final List<EmergencyContact> othersTrustedContacts =
info?.othersEmergencyContact ?? [];
final List<EmergencyContact> trustedContacts = info?.contacts ?? [];
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: TitleBarTitleWidget(
title: S.of(context).legacy,
),
),
if (info == null)
const SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: EnteLoadingWidget(),
),
),
if (info != null)
if (info!.recoverSessions.isNotEmpty)
SliverPadding(
padding: const EdgeInsets.only(
top: 20,
left: 16,
right: 16,
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: NotificationWidget(
startIcon: Icons.warning_amber_rounded,
text: context.l10n.recoveryWarning,
actionIcon: null,
onTap: () {},
),
);
}
final RecoverySessions recoverSession =
info!.recoverSessions[index - 1];
return MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: recoverSession.emergencyContact.email,
makeTextBold: recoverSession.status.isNotEmpty,
textColor: colorScheme.warning500,
),
leadingIconWidget: UserAvatarWidget(
recoverSession.emergencyContact,
currentUserID: currentUserID,
),
leadingIconSize: 24,
menuItemColor: colorScheme.fillFaint,
singleBorderRadius: 8,
trailingIcon: Icons.chevron_right,
onTap: () async {
await showRejectRecoveryDialog(recoverSession);
},
);
},
childCount: 1 + info!.recoverSessions.length,
),
),
),
if (info != null)
SliverPadding(
padding: const EdgeInsets.only(
top: 16,
left: 16,
right: 16,
bottom: 8,
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == 0 && trustedContacts.isNotEmpty) {
return MenuSectionTitle(
title: S.of(context).trustedContacts,
);
} else if (index > 0 && index <= trustedContacts.length) {
final listIndex = index - 1;
final contact = trustedContacts[listIndex];
return Column(
children: [
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: contact.emergencyContact.email,
subTitle: contact.isPendingInvite() ? "" : null,
makeTextBold: contact.isPendingInvite(),
),
leadingIconSize: 24.0,
surfaceExecutionStates: false,
alwaysShowSuccessState: false,
leadingIconWidget: UserAvatarWidget(
contact.emergencyContact,
type: AvatarType.mini,
currentUserID: currentUserID,
),
menuItemColor:
getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right,
trailingIconIsMuted: true,
onTap: () async {
await showRevokeOrRemoveDialog(context, contact);
},
isTopBorderRadiusRemoved: listIndex > 0,
isBottomBorderRadiusRemoved: true,
singleBorderRadius: 8,
),
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).fillFaint,
),
],
);
} else if (index == (1 + trustedContacts.length)) {
if (trustedContacts.isEmpty) {
return Column(
children: [
const SizedBox(height: 20),
Text(
context.l10n.legacyPageDesc,
style: getEnteTextTheme(context).body,
),
SizedBox(
height: 200,
width: 200,
child: SvgPicture.asset(
getEnteColorScheme(context).backdropBase ==
backgroundBaseDark
? "assets/icons/legacy-light.svg"
: "assets/icons/legacy-dark.svg",
width: 156,
height: 152,
),
),
Text(
context.l10n.legacyPageDesc2,
style: getEnteTextTheme(context).smallMuted,
),
const SizedBox(height: 16),
ButtonWidget(
buttonType: ButtonType.primary,
labelText: S.of(context).addTrustedContact,
shouldSurfaceExecutionStates: false,
onTap: () async {
await routeToPage(
context,
AddContactPage(info!),
forceCustomPageRoute: true,
);
unawaited(_fetchData());
},
),
],
);
}
return MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: trustedContacts.isNotEmpty
? S.of(context).addMore
: S.of(context).addTrustedContact,
makeTextBold: true,
),
leadingIcon: Icons.add_outlined,
surfaceExecutionStates: false,
menuItemColor: getEnteColorScheme(context).fillFaint,
onTap: () async {
await routeToPage(
context,
AddContactPage(info!),
forceCustomPageRoute: true,
);
unawaited(_fetchData());
},
isTopBorderRadiusRemoved: trustedContacts.isNotEmpty,
singleBorderRadius: 8,
);
}
return const SizedBox.shrink();
},
childCount: 1 + trustedContacts.length + 1,
),
),
),
if (info != null && info!.othersEmergencyContact.isNotEmpty)
SliverPadding(
padding: const EdgeInsets.only(top: 0, left: 16, right: 16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == 0 && (othersTrustedContacts.isNotEmpty)) {
return Column(
children: [
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: DividerWidget(
dividerType: DividerType.solid,
),
),
MenuSectionTitle(
title: context.l10n.legacyAccounts,
),
],
);
} else if (index > 0 &&
index <= othersTrustedContacts.length) {
final listIndex = index - 1;
final currentUser = othersTrustedContacts[listIndex];
final isLastItem = index == othersTrustedContacts.length;
return Column(
children: [
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: currentUser.user.email,
makeTextBold: currentUser.isPendingInvite(),
subTitle:
currentUser.isPendingInvite() ? "" : null,
),
leadingIconSize: 24.0,
leadingIconWidget: UserAvatarWidget(
currentUser.user,
type: AvatarType.mini,
currentUserID: currentUserID,
),
menuItemColor:
getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right,
trailingIconIsMuted: true,
onTap: () async {
if (currentUser.isPendingInvite()) {
await showAcceptOrDeclineDialog(
context,
currentUser,
);
} else {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return OtherContactPage(
contact: currentUser,
emergencyInfo: info!,
);
},
),
);
// await routeToPage(
// context,
// OtherContactPage(
// contact: currentUser,
// emergencyInfo: info!,
// ),
// );
if (mounted) {
unawaited(_fetchData());
}
}
},
isTopBorderRadiusRemoved: listIndex > 0,
isBottomBorderRadiusRemoved: !isLastItem,
singleBorderRadius: 8,
surfaceExecutionStates: false,
),
isLastItem
? const SizedBox.shrink()
: DividerWidget(
dividerType: DividerType.menu,
bgColor:
getEnteColorScheme(context).fillFaint,
),
],
);
}
return const SizedBox.shrink();
},
childCount: 1 + othersTrustedContacts.length + 1,
),
),
),
],
),
);
}
Future<void> showRevokeOrRemoveDialog(
BuildContext context,
EmergencyContact contact,
) async {
if (contact.isPendingInvite()) {
await showActionSheet(
context: context,
body:
"You have invited ${contact.emergencyContact.email} to be a trusted contact",
bodyHighlight: "They are yet to accept your invite",
buttons: [
ButtonWidget(
labelText: S.of(context).removeInvite,
buttonType: ButtonType.critical,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
shouldStickToDarkTheme: true,
shouldSurfaceExecutionStates: true,
shouldShowSuccessConfirmation: false,
onTap: () async {
await EmergencyContactService.instance
.updateContact(contact, ContactState.userRevokedContact);
info?.contacts.remove(contact);
if (mounted) {
setState(() {});
unawaited(_fetchData());
}
},
isInAlert: true,
),
ButtonWidget(
labelText: S.of(context).cancel,
buttonType: ButtonType.tertiary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
isInAlert: true,
),
],
);
} else {
await showActionSheet(
context: context,
body:
"You have added ${contact.emergencyContact.email} as a trusted contact",
bodyHighlight: "They have accepted your invite",
buttons: [
ButtonWidget(
labelText: S.of(context).remove,
buttonType: ButtonType.critical,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
shouldSurfaceExecutionStates: true,
shouldShowSuccessConfirmation: false,
onTap: () async {
await EmergencyContactService.instance
.updateContact(contact, ContactState.userRevokedContact);
info?.contacts.remove(contact);
if (mounted) {
setState(() {});
unawaited(_fetchData());
}
},
isInAlert: true,
),
ButtonWidget(
labelText: S.of(context).cancel,
buttonType: ButtonType.tertiary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.third,
shouldStickToDarkTheme: true,
isInAlert: true,
),
],
);
}
}
Future<void> showAcceptOrDeclineDialog(
BuildContext context,
EmergencyContact contact,
) async {
await showActionSheet(
context: context,
buttons: [
ButtonWidget(
labelText: S.of(context).acceptTrustInvite,
buttonType: ButtonType.primary,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.first,
onTap: () async {
await EmergencyContactService.instance
.updateContact(contact, ContactState.contactAccepted);
final updatedContact =
contact.copyWith(state: ContactState.contactAccepted);
info?.othersEmergencyContact.remove(contact);
info?.othersEmergencyContact.add(updatedContact);
if (mounted) {
setState(() {});
}
},
isInAlert: true,
),
ButtonWidget(
labelText: S.of(context).declineTrustInvite,
buttonType: ButtonType.critical,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
onTap: () async {
await EmergencyContactService.instance
.updateContact(contact, ContactState.contactDenied);
info?.othersEmergencyContact.remove(contact);
if (mounted) {
setState(() {});
}
},
isInAlert: true,
),
ButtonWidget(
labelText: S.of(context).cancel,
buttonType: ButtonType.tertiary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.third,
shouldStickToDarkTheme: true,
isInAlert: true,
),
],
body: S.of(context).legacyInvite(contact.user.email),
actionSheetType: ActionSheetType.defaultActionSheet,
);
return;
}
Future<void> showRejectRecoveryDialog(RecoverySessions session) async {
final String emergencyContactEmail = session.emergencyContact.email;
await showActionSheet(
context: context,
buttons: [
ButtonWidget(
labelText: context.l10n.rejectRecovery,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonType: ButtonType.critical,
buttonAction: ButtonAction.first,
onTap: () async {
await EmergencyContactService.instance.rejectRecovery(session);
info?.recoverSessions
.removeWhere((element) => element.id == session.id);
if (mounted) {
setState(() {});
}
unawaited(_fetchData());
},
isInAlert: true,
),
if (kDebugMode)
ButtonWidget(
labelText: "Approve recovery (to be removed)",
buttonType: ButtonType.primary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
onTap: () async {
await EmergencyContactService.instance.approveRecovery(session);
if (mounted) {
setState(() {});
}
unawaited(_fetchData());
},
isInAlert: true,
),
ButtonWidget(
labelText: S.of(context).cancel,
buttonType: ButtonType.tertiary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.third,
shouldStickToDarkTheme: true,
isInAlert: true,
),
],
body: context.l10n.recoveryWarningBody(emergencyContactEmail),
actionSheetType: ActionSheetType.defaultActionSheet,
);
return;
}
}

View File

@@ -0,0 +1,275 @@
import "dart:convert";
import "dart:math";
import "dart:typed_data";
import "package:dio/dio.dart";
import "package:flutter/cupertino.dart";
import "package:logging/logging.dart";
import "package:photos/core/configuration.dart";
import "package:photos/core/network/network.dart";
import "package:photos/emergency/model.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/api/user/srp.dart";
import "package:photos/models/key_attributes.dart";
import "package:photos/models/set_keys_request.dart";
import "package:photos/services/user_service.dart";
import "package:photos/ui/common/user_dialogs.dart";
import "package:photos/utils/crypto_util.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/email_util.dart";
import "package:pointycastle/pointycastle.dart";
import "package:pointycastle/random/fortuna_random.dart";
import "package:pointycastle/srp/srp6_client.dart";
import "package:pointycastle/srp/srp6_standard_groups.dart";
import "package:pointycastle/srp/srp6_util.dart";
import "package:pointycastle/srp/srp6_verifier_generator.dart";
import "package:uuid/uuid.dart";
class EmergencyContactService {
late Dio _enteDio;
late UserService _userService;
late Configuration _config;
late final Logger _logger = Logger("EmergencyContactService");
EmergencyContactService._privateConstructor() {
_enteDio = NetworkClient.instance.enteDio;
_userService = UserService.instance;
_config = Configuration.instance;
}
static final EmergencyContactService instance =
EmergencyContactService._privateConstructor();
Future<bool> addContact(BuildContext context, String email) async {
if (!isValidEmail(email)) {
await showErrorDialog(
context,
S.of(context).invalidEmailAddress,
S.of(context).enterValidEmail,
);
return false;
} else if (email.trim() == Configuration.instance.getEmail()) {
await showErrorDialog(
context,
S.of(context).oops,
S.of(context).youCannotShareWithYourself,
);
return false;
}
final String? publicKey = await _userService.getPublicKey(email);
if (publicKey == null) {
await showInviteDialog(context, email);
return false;
}
final Uint8List recoveryKey = Configuration.instance.getRecoveryKey();
final encryptedKey = CryptoUtil.sealSync(
recoveryKey,
CryptoUtil.base642bin(publicKey),
);
await _enteDio.post(
"/emergency-contacts/add",
data: {
"email": email.trim(),
"encryptedKey": CryptoUtil.bin2base64(encryptedKey),
},
);
return true;
}
Future<EmergencyInfo> getInfo() async {
try {
final response = await _enteDio.get("/emergency-contacts/info");
return EmergencyInfo.fromJson(response.data);
} catch (e, s) {
Logger("EmergencyContact").severe('failed to get info', e, s);
rethrow;
}
}
Future<void> updateContact(
EmergencyContact contact,
ContactState state,
) async {
try {
await _enteDio.post(
"/emergency-contacts/update",
data: {
"userID": contact.user.id,
"emergencyContactID": contact.emergencyContact.id,
"state": state.stringValue,
},
);
} catch (e, s) {
Logger("EmergencyContact").severe('failed to update contact', e, s);
rethrow;
}
}
Future<void> startRecovery(EmergencyContact contact) async {
try {
await _enteDio.post(
"/emergency-contacts/start-recovery",
data: {
"userID": contact.user.id,
"emergencyContactID": contact.emergencyContact.id,
},
);
} catch (e, s) {
Logger("EmergencyContact").severe('failed to start recovery', e, s);
rethrow;
}
}
Future<void> stopRecovery(RecoverySessions session) async {
try {
await _enteDio.post(
"/emergency-contacts/stop-recovery",
data: {
"userID": session.user.id,
"emergencyContactID": session.emergencyContact.id,
"id": session.id,
},
);
} catch (e, s) {
Logger("EmergencyContact").severe('failed to stop recovery', e, s);
rethrow;
}
}
Future<void> rejectRecovery(RecoverySessions session) async {
try {
await _enteDio.post(
"/emergency-contacts/reject-recovery",
data: {
"userID": session.user.id,
"emergencyContactID": session.emergencyContact.id,
"id": session.id,
},
);
} catch (e, s) {
Logger("EmergencyContact").severe('failed to stop recovery', e, s);
rethrow;
}
}
Future<void> approveRecovery(RecoverySessions session) async {
try {
await _enteDio.post(
"/emergency-contacts/approve-recovery",
data: {
"userID": session.user.id,
"emergencyContactID": session.emergencyContact.id,
"id": session.id,
},
);
} catch (e, s) {
Logger("EmergencyContact").severe('failed to approve recovery', e, s);
rethrow;
}
}
Future<(String, KeyAttributes)> getRecoveryInfo(
RecoverySessions sessions,
) async {
try {
final resp = await _enteDio.get(
"/emergency-contacts/recovery-info/${sessions.id}",
);
final String encryptedKey = resp.data["encryptedKey"]!;
final decryptedKey = CryptoUtil.openSealSync(
CryptoUtil.base642bin(encryptedKey),
CryptoUtil.base642bin(_config.getKeyAttributes()!.publicKey),
_config.getSecretKey()!,
);
final String hexRecoveryKey = CryptoUtil.bin2hex(decryptedKey);
final KeyAttributes keyAttributes =
KeyAttributes.fromMap(resp.data['userKeyAttr']);
return (hexRecoveryKey, keyAttributes);
} catch (e, s) {
Logger("EmergencyContact").severe('failed to stop recovery', e, s);
rethrow;
}
}
Future<void> changePasswordForOther(
Uint8List loginKey,
SetKeysRequest setKeysRequest,
RecoverySessions recoverySessions,
) async {
try {
final SRP6GroupParameters kDefaultSrpGroup =
SRP6StandardGroups.rfc5054_4096;
final String username = const Uuid().v4().toString();
final SecureRandom random = _getSecureRandom();
final Uint8List identity = Uint8List.fromList(utf8.encode(username));
final Uint8List password = loginKey;
final Uint8List salt = random.nextBytes(16);
final gen = SRP6VerifierGenerator(
group: kDefaultSrpGroup,
digest: Digest('SHA-256'),
);
final v = gen.generateVerifier(salt, identity, password);
final client = SRP6Client(
group: kDefaultSrpGroup,
digest: Digest('SHA-256'),
random: random,
);
final A = client.generateClientCredentials(salt, identity, password);
final request = SetupSRPRequest(
srpUserID: username,
srpSalt: base64Encode(salt),
srpVerifier: base64Encode(SRP6Util.encodeBigInt(v)),
srpA: base64Encode(SRP6Util.encodeBigInt(A!)),
isUpdate: false,
);
final response = await _enteDio.post(
"/emergency-contacts/init-change-password",
data: {
"recoveryID": recoverySessions.id,
"setupSRPRequest": request.toMap(),
},
);
if (response.statusCode == 200) {
final SetupSRPResponse setupSRPResponse =
SetupSRPResponse.fromJson(response.data);
final serverB =
SRP6Util.decodeBigInt(base64Decode(setupSRPResponse.srpB));
// ignore: unused_local_variable
final clientS = client.calculateSecret(serverB);
final clientM = client.calculateClientEvidenceMessage();
// ignore: unused_local_variable
late Response srpCompleteResponse;
srpCompleteResponse = await _enteDio.post(
"/emergency-contacts/change-password",
data: {
"recoveryID": recoverySessions.id,
'updateSrpAndKeysRequest': {
'setupID': setupSRPResponse.setupID,
'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)),
'updatedKeyAttr': setKeysRequest.toMap(),
},
},
);
} else {
throw Exception("register-srp action failed");
}
} catch (e, s) {
_logger.severe("failed to change password for other", e, s);
rethrow;
}
}
SecureRandom _getSecureRandom() {
final List<int> seeds = [];
final random = Random.secure();
for (int i = 0; i < 32; i++) {
seeds.add(random.nextInt(255));
}
final secureRandom = FortunaRandom();
secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));
return secureRandom;
}
}

View File

@@ -0,0 +1,153 @@
import "package:photos/models/api/collection/user.dart";
enum ContactState {
userInvitedContact,
userRevokedContact,
contactAccepted,
contactLeft,
contactDenied,
unknown,
}
extension ContactStateExtension on ContactState {
String get stringValue {
switch (this) {
case ContactState.userInvitedContact:
return "INVITED";
case ContactState.userRevokedContact:
return "REVOKED";
case ContactState.contactAccepted:
return "ACCEPTED";
case ContactState.contactLeft:
return "CONTACT_LEFT";
case ContactState.contactDenied:
return "CONTACT_DENIED";
default:
return "UNKNOWN";
}
}
static ContactState fromString(String value) {
switch (value) {
case "INVITED":
return ContactState.userInvitedContact;
case "REVOKED":
return ContactState.userRevokedContact;
case "ACCEPTED":
return ContactState.contactAccepted;
case "CONTACT_LEFT":
return ContactState.contactLeft;
case "CONTACT_DENIED":
return ContactState.contactDenied;
default:
return ContactState.unknown;
}
}
}
class EmergencyContact {
final User user;
final User emergencyContact;
final ContactState state;
final int recoveryNoticeInDays;
EmergencyContact(
this.user,
this.emergencyContact,
this.state,
this.recoveryNoticeInDays,
);
// copyWith
EmergencyContact copyWith({
User? user,
User? emergencyContact,
ContactState? state,
int? recoveryNoticeInDays,
}) {
return EmergencyContact(
user ?? this.user,
emergencyContact ?? this.emergencyContact,
state ?? this.state,
recoveryNoticeInDays ?? this.recoveryNoticeInDays,
);
}
// fromJson
EmergencyContact.fromJson(Map<String, dynamic> json)
: user = User.fromMap(json['user']),
emergencyContact = User.fromMap(json['emergencyContact']),
state = ContactStateExtension.fromString(json['state'] as String),
recoveryNoticeInDays = json['recoveryNoticeInDays'];
bool isCurrentUserContact(int userID) {
return user.id == userID;
}
bool isPendingInvite() {
return state == ContactState.userInvitedContact;
}
}
class EmergencyInfo {
// List of emergency contacts added by the user
final List<EmergencyContact> contacts;
// List of recovery sessions that are created to recover current user account
final List<RecoverySessions> recoverSessions;
// List of emergency contacts that have added current user as their emergency contact
final List<EmergencyContact> othersEmergencyContact;
// List of recovery sessions that are created to recover grantor's account
final List<RecoverySessions> othersRecoverySession;
EmergencyInfo(
this.contacts,
this.recoverSessions,
this.othersEmergencyContact,
this.othersRecoverySession,
);
// from json
EmergencyInfo.fromJson(Map<String, dynamic> json)
: contacts = (json['contacts'] as List)
.map((contact) => EmergencyContact.fromJson(contact))
.toList(),
recoverSessions = (json['recoverSessions'] as List)
.map((session) => RecoverySessions.fromJson(session))
.toList(),
othersEmergencyContact = (json['othersEmergencyContact'] as List)
.map((grantor) => EmergencyContact.fromJson(grantor))
.toList(),
othersRecoverySession = (json['othersRecoverySession'] as List)
.map((session) => RecoverySessions.fromJson(session))
.toList();
}
class RecoverySessions {
final String id;
final User user;
final User emergencyContact;
final String status;
final int waitTill;
final int createdAt;
RecoverySessions(
this.id,
this.user,
this.emergencyContact,
this.status,
this.waitTill,
this.createdAt,
);
// fromJson
RecoverySessions.fromJson(Map<String, dynamic> json)
: id = json['id'],
user = User.fromMap(json['user']),
emergencyContact = User.fromMap(json['emergencyContact']),
status = json['status'],
waitTill = json['waitTill'],
createdAt = json['createdAt'];
}

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