Compare commits

...

253 commits

Author SHA1 Message Date
Neeraj Gupta ae61fc9c6f
Wrap add person name banner inside safeArea (#1887)
## Description

## Tests
2024-05-27 18:12:45 +05:30
Neeraj Gupta c291fa70d3 Wrap add person name banner inside safeArea 2024-05-27 18:12:21 +05:30
Laurens Priem 232acfa211
Face (#1885)
## Description

- Several fixes for Faces
2024-05-27 17:46:05 +05:30
laurenspriem f25f119ca1 [mob][photos] Copy 2024-05-27 17:26:14 +05:30
laurenspriem 89a61b3bf7 [mob][photos] Bump 2024-05-27 17:21:29 +05:30
laurenspriem 380d37267b [mob][photos] Don't pop too often 2024-05-27 17:19:06 +05:30
laurenspriem 9cf5691e42 [mob][photos] Delete instead of drop table 2024-05-27 17:09:33 +05:30
laurenspriem 8f474a4500 [mob][photos] Set MLController timer to 10 seconds 2024-05-27 15:54:10 +05:30
Manav Rathi c7be2270ff
[desktop] RC fixes (#1884) 2024-05-27 15:16:04 +05:30
laurenspriem ced1f0bd79 [mob][photos] Don't remove last cluster of person 2024-05-27 14:55:52 +05:30
Manav Rathi 9f58f1eeb3
Fix error on refresh while a folder watch is being set up
Notes:

From QA

> This error mostly happens if i add a watch folder and before watch folders
  start to upload and i refresh the app.

e is undefined in

    let {watches: e, removeWatch: n} = t;
    return 0 === e.length ? (0,...

Results in Next throwing

    Application error: a client-side exception has occurred (see the browser console for more information).
2024-05-27 14:42:56 +05:30
Manav Rathi 04be2b6a2c
Update electron updater
Trying to rule out https://github.com/electron-userland/electron-builder/issues/7127
2024-05-27 14:00:24 +05:30
laurenspriem 9f361237b1 [mob][photos] Fix cluster appbar not showing 2024-05-27 13:04:20 +05:30
Manav Rathi 8cb7cae7b7
[web] Fix display of auth codes on Safari (#1882) 2024-05-27 13:03:19 +05:30
Manav Rathi a2a209a849
[web] Fix display of codes on Safari 2024-05-27 12:59:32 +05:30
ashilkn d413c4f4c1 [mob][photos] Add try catch + logs for debugging in FaceMLDataDB 2024-05-27 12:57:25 +05:30
ashilkn ee8976e92b [mob][photos] Add schema migration easier on FaceMLDataDB 2024-05-27 12:56:20 +05:30
laurenspriem baa90c42ad [mob][photos] Remove stale comments 2024-05-27 11:59:36 +05:30
laurenspriem 30ade541df [mob][photos] Logging 2024-05-27 11:57:46 +05:30
laurenspriem 86fb8ebfaf [mob][photos] Fix indexing issue on iOS 2024-05-27 11:57:40 +05:30
laurenspriem b2e8c3c0eb [mob][photos] Remove restriction for ML for F-Droid 2024-05-27 11:51:20 +05:30
Ashil e203a8378e
[mob][photos] Trigger send logs if app is stuck in spalsh screen for >= 15 seconds (#1796) 2024-05-27 11:31:18 +05:30
laurenspriem b100f1d4bf [mob][photos] Catch and stopwatch on faces db creation 2024-05-27 11:28:05 +05:30
laurenspriem 7b4559f3ca [mob][photos] Reduce clustering frequency 2024-05-27 10:49:42 +05:30
Neeraj Gupta eac142025d
[mob] Increase limit to 50 for adding asset from device (#1873)
## Description

## Tests
2024-05-27 10:31:46 +05:30
Manav Rathi c5aa536c3b
[web] App context refactoring (#1879) 2024-05-26 22:03:33 +05:30
Manav Rathi 05406333e4
Split types 2024-05-26 21:55:16 +05:30
Manav Rathi 8ebd50606a
lf 2024-05-26 21:32:24 +05:30
Manav Rathi cbcfc243fc
lf 2024-05-26 21:02:48 +05:30
Manav Rathi 7d497b5ae1
Revert reimportability 2024-05-26 20:43:53 +05:30
Manav Rathi b28f6c3d8c
reduce auth 2024-05-26 20:31:32 +05:30
Manav Rathi 71a8049a35
reduce accounts 2024-05-26 20:28:59 +05:30
Manav Rathi e95cba0ace
Reduce boilerplate 2024-05-26 20:25:02 +05:30
Manav Rathi e836ada0d6
Refactor 2024-05-26 20:13:53 +05:30
Manav Rathi 19a104374d
Refactor 2024-05-26 19:49:23 +05:30
Manav Rathi 693ef45e2c
Refactor 2024-05-26 19:39:32 +05:30
Manav Rathi 55bdb070ce
Wrap 2024-05-26 19:14:35 +05:30
Manav Rathi 27127ff3d4
2fa 2024-05-26 19:12:12 +05:30
Manav Rathi 345c706814
ce 2024-05-26 19:07:48 +05:30
Manav Rathi 49133b7b86
Move 2024-05-26 19:02:47 +05:30
Manav Rathi 3a5311cdcc
cp 2024-05-26 18:58:57 +05:30
Neeraj Gupta 7182795732
[auth] New translations (#1836)
New translations from
[Crowdin](https://crowdin.com/project/ente-authenticator-app)
2024-05-26 18:55:51 +05:30
Manav Rathi ca00b3b558
creds 2024-05-26 18:55:20 +05:30
Manav Rathi 4bcb765810
[web] Passkey fixes (#1866)
@ua741 Not sure if passkey code is supposed to work on web yet, but I
was doing an unrelated change and noticed that clicking passkeys didn't
even try to redirect to accounts. I don't have a test setup for
passkeys, so don't know if these changes are 100% correct, but at least
now it redirects to accounts. Can test fully when doing final
integration.

- Use correct origin for passkey API requests
- Fix key length error
- Fix param name to match server
- Pass the token instead of a query param
2024-05-26 18:55:11 +05:30
Manav Rathi 17b49595a0
generate 2024-05-26 18:23:55 +05:30
Manav Rathi b99c573d3a
verify 2024-05-26 18:22:07 +05:30
Manav Rathi d3d3e4dbed
signup 2024-05-26 18:19:12 +05:30
Manav Rathi ba1af5eaf0
Move 2024-05-26 18:14:34 +05:30
Manav Rathi 14cf59c1e5
recover 2024-05-26 18:13:02 +05:30
Manav Rathi 452872156a
login 2024-05-26 18:10:22 +05:30
Manav Rathi 4f31bd625d
Context 2024-05-26 18:05:04 +05:30
Manav Rathi 6bf6f78147
Refactor app context types 2024-05-26 17:53:49 +05:30
Neeraj Gupta 5576f99548 [mob] Increase limit to 50 for adding asset from device 2024-05-26 16:55:31 +05:30
Manav Rathi 5bbe768acb
Scaffold 2024-05-26 16:06:29 +05:30
Manav Rathi babe378301
Move 2024-05-26 16:03:16 +05:30
Manav Rathi b2fda16561
Home route 2024-05-26 15:55:41 +05:30
Manav Rathi 6d289d73db
Add a new type 2024-05-26 15:50:02 +05:30
Manav Rathi 17acf4b3ee
[web] New translations (#1872)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2024-05-26 15:33:35 +05:30
Crowdin Bot 4d666d4b01 New Crowdin translations by GitHub Action 2024-05-26 10:00:34 +00:00
Manav Rathi 619f8319ed
[web] Title improvements - P1 (#1871)
Opening the PR to sync the translations, will make other changes
subsequently.
2024-05-26 15:24:51 +05:30
Manav Rathi 3261da3515
title 2024-05-26 15:19:05 +05:30
Manav Rathi d0d491f7f5
Pass the token instead of a query param 2024-05-26 08:36:57 +05:30
Manav Rathi db3764d448
Fix param name to match server 2024-05-26 08:36:57 +05:30
Manav Rathi 5fe5451f5c
Fix key length error
[error] failed to redirect to accounts page: TypeError: invalid key length
2024-05-26 08:36:57 +05:30
Manav Rathi 6d3d5d03f8
Use correct origin for passkey API requests 2024-05-26 08:36:57 +05:30
Manav Rathi 582eb9e1ea
[web] Enable Typescript's strict mode for auth's code (#1865) 2024-05-26 08:35:11 +05:30
Manav Rathi 51770a11ef
Tweak 2024-05-26 08:12:52 +05:30
Manav Rathi 1ea7a8f3a7
tweak 2024-05-26 07:20:52 +05:30
Manav Rathi b4536a7aee
[meta] Update issue template (#1864) 2024-05-26 05:31:17 +05:30
Manav Rathi 9d2be29fad
[meta] Update issue template 2024-05-26 05:16:36 +05:30
Manav Rathi f92a18efca
[server] Mention more details around s3 provider config (#1863) 2024-05-26 04:53:03 +05:30
Manav Rathi af382d483d
[server] Mention more details around s3 provider config 2024-05-26 04:50:44 +05:30
Manav Rathi 99f1ba799d
lhs of && cannot be a number
needs to be false for the hole
2024-05-25 20:56:46 +05:30
Manav Rathi 1548bcd378
Fix dialog 2024-05-25 20:30:43 +05:30
Vishnu Mohandas c2fc0a3d57
Update verification email address (#1855) 2024-05-25 18:48:50 +05:30
vishnukvmd 39a706ea20 Update verification email address 2024-05-25 18:47:19 +05:30
Manav Rathi 38d6464f55
muppets 2024-05-25 18:13:11 +05:30
Manav Rathi c5b6297cea
Wrap 2024-05-25 18:05:22 +05:30
Manav Rathi 390b4b1f81
Towards noUncheckedIndexedAccess 2024-05-25 17:44:49 +05:30
Manav Rathi b19b34b3dc
Prune 2024-05-25 17:39:45 +05:30
Manav Rathi 5690d613bb
tsc 2024-05-25 17:17:21 +05:30
Manav Rathi bb713cfc76
Cannot avoid a undefined initial app context 2024-05-25 17:14:08 +05:30
Manav Rathi 4a0c93373d
st 2024-05-25 17:00:51 +05:30
Manav Rathi b42759d473
tsc 2024-05-25 16:55:31 +05:30
Manav Rathi 2e93281368
tsc 2024-05-25 16:51:58 +05:30
Manav Rathi c18be32c09
Rearrange 2024-05-25 16:48:13 +05:30
Manav Rathi 650163c341
id is always sent be server 2024-05-25 16:40:28 +05:30
Manav Rathi d101208baa
tsc 2024-05-25 16:34:10 +05:30
Manav Rathi 76f7215269
Filter 2024-05-25 16:31:42 +05:30
Manav Rathi 621c482529
tsc 2024-05-25 16:27:46 +05:30
Manav Rathi 314c8f69f2
Comment out 2024-05-25 16:24:14 +05:30
Manav Rathi 1f45cf00c7
tsc 2024-05-25 16:20:47 +05:30
Manav Rathi e0e80ee91f
tsc 2024-05-25 16:08:50 +05:30
Manav Rathi 225278adb7
tsc 2024-05-25 16:06:24 +05:30
Manav Rathi 8d30bfbefa
tsc 2024-05-25 15:43:08 +05:30
Manav Rathi ad96f679c9
tsc 2024-05-25 15:39:20 +05:30
Manav Rathi 4b896d3aab
tsc 2024-05-25 15:37:05 +05:30
Manav Rathi 533e6d06e7
tsc 2024-05-25 15:32:56 +05:30
Manav Rathi e88b5c99ba
tsc 2024-05-25 15:29:01 +05:30
laurenspriem 1ec7e02695 [mob][photos] Copy change 2024-05-25 12:03:34 +05:30
Manav Rathi 19e08cf803
tsc 2024-05-25 10:15:43 +05:30
Manav Rathi 08073b927c
tsc 2024-05-25 10:12:40 +05:30
Manav Rathi 711a44412d
tsc 2024-05-25 10:08:14 +05:30
Manav Rathi c9f94f062b
tsc 2024-05-25 10:04:54 +05:30
Manav Rathi c8205b8475
tsc
The only place I can currently find where this code would run is on the delete
account dialog, where props.color is being passed.
2024-05-25 10:02:09 +05:30
Manav Rathi b0d3fcfe79
tsc 2024-05-25 09:38:45 +05:30
Manav Rathi 11a354c560
tsc 2024-05-25 09:37:07 +05:30
Manav Rathi 823f739c32
tsc 2024-05-25 09:31:09 +05:30
Manav Rathi f8876c8154
[docs] Add steam import guide to sidebar (#1850) 2024-05-25 08:37:35 +05:30
Manav Rathi 90db45d845
uploading 2024-05-25 08:35:41 +05:30
Manav Rathi 6a1f5945b9
pretty 2024-05-25 08:34:36 +05:30
Manav Rathi f7ca838428
Add to sidebar 2024-05-25 08:33:51 +05:30
Manav Rathi 2b065dd68d
yarn pretty 2024-05-25 08:32:00 +05:30
Manav Rathi f168ea9e1e
[docs] Mention troubleshooting tips for 403 forbidden when self-hosting (#1849) 2024-05-25 08:27:46 +05:30
Manav Rathi 58702103f3
Add link to example 2024-05-25 08:26:52 +05:30
Manav Rathi dfb3a6f65c
[docs] Add a section about 403 forbidden 2024-05-25 08:19:12 +05:30
Manav Rathi 491f38b120
tsc 2024-05-25 07:44:16 +05:30
Manav Rathi 79c0880c9c
tsc 2024-05-25 07:40:38 +05:30
Manav Rathi 834b8f78b7
opts 2024-05-25 07:39:24 +05:30
Manav Rathi cbf0336cd0
More 2024-05-25 07:37:53 +05:30
Manav Rathi 431d629641
Start tackling strict null 2024-05-25 07:35:07 +05:30
Manav Rathi 94c1cc011b
lf 2024-05-25 07:26:20 +05:30
Manav Rathi b26b0759d6
tsc 2024-05-25 07:10:47 +05:30
Manav Rathi d51fb99fd3
type for tsc 2024-05-25 06:34:13 +05:30
Manav Rathi 0379216e05
Remove sx prop (in prep for typing) 2024-05-25 06:30:21 +05:30
Manav Rathi ccd486f659
tsc 2024-05-25 06:22:11 +05:30
Manav Rathi ce3ab55069
tsc 2024-05-25 06:21:02 +05:30
Manav Rathi 34effef810
tsc 2024-05-25 06:19:01 +05:30
Manav Rathi 56aceb589d
tsc 2024-05-25 06:06:29 +05:30
Manav Rathi 92a2506f8a
Reduce prop scope 2024-05-25 06:02:47 +05:30
Manav Rathi e23bc2602f
Reorder 2024-05-25 06:01:06 +05:30
Manav Rathi 69beecb7bb
tsc
Omit<...,"inherit"> doesn't resolve

    Element implicitly has an 'any' type because expression of type 'OverridableStringUnion<"error" | "inherit" | "secondary" | "primary" | "info" | "success" | "warning", ButtonPropsColorOverrides>' can't be used to index type 'Palette'.
      Property 'inherit' does not exist on type 'Palette'.
2024-05-25 05:57:33 +05:30
Manav Rathi 880b13f436
Fix 2024-05-24 20:48:07 +05:30
Manav Rathi 9061caac99
Ditto 2024-05-24 20:43:32 +05:30
Manav Rathi 11cc8e46b7
Session storage shouldn't be undefined in newer browsers
Tried FF incognito
2024-05-24 20:41:11 +05:30
Manav Rathi 54820689c2
Towards removing implicit anys 2024-05-24 20:16:55 +05:30
Manav Rathi acebb86fec
Towards strict 2024-05-24 19:49:11 +05:30
Manav Rathi 367e09599d
Enable more 2024-05-24 19:43:10 +05:30
Manav Rathi b9fe509567
Enable noImplicitReturns 2024-05-24 19:41:37 +05:30
Manav Rathi 82bffd81de
[web] Tighten auth's tsconfig.json (#1846)
Ongoing process, just some steps in the direction we wish.
2024-05-24 19:03:53 +05:30
Manav Rathi 7340443b86
lf 2024-05-24 18:57:32 +05:30
Manav Rathi 2cd1dfd720
Chip away 2024-05-24 18:54:16 +05:30
Neeraj Gupta 3c8d29bcdc
[mob] Use custom assetPickerTextDelegate to use en as default (#1844)
## Description

## Tests
Tested locally
2024-05-24 18:24:48 +05:30
Laurens Priem 06a698ddbb
Face wake (#1843)
## Description

- Fix issue with thumbnail decoding in indexing
- Fix show correct cluster progress counter
- Add wakelock to ML settings page
- Show in settings when device health is low

## Tests

Tested in debug on my pixel
2024-05-24 18:22:00 +05:30
Manav Rathi 3b8c48e92d
Create a next specific base
The include still needs to be specified in the importing tsconfig otherwise the
"." is resolved relative to the @/build-config.
2024-05-24 18:17:59 +05:30
Neeraj Gupta 3c0cb20a9b [mob] Use custom assetPickerTextDelegate to use en as default 2024-05-24 18:13:09 +05:30
Manav Rathi 74bb169f0d
Equivalent to "**/*.ts", "**/*.tsx", "**/*.d.ts"
From the docs: https://www.typescriptlang.org/tsconfig/#include

> If the last path segment in a pattern does not contain a file extension or
  wildcard character, then it is treated as a directory, and files with
  supported extensions inside that directory are included (e.g. .ts, .tsx, and
  d.ts by default).
2024-05-24 17:54:05 +05:30
laurenspriem 302890baef [mob][photos] Fix for PlatformException in video thumbnails 2024-05-24 17:48:03 +05:30
Manav Rathi 54e33d3f42
Create a WIP replacement 2024-05-24 17:29:06 +05:30
Manav Rathi 0adb94f405
Link to @/build-config 2024-05-24 17:17:55 +05:30
Manav Rathi 7d634aa703
Add a note 2024-05-24 17:16:16 +05:30
laurenspriem b1e0c83733 [mob][photos] Show pause status copy when device is unhealthy 2024-05-24 17:04:35 +05:30
laurenspriem d4af7792d4 [mob][photos] Forgot this in previous commit 2024-05-24 16:40:14 +05:30
laurenspriem f301ab57f2 [mob][photos] Use EnteWakeLock in ML settings page 2024-05-24 16:39:42 +05:30
laurenspriem 7b0f5909b5 [mob][photos] Ente wakelock utility 2024-05-24 16:39:24 +05:30
laurenspriem e9064f6904 [mob][photos] Correct cluster progress counter 2024-05-24 16:29:00 +05:30
laurenspriem 0d21fc77b5 [mob][photos] Keep ML settings page awake 2024-05-24 14:45:16 +05:30
Manav Rathi b26c6e9c0d
[web] Auth - Improve HOTP support (#1842)
- Use HOTP counter
- Don't advance the bar for HOTPs
2024-05-24 14:43:57 +05:30
Manav Rathi 057d11f39b
Fix typo 2024-05-24 14:38:49 +05:30
Manav Rathi c9de6d7a82
Don't advance the bar for HOTPs 2024-05-24 14:35:59 +05:30
Manav Rathi 698ac9f29e
Use HOTP counter 2024-05-24 14:30:05 +05:30
Manav Rathi a0d26c860c
[web] Fix auth ticker (#1841) 2024-05-24 14:16:32 +05:30
Manav Rathi bd2444d353
[web] Fix auth ticker 2024-05-24 14:11:56 +05:30
Manav Rathi ca24a86179
[web] Steam support on web version of auth (#1840) 2024-05-24 14:01:06 +05:30
Manav Rathi fffe96a4c7
Tweak 2024-05-24 13:49:21 +05:30
Manav Rathi 0ec75c2435
Parse the type 2024-05-24 13:47:11 +05:30
Manav Rathi cb78c848d6
Impl 2024-05-24 13:36:55 +05:30
Manav Rathi 6594db9393
Encode counter 2024-05-24 13:26:16 +05:30
Manav Rathi f6c40ee67d
fromBase32 is exposed in the library API 2024-05-24 13:18:42 +05:30
Manav Rathi 36aa33ed5a
Move to separate file 2024-05-24 13:08:41 +05:30
Neeraj Gupta 776dba4fb0
Face small improvements (#1839)
## Description

- Fix embeddings fetch issue
- Decrypt embeddings in computer
- Change clustering sorting and remove restrictions
- Cleaned up faces status page


## Tests

Tested in debug mode on pixel phone.
2024-05-24 12:52:41 +05:30
laurenspriem 7f49f530c5 [mob][photos] Bump 2024-05-24 12:47:10 +05:30
laurenspriem ef6fe80944 [mob][photos] Fix 400 on embedding fetch 2024-05-24 12:44:01 +05:30
Manav Rathi 370b28f9e4
Type 2024-05-24 12:39:06 +05:30
Manav Rathi 05e737cb11
Add steam as a type 2024-05-24 12:32:58 +05:30
laurenspriem 0fdb58eda1 [mob][photos] Force clustering first if too many unclustered faces 2024-05-24 12:30:22 +05:30
Manav Rathi 1ce90839fe
Remove type from auth UI 2024-05-24 12:18:28 +05:30
Manav Rathi 697946f415
Scaffold 2024-05-24 12:12:06 +05:30
laurenspriem cc91cb8012 [mob][photos] Correct mistake 2024-05-24 11:16:40 +05:30
Manav Rathi 754de7065f
[web] Auth cleanup - Part 3/3 (#1838)
Prep done.
2024-05-24 11:02:45 +05:30
laurenspriem 5587373b42 [mob][photos] Remove clustering restriction based on indexed amount 2024-05-24 11:00:05 +05:30
laurenspriem f1d1a4a9e1 [mob][photos] Clustering sort to cluster new files first 2024-05-24 10:57:27 +05:30
Manav Rathi dc38a8bc9f
Account for node/browser discrepancy 2024-05-24 10:51:19 +05:30
laurenspriem edf9f743f4 [mob][photos] Prefer using getFileIdFromFaceId 2024-05-24 10:27:16 +05:30
Manav Rathi fec040e528
Tweak error report 2024-05-24 10:20:58 +05:30
laurenspriem 86f96a5713 [mob][photos] Show intermediate clustering results 2024-05-24 10:19:24 +05:30
laurenspriem c3fb472287 [mob][photos] Fix clustering progress number 2024-05-24 10:18:17 +05:30
Manav Rathi eaf8b9cebc
Also include same workaround as mobile app 2024-05-24 10:10:59 +05:30
Manav Rathi 2ce9212457
We encodeURIComponent the pathname 2024-05-24 09:58:50 +05:30
laurenspriem 4fa59ce258 [mob][photos] Common ml util for getting indexable files across faces and clip 2024-05-24 09:56:10 +05:30
Manav Rathi 59ed89cba1
.get returns null when the property is not present 2024-05-24 09:49:20 +05:30
Manav Rathi 623b71715d
Wrap 2024-05-24 09:42:23 +05:30
laurenspriem a74943698f Merge remote-tracking branch 'origin/main' into face_small_improvements 2024-05-24 09:37:53 +05:30
Manav Rathi bfe8fd83ac
Take 2 2024-05-24 09:29:54 +05:30
Manav Rathi 0a01cac57b
Take 1 (incorrect) 2024-05-24 09:27:28 +05:30
Crowdin Bot b7f248fa93 New Crowdin translations by GitHub Action 2024-05-24 02:06:42 +00:00
Manav Rathi d814b6cdf0
Use standard URL parsing - WIP 1 2024-05-23 21:01:18 +05:30
Manav Rathi 1712bf60cb
[web] Auth cleanup - Part 2/x (#1834)
Preparing for steam support (sibling of
https://github.com/ente-io/ente/pull/1820)
2024-05-23 20:36:08 +05:30
Manav Rathi 369a5a5233
lf 2024-05-23 20:19:20 +05:30
Manav Rathi 9bae31d748
Parse 2024-05-23 19:38:23 +05:30
Manav Rathi 11453b327f
Improve docs with hints from otpauth
https://github.com/hectorm/otpauth
2024-05-23 19:34:53 +05:30
Manav Rathi 7780c1c7b7
Move to the correct place 2024-05-23 19:29:56 +05:30
Manav Rathi 0f1c98d0d0
Reword 2024-05-23 19:22:45 +05:30
Manav Rathi 48fcbdc98c
Reword 2024-05-23 19:10:42 +05:30
Manav Rathi 90d0196d47
Extract logic 2024-05-23 19:06:06 +05:30
Ashil 30a8691c7f
[mob][photos] Fix infinite loading on searching (#1830)
## Description

Search was infinitely loading even after all search results are ready.
2024-05-23 18:59:36 +05:30
Manav Rathi 69cea6786d
Redistr 2024-05-23 18:54:55 +05:30
laurenspriem ccac5e73a3 [mob][photos] Remove found faces from status 2024-05-23 18:13:47 +05:30
laurenspriem 3e79c8cf28 [mob][photos] Decrypt remote embeddings in computer 2024-05-23 18:12:41 +05:30
Neeraj Gupta 31dee1249d
Steam Authenticator migration guide (#1825)
A quick guide on how to use steamguard-cli to generate a Steam 2FA QR
code for Ente Auth

Inspired by
https://github.com/beemdevelopment/Aegis/wiki/Adding-Steam-to-Aegis-from-steamguard-cli,
but updated to utilize the latest flags provide by the steamguard-cli

addresses this:
https://github.com/ente-io/ente/discussions/1038#discussioncomment-9520070
2024-05-23 17:13:50 +05:30
Neeraj Gupta e5a293a6ab
Dart UI isolate fix (#1829)
## Description

Forgot to bump version in previous PR
2024-05-23 17:10:08 +05:30
laurenspriem ffcb68b32f [mob][photos] Bump 2024-05-23 17:05:15 +05:30
laurenspriem a8af90dfee [mob][photos] Bump 2024-05-23 17:02:47 +05:30
Neeraj Gupta 6ee38cb291
Dart UI isolate fix (#1828)
## Description

- Fix for using dart_ui_isolate package properly

## Test

Neeraj tested it
2024-05-23 16:45:17 +05:30
laurenspriem 3810df1b20 [mob][photos] Fix for dart_ui_isolate 2024-05-23 16:37:34 +05:30
laurenspriem cc8e345a17 Revert "[mob][photos] Revert back to FlutterIsolate"
This reverts commit c4a6011621.
2024-05-23 16:35:45 +05:30
laurenspriem 63653411b8 [mob][photos] Logs 2024-05-23 16:33:21 +05:30
laurenspriem c4a6011621 [mob][photos] Revert back to FlutterIsolate 2024-05-23 16:32:25 +05:30
Manav Rathi 1ee52c780f
[desktop] Allow refreshing when inside an album (#1827)
Steps to reproduce on Linux:

- Open an album
- Open a photo
- View > Reload

Causes a 404 page to be displayed.
2024-05-23 16:17:41 +05:30
Manav Rathi b402662c09
[desktop] Allow refreshing when inside an album
Steps to reproduce on Linux:

- Open an album
- Open a photo
- View > Reload

Causes a 404 page to be displayed.
2024-05-23 16:13:21 +05:30
Rex Ng 51756d45d9
Steam Authenticator migration guide
guide on how to use steamguard-cli to generate a qr code for Ente Auth
2024-05-23 17:41:15 +08:00
Neeraj Gupta a3bb7ad85a
[mob][photos] Use flutter 3.22 for internal build (#1824) 2024-05-23 14:51:44 +05:30
laurenspriem 17058299c1 [mob][photos] Use flutter 3.22 for internal build 2024-05-23 14:50:37 +05:30
Laurens Priem 65de02d8d9
Face fix (#1823)
## Description

- Bug fixes
- Logging

## Tests

Tested on my pixel phone with remote embedding fetch disabled.
2024-05-23 14:42:04 +05:30
Laurens Priem 1f9e222d6e
Merge branch 'main' into face_fix 2024-05-23 14:40:26 +05:30
Manav Rathi 3d96be6c27
[desktop] Keep integral millisecond precision for modified time (#1822)
Fixes the following upload:

> metadata: {title: xxx.jpeg, creationTime: 1715925330480368.8,
modificationTime: 1715925330480368.8, latitude: null, longitude: null,
fileType: 0, hash: ...}

Related: https://github.com/ente-io/ente/pull/1821
2024-05-23 14:38:31 +05:30
laurenspriem 1bbe495306 [mob][photos] Bump 2024-05-23 14:36:17 +05:30
laurenspriem a76f3ca1b3 [mob][photos] Logging 2024-05-23 14:35:22 +05:30
laurenspriem 7800b7db32 [mob][photos] Regularly check for wifi 2024-05-23 14:35:15 +05:30
Manav Rathi ea2a355bcc
Revert to the behaviour of the existing 1.6.63 client 2024-05-23 14:34:24 +05:30
laurenspriem d585b75514 [mob][photos] Logging 2024-05-23 14:27:29 +05:30
Manav Rathi 5caa32b1e0
Also add for zip reading 2024-05-23 14:27:17 +05:30
laurenspriem 11402d7819 [mob][photos] Fix indexing pausing 2024-05-23 14:27:12 +05:30
Ashil a41f705dad
Upgrade to flutter 3.22.0 (#1804) 2024-05-23 14:17:47 +05:30
Neeraj Gupta 69b808e62c
[mobile] New translations (#1788)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-app)
2024-05-23 14:10:39 +05:30
laurenspriem 1e1e629891 [mob][photos] Set parallel fetch to five 2024-05-23 14:07:04 +05:30
Manav Rathi a7e96d055c
[web] Auth cleanup - Part 1/x (#1820)
In preparation for adding steam support
2024-05-23 13:42:34 +05:30
Manav Rathi 5e2261f793
Unclass 2024-05-23 13:36:44 +05:30
Manav Rathi 206be5c16f
Document 2024-05-23 13:19:05 +05:30
Manav Rathi 41c87efc5a
Use the union 2024-05-23 13:07:33 +05:30
Manav Rathi 171af35d85
Reword 2024-05-23 13:06:27 +05:30
Manav Rathi 99f47dc1ae
Move into the function 2024-05-23 13:03:31 +05:30
Manav Rathi 26436f116f
Nonopt 2024-05-23 12:58:47 +05:30
Manav Rathi 14655e5633
Fix 2024-05-23 12:47:29 +05:30
Manav Rathi 51dc8d1de6
Rearrange 2024-05-23 12:40:35 +05:30
Manav Rathi 51568e6c56
non optional 2024-05-23 12:20:04 +05:30
Manav Rathi d2743f4121
Unclass 2024-05-23 12:16:02 +05:30
Manav Rathi 2504046e26
Move 2024-05-23 12:11:11 +05:30
Manav Rathi a104f36561
Inline 2024-05-23 12:06:54 +05:30
Manav Rathi b26afdcf2e
Inline 2024-05-23 11:43:35 +05:30
Manav Rathi bf707ae02d
Inline 2024-05-23 11:37:55 +05:30
Manav Rathi 68648d2f6c
Remove nesting 2024-05-23 11:35:44 +05:30
Crowdin Bot 5724fad813 New Crowdin translations by GitHub Action 2024-05-21 01:57:44 +00:00
299 changed files with 2896 additions and 2363 deletions

View file

@ -4,11 +4,12 @@ labels: ["triage"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: > value: |
Before opening a new issue, please ensure you are on the latest Before opening a new bug report, please ensure
version (it might've already been fixed), and that you've searched 1. you are on the latest version (it might've already been fixed),
for existing issues (please add you observations as a comment 2. you've searched for existing issues (please add your observations as a comment there instead of creating a duplicate).
there instead of creating a duplicate).
If you are self hosting, please create a community [Q&A](https://github.com/ente-io/ente/discussions/categories/q-a) instead.
- type: textarea - type: textarea
attributes: attributes:
label: Description label: Description
@ -16,7 +17,8 @@ body:
Please describe the bug. If possible, also include the steps to Please describe the bug. If possible, also include the steps to
reproduce the behaviour, and the expected behaviour (sometimes reproduce the behaviour, and the expected behaviour (sometimes
bugs are just expectation mismatches, in which case this would be bugs are just expectation mismatches, in which case this would be
a good fit for Discussions). a good fit for [feature
requests](https://github.com/ente-io/ente/discussions/categories/feature-requests)).
validations: validations:
required: true required: true
- type: input - type: input

View file

@ -4,7 +4,7 @@ on:
workflow_dispatch: # Allow manually running the action workflow_dispatch: # Allow manually running the action
env: env:
FLUTTER_VERSION: "3.19.3" FLUTTER_VERSION: "3.22.0"
jobs: jobs:
build: build:

View file

@ -9,7 +9,7 @@ on:
- ".github/workflows/mobile-lint.yml" - ".github/workflows/mobile-lint.yml"
env: env:
FLUTTER_VERSION: "3.19.5" FLUTTER_VERSION: "3.22.0"
jobs: jobs:
lint: lint:

View file

@ -20,6 +20,8 @@
"codeIssuerHint": "発行者", "codeIssuerHint": "発行者",
"codeSecretKeyHint": "秘密鍵", "codeSecretKeyHint": "秘密鍵",
"codeAccountHint": "アカウント (you@domain.com)", "codeAccountHint": "アカウント (you@domain.com)",
"codeTagHint": "タグ",
"accountKeyType": "鍵の種類",
"sessionExpired": "セッションが失効しました", "sessionExpired": "セッションが失効しました",
"@sessionExpired": { "@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired" "description": "Title of the dialog when the users current session is invalid/expired"
@ -77,6 +79,7 @@
"data": "データ", "data": "データ",
"importCodes": "コードをインポート", "importCodes": "コードをインポート",
"importTypePlainText": "プレーンテキスト", "importTypePlainText": "プレーンテキスト",
"importTypeEnteEncrypted": "Ente 暗号化されたエクスポート",
"passwordForDecryptingExport": "復号化用パスワード", "passwordForDecryptingExport": "復号化用パスワード",
"passwordEmptyError": "パスワードは空欄にできません", "passwordEmptyError": "パスワードは空欄にできません",
"importFromApp": "{appName} からコードをインポート", "importFromApp": "{appName} からコードをインポート",
@ -121,6 +124,7 @@
"suggestFeatures": "機能を提案", "suggestFeatures": "機能を提案",
"faq": "FAQ", "faq": "FAQ",
"faq_q_1": "Authはどのくらい安全ですか", "faq_q_1": "Authはどのくらい安全ですか",
"faq_a_1": "Ente Authでバックアップされたコードはすべてエンドツーエンドで暗号化されて保存されます。つまり、コードにアクセスできるのはあなただけです。当社のアプリはオープンソースであり、暗号化技術は外部監査を受けています。",
"faq_q_2": "パソコンから私のコードにアクセスできますか?", "faq_q_2": "パソコンから私のコードにアクセスできますか?",
"faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。", "faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。",
"faq_q_3": "コードを削除するにはどうすればいいですか?", "faq_q_3": "コードを削除するにはどうすればいいですか?",
@ -154,6 +158,7 @@
} }
} }
}, },
"invalidQRCode": "QRコードが無効です",
"noRecoveryKeyTitle": "回復キーがありませんか?", "noRecoveryKeyTitle": "回復キーがありませんか?",
"enterEmailHint": "メールアドレスを入力してください", "enterEmailHint": "メールアドレスを入力してください",
"invalidEmailTitle": "メールアドレスが無効です", "invalidEmailTitle": "メールアドレスが無効です",
@ -347,6 +352,7 @@
"deleteCodeAuthMessage": "コードを削除するためには認証が必要です", "deleteCodeAuthMessage": "コードを削除するためには認証が必要です",
"showQRAuthMessage": "QR コードを表示するためには認証が必要です", "showQRAuthMessage": "QR コードを表示するためには認証が必要です",
"confirmAccountDeleteTitle": "アカウントの削除に同意", "confirmAccountDeleteTitle": "アカウントの削除に同意",
"confirmAccountDeleteMessage": "このアカウントは他のEnteアプリも使用している場合はそれらにも紐づけされています。\nすべてのEnteアプリでアップロードされたデータは削除され、アカウントは完全に削除されます。",
"androidBiometricHint": "本人を確認する", "androidBiometricHint": "本人を確認する",
"@androidBiometricHint": { "@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
@ -417,5 +423,18 @@
"invalidEndpoint": "無効なエンドポイントです", "invalidEndpoint": "無効なエンドポイントです",
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。", "invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
"endpointUpdatedMessage": "エンドポイントの更新に成功しました", "endpointUpdatedMessage": "エンドポイントの更新に成功しました",
"customEndpoint": "{endpoint} に接続しました" "customEndpoint": "{endpoint} に接続しました",
"pinText": "固定",
"unpinText": "固定を解除",
"pinnedCodeMessage": "{code} を固定しました",
"unpinnedCodeMessage": "{code} の固定が解除されました",
"tags": "タグ",
"createNewTag": "新しいタグの作成",
"tag": "タグ",
"create": "作成",
"editTag": "タグの編集",
"deleteTagTitle": "タグを削除しますか?",
"deleteTagMessage": "このタグを削除してもよろしいですか?この操作は取り消しできません。",
"somethingWentWrongParsingCode": "{x} のコードを解析できませんでした。",
"updateNotAvailable": "アップデートは利用できません"
} }

View file

@ -30,7 +30,7 @@
"compare-versions": "^6.1", "compare-versions": "^6.1",
"electron-log": "^5.1", "electron-log": "^5.1",
"electron-store": "^8.2", "electron-store": "^8.2",
"electron-updater": "^6.1", "electron-updater": "^6.2",
"ffmpeg-static": "^5.2", "ffmpeg-static": "^5.2",
"html-entities": "^2.5", "html-entities": "^2.5",
"jpeg-js": "^0.4", "jpeg-js": "^0.4",

View file

@ -106,7 +106,7 @@ const handleRead = async (path: string) => {
res.headers.set("Content-Length", `${fileSize}`); res.headers.set("Content-Length", `${fileSize}`);
// Add the file's last modified time (as epoch milliseconds). // Add the file's last modified time (as epoch milliseconds).
const mtimeMs = stat.mtimeMs; const mtimeMs = stat.mtime.getTime();
res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`); res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`);
} }
return res; return res;
@ -132,6 +132,13 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
// Close the zip handle when the underlying stream closes. // Close the zip handle when the underlying stream closes.
stream.on("end", () => void zip.close()); stream.on("end", () => void zip.close());
// While it is documented that entry.time is the modification time,
// the units are not mentioned. By seeing the source code, we can
// verify that it is indeed epoch milliseconds. See `parseZipTime`
// in the node-stream-zip source,
// https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js
const modifiedMs = entry.time;
return new Response(webReadableStream, { return new Response(webReadableStream, {
headers: { headers: {
// We don't know the exact type, but it doesn't really matter, just // We don't know the exact type, but it doesn't really matter, just
@ -139,12 +146,7 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
// doesn't tinker with it thinking of it as text. // doesn't tinker with it thinking of it as text.
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
"Content-Length": `${entry.size}`, "Content-Length": `${entry.size}`,
// While it is documented that entry.time is the modification time, "X-Last-Modified-Ms": `${modifiedMs}`,
// the units are not mentioned. By seeing the source code, we can
// verify that it is indeed epoch milliseconds. See `parseZipTime`
// in the node-stream-zip source,
// https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js
"X-Last-Modified-Ms": `${entry.time}`,
}, },
}); });
}; };

View file

@ -743,10 +743,10 @@ buffer@^5.1.0, buffer@^5.5.0:
base64-js "^1.3.1" base64-js "^1.3.1"
ieee754 "^1.1.13" ieee754 "^1.1.13"
builder-util-runtime@9.2.3: builder-util-runtime@9.2.4:
version "9.2.3" version "9.2.4"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a"
integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw== integrity sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==
dependencies: dependencies:
debug "^4.3.4" debug "^4.3.4"
sax "^1.2.4" sax "^1.2.4"
@ -1251,12 +1251,12 @@ electron-store@^8.2:
conf "^10.2.0" conf "^10.2.0"
type-fest "^2.17.0" type-fest "^2.17.0"
electron-updater@^6.1: electron-updater@^6.2:
version "6.1.8" version "6.2.1"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8" resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.2.1.tgz#1c9adb9ba2a21a5dc50a8c434c45360d5e9fe6c9"
integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ== integrity sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==
dependencies: dependencies:
builder-util-runtime "9.2.3" builder-util-runtime "9.2.4"
fs-extra "^10.1.0" fs-extra "^10.1.0"
js-yaml "^4.1.0" js-yaml "^4.1.0"
lazy-val "^1.0.5" lazy-val "^1.0.5"

View file

@ -163,6 +163,10 @@ export const sidebar = [
text: "From Authy", text: "From Authy",
link: "/auth/migration-guides/authy/", link: "/auth/migration-guides/authy/",
}, },
{
text: "From Steam",
link: "/auth/migration-guides/steam/",
},
{ {
text: "Exporting your data", text: "Exporting your data",
link: "/auth/migration-guides/export", link: "/auth/migration-guides/export",

View file

@ -7,4 +7,5 @@ description:
# Migrating to/from Ente Auth # Migrating to/from Ente Auth
- [Migrating from Authy](authy/) - [Migrating from Authy](authy/)
- [Importing codes from Steam](steam/)
- [Exporting your data out of Ente Auth](export) - [Exporting your data out of Ente Auth](export)

View file

@ -0,0 +1,79 @@
---
title: Migrating from Steam Authenticator
description: Guide for importing from Steam Authenticator to Ente Auth
---
# Migrating from Steam Authenticator
A guide written by an ente.io lover
> [!WARNING]
>
> Steam Authenticator code is only supported after auth-v3.0.3, check the app's
> version number before migration.
One way to migrate is to
[use this tool by dyc3](https://github.com/dyc3/steamguard-cli/releases/latest)
to simplify the process and skip directly to generating a qr code to Ente
Authenticator.
## Download/Install steamguard-cli
### Windows
1. Download `steamguard.exe` from the [releases page][releases].
2. Place `steamguard.exe` in a folder of your choice. For this example, we will
use `%USERPROFILE%\Desktop`.
3. Open Powershell or Command Prompt. The prompt should be at `%USERPROFILE%`
(eg. `C:\Users\<username>`).
4. Use `cd` to change directory into the folder where you placed
`steamguard.exe`. For this example, it would be `cd Desktop`.
5. You should now be able to run `steamguard.exe` by typing
`.\steamguard.exe --help` and pressing enter.
### Linux
#### Ubuntu/Debian
1. Download the `.deb` from the [releases page][releases].
2. Open a terminal and run this to install it:
```bash
sudo dpkg -i ./steamguard-cli_<version>_amd64.deb
```
#### Other Linux
1. Download `steamguard` from the [releases page][releases]
2. Make it executable, and move `steamguard` to `/usr/local/bin` or any other
directory in your `$PATH`.
```bash
chmod +x ./steamguard
sudo mv ./steamguard /usr/local/bin
```
3. You should now be able to run `steamguard` by typing `steamguard --help` and
pressing enter.
## Login to Steam account
Set up a new account with steamguard-cli
```bash
steamguard setup # set up a new account with steamguard-cli
```
## Generate & importing QR codes
steamguard-cli can then generate a QR code for your 2FA secret.
```bash
steamguard qr # print QR code for the first account in your maFiles
steamguard -u <account name> qr # print QR code for a specific account
```
Open Ente Auth, press the '+' button, select `Scan a QR code`, and scan the qr
code.
You should now have your steam code inside Ente Auth

View file

@ -78,3 +78,23 @@ To summarize:
Set the S3 bucket `endpoint` in `credentials.yaml` to a `yourserverip:3200` or Set the S3 bucket `endpoint` in `credentials.yaml` to a `yourserverip:3200` or
some such IP/hostname that accessible from both where you are running the Ente some such IP/hostname that accessible from both where you are running the Ente
clients (e.g. the mobile app) and also from within the Docker compose cluster. clients (e.g. the mobile app) and also from within the Docker compose cluster.
### 403 Forbidden
If museum (`2`) is able to make a network connection to your S3 bucket (`3`) but
uploads are still failing, it could be a credentials or permissions issue. A
telltale sign of this is that in the museum logs you can see `403 Forbidden`
errors about it not able to find the size of a file even though the
corresponding object exists in the S3 bucket.
To fix these, you should ensure the following:
1. The bucket CORS rules do not allow museum to access these objects.
> For uploading files from the browser, you will need to currently set
> allowedOrigins to "\*", and allow the "X-Auth-Token", "X-Client-Package"
> headers configuration too.
> [Here is an example of a working configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204).
2. The credentials are not being picked up (you might be setting the correct
creds, but not in the place where museum picks them from).

View file

@ -46,7 +46,7 @@ You can alternatively install the build from PlayStore or F-Droid.
## 🧑‍💻 Building from source ## 🧑‍💻 Building from source
1. [Install Flutter v3.19.3](https://flutter.dev/docs/get-started/install). 1. [Install Flutter v3.22.0](https://flutter.dev/docs/get-started/install).
2. Pull in all submodules with `git submodule update --init --recursive` 2. Pull in all submodules with `git submodule update --init --recursive`

View file

@ -427,7 +427,7 @@ SPEC CHECKSUMS:
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892 in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892
integration_test: 13825b8a9334a850581300559b8839134b124670 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98 local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9

View file

@ -35,10 +35,10 @@ import 'package:photos/services/sync_service.dart';
import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_uploader.dart'; import 'package:photos/utils/file_uploader.dart';
import 'package:photos/utils/validator_util.dart'; import 'package:photos/utils/validator_util.dart';
import "package:photos/utils/wakelock_util.dart";
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import "package:tuple/tuple.dart"; import "package:tuple/tuple.dart";
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class Configuration { class Configuration {
Configuration._privateConstructor(); Configuration._privateConstructor();
@ -585,7 +585,7 @@ class Configuration {
Future<void> setShouldKeepDeviceAwake(bool value) async { Future<void> setShouldKeepDeviceAwake(bool value) async {
await _preferences.setBool(keyShouldKeepDeviceAwake, value); await _preferences.setBool(keyShouldKeepDeviceAwake, value);
await WakelockPlus.toggle(enable: value); await EnteWakeLock.toggle(enable: value);
} }
Future<void> setShouldBackupVideos(bool value) async { Future<void> setShouldBackupVideos(bool value) async {

View file

@ -69,6 +69,8 @@ const galleryGridSpacing = 2.0;
const kSearchSectionLimit = 9; const kSearchSectionLimit = 9;
const maxPickAssetLimit = 50;
const iOSGroupID = "group.io.ente.frame.SlideshowWidget"; const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'

View file

@ -13,6 +13,8 @@ import "package:photos/face/model/face.dart";
import "package:photos/models/file/file.dart"; import "package:photos/models/file/file.dart";
import "package:photos/services/machine_learning/face_ml/face_clustering/face_info_for_clustering.dart"; import "package:photos/services/machine_learning/face_ml/face_clustering/face_info_for_clustering.dart";
import 'package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart'; import 'package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart';
import "package:photos/services/machine_learning/face_ml/face_ml_result.dart";
import "package:photos/utils/ml_util.dart";
import 'package:sqlite_async/sqlite_async.dart'; import 'package:sqlite_async/sqlite_async.dart';
/// Stores all data for the FacesML-related features. The database can be accessed by `FaceMLDataDB.instance.database`. /// Stores all data for the FacesML-related features. The database can be accessed by `FaceMLDataDB.instance.database`.
@ -33,6 +35,15 @@ class FaceMLDataDB {
static final FaceMLDataDB instance = FaceMLDataDB._privateConstructor(); static final FaceMLDataDB instance = FaceMLDataDB._privateConstructor();
static final _migrationScripts = [
createFacesTable,
createFaceClustersTable,
createClusterPersonTable,
createClusterSummaryTable,
createNotPersonFeedbackTable,
fcClusterIDIndex,
];
// only have a single app-wide reference to the database // only have a single app-wide reference to the database
static Future<SqliteDatabase>? _sqliteAsyncDBFuture; static Future<SqliteDatabase>? _sqliteAsyncDBFuture;
@ -48,17 +59,42 @@ class FaceMLDataDB {
_logger.info("Opening sqlite_async access: DB path " + databaseDirectory); _logger.info("Opening sqlite_async access: DB path " + databaseDirectory);
final asyncDBConnection = final asyncDBConnection =
SqliteDatabase(path: databaseDirectory, maxReaders: 2); SqliteDatabase(path: databaseDirectory, maxReaders: 2);
await _onCreate(asyncDBConnection); final stopwatch = Stopwatch()..start();
_logger.info("FaceMLDataDB: Starting migration");
await _migrate(asyncDBConnection);
_logger.info(
"FaceMLDataDB Migration took ${stopwatch.elapsedMilliseconds} ms",
);
stopwatch.stop();
return asyncDBConnection; return asyncDBConnection;
} }
Future<void> _onCreate(SqliteDatabase asyncDBConnection) async { Future<void> _migrate(
await asyncDBConnection.execute(createFacesTable); SqliteDatabase database,
await asyncDBConnection.execute(createFaceClustersTable); ) async {
await asyncDBConnection.execute(createClusterPersonTable); final result = await database.execute('PRAGMA user_version');
await asyncDBConnection.execute(createClusterSummaryTable); final currentVersion = result[0]['user_version'] as int;
await asyncDBConnection.execute(createNotPersonFeedbackTable); final toVersion = _migrationScripts.length;
await asyncDBConnection.execute(fcClusterIDIndex);
if (currentVersion < toVersion) {
_logger.info("Migrating database from $currentVersion to $toVersion");
await database.writeTransaction((tx) async {
for (int i = currentVersion + 1; i <= toVersion; i++) {
try {
await tx.execute(_migrationScripts[i - 1]);
} catch (e) {
_logger.severe("Error running migration script index ${i - 1}", e);
rethrow;
}
}
await tx.execute('PRAGMA user_version = $toVersion');
});
} else if (currentVersion > toVersion) {
throw AssertionError(
"currentVersion($currentVersion) cannot be greater than toVersion($toVersion)",
);
}
} }
// bulkInsertFaces inserts the faces in the database in batches of 1000. // bulkInsertFaces inserts the faces in the database in batches of 1000.
@ -193,10 +229,10 @@ class FaceMLDataDB {
final db = await instance.asyncDB; final db = await instance.asyncDB;
await db.execute(deleteFacesTable); await db.execute(deleteFacesTable);
await db.execute(dropClusterPersonTable); await db.execute(deleteFaceClustersTable);
await db.execute(dropClusterSummaryTable); await db.execute(deleteClusterPersonTable);
await db.execute(deletePersonTable); await db.execute(deleteClusterSummaryTable);
await db.execute(dropNotPersonFeedbackTable); await db.execute(deleteNotPersonFeedbackTable);
} }
Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster( Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster(
@ -249,7 +285,7 @@ class FaceMLDataDB {
final List<int> fileId = [recentFileID]; final List<int> fileId = [recentFileID];
int? avatarFileId; int? avatarFileId;
if (avatarFaceId != null) { if (avatarFaceId != null) {
avatarFileId = int.tryParse(avatarFaceId.split('_')[0]); avatarFileId = tryGetFileIdFromFaceId(avatarFaceId);
if (avatarFileId != null) { if (avatarFileId != null) {
fileId.add(avatarFileId); fileId.add(avatarFileId);
} }
@ -401,8 +437,10 @@ class FaceMLDataDB {
final personID = map[personIdColumn] as String; final personID = map[personIdColumn] as String;
final clusterID = map[fcClusterID] as int; final clusterID = map[fcClusterID] as int;
final faceID = map[fcFaceId] as String; final faceID = map[fcFaceId] as String;
result.putIfAbsent(personID, () => {}).putIfAbsent(clusterID, () => {}) result
.add(faceID); .putIfAbsent(personID, () => {})
.putIfAbsent(clusterID, () => {})
.add(faceID);
} }
return result; return result;
} }
@ -476,8 +514,7 @@ class FaceMLDataDB {
for (final map in maps) { for (final map in maps) {
final clusterID = map[fcClusterID] as int; final clusterID = map[fcClusterID] as int;
final faceID = map[fcFaceId] as String; final faceID = map[fcFaceId] as String;
final x = faceID.split('_').first; final fileID = getFileIdFromFaceId(faceID);
final fileID = int.parse(x);
result[fileID] = (result[fileID] ?? {})..add(clusterID); result[fileID] = (result[fileID] ?? {})..add(clusterID);
} }
return result; return result;
@ -665,19 +702,55 @@ class FaceMLDataDB {
return maps.first['count'] as int; return maps.first['count'] as int;
} }
Future<int> getClusteredFaceCount() async { Future<int> getClusteredOrFacelessFileCount() async {
final db = await instance.asyncDB; final db = await instance.asyncDB;
final List<Map<String, dynamic>> maps = await db.getAll( final List<Map<String, dynamic>> clustered = await db.getAll(
'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable', 'SELECT $fcFaceId FROM $faceClustersTable',
); );
return maps.first['count'] as int; final Set<int> clusteredFileIDs = {};
for (final map in clustered) {
final int fileID = getFileIdFromFaceId(map[fcFaceId] as String);
clusteredFileIDs.add(fileID);
}
final List<Map<String, dynamic>> badFacesFiles = await db.getAll(
'SELECT DISTINCT $fileIDColumn FROM $facesTable WHERE $faceScore <= $kMinimumQualityFaceScore OR $faceBlur <= $kLaplacianHardThreshold',
);
final Set<int> badFileIDs = {};
for (final map in badFacesFiles) {
badFileIDs.add(map[fileIDColumn] as int);
}
final List<Map<String, dynamic>> goodFacesFiles = await db.getAll(
'SELECT DISTINCT $fileIDColumn FROM $facesTable WHERE $faceScore > $kMinimumQualityFaceScore AND $faceBlur > $kLaplacianHardThreshold',
);
final Set<int> goodFileIDs = {};
for (final map in goodFacesFiles) {
goodFileIDs.add(map[fileIDColumn] as int);
}
final trulyFacelessFiles = badFileIDs.difference(goodFileIDs);
return clusteredFileIDs.length + trulyFacelessFiles.length;
} }
Future<double> getClusteredToTotalFacesRatio() async { Future<double> getClusteredToIndexableFilesRatio() async {
final int totalFaces = await getTotalFaceCount(); final int indexableFiles = (await getIndexableFileIDs()).length;
final int clusteredFaces = await getClusteredFaceCount(); final int clusteredFiles = await getClusteredOrFacelessFileCount();
return clusteredFaces / totalFaces; return clusteredFiles / indexableFiles;
}
Future<int> getUnclusteredFaceCount() async {
final db = await instance.asyncDB;
const String query = '''
SELECT f.$faceIDColumn
FROM $facesTable f
LEFT JOIN $faceClustersTable fc ON f.$faceIDColumn = fc.$fcFaceId
WHERE f.$faceScore > $kMinimumQualityFaceScore
AND f.$faceBlur > $kLaplacianHardThreshold
AND fc.$fcFaceId IS NULL
''';
final List<Map<String, dynamic>> maps = await db.getAll(query);
return maps.length;
} }
Future<int> getBlurryFaceCount([ Future<int> getBlurryFaceCount([
@ -695,7 +768,7 @@ class FaceMLDataDB {
try { try {
final db = await instance.asyncDB; final db = await instance.asyncDB;
await db.execute(dropFaceClustersTable); await db.execute(deleteFaceClustersTable);
await db.execute(createFaceClustersTable); await db.execute(createFaceClustersTable);
await db.execute(fcClusterIDIndex); await db.execute(fcClusterIDIndex);
} catch (e, s) { } catch (e, s) {
@ -795,7 +868,7 @@ class FaceMLDataDB {
for (final map in maps) { for (final map in maps) {
final clusterID = map[clusterIDColumn] as int; final clusterID = map[clusterIDColumn] as int;
final String faceID = map[fcFaceId] as String; final String faceID = map[fcFaceId] as String;
final fileID = int.parse(faceID.split('_').first); final fileID = getFileIdFromFaceId(faceID);
result[fileID] = (result[fileID] ?? {})..add(clusterID); result[fileID] = (result[fileID] ?? {})..add(clusterID);
} }
return result; return result;
@ -814,8 +887,8 @@ class FaceMLDataDB {
final Map<int, Set<int>> result = {}; final Map<int, Set<int>> result = {};
for (final map in maps) { for (final map in maps) {
final clusterID = map[fcClusterID] as int; final clusterID = map[fcClusterID] as int;
final faceId = map[fcFaceId] as String; final faceID = map[fcFaceId] as String;
final fileID = int.parse(faceId.split("_").first); final fileID = getFileIdFromFaceId(faceID);
result[fileID] = (result[fileID] ?? {})..add(clusterID); result[fileID] = (result[fileID] ?? {})..add(clusterID);
} }
return result; return result;
@ -906,16 +979,15 @@ class FaceMLDataDB {
if (faces) { if (faces) {
await db.execute(deleteFacesTable); await db.execute(deleteFacesTable);
await db.execute(createFacesTable); await db.execute(createFacesTable);
await db.execute(dropFaceClustersTable); await db.execute(deleteFaceClustersTable);
await db.execute(createFaceClustersTable); await db.execute(createFaceClustersTable);
await db.execute(fcClusterIDIndex); await db.execute(fcClusterIDIndex);
} }
await db.execute(deletePersonTable); await db.execute(deleteClusterPersonTable);
await db.execute(dropClusterPersonTable); await db.execute(deleteNotPersonFeedbackTable);
await db.execute(dropNotPersonFeedbackTable); await db.execute(deleteClusterSummaryTable);
await db.execute(dropClusterSummaryTable); await db.execute(deleteFaceClustersTable);
await db.execute(dropFaceClustersTable);
await db.execute(createClusterPersonTable); await db.execute(createClusterPersonTable);
await db.execute(createNotPersonFeedbackTable); await db.execute(createNotPersonFeedbackTable);
@ -933,9 +1005,8 @@ class FaceMLDataDB {
final db = await instance.asyncDB; final db = await instance.asyncDB;
// Drop the tables // Drop the tables
await db.execute(deletePersonTable); await db.execute(deleteClusterPersonTable);
await db.execute(dropClusterPersonTable); await db.execute(deleteNotPersonFeedbackTable);
await db.execute(dropNotPersonFeedbackTable);
// Recreate the tables // Recreate the tables
await db.execute(createClusterPersonTable); await db.execute(createClusterPersonTable);
@ -964,7 +1035,7 @@ class FaceMLDataDB {
final Map<String, int> faceIDToClusterID = {}; final Map<String, int> faceIDToClusterID = {};
for (final row in faceIdsResult) { for (final row in faceIdsResult) {
final faceID = row[fcFaceId] as String; final faceID = row[fcFaceId] as String;
if (fileIds.contains(faceID.split('_').first)) { if (fileIds.contains(getFileIdFromFaceId(faceID))) {
maxClusterID += 1; maxClusterID += 1;
faceIDToClusterID[faceID] = maxClusterID; faceIDToClusterID[faceID] = maxClusterID;
} }
@ -990,7 +1061,7 @@ class FaceMLDataDB {
final Map<String, int> faceIDToClusterID = {}; final Map<String, int> faceIDToClusterID = {};
for (final row in faceIdsResult) { for (final row in faceIdsResult) {
final faceID = row[fcFaceId] as String; final faceID = row[fcFaceId] as String;
if (fileIds.contains(faceID.split('_').first)) { if (fileIds.contains(getFileIdFromFaceId(faceID))) {
maxClusterID += 1; maxClusterID += 1;
faceIDToClusterID[faceID] = maxClusterID; faceIDToClusterID[faceID] = maxClusterID;
} }

View file

@ -29,7 +29,7 @@ const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable (
); );
'''; ''';
const deleteFacesTable = 'DROP TABLE IF EXISTS $facesTable'; const deleteFacesTable = 'DELETE FROM $facesTable';
// End of Faces Table Fields & Schema Queries // End of Faces Table Fields & Schema Queries
//##region Face Clusters Table Fields & Schema Queries //##region Face Clusters Table Fields & Schema Queries
@ -48,15 +48,9 @@ CREATE TABLE IF NOT EXISTS $faceClustersTable (
// -- Creating a non-unique index on clusterID for query optimization // -- Creating a non-unique index on clusterID for query optimization
const fcClusterIDIndex = const fcClusterIDIndex =
'''CREATE INDEX IF NOT EXISTS idx_fcClusterID ON $faceClustersTable($fcClusterID);'''; '''CREATE INDEX IF NOT EXISTS idx_fcClusterID ON $faceClustersTable($fcClusterID);''';
const dropFaceClustersTable = 'DROP TABLE IF EXISTS $faceClustersTable'; const deleteFaceClustersTable = 'DELETE FROM $faceClustersTable';
//##endregion //##endregion
// People Table Fields & Schema Queries
const personTable = 'person';
const deletePersonTable = 'DROP TABLE IF EXISTS $personTable';
//End People Table Fields & Schema Queries
// Clusters Table Fields & Schema Queries // Clusters Table Fields & Schema Queries
const clusterPersonTable = 'cluster_person'; const clusterPersonTable = 'cluster_person';
const personIdColumn = 'person_id'; const personIdColumn = 'person_id';
@ -69,7 +63,7 @@ CREATE TABLE IF NOT EXISTS $clusterPersonTable (
PRIMARY KEY($personIdColumn, $clusterIDColumn) PRIMARY KEY($personIdColumn, $clusterIDColumn)
); );
'''; ''';
const dropClusterPersonTable = 'DROP TABLE IF EXISTS $clusterPersonTable'; const deleteClusterPersonTable = 'DELETE FROM $clusterPersonTable';
// End Clusters Table Fields & Schema Queries // End Clusters Table Fields & Schema Queries
/// Cluster Summary Table Fields & Schema Queries /// Cluster Summary Table Fields & Schema Queries
@ -85,7 +79,7 @@ CREATE TABLE IF NOT EXISTS $clusterSummaryTable (
); );
'''; ''';
const dropClusterSummaryTable = 'DROP TABLE IF EXISTS $clusterSummaryTable'; const deleteClusterSummaryTable = 'DELETE FROM $clusterSummaryTable';
/// End Cluster Summary Table Fields & Schema Queries /// End Cluster Summary Table Fields & Schema Queries
@ -99,5 +93,5 @@ CREATE TABLE IF NOT EXISTS $notPersonFeedback (
PRIMARY KEY($personIdColumn, $clusterIDColumn) PRIMARY KEY($personIdColumn, $clusterIDColumn)
); );
'''; ''';
const dropNotPersonFeedbackTable = 'DROP TABLE IF EXISTS $notPersonFeedback'; const deleteNotPersonFeedbackTable = 'DELETE FROM $notPersonFeedback';
// End Clusters Table Fields & Schema Queries // End Clusters Table Fields & Schema Queries

View file

@ -54,6 +54,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"), "locations": MessageLookupByLibrary.simpleMessage("Locations"),
"longPressAnEmailToVerifyEndToEndEncryption": "longPressAnEmailToVerifyEndToEndEncryption":

View file

@ -819,6 +819,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Falscher Wiederherstellungs-Schlüssel"), "Falscher Wiederherstellungs-Schlüssel"),
"indexedItems": "indexedItems":
MessageLookupByLibrary.simpleMessage("Indizierte Elemente"), MessageLookupByLibrary.simpleMessage("Indizierte Elemente"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice": "insecureDevice":
MessageLookupByLibrary.simpleMessage("Unsicheres Gerät"), MessageLookupByLibrary.simpleMessage("Unsicheres Gerät"),
"installManually": "installManually":

View file

@ -813,6 +813,8 @@ class MessageLookup extends MessageLookupByLibrary {
"incorrectRecoveryKeyTitle": "incorrectRecoveryKeyTitle":
MessageLookupByLibrary.simpleMessage("Incorrect recovery key"), MessageLookupByLibrary.simpleMessage("Incorrect recovery key"),
"indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"), "indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused. It will automatically resume when device is ready."),
"insecureDevice": "insecureDevice":
MessageLookupByLibrary.simpleMessage("Insecure device"), MessageLookupByLibrary.simpleMessage("Insecure device"),
"installManually": "installManually":

View file

@ -699,6 +699,8 @@ class MessageLookup extends MessageLookupByLibrary {
"La clave de recuperación introducida es incorrecta"), "La clave de recuperación introducida es incorrecta"),
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage( "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
"Clave de recuperación incorrecta"), "Clave de recuperación incorrecta"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice": "insecureDevice":
MessageLookupByLibrary.simpleMessage("Dispositivo inseguro"), MessageLookupByLibrary.simpleMessage("Dispositivo inseguro"),
"installManually": "installManually":

View file

@ -804,6 +804,8 @@ class MessageLookup extends MessageLookupByLibrary {
"La clé de secours que vous avez entrée est incorrecte"), "La clé de secours que vous avez entrée est incorrecte"),
"incorrectRecoveryKeyTitle": "incorrectRecoveryKeyTitle":
MessageLookupByLibrary.simpleMessage("Clé de secours non valide"), MessageLookupByLibrary.simpleMessage("Clé de secours non valide"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice": "insecureDevice":
MessageLookupByLibrary.simpleMessage("Appareil non sécurisé"), MessageLookupByLibrary.simpleMessage("Appareil non sécurisé"),
"installManually": "installManually":

View file

@ -773,6 +773,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Il codice che hai inserito non è corretto"), "Il codice che hai inserito non è corretto"),
"incorrectRecoveryKeyTitle": "incorrectRecoveryKeyTitle":
MessageLookupByLibrary.simpleMessage("Chiave di recupero errata"), MessageLookupByLibrary.simpleMessage("Chiave di recupero errata"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice": "insecureDevice":
MessageLookupByLibrary.simpleMessage("Dispositivo non sicuro"), MessageLookupByLibrary.simpleMessage("Dispositivo non sicuro"),
"installManually": "installManually":

View file

@ -54,6 +54,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"), "locations": MessageLookupByLibrary.simpleMessage("Locations"),
"longPressAnEmailToVerifyEndToEndEncryption": "longPressAnEmailToVerifyEndToEndEncryption":

View file

@ -840,6 +840,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Onjuiste herstelsleutel"), MessageLookupByLibrary.simpleMessage("Onjuiste herstelsleutel"),
"indexedItems": "indexedItems":
MessageLookupByLibrary.simpleMessage("Geïndexeerde bestanden"), MessageLookupByLibrary.simpleMessage("Geïndexeerde bestanden"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice": "insecureDevice":
MessageLookupByLibrary.simpleMessage("Onveilig apparaat"), MessageLookupByLibrary.simpleMessage("Onveilig apparaat"),
"installManually": "installManually":

View file

@ -72,6 +72,8 @@ class MessageLookup extends MessageLookupByLibrary {
"feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"), "feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"invalidEmailAddress": "invalidEmailAddress":
MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"), MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),

View file

@ -131,6 +131,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Kod jest nieprawidłowy"), MessageLookupByLibrary.simpleMessage("Kod jest nieprawidłowy"),
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage( "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
"Nieprawidłowy klucz odzyskiwania"), "Nieprawidłowy klucz odzyskiwania"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"invalidEmailAddress": "invalidEmailAddress":
MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"), MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),

View file

@ -98,7 +98,7 @@ class MessageLookup extends MessageLookupByLibrary {
"${storageAmountInGB} GB cada vez que alguém se inscrever para um plano pago e aplica o seu código"; "${storageAmountInGB} GB cada vez que alguém se inscrever para um plano pago e aplica o seu código";
static String m25(freeAmount, storageUnit) => static String m25(freeAmount, storageUnit) =>
"${freeAmount} ${storageUnit} grátis"; "${freeAmount} ${storageUnit} livre";
static String m26(endDate) => "Teste gratuito acaba em ${endDate}"; static String m26(endDate) => "Teste gratuito acaba em ${endDate}";
@ -225,6 +225,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são <underline>criptografados de ponta a ponta</underline>."), "Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são <underline>criptografados de ponta a ponta</underline>."),
"activeSessions": "activeSessions":
MessageLookupByLibrary.simpleMessage("Sessões ativas"), MessageLookupByLibrary.simpleMessage("Sessões ativas"),
"addAName": MessageLookupByLibrary.simpleMessage("Adicione um nome"),
"addANewEmail": "addANewEmail":
MessageLookupByLibrary.simpleMessage("Adicionar um novo email"), MessageLookupByLibrary.simpleMessage("Adicionar um novo email"),
"addCollaborator": "addCollaborator":
@ -446,7 +447,7 @@ class MessageLookup extends MessageLookupByLibrary {
"clubByFileName": MessageLookupByLibrary.simpleMessage( "clubByFileName": MessageLookupByLibrary.simpleMessage(
"Agrupar pelo nome de arquivo"), "Agrupar pelo nome de arquivo"),
"clusteringProgress": "clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"), MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"),
"codeAppliedPageTitle": "codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Código aplicado"), MessageLookupByLibrary.simpleMessage("Código aplicado"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
@ -692,6 +693,8 @@ class MessageLookup extends MessageLookupByLibrary {
"enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"), "enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Insira a senha para criptografar seus dados"), "Insira a senha para criptografar seus dados"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage( "enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Insira o código de referência"), "Insira o código de referência"),
"enterThe6digitCodeFromnyourAuthenticatorApp": "enterThe6digitCodeFromnyourAuthenticatorApp":
@ -717,9 +720,9 @@ class MessageLookup extends MessageLookupByLibrary {
"exportYourData": "exportYourData":
MessageLookupByLibrary.simpleMessage("Exportar seus dados"), MessageLookupByLibrary.simpleMessage("Exportar seus dados"),
"faceRecognition": "faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"), MessageLookupByLibrary.simpleMessage("Reconhecimento facial"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados."),
"faces": MessageLookupByLibrary.simpleMessage("Rostos"), "faces": MessageLookupByLibrary.simpleMessage("Rostos"),
"failedToApplyCode": "failedToApplyCode":
MessageLookupByLibrary.simpleMessage("Falha ao aplicar o código"), MessageLookupByLibrary.simpleMessage("Falha ao aplicar o código"),
@ -761,12 +764,15 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Arquivos excluídos"), MessageLookupByLibrary.simpleMessage("Arquivos excluídos"),
"filesSavedToGallery": "filesSavedToGallery":
MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"), MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"),
"findPeopleByName": MessageLookupByLibrary.simpleMessage(
"Encontre pessoas rapidamente por nome"),
"flip": MessageLookupByLibrary.simpleMessage("Inverter"), "flip": MessageLookupByLibrary.simpleMessage("Inverter"),
"forYourMemories": "forYourMemories":
MessageLookupByLibrary.simpleMessage("para suas memórias"), MessageLookupByLibrary.simpleMessage("para suas memórias"),
"forgotPassword": "forgotPassword":
MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"), MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "foundFaces":
MessageLookupByLibrary.simpleMessage("Rostos encontrados"),
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
"Armazenamento gratuito reivindicado"), "Armazenamento gratuito reivindicado"),
"freeStorageOnReferralSuccess": m24, "freeStorageOnReferralSuccess": m24,
@ -830,6 +836,8 @@ class MessageLookup extends MessageLookupByLibrary {
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage( "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
"Chave de recuperação incorreta"), "Chave de recuperação incorreta"),
"indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"), "indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice": "insecureDevice":
MessageLookupByLibrary.simpleMessage("Dispositivo não seguro"), MessageLookupByLibrary.simpleMessage("Dispositivo não seguro"),
"installManually": "installManually":
@ -1064,6 +1072,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"),
"pendingSync": "pendingSync":
MessageLookupByLibrary.simpleMessage("Sincronização pendente"), MessageLookupByLibrary.simpleMessage("Sincronização pendente"),
"people": MessageLookupByLibrary.simpleMessage("Pessoas"),
"peopleUsingYourCode": "peopleUsingYourCode":
MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"), MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"),
"permDeleteWarning": MessageLookupByLibrary.simpleMessage( "permDeleteWarning": MessageLookupByLibrary.simpleMessage(
@ -1197,6 +1206,8 @@ class MessageLookup extends MessageLookupByLibrary {
"removeParticipant": "removeParticipant":
MessageLookupByLibrary.simpleMessage("Remover participante"), MessageLookupByLibrary.simpleMessage("Remover participante"),
"removeParticipantBody": m43, "removeParticipantBody": m43,
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"),
"removePublicLink": "removePublicLink":
MessageLookupByLibrary.simpleMessage("Remover link público"), MessageLookupByLibrary.simpleMessage("Remover link público"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(
@ -1260,7 +1271,7 @@ class MessageLookup extends MessageLookupByLibrary {
"searchDatesEmptySection": MessageLookupByLibrary.simpleMessage( "searchDatesEmptySection": MessageLookupByLibrary.simpleMessage(
"Pesquisar por data, mês ou ano"), "Pesquisar por data, mês ou ano"),
"searchFaceEmptySection": MessageLookupByLibrary.simpleMessage( "searchFaceEmptySection": MessageLookupByLibrary.simpleMessage(
"Encontre todas as fotos de uma pessoa"), "Pessoas serão exibidas aqui uma vez que a indexação é feita"),
"searchFileTypesAndNamesEmptySection": "searchFileTypesAndNamesEmptySection":
MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"),
"searchHint1": MessageLookupByLibrary.simpleMessage( "searchHint1": MessageLookupByLibrary.simpleMessage(

View file

@ -686,6 +686,8 @@ class MessageLookup extends MessageLookupByLibrary {
"incorrectRecoveryKeyTitle": "incorrectRecoveryKeyTitle":
MessageLookupByLibrary.simpleMessage("不正确的恢复密钥"), MessageLookupByLibrary.simpleMessage("不正确的恢复密钥"),
"indexedItems": MessageLookupByLibrary.simpleMessage("已索引项目"), "indexedItems": MessageLookupByLibrary.simpleMessage("已索引项目"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice": MessageLookupByLibrary.simpleMessage("设备不安全"), "insecureDevice": MessageLookupByLibrary.simpleMessage("设备不安全"),
"installManually": MessageLookupByLibrary.simpleMessage("手动安装"), "installManually": MessageLookupByLibrary.simpleMessage("手动安装"),
"invalidEmailAddress": "invalidEmailAddress":

View file

@ -8793,6 +8793,16 @@ class S {
args: [], args: [],
); );
} }
/// `Indexing is paused. It will automatically resume when device is ready.`
String get indexingIsPaused {
return Intl.message(
'Indexing is paused. It will automatically resume when device is ready.',
name: 'indexingIsPaused',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<S> { class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View file

@ -24,5 +24,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -1212,5 +1212,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -1235,5 +1235,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
} "indexingIsPaused": "Indexing is paused. It will automatically resume when device is ready."
}

View file

@ -986,5 +986,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -1167,5 +1167,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -1129,5 +1129,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -24,5 +24,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -1230,5 +1230,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -38,5 +38,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -125,5 +125,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -987,7 +987,7 @@
"fileTypesAndNames": "Tipos de arquivo e nomes", "fileTypesAndNames": "Tipos de arquivo e nomes",
"location": "Local", "location": "Local",
"moments": "Momentos", "moments": "Momentos",
"searchFaceEmptySection": "Encontre todas as fotos de uma pessoa", "searchFaceEmptySection": "Pessoas serão exibidas aqui uma vez que a indexação é feita",
"searchDatesEmptySection": "Pesquisar por data, mês ou ano", "searchDatesEmptySection": "Pesquisar por data, mês ou ano",
"searchLocationEmptySection": "Fotos de grupo que estão sendo tiradas em algum raio da foto", "searchLocationEmptySection": "Fotos de grupo que estão sendo tiradas em algum raio da foto",
"searchPeopleEmptySection": "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui", "searchPeopleEmptySection": "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui",
@ -1042,7 +1042,7 @@
"@storageUsageInfo": { "@storageUsageInfo": {
"description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used"
}, },
"freeStorageSpace": "{freeAmount} {storageUnit} grátis", "freeStorageSpace": "{freeAmount} {storageUnit} livre",
"appVersion": "Versão: {versionValue}", "appVersion": "Versão: {versionValue}",
"verifyIDLabel": "Verificar", "verifyIDLabel": "Verificar",
"fileInfoAddDescHint": "Adicionar descrição...", "fileInfoAddDescHint": "Adicionar descrição...",
@ -1171,6 +1171,7 @@
} }
}, },
"faces": "Rostos", "faces": "Rostos",
"people": "Pessoas",
"contents": "Conteúdos", "contents": "Conteúdos",
"addNew": "Adicionar novo", "addNew": "Adicionar novo",
"@addNew": { "@addNew": {
@ -1196,14 +1197,14 @@
"verifyPasskey": "Verificar chave de acesso", "verifyPasskey": "Verificar chave de acesso",
"playOnTv": "Reproduzir álbum na TV", "playOnTv": "Reproduzir álbum na TV",
"pair": "Parear", "pair": "Parear",
"autoPair": "Pareamento automático",
"pairWithPin": "Parear com PIN",
"deviceNotFound": "Dispositivo não encontrado", "deviceNotFound": "Dispositivo não encontrado",
"castInstruction": "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV.", "castInstruction": "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV.",
"deviceCodeHint": "Insira o código", "deviceCodeHint": "Insira o código",
"joinDiscord": "Junte-se ao Discord", "joinDiscord": "Junte-se ao Discord",
"locations": "Locais", "locations": "Locais",
"descriptions": "Descrições", "descriptions": "Descrições",
"addAName": "Adicione um nome",
"findPeopleByName": "Encontre pessoas rapidamente por nome",
"addViewers": "{count, plural, zero {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar Visualizadores}}", "addViewers": "{count, plural, zero {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar Visualizadores}}",
"addCollaborators": "{count, plural, zero {Adicionar colaborador} one {Adicionar coloborador} other {Adicionar colaboradores}}", "addCollaborators": "{count, plural, zero {Adicionar colaborador} one {Adicionar coloborador} other {Adicionar colaboradores}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta.", "longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta.",
@ -1216,6 +1217,8 @@
"customEndpoint": "Conectado a {endpoint}", "customEndpoint": "Conectado a {endpoint}",
"createCollaborativeLink": "Criar link colaborativo", "createCollaborativeLink": "Criar link colaborativo",
"search": "Pesquisar", "search": "Pesquisar",
"enterPersonName": "Inserir nome da pessoa",
"removePersonLabel": "Remover etiqueta da pessoa",
"autoPairDesc": "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast.", "autoPairDesc": "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast.",
"manualPairDesc": "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado.", "manualPairDesc": "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado.",
"connectToDevice": "Conectar ao dispositivo", "connectToDevice": "Conectar ao dispositivo",
@ -1227,8 +1230,11 @@
"castIPMismatchTitle": "Falha ao transmitir álbum", "castIPMismatchTitle": "Falha ao transmitir álbum",
"castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.", "castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.",
"pairingComplete": "Pareamento concluído", "pairingComplete": "Pareamento concluído",
"faceRecognition": "Face recognition", "autoPair": "Pareamento automático",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "pairWithPin": "Parear com PIN",
"foundFaces": "Found faces", "faceRecognition": "Reconhecimento facial",
"clusteringProgress": "Clustering progress" "faceRecognitionIndexingDescription": "Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados.",
"foundFaces": "Rostos encontrados",
"clusteringProgress": "Progresso de agrupamento",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -1230,5 +1230,6 @@
"faceRecognition": "Face recognition", "faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress" "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -51,6 +51,7 @@ import 'package:photos/services/user_service.dart';
import 'package:photos/ui/tools/app_lock.dart'; import 'package:photos/ui/tools/app_lock.dart';
import 'package:photos/ui/tools/lock_screen.dart'; import 'package:photos/ui/tools/lock_screen.dart';
import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/crypto_util.dart';
import "package:photos/utils/email_util.dart";
import 'package:photos/utils/file_uploader.dart'; import 'package:photos/utils/file_uploader.dart';
import 'package:photos/utils/local_settings.dart'; import 'package:photos/utils/local_settings.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -180,6 +181,16 @@ void _headlessTaskHandler(HeadlessTask task) {
} }
Future<void> _init(bool isBackground, {String via = ''}) async { Future<void> _init(bool isBackground, {String via = ''}) async {
bool initComplete = false;
Future.delayed(const Duration(seconds: 15), () {
if (!initComplete && !isBackground) {
sendLogsForInit(
"support@ente.io",
"Stuck on splash screen for >= 15 seconds",
null,
);
}
});
_isProcessRunning = true; _isProcessRunning = true;
_logger.info("Initializing... inBG =$isBackground via: $via"); _logger.info("Initializing... inBG =$isBackground via: $via");
final SharedPreferences preferences = await SharedPreferences.getInstance(); final SharedPreferences preferences = await SharedPreferences.getInstance();
@ -235,17 +246,11 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
unawaited(SemanticSearchService.instance.init()); unawaited(SemanticSearchService.instance.init());
MachineLearningController.instance.init(); MachineLearningController.instance.init();
// Can not including existing tf/ml binaries as they are not being built if (flagService.faceSearchEnabled) {
// from source. unawaited(FaceMlService.instance.init());
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819 } else {
if (!UpdateService.instance.isFdroidFlavor()) { if (LocalSettings.instance.isFaceIndexingEnabled) {
// unawaited(ObjectDetectionService.instance.init()); unawaited(LocalSettings.instance.toggleFaceIndexing());
if (flagService.faceSearchEnabled) {
unawaited(FaceMlService.instance.init());
} else {
if (LocalSettings.instance.isFaceIndexingEnabled) {
unawaited(LocalSettings.instance.toggleFaceIndexing());
}
} }
} }
PersonService.init( PersonService.init(
@ -254,6 +259,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
preferences, preferences,
); );
initComplete = true;
_logger.info("Initialization done"); _logger.info("Initialization done");
} }

View file

@ -498,19 +498,8 @@ class FaceClusteringService {
} }
} }
// Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first
if (fileIDToCreationTime != null) { if (fileIDToCreationTime != null) {
faceInfos.sort((a, b) { _sortFaceInfosOnCreationTime(faceInfos);
if (a.fileCreationTime == null && b.fileCreationTime == null) {
return 0;
} else if (a.fileCreationTime == null) {
return 1;
} else if (b.fileCreationTime == null) {
return -1;
} else {
return a.fileCreationTime!.compareTo(b.fileCreationTime!);
}
});
} }
// Sort the faceInfos such that the ones with null clusterId are at the end // Sort the faceInfos such that the ones with null clusterId are at the end
@ -796,19 +785,8 @@ class FaceClusteringService {
); );
} }
// Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first
if (fileIDToCreationTime != null) { if (fileIDToCreationTime != null) {
faceInfos.sort((a, b) { _sortFaceInfosOnCreationTime(faceInfos);
if (a.fileCreationTime == null && b.fileCreationTime == null) {
return 0;
} else if (a.fileCreationTime == null) {
return 1;
} else if (b.fileCreationTime == null) {
return -1;
} else {
return a.fileCreationTime!.compareTo(b.fileCreationTime!);
}
});
} }
if (faceInfos.isEmpty) { if (faceInfos.isEmpty) {
@ -996,19 +974,8 @@ class FaceClusteringService {
); );
} }
// Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first
if (fileIDToCreationTime != null) { if (fileIDToCreationTime != null) {
faceInfos.sort((a, b) { _sortFaceInfosOnCreationTime(faceInfos);
if (a.fileCreationTime == null && b.fileCreationTime == null) {
return 0;
} else if (a.fileCreationTime == null) {
return 1;
} else if (b.fileCreationTime == null) {
return -1;
} else {
return a.fileCreationTime!.compareTo(b.fileCreationTime!);
}
});
} }
// Get the embeddings // Get the embeddings
@ -1027,3 +994,20 @@ class FaceClusteringService {
return clusteredFaceIDs; return clusteredFaceIDs;
} }
} }
/// Sort the faceInfos based on fileCreationTime, in descending order, so newest faces are first
void _sortFaceInfosOnCreationTime(
List<FaceInfo> faceInfos,
) {
faceInfos.sort((b, a) {
if (a.fileCreationTime == null && b.fileCreationTime == null) {
return 0;
} else if (a.fileCreationTime == null) {
return 1;
} else if (b.fileCreationTime == null) {
return -1;
} else {
return a.fileCreationTime!.compareTo(b.fileCreationTime!);
}
});
}

View file

@ -8,6 +8,18 @@ class GeneralFaceMlException implements Exception {
String toString() => 'GeneralFaceMlException: $message'; String toString() => 'GeneralFaceMlException: $message';
} }
class ThumbnailRetrievalException implements Exception {
final String message;
final StackTrace stackTrace;
ThumbnailRetrievalException(this.message, this.stackTrace);
@override
String toString() {
return 'ThumbnailRetrievalException: $message\n$stackTrace';
}
}
class CouldNotRetrieveAnyFileData implements Exception {} class CouldNotRetrieveAnyFileData implements Exception {}
class CouldNotInitializeFaceDetector implements Exception {} class CouldNotInitializeFaceDetector implements Exception {}

View file

@ -310,5 +310,9 @@ class FaceResultBuilder {
} }
int getFileIdFromFaceId(String faceId) { int getFileIdFromFaceId(String faceId) {
return int.parse(faceId.split("_")[0]); return int.parse(faceId.split("_").first);
} }
int? tryGetFileIdFromFaceId(String faceId) {
return int.tryParse(faceId.split("_").first);
}

View file

@ -9,10 +9,10 @@ import "dart:ui" show Image;
import "package:computer/computer.dart"; import "package:computer/computer.dart";
import "package:dart_ui_isolate/dart_ui_isolate.dart"; import "package:dart_ui_isolate/dart_ui_isolate.dart";
import "package:flutter/foundation.dart" show debugPrint, kDebugMode; import "package:flutter/foundation.dart" show debugPrint, kDebugMode;
import "package:flutter/services.dart";
import "package:logging/logging.dart"; import "package:logging/logging.dart";
import "package:onnxruntime/onnxruntime.dart"; import "package:onnxruntime/onnxruntime.dart";
import "package:package_info_plus/package_info_plus.dart"; import "package:package_info_plus/package_info_plus.dart";
import "package:photos/core/configuration.dart";
import "package:photos/core/event_bus.dart"; import "package:photos/core/event_bus.dart";
import "package:photos/db/files_db.dart"; import "package:photos/db/files_db.dart";
import "package:photos/events/diff_sync_complete_event.dart"; import "package:photos/events/diff_sync_complete_event.dart";
@ -43,6 +43,7 @@ import 'package:photos/services/machine_learning/face_ml/face_ml_result.dart';
import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart";
import 'package:photos/services/machine_learning/file_ml/file_ml.dart'; import 'package:photos/services/machine_learning/file_ml/file_ml.dart';
import 'package:photos/services/machine_learning/file_ml/remote_fileml_service.dart'; import 'package:photos/services/machine_learning/file_ml/remote_fileml_service.dart';
import "package:photos/services/machine_learning/machine_learning_controller.dart";
import "package:photos/services/search_service.dart"; import "package:photos/services/search_service.dart";
import "package:photos/utils/file_util.dart"; import "package:photos/utils/file_util.dart";
import 'package:photos/utils/image_ml_isolate.dart'; import 'package:photos/utils/image_ml_isolate.dart';
@ -97,8 +98,9 @@ class FaceMlService {
bool _shouldSyncPeople = false; bool _shouldSyncPeople = false;
bool _isSyncing = false; bool _isSyncing = false;
final int _fileDownloadLimit = 10; final int _fileDownloadLimit = 5;
final int _embeddingFetchLimit = 200; final int _embeddingFetchLimit = 200;
final int _kForceClusteringFaceCount = 8000;
Future<void> init({bool initializeImageMlIsolate = false}) async { Future<void> init({bool initializeImageMlIsolate = false}) async {
if (LocalSettings.instance.isFaceIndexingEnabled == false) { if (LocalSettings.instance.isFaceIndexingEnabled == false) {
@ -109,6 +111,7 @@ class FaceMlService {
return; return;
} }
_logger.info("init called"); _logger.info("init called");
_logStatus();
await _computer.compute(initOrtEnv); await _computer.compute(initOrtEnv);
try { try {
await FaceDetectionService.instance.init(); await FaceDetectionService.instance.init();
@ -152,8 +155,8 @@ class FaceMlService {
_logger.info( _logger.info(
"MLController allowed running ML, faces indexing starting", "MLController allowed running ML, faces indexing starting",
); );
unawaited(indexAndClusterAll());
} }
unawaited(indexAndClusterAll());
} else { } else {
_logger.info( _logger.info(
"MLController stopped running ML, faces indexing will be paused (unless it's fetching embeddings)", "MLController stopped running ML, faces indexing will be paused (unless it's fetching embeddings)",
@ -161,9 +164,16 @@ class FaceMlService {
pauseIndexingAndClustering(); pauseIndexingAndClustering();
} }
}); });
if (Platform.isIOS &&
MachineLearningController.instance.isDeviceHealthy) {
_logger.info("Starting face indexing and clustering on iOS from init");
unawaited(indexAndClusterAll());
}
_listenIndexOnDiffSync(); _listenIndexOnDiffSync();
_listenOnPeopleChangedSync(); _listenOnPeopleChangedSync();
_logger.info('init done');
}); });
} }
@ -245,6 +255,7 @@ class FaceMlService {
} }
/// The main execution function of the isolate. /// The main execution function of the isolate.
@pragma('vm:entry-point')
static void _isolateMain(SendPort mainSendPort) async { static void _isolateMain(SendPort mainSendPort) async {
final receivePort = ReceivePort(); final receivePort = ReceivePort();
mainSendPort.send(receivePort.sendPort); mainSendPort.send(receivePort.sendPort);
@ -287,10 +298,6 @@ class FaceMlService {
return _functionLock.synchronized(() async { return _functionLock.synchronized(() async {
_resetInactivityTimer(); _resetInactivityTimer();
if (_shouldPauseIndexingAndClustering == false) {
return null;
}
final completer = Completer<dynamic>(); final completer = Completer<dynamic>();
final answerPort = ReceivePort(); final answerPort = ReceivePort();
@ -360,16 +367,17 @@ class FaceMlService {
if (_cannotRunMLFunction()) return; if (_cannotRunMLFunction()) return;
await sync(forceSync: _shouldSyncPeople); await sync(forceSync: _shouldSyncPeople);
await indexAllImages();
final indexingCompleteRatio = await _getIndexedDoneRatio(); final int unclusteredFacesCount =
if (indexingCompleteRatio < 0.95) { await FaceMLDataDB.instance.getUnclusteredFaceCount();
if (unclusteredFacesCount > _kForceClusteringFaceCount) {
_logger.info( _logger.info(
"Indexing is not far enough to start clustering, skipping clustering. Indexing is at $indexingCompleteRatio", "There are $unclusteredFacesCount unclustered faces, doing clustering first",
); );
return;
} else {
await clusterAllImages(); await clusterAllImages();
} }
await indexAllImages();
await clusterAllImages();
} }
void pauseIndexingAndClustering() { void pauseIndexingAndClustering() {
@ -447,7 +455,8 @@ class FaceMlService {
if (LocalSettings.instance.remoteFetchEnabled) { if (LocalSettings.instance.remoteFetchEnabled) {
try { try {
final List<int> fileIds = []; final Set<int> fileIds =
{}; // if there are duplicates here server returns 400
// Try to find embeddings on the remote server // Try to find embeddings on the remote server
for (final f in chunk) { for (final f in chunk) {
fileIds.add(f.uploadedFileID!); fileIds.add(f.uploadedFileID!);
@ -512,12 +521,19 @@ class FaceMlService {
rethrow; rethrow;
} }
} }
} } else {
if (!await canUseHighBandwidth()) { _logger.warning(
continue; 'Not fetching embeddings because user manually disabled it in debug options',
);
} }
final smallerChunks = chunk.chunks(_fileDownloadLimit); final smallerChunks = chunk.chunks(_fileDownloadLimit);
for (final smallestChunk in smallerChunks) { for (final smallestChunk in smallerChunks) {
if (!await canUseHighBandwidth()) {
_logger.info(
'stopping indexing because user is not connected to wifi',
);
break outerLoop;
}
for (final enteFile in smallestChunk) { for (final enteFile in smallestChunk) {
if (_shouldPauseIndexingAndClustering) { if (_shouldPauseIndexingAndClustering) {
_logger.info("indexAllImages() was paused, stopping"); _logger.info("indexAllImages() was paused, stopping");
@ -543,8 +559,9 @@ class FaceMlService {
stopwatch.stop(); stopwatch.stop();
_logger.info( _logger.info(
"`indexAllImages()` finished. Fetched $fetchedCount and analyzed $fileAnalyzedCount images, in ${stopwatch.elapsed.inSeconds} seconds (avg of ${stopwatch.elapsed.inSeconds / fileAnalyzedCount} seconds per image, skipped $fileSkippedCount images. MLController status: $_mlControllerStatus)", "`indexAllImages()` finished. Fetched $fetchedCount and analyzed $fileAnalyzedCount images, in ${stopwatch.elapsed.inSeconds} seconds (avg of ${stopwatch.elapsed.inSeconds / fileAnalyzedCount} seconds per image, skipped $fileSkippedCount images)",
); );
_logStatus();
} catch (e, s) { } catch (e, s) {
_logger.severe("indexAllImages failed", e, s); _logger.severe("indexAllImages failed", e, s);
} finally { } finally {
@ -584,8 +601,8 @@ class FaceMlService {
allFaceInfoForClustering.add(faceInfo); allFaceInfoForClustering.add(faceInfo);
} }
} }
// sort the embeddings based on file creation time, oldest first // sort the embeddings based on file creation time, newest first
allFaceInfoForClustering.sort((a, b) { allFaceInfoForClustering.sort((b, a) {
return fileIDToCreationTime[a.fileID]! return fileIDToCreationTime[a.fileID]!
.compareTo(fileIDToCreationTime[b.fileID]!); .compareTo(fileIDToCreationTime[b.fileID]!);
}); });
@ -758,6 +775,9 @@ class FaceMlService {
// disposeImageIsolateAfterUse: false, // disposeImageIsolateAfterUse: false,
); );
if (result == null) { if (result == null) {
_logger.severe(
"Failed to analyze image with uploadedFileID: ${enteFile.uploadedFileID}",
);
return false; return false;
} }
final List<Face> faces = []; final List<Face> faces = [];
@ -834,13 +854,22 @@ class FaceMlService {
} }
await FaceMLDataDB.instance.bulkInsertFaces(faces); await FaceMLDataDB.instance.bulkInsertFaces(faces);
return true; return true;
} on ThumbnailRetrievalException catch (e, s) {
_logger.severe(
'ThumbnailRetrievalException while processing image with ID ${enteFile.uploadedFileID}, storing empty face so indexing does not get stuck',
e,
s,
);
await FaceMLDataDB.instance
.bulkInsertFaces([Face.empty(enteFile.uploadedFileID!, error: true)]);
return true;
} catch (e, s) { } catch (e, s) {
_logger.severe( _logger.severe(
"Failed to analyze using FaceML for image with ID: ${enteFile.uploadedFileID}", "Failed to analyze using FaceML for image with ID: ${enteFile.uploadedFileID}",
e, e,
s, s,
); );
return true; return false;
} }
} }
@ -877,6 +906,7 @@ class FaceMlService {
), ),
) as String?; ) as String?;
if (resultJsonString == null) { if (resultJsonString == null) {
_logger.severe('Analyzing image in isolate is giving back null');
return null; return null;
} }
result = FaceMlResult.fromJsonString(resultJsonString); result = FaceMlResult.fromJsonString(resultJsonString);
@ -993,7 +1023,16 @@ class FaceMlService {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
File? file; File? file;
if (enteFile.fileType == FileType.video) { if (enteFile.fileType == FileType.video) {
file = await getThumbnailForUploadedFile(enteFile); try {
file = await getThumbnailForUploadedFile(enteFile);
} on PlatformException catch (e, s) {
_logger.severe(
"Could not get thumbnail for $enteFile due to PlatformException",
e,
s,
);
throw ThumbnailRetrievalException(e.toString(), s);
}
} else { } else {
file = await getFile(enteFile, isOrigin: true); file = await getFile(enteFile, isOrigin: true);
// TODO: This is returning null for Pragadees for all files, so something is wrong here! // TODO: This is returning null for Pragadees for all files, so something is wrong here!
@ -1161,24 +1200,6 @@ class FaceMlService {
} }
} }
Future<double> _getIndexedDoneRatio() async {
final w = (kDebugMode ? EnteWatch('_getIndexedDoneRatio') : null)?..start();
final int alreadyIndexedCount = await FaceMLDataDB.instance
.getIndexedFileCount(minimumMlVersion: faceMlVersion);
final int totalIndexableCount = (await getIndexableFileIDs()).length;
final ratio = alreadyIndexedCount / totalIndexableCount;
w?.log('getIndexedDoneRatio');
return ratio;
}
static Future<List<int>> getIndexableFileIDs() async {
return FilesDB.instance
.getOwnedFileIDs(Configuration.instance.getUserID()!);
}
bool _skipAnalysisEnteFile(EnteFile enteFile, Map<int, int> indexedFileIds) { bool _skipAnalysisEnteFile(EnteFile enteFile, Map<int, int> indexedFileIds) {
if (_isIndexingOrClusteringRunning == false || if (_isIndexingOrClusteringRunning == false ||
_mlControllerStatus == false) { _mlControllerStatus == false) {

View file

@ -1,6 +1,7 @@
import "dart:async"; import "dart:async";
import "dart:convert"; import "dart:convert";
import "package:computer/computer.dart";
import "package:logging/logging.dart"; import "package:logging/logging.dart";
import "package:photos/core/network/network.dart"; import "package:photos/core/network/network.dart";
import "package:photos/db/files_db.dart"; import "package:photos/db/files_db.dart";
@ -16,6 +17,8 @@ import "package:shared_preferences/shared_preferences.dart";
class RemoteFileMLService { class RemoteFileMLService {
RemoteFileMLService._privateConstructor(); RemoteFileMLService._privateConstructor();
static final Computer _computer = Computer.shared();
static final RemoteFileMLService instance = static final RemoteFileMLService instance =
RemoteFileMLService._privateConstructor(); RemoteFileMLService._privateConstructor();
@ -52,13 +55,13 @@ class RemoteFileMLService {
} }
Future<FilesMLDataResponse> getFilessEmbedding( Future<FilesMLDataResponse> getFilessEmbedding(
List<int> fileIds, Set<int> fileIds,
) async { ) async {
try { try {
final res = await _dio.post( final res = await _dio.post(
"/embeddings/files", "/embeddings/files",
data: { data: {
"fileIDs": fileIds, "fileIDs": fileIds.toList(),
"model": 'file-ml-clip-face', "model": 'file-ml-clip-face',
}, },
); );
@ -107,15 +110,17 @@ class RemoteFileMLService {
final input = EmbeddingsDecoderInput(embedding, fileKey); final input = EmbeddingsDecoderInput(embedding, fileKey);
inputs.add(input); inputs.add(input);
} }
// todo: use compute or isolate return _computer.compute<Map<String, dynamic>, Map<int, FileMl>>(
return decryptFileMLComputer( _decryptFileMLComputer,
{ param: {
"inputs": inputs, "inputs": inputs,
}, },
); );
} }
Future<Map<int, FileMl>> decryptFileMLComputer( }
Future<Map<int, FileMl>> _decryptFileMLComputer(
Map<String, dynamic> args, Map<String, dynamic> args,
) async { ) async {
final result = <int, FileMl>{}; final result = <int, FileMl>{};
@ -134,5 +139,4 @@ class RemoteFileMLService {
result[input.embedding.fileID] = decodedEmbedding; result[input.embedding.fileID] = decodedEmbedding;
} }
return result; return result;
} }
}

View file

@ -4,7 +4,6 @@ import "dart:io";
import "package:battery_info/battery_info_plugin.dart"; import "package:battery_info/battery_info_plugin.dart";
import "package:battery_info/model/android_battery_info.dart"; import "package:battery_info/model/android_battery_info.dart";
import "package:battery_info/model/iso_battery_info.dart"; import "package:battery_info/model/iso_battery_info.dart";
import "package:flutter/foundation.dart" show kDebugMode;
import "package:logging/logging.dart"; import "package:logging/logging.dart";
import "package:photos/core/event_bus.dart"; import "package:photos/core/event_bus.dart";
import "package:photos/events/machine_learning_control_event.dart"; import "package:photos/events/machine_learning_control_event.dart";
@ -19,8 +18,7 @@ class MachineLearningController {
static const kMaximumTemperature = 42; // 42 degree celsius static const kMaximumTemperature = 42; // 42 degree celsius
static const kMinimumBatteryLevel = 20; // 20% static const kMinimumBatteryLevel = 20; // 20%
static const kDefaultInteractionTimeout = static const kDefaultInteractionTimeout = Duration(seconds: 10);
kDebugMode ? Duration(seconds: 3) : Duration(seconds: 5);
static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"]; static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"];
bool _isDeviceHealthy = true; bool _isDeviceHealthy = true;
@ -28,7 +26,10 @@ class MachineLearningController {
bool _canRunML = false; bool _canRunML = false;
late Timer _userInteractionTimer; late Timer _userInteractionTimer;
bool get isDeviceHealthy => _isDeviceHealthy;
void init() { void init() {
_logger.info('init called');
if (Platform.isAndroid) { if (Platform.isAndroid) {
_startInteractionTimer(); _startInteractionTimer();
BatteryInfoPlugin() BatteryInfoPlugin()
@ -45,6 +46,7 @@ class MachineLearningController {
}); });
} }
_fireControlEvent(); _fireControlEvent();
_logger.info('init done');
} }
void onUserInteraction() { void onUserInteraction() {

View file

@ -23,6 +23,7 @@ import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx
import "package:photos/utils/debouncer.dart"; import "package:photos/utils/debouncer.dart";
import "package:photos/utils/device_info.dart"; import "package:photos/utils/device_info.dart";
import "package:photos/utils/local_settings.dart"; import "package:photos/utils/local_settings.dart";
import "package:photos/utils/ml_util.dart";
import "package:photos/utils/thumbnail_util.dart"; import "package:photos/utils/thumbnail_util.dart";
class SemanticSearchService { class SemanticSearchService {
@ -160,8 +161,7 @@ class SemanticSearchService {
} }
Future<IndexStatus> getIndexStatus() async { Future<IndexStatus> getIndexStatus() async {
final indexableFileIDs = await FilesDB.instance final indexableFileIDs = await getIndexableFileIDs();
.getOwnedFileIDs(Configuration.instance.getUserID()!);
return IndexStatus( return IndexStatus(
min(_cachedEmbeddings.length, indexableFileIDs.length), min(_cachedEmbeddings.length, indexableFileIDs.length),
(await _getFileIDsToBeIndexed()).length, (await _getFileIDsToBeIndexed()).length,
@ -222,8 +222,7 @@ class SemanticSearchService {
} }
Future<List<int>> _getFileIDsToBeIndexed() async { Future<List<int>> _getFileIDsToBeIndexed() async {
final uploadedFileIDs = await FilesDB.instance final uploadedFileIDs = await getIndexableFileIDs();
.getOwnedFileIDs(Configuration.instance.getUserID()!);
final embeddedFileIDs = final embeddedFileIDs =
await EmbeddingsDB.instance.getFileIDs(_currentModel); await EmbeddingsDB.instance.getFileIDs(_currentModel);

View file

@ -754,15 +754,6 @@ class SearchService {
Future<List<GenericSearchResult>> getAllFace(int? limit) async { Future<List<GenericSearchResult>> getAllFace(int? limit) async {
try { try {
// Don't return anything if clustering is not nearly complete yet
final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount();
final clusteredFaces =
await FaceMLDataDB.instance.getClusteredFaceCount();
final clusteringDoneRatio = clusteredFaces / foundFaces;
if (clusteringDoneRatio < 0.9) {
return [];
}
debugPrint("getting faces"); debugPrint("getting faces");
final Map<int, Set<int>> fileIdToClusterID = final Map<int, Set<int>> fileIdToClusterID =
await FaceMLDataDB.instance.getFileIdToClusterIds(); await FaceMLDataDB.instance.getFileIdToClusterIds();

View file

@ -177,7 +177,7 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
sectionOptionSpacing, sectionOptionSpacing,
MenuItemWidget( MenuItemWidget(
captionedTextWidget: FutureBuilder<double>( captionedTextWidget: FutureBuilder<double>(
future: FaceMLDataDB.instance.getClusteredToTotalFacesRatio(), future: FaceMLDataDB.instance.getClusteredToIndexableFilesRatio(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return CaptionedTextWidget( return CaptionedTextWidget(

View file

@ -11,6 +11,7 @@ import "package:photos/generated/l10n.dart";
import "package:photos/models/ml/ml_versions.dart"; import "package:photos/models/ml/ml_versions.dart";
import "package:photos/service_locator.dart"; import "package:photos/service_locator.dart";
import "package:photos/services/machine_learning/face_ml/face_ml_service.dart"; import "package:photos/services/machine_learning/face_ml/face_ml_service.dart";
import "package:photos/services/machine_learning/machine_learning_controller.dart";
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart'; import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart'; import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
import "package:photos/services/remote_assets_service.dart"; import "package:photos/services/remote_assets_service.dart";
@ -26,6 +27,8 @@ import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/ui/components/toggle_switch_widget.dart"; import "package:photos/ui/components/toggle_switch_widget.dart";
import "package:photos/utils/data_util.dart"; import "package:photos/utils/data_util.dart";
import "package:photos/utils/local_settings.dart"; import "package:photos/utils/local_settings.dart";
import "package:photos/utils/ml_util.dart";
import "package:photos/utils/wakelock_util.dart";
final _logger = Logger("MachineLearningSettingsPage"); final _logger = Logger("MachineLearningSettingsPage");
@ -40,6 +43,7 @@ class MachineLearningSettingsPage extends StatefulWidget {
class _MachineLearningSettingsPageState class _MachineLearningSettingsPageState
extends State<MachineLearningSettingsPage> { extends State<MachineLearningSettingsPage> {
late InitializationState _state; late InitializationState _state;
final EnteWakeLock _wakeLock = EnteWakeLock();
late StreamSubscription<MLFrameworkInitializationUpdateEvent> late StreamSubscription<MLFrameworkInitializationUpdateEvent>
_eventSubscription; _eventSubscription;
@ -53,6 +57,7 @@ class _MachineLearningSettingsPageState
setState(() {}); setState(() {});
}); });
_fetchState(); _fetchState();
_wakeLock.enable();
} }
void _fetchState() { void _fetchState() {
@ -63,6 +68,7 @@ class _MachineLearningSettingsPageState
void dispose() { void dispose() {
super.dispose(); super.dispose();
_eventSubscription.cancel(); _eventSubscription.cancel();
_wakeLock.disable();
} }
@override @override
@ -83,8 +89,8 @@ class _MachineLearningSettingsPageState
iconButtonType: IconButtonType.secondary, iconButtonType: IconButtonType.secondary,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context); if (Navigator.canPop(context)) Navigator.pop(context);
Navigator.pop(context); if (Navigator.canPop(context)) Navigator.pop(context);
}, },
), ),
], ],
@ -438,19 +444,24 @@ class FaceRecognitionStatusWidgetState
}); });
} }
Future<(int, int, int, double)> getIndexStatus() async { Future<(int, int, double, bool)> getIndexStatus() async {
try { try {
final indexedFiles = await FaceMLDataDB.instance final indexedFiles = await FaceMLDataDB.instance
.getIndexedFileCount(minimumMlVersion: faceMlVersion); .getIndexedFileCount(minimumMlVersion: faceMlVersion);
final indexableFiles = (await FaceMlService.getIndexableFileIDs()).length; final indexableFiles = (await getIndexableFileIDs()).length;
final showIndexedFiles = min(indexedFiles, indexableFiles); final showIndexedFiles = min(indexedFiles, indexableFiles);
final pendingFiles = max(indexableFiles - indexedFiles, 0); final pendingFiles = max(indexableFiles - indexedFiles, 0);
final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount(); final clusteringDoneRatio =
final clusteredFaces = await FaceMLDataDB.instance.getClusteredToIndexableFilesRatio();
await FaceMLDataDB.instance.getClusteredFaceCount(); final bool deviceIsHealthy =
final clusteringDoneRatio = clusteredFaces / foundFaces; MachineLearningController.instance.isDeviceHealthy;
return (showIndexedFiles, pendingFiles, foundFaces, clusteringDoneRatio); return (
showIndexedFiles,
pendingFiles,
clusteringDoneRatio,
deviceIsHealthy
);
} catch (e, s) { } catch (e, s) {
_logger.severe('Error getting face recognition status', e, s); _logger.severe('Error getting face recognition status', e, s);
rethrow; rethrow;
@ -479,10 +490,17 @@ class FaceRecognitionStatusWidgetState
if (snapshot.hasData) { if (snapshot.hasData) {
final int indexedFiles = snapshot.data!.$1; final int indexedFiles = snapshot.data!.$1;
final int pendingFiles = snapshot.data!.$2; final int pendingFiles = snapshot.data!.$2;
final int foundFaces = snapshot.data!.$3; final double clusteringDoneRatio = snapshot.data!.$3;
final double clusteringDoneRatio = snapshot.data!.$4;
final double clusteringPercentage = final double clusteringPercentage =
(clusteringDoneRatio * 100).clamp(0, 100); (clusteringDoneRatio * 100).clamp(0, 100);
final bool isDeviceHealthy = snapshot.data!.$4;
if (!isDeviceHealthy &&
(pendingFiles > 0 || clusteringPercentage < 99)) {
return MenuSectionDescriptionWidget(
content: S.of(context).indexingIsPaused,
);
}
return Column( return Column(
children: [ children: [
@ -512,19 +530,6 @@ class FaceRecognitionStatusWidgetState
isGestureDetectorDisabled: true, isGestureDetectorDisabled: true,
key: ValueKey("pending_items_" + pendingFiles.toString()), key: ValueKey("pending_items_" + pendingFiles.toString()),
), ),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).foundFaces,
),
trailingWidget: Text(
NumberFormat().format(foundFaces),
style: Theme.of(context).textTheme.bodySmall,
),
singleBorderRadius: 8,
alignCaptionedTextToLeft: true,
isGestureDetectorDisabled: true,
key: ValueKey("found_faces_" + foundFaces.toString()),
),
MenuItemWidget( MenuItemWidget(
captionedTextWidget: CaptionedTextWidget( captionedTextWidget: CaptionedTextWidget(
title: S.of(context).clusteringProgress, title: S.of(context).clusteringProgress,

View file

@ -17,9 +17,9 @@ import 'package:photos/ui/viewer/file/video_controls.dart';
import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/dialog_util.dart";
import 'package:photos/utils/file_util.dart'; import 'package:photos/utils/file_util.dart';
import 'package:photos/utils/toast_util.dart'; import 'package:photos/utils/toast_util.dart';
import "package:photos/utils/wakelock_util.dart";
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class VideoWidget extends StatefulWidget { class VideoWidget extends StatefulWidget {
final EnteFile file; final EnteFile file;
@ -45,7 +45,7 @@ class _VideoWidgetState extends State<VideoWidget> {
ChewieController? _chewieController; ChewieController? _chewieController;
final _progressNotifier = ValueNotifier<double?>(null); final _progressNotifier = ValueNotifier<double?>(null);
bool _isPlaying = false; bool _isPlaying = false;
bool _wakeLockEnabledHere = false; final EnteWakeLock _wakeLock = EnteWakeLock();
@override @override
void initState() { void initState() {
@ -126,13 +126,7 @@ class _VideoWidgetState extends State<VideoWidget> {
_chewieController?.dispose(); _chewieController?.dispose();
_progressNotifier.dispose(); _progressNotifier.dispose();
if (_wakeLockEnabledHere) { _wakeLock.dispose();
unawaited(
WakelockPlus.enabled.then((isEnabled) {
isEnabled ? WakelockPlus.disable() : null;
}),
);
}
super.dispose(); super.dispose();
} }
@ -257,17 +251,10 @@ class _VideoWidgetState extends State<VideoWidget> {
Future<void> _keepScreenAliveOnPlaying(bool isPlaying) async { Future<void> _keepScreenAliveOnPlaying(bool isPlaying) async {
if (isPlaying) { if (isPlaying) {
return WakelockPlus.enabled.then((value) { _wakeLock.enable();
if (value == false) {
WakelockPlus.enable();
//wakeLockEnabledHere will not be set to true if wakeLock is already enabled from settings on iOS.
//We shouldn't disable when video is not playing if it was enabled manually by the user from ente settings by user.
_wakeLockEnabledHere = true;
}
});
} }
if (_wakeLockEnabledHere && !isPlaying) { if (!isPlaying) {
return WakelockPlus.disable(); _wakeLock.disable();
} }
} }

View file

@ -5,6 +5,7 @@ import "package:flutter/material.dart";
import "package:flutter_animate/flutter_animate.dart"; import "package:flutter_animate/flutter_animate.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
import "package:photos/core/configuration.dart"; import "package:photos/core/configuration.dart";
import "package:photos/core/constants.dart";
import "package:photos/db/files_db.dart"; import "package:photos/db/files_db.dart";
import "package:photos/generated/l10n.dart"; import "package:photos/generated/l10n.dart";
import "package:photos/l10n/l10n.dart"; import "package:photos/l10n/l10n.dart";
@ -167,7 +168,14 @@ class AddPhotosPhotoWidget extends StatelessWidget {
Future<void> _onPickFromDeviceClicked(BuildContext context) async { Future<void> _onPickFromDeviceClicked(BuildContext context) async {
try { try {
final List<AssetEntity>? result = await AssetPicker.pickAssets(context); final assetPickerTextDelegate = await _getAssetPickerTextDelegate();
final List<AssetEntity>? result = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
maxAssets: maxPickAssetLimit,
textDelegate: assetPickerTextDelegate,
),
);
if (result != null && result.isNotEmpty) { if (result != null && result.isNotEmpty) {
final ca = CollectionActions( final ca = CollectionActions(
CollectionsService.instance, CollectionsService.instance,
@ -204,6 +212,39 @@ class AddPhotosPhotoWidget extends StatelessWidget {
} }
} }
} }
// _getAssetPickerTextDelegate returns the text delegate for the asset picker
// This custom method is required to enforce English as the default fallback
// instead of Chinese.
Future<AssetPickerTextDelegate> _getAssetPickerTextDelegate() async {
final Locale locale = await getLocale();
switch (locale.languageCode.toLowerCase()) {
case "en":
return const EnglishAssetPickerTextDelegate();
case "he":
return const HebrewAssetPickerTextDelegate();
case "de":
return const GermanAssetPickerTextDelegate();
case "ru":
return const RussianAssetPickerTextDelegate();
case "ja":
return const JapaneseAssetPickerTextDelegate();
case "ar":
return const ArabicAssetPickerTextDelegate();
case "fr":
return const FrenchAssetPickerTextDelegate();
case "vi":
return const VietnameseAssetPickerTextDelegate();
case "tr":
return const TurkishAssetPickerTextDelegate();
case "ko":
return const KoreanAssetPickerTextDelegate();
case "zh":
return const AssetPickerTextDelegate();
default:
return const EnglishAssetPickerTextDelegate();
}
}
} }
class DelayedGallery extends StatefulWidget { class DelayedGallery extends StatefulWidget {

View file

@ -97,7 +97,7 @@ class _AppBarWidgetState extends State<ClusterAppBar> {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
actions: kDebugMode ? _getDefaultActions(context) : null, actions: _getDefaultActions(context),
); );
} }

View file

@ -161,43 +161,45 @@ class _ClusterPageState extends State<ClusterPage> {
), ),
), ),
showNamingBanner showNamingBanner
? Dismissible( ? SafeArea(
key: const Key("namingBanner"), child: Dismissible(
direction: DismissDirection.horizontal, key: const Key("namingBanner"),
onDismissed: (direction) { direction: DismissDirection.horizontal,
setState(() { onDismissed: (direction) {
userDismissedNamingBanner = true; setState(() {
}); userDismissedNamingBanner = true;
}, });
child: PeopleBanner(
type: PeopleBannerType.addName,
faceWidget: PersonFaceWidget(
files.first,
clusterID: widget.clusterID,
),
actionIcon: Icons.add_outlined,
text: S.of(context).addAName,
subText: S.of(context).findPeopleByName,
onTap: () async {
if (widget.personID == null) {
final result = await showAssignPersonAction(
context,
clusterID: widget.clusterID,
);
if (result != null &&
result is (PersonEntity, EnteFile)) {
Navigator.pop(context);
// ignore: unawaited_futures
routeToPage(context, PeoplePage(person: result.$1));
} else if (result != null && result is PersonEntity) {
Navigator.pop(context);
// ignore: unawaited_futures
routeToPage(context, PeoplePage(person: result));
}
} else {
showShortToast(context, "No personID or clusterID");
}
}, },
child: PeopleBanner(
type: PeopleBannerType.addName,
faceWidget: PersonFaceWidget(
files.first,
clusterID: widget.clusterID,
),
actionIcon: Icons.add_outlined,
text: S.of(context).addAName,
subText: S.of(context).findPeopleByName,
onTap: () async {
if (widget.personID == null) {
final result = await showAssignPersonAction(
context,
clusterID: widget.clusterID,
);
if (result != null &&
result is (PersonEntity, EnteFile)) {
Navigator.pop(context);
// ignore: unawaited_futures
routeToPage(context, PeoplePage(person: result.$1));
} else if (result != null && result is PersonEntity) {
Navigator.pop(context);
// ignore: unawaited_futures
routeToPage(context, PeoplePage(person: result));
}
} else {
showShortToast(context, "No personID or clusterID");
}
},
),
), ),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),

View file

@ -38,12 +38,17 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
.getClusterFilesForPersonID(widget.person.remoteID), .getClusterFilesForPersonID(widget.person.remoteID),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final List<int> keys = snapshot.data!.keys.toList(); final clusters = snapshot.data!;
final List<int> keys = clusters.keys.toList();
// Sort the clusters by the number of files in each cluster, largest first
keys.sort(
(b, a) => clusters[a]!.length.compareTo(clusters[b]!.length),
);
return ListView.builder( return ListView.builder(
itemCount: keys.length, itemCount: keys.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final int clusterID = keys[index]; final int clusterID = keys[index];
final List<EnteFile> files = snapshot.data![keys[index]]!; final List<EnteFile> files = clusters[clusterID]!;
return InkWell( return InkWell(
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
@ -93,34 +98,37 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text( Text(
"${snapshot.data![keys[index]]!.length} photos", "${files.length} photos",
style: getEnteTextTheme(context).body, style: getEnteTextTheme(context).body,
), ),
GestureDetector( (index != 0)
onTap: () async { ? GestureDetector(
try { onTap: () async {
await PersonService.instance try {
.removeClusterToPerson( await PersonService.instance
personID: widget.person.remoteID, .removeClusterToPerson(
clusterID: clusterID, personID: widget.person.remoteID,
); clusterID: clusterID,
_logger.info( );
"Removed cluster $clusterID from person ${widget.person.remoteID}", _logger.info(
); "Removed cluster $clusterID from person ${widget.person.remoteID}",
Bus.instance.fire(PeopleChangedEvent()); );
setState(() {}); Bus.instance
} catch (e) { .fire(PeopleChangedEvent());
_logger.severe( setState(() {});
"removing cluster from person,", } catch (e) {
e, _logger.severe(
); "removing cluster from person,",
} e,
}, );
child: const Icon( }
CupertinoIcons.minus_circled, },
color: Colors.red, child: const Icon(
), CupertinoIcons.minus_circled,
), color: Colors.red,
),
)
: const SizedBox.shrink(),
], ],
), ),
), ),

View file

@ -203,7 +203,7 @@ class SearchWidgetState extends State<SearchWidget> {
String query, String query,
) { ) {
int resultCount = 0; int resultCount = 0;
final maxResultCount = _isYearValid(query) ? 13 : 12; final maxResultCount = _isYearValid(query) ? 12 : 11;
final streamController = StreamController<List<SearchResult>>(); final streamController = StreamController<List<SearchResult>>();
if (query.isEmpty) { if (query.isEmpty) {
@ -260,10 +260,11 @@ class SearchWidgetState extends State<SearchWidget> {
onResultsReceived(locationResult); onResultsReceived(locationResult);
}, },
); );
_searchService.getAllFace(null).then( _searchService.getAllFace(null).then(
(locationResult) { (faceResult) {
final List<GenericSearchResult> filteredResults = []; final List<GenericSearchResult> filteredResults = [];
for (final result in locationResult) { for (final result in faceResult) {
if (result.name().toLowerCase().contains(query.toLowerCase())) { if (result.name().toLowerCase().contains(query.toLowerCase())) {
filteredResults.add(result); filteredResults.add(result);
} }

View file

@ -16,6 +16,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:photos/core/configuration.dart'; import 'package:photos/core/configuration.dart';
import 'package:photos/core/error-reporting/super_logging.dart'; import 'package:photos/core/error-reporting/super_logging.dart';
import "package:photos/generated/l10n.dart"; import "package:photos/generated/l10n.dart";
import "package:photos/ui/common/progress_dialog.dart";
import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/buttons/button_widget.dart';
import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart';
import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/components/models/button_type.dart';
@ -122,9 +123,28 @@ Future<void> _sendLogs(
} }
} }
Future<String> getZippedLogsFile(BuildContext context) async { Future<void> sendLogsForInit(
final dialog = createProgressDialog(context, S.of(context).preparingLogs); String toEmail,
await dialog.show(); String? subject,
String? body,
) async {
final String zipFilePath = await getZippedLogsFile(null);
final Email email = Email(
recipients: [toEmail],
subject: subject ?? '',
body: body ?? '',
attachmentPaths: [zipFilePath],
isHTML: false,
);
await FlutterEmailSender.send(email);
}
Future<String> getZippedLogsFile(BuildContext? context) async {
late final ProgressDialog dialog;
if (context != null) {
dialog = createProgressDialog(context, S.of(context).preparingLogs);
await dialog.show();
}
final logsPath = (await getApplicationSupportDirectory()).path; final logsPath = (await getApplicationSupportDirectory()).path;
final logsDirectory = Directory(logsPath + "/logs"); final logsDirectory = Directory(logsPath + "/logs");
final tempPath = (await getTemporaryDirectory()).path; final tempPath = (await getTemporaryDirectory()).path;
@ -134,7 +154,9 @@ Future<String> getZippedLogsFile(BuildContext context) async {
encoder.create(zipFilePath); encoder.create(zipFilePath);
await encoder.addDirectory(logsDirectory); await encoder.addDirectory(logsDirectory);
encoder.close(); encoder.close();
await dialog.hide(); if (context != null) {
await dialog.hide();
}
return zipFilePath; return zipFilePath;
} }

View file

@ -0,0 +1,7 @@
import "package:photos/core/configuration.dart";
import "package:photos/db/files_db.dart";
Future<List<int>> getIndexableFileIDs() async {
return FilesDB.instance
.getOwnedFileIDs(Configuration.instance.getUserID()!);
}

View file

@ -0,0 +1,38 @@
import "dart:async" show unawaited;
import "package:wakelock_plus/wakelock_plus.dart";
class EnteWakeLock {
bool _wakeLockEnabledHere = false;
void enable() {
WakelockPlus.enabled.then((value) {
if (value == false) {
WakelockPlus.enable();
//wakeLockEnabledHere will not be set to true if wakeLock is already enabled from settings on iOS.
//We shouldn't disable when video is not playing if it was enabled manually by the user from ente settings by user.
_wakeLockEnabledHere = true;
}
});
}
void disable() {
if (_wakeLockEnabledHere) {
WakelockPlus.disable();
}
}
void dispose() {
if (_wakeLockEnabledHere) {
unawaited(
WakelockPlus.enabled.then((isEnabled) {
isEnabled ? WakelockPlus.disable() : null;
}),
);
}
}
static Future<void> toggle({required bool enable}) async {
await WakelockPlus.toggle(enable: enable);
}
}

View file

@ -45,10 +45,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: animated_list_plus name: animated_list_plus
sha256: fe66f9c300d715254727fbdf050487844d17b013fec344fa28081d29bddbdf1a sha256: fb3d7f1fbaf5af84907f3c739236bacda8bf32cbe1f118dd51510752883ff50c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.5" version: "0.5.2"
animated_stack_widget: animated_stack_widget:
dependency: transitive dependency: transitive
description: description:
@ -971,10 +971,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: home_widget name: home_widget
sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3" sha256: "2a0fdd6267ff975bd07bedf74686bd5577200f504f5de36527ac1b56bdbe68e3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "0.6.0"
html: html:
dependency: transitive dependency: transitive
description: description:
@ -1152,26 +1152,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.0" version: "10.0.4"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "3.0.3"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "3.0.1"
like_button: like_button:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1368,10 +1368,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.0" version: "1.12.0"
mgrs_dart: mgrs_dart:
dependency: transitive dependency: transitive
description: description:
@ -2144,10 +2144,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: styled_text name: styled_text
sha256: f72928d1ebe8cb149e3b34a689cb1ddca696b808187cf40ac3a0bd183dff379c sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "8.1.0"
sync_http: sync_http:
dependency: transitive dependency: transitive
description: description:
@ -2160,18 +2160,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: syncfusion_flutter_core name: syncfusion_flutter_core
sha256: "9be1bb9bbdb42823439a18da71484f1964c14dbe1c255ab1b931932b12fa96e8" sha256: "63108a33f9b0d89f7b6b56cce908b8e519fe433dbbe0efcf41ad3e8bb2081bd9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "19.4.56" version: "25.2.5"
syncfusion_flutter_sliders: syncfusion_flutter_sliders:
dependency: "direct main" dependency: "direct main"
description: description:
name: syncfusion_flutter_sliders name: syncfusion_flutter_sliders
sha256: "1f6a63ccab4180b544074b9264a20f01ee80b553de154192fe1d7b434089d3c2" sha256: f27310bedc0e96e84054f0a70ac593d1a3c38397c158c5226ba86027ad77b2c1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "19.4.56" version: "25.2.5"
synchronized: synchronized:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2192,26 +2192,26 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: test name: test
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.24.9" version: "1.25.2"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.1" version: "0.7.0"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.9" version: "0.6.0"
timezone: timezone:
dependency: transitive dependency: transitive
description: description:
@ -2441,10 +2441,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "13.0.0" version: "14.2.1"
volume_controller: volume_controller:
dependency: transitive dependency: transitive
description: description:
@ -2591,4 +2591,4 @@ packages:
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.3.0 <4.0.0" dart: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0" flutter: ">=3.20.0-1.2.pre"

View file

@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.8.106+630 version: 0.8.112+636
publish_to: none publish_to: none
environment: environment:
@ -21,7 +21,7 @@ environment:
dependencies: dependencies:
adaptive_theme: ^3.1.0 adaptive_theme: ^3.1.0
animate_do: ^2.0.0 animate_do: ^2.0.0
animated_list_plus: ^0.4.5 animated_list_plus: ^0.5.2
archive: ^3.1.2 archive: ^3.1.2
background_fetch: ^1.2.1 background_fetch: ^1.2.1
battery_info: ^1.1.1 battery_info: ^1.1.1
@ -93,13 +93,13 @@ dependencies:
fluttertoast: ^8.0.6 fluttertoast: ^8.0.6
freezed_annotation: ^2.4.1 freezed_annotation: ^2.4.1
google_nav_bar: ^5.0.5 google_nav_bar: ^5.0.5
home_widget: ^0.5.0 home_widget: ^0.6.0
html_unescape: ^2.0.0 html_unescape: ^2.0.0
http: ^1.1.0 http: ^1.1.0
image: ^4.0.17 image: ^4.0.17
image_editor: ^1.3.0 image_editor: ^1.3.0
in_app_purchase: ^3.0.7 in_app_purchase: ^3.0.7
intl: ^0.18.0 intl: ^0.19.0
json_annotation: ^4.8.0 json_annotation: ^4.8.0
latlong2: ^0.9.0 latlong2: ^0.9.0
like_button: ^2.0.5 like_button: ^2.0.5
@ -152,9 +152,9 @@ dependencies:
sqlite3_flutter_libs: ^0.5.20 sqlite3_flutter_libs: ^0.5.20
sqlite_async: ^0.6.1 sqlite_async: ^0.6.1
step_progress_indicator: ^1.0.2 step_progress_indicator: ^1.0.2
styled_text: ^7.0.0 styled_text: ^8.1.0
syncfusion_flutter_core: ^19.2.49 syncfusion_flutter_core: ^25.2.5
syncfusion_flutter_sliders: ^19.2.49 syncfusion_flutter_sliders: ^25.2.5
synchronized: ^3.1.0 synchronized: ^3.1.0
tuple: ^2.0.0 tuple: ^2.0.0
uni_links: ^0.5.1 uni_links: ^0.5.1
@ -177,6 +177,7 @@ dependency_overrides:
# Remove this after removing dependency from flutter_sodium. # Remove this after removing dependency from flutter_sodium.
# Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0 # Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0
ffi: 2.1.0 ffi: 2.1.0
intl: 0.18.1
video_player: video_player:
git: git:
url: https://github.com/ente-io/packages.git url: https://github.com/ente-io/packages.git

View file

@ -95,6 +95,15 @@ db:
# Map of data centers # Map of data centers
# #
# Each data center also specifies which bucket in that provider should be used. # Each data center also specifies which bucket in that provider should be used.
#
# If you're not using replication (it is off by default), you only need to
# provide valid credentials for the first entry (the default hot storage,
# "b2-eu-cen").
#
# Note that you need to use the same key names (e.g. "b2-eu-cen") as below. The
# values and the S3 provider itself can any arbitrary S3 storage, it is not tied
# to the region (eu-cen) or provider (b2, wasabi), but for historical reasons
# the key names have to be one of those in the list below.
s3: s3:
# Override the primary and secondary hot storage. The commented out values # Override the primary and secondary hot storage. The commented out values
# are the defaults. # are the defaults.

File diff suppressed because one or more lines are too long

View file

@ -1,17 +1,16 @@
import { CustomHead } from "@/next/components/Head"; import { CustomHead } from "@/next/components/Head";
import { setupI18n } from "@/next/i18n"; import { setupI18n } from "@/next/i18n";
import { logUnhandledErrorsAndRejections } from "@/next/log-web"; import { logUnhandledErrorsAndRejections } from "@/next/log-web";
import type { AppName, BaseAppContextT } from "@/next/types/app";
import { ensure } from "@/utils/ensure";
import { PAGES } from "@ente/accounts/constants/pages"; import { PAGES } from "@ente/accounts/constants/pages";
import { accountLogout } from "@ente/accounts/services/logout"; import { accountLogout } from "@ente/accounts/services/logout";
import { APPS, APP_TITLES } from "@ente/shared/apps/constants"; import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container"; import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import { import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
DialogBoxAttributesV2,
SetDialogBoxAttributesV2,
} from "@ente/shared/components/DialogBoxV2/types";
import EnteSpinner from "@ente/shared/components/EnteSpinner"; import EnteSpinner from "@ente/shared/components/EnteSpinner";
import AppNavbar from "@ente/shared/components/Navbar/app"; import { AppNavbar } from "@ente/shared/components/Navbar/app";
import { useLocalState } from "@ente/shared/hooks/useLocalState"; import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService"; import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
@ -22,25 +21,28 @@ import { ThemeProvider } from "@mui/material/styles";
import { t } from "i18next"; import { t } from "i18next";
import { AppProps } from "next/app"; import { AppProps } from "next/app";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { createContext, useEffect, useState } from "react"; import { createContext, useContext, useEffect, useState } from "react";
import "styles/global.css"; import "styles/global.css";
interface AppContextProps { /** The accounts app has no extra properties on top of the base context. */
isMobile: boolean; type AppContextT = BaseAppContextT;
showNavBar: (show: boolean) => void;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
logout: () => void;
}
export const AppContext = createContext<AppContextProps>({} as AppContextProps); /** The React {@link Context} available to all pages. */
export const AppContext = createContext<AppContextT | undefined>(undefined);
/** Utility hook to reduce amount of boilerplate in account related pages. */
export const useAppContext = () => ensure(useContext(AppContext));
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
const appName: AppName = "account";
const [isI18nReady, setIsI18nReady] = useState<boolean>(false); const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [showNavbar, setShowNavBar] = useState(false); const [showNavbar, setShowNavBar] = useState(false);
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
useState<DialogBoxAttributesV2>(); DialogBoxAttributesV2 | undefined
>();
const [dialogBoxV2View, setDialogBoxV2View] = useState(false); const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
@ -85,8 +87,17 @@ export default function App({ Component, pageProps }: AppProps) {
void accountLogout().then(() => router.push(PAGES.ROOT)); void accountLogout().then(() => router.push(PAGES.ROOT));
}; };
const appContext = {
appName,
logout,
showNavBar,
isMobile,
setDialogBoxAttributesV2,
};
// TODO: This string doesn't actually exist
const title = isI18nReady const title = isI18nReady
? t("TITLE", { context: APPS.ACCOUNTS }) ? t("title", { context: "accounts" })
: APP_TITLES.get(APPS.ACCOUNTS); : APP_TITLES.get(APPS.ACCOUNTS);
return ( return (
@ -102,15 +113,7 @@ export default function App({ Component, pageProps }: AppProps) {
attributes={dialogBoxAttributeV2 as any} attributes={dialogBoxAttributeV2 as any}
/> />
<AppContext.Provider <AppContext.Provider value={appContext}>
value={{
isMobile,
showNavBar,
setDialogBoxAttributesV2:
setDialogBoxAttributesV2 as any,
logout,
}}
>
{!isI18nReady && ( {!isI18nReady && (
<Overlay <Overlay
sx={(theme) => ({ sx={(theme) => ({

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/credentials";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,9 +0,0 @@
import CredentialPage from "@ente/accounts/pages/credentials";
import { APPS } from "@ente/shared/apps/constants";
import { useContext } from "react";
import { AppContext } from "../_app";
export default function Credential() {
const appContext = useContext(AppContext);
return <CredentialPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/generate";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,9 +0,0 @@
import GeneratePage from "@ente/accounts/pages/generate";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Generate() {
const appContext = useContext(AppContext);
return <GeneratePage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/login";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,9 +0,0 @@
import LoginPage from "@ente/accounts/pages/login";
import { APPS } from "@ente/shared/apps/constants";
import { useContext } from "react";
import { AppContext } from "../_app";
export default function Login() {
const appContext = useContext(AppContext);
return <LoginPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

@ -1,16 +1,12 @@
import { TwoFactorType } from "@ente/accounts/constants/twofactor"; import { TwoFactorType } from "@ente/accounts/constants/twofactor";
import RecoverPage from "@ente/accounts/pages/recover"; import RecoverPage from "@ente/accounts/pages/two-factor/recover";
import { APPS } from "@ente/shared/apps/constants"; import { useAppContext } from "../../_app";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Recover() { const Page = () => (
const appContext = useContext(AppContext); <RecoverPage
return ( appContext={useAppContext()}
<RecoverPage twoFactorType={TwoFactorType.PASSKEY}
appContext={appContext} />
appName={APPS.PHOTOS} );
twoFactorType={TwoFactorType.PASSKEY}
/> export default Page;
);
}

View file

@ -9,14 +9,8 @@ import { t } from "i18next";
import _sodium from "libsodium-wrappers"; import _sodium from "libsodium-wrappers";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AppContext } from "pages/_app"; import { AppContext } from "pages/_app";
import { import type { Dispatch, SetStateAction } from "react";
Dispatch, import { createContext, useContext, useEffect, useState } from "react";
SetStateAction,
createContext,
useContext,
useEffect,
useState,
} from "react";
import { Passkey } from "types/passkey"; import { Passkey } from "types/passkey";
import { import {
finishPasskeyRegistration, finishPasskeyRegistration,

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/recover";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,9 +0,0 @@
import RecoverPage from "@ente/accounts/pages/recover";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Recover() {
const appContext = useContext(AppContext);
return <RecoverPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/signup";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,9 +0,0 @@
import SignupPage from "@ente/accounts/pages/signup";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Sigup() {
const appContext = useContext(AppContext);
return <SignupPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/two-factor/recover";
import { useAppContext } from "../_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,11 +0,0 @@
import TwoFactorRecoverPage from "@ente/accounts/pages/two-factor/recover";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function TwoFactorRecover() {
const appContext = useContext(AppContext);
return (
<TwoFactorRecoverPage appContext={appContext} appName={APPS.ACCOUNTS} />
);
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/two-factor/setup";
import { useAppContext } from "../_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,11 +0,0 @@
import TwoFactorSetupPage from "@ente/accounts/pages/two-factor/setup";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function TwoFactorSetup() {
const appContext = useContext(AppContext);
return (
<TwoFactorSetupPage appContext={appContext} appName={APPS.ACCOUNTS} />
);
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/two-factor/verify";
import { useAppContext } from "../_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,11 +0,0 @@
import TwoFactorVerifyPage from "@ente/accounts/pages/two-factor/verify";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function TwoFactorVerify() {
const appContext = useContext(AppContext);
return (
<TwoFactorVerifyPage appContext={appContext} appName={APPS.ACCOUNTS} />
);
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/verify";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,9 +0,0 @@
import VerifyPage from "@ente/accounts/pages/verify";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Verify() {
const appContext = useContext(AppContext);
return <VerifyPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

@ -1 +1,4 @@
NEXT_TELEMETRY_DISABLED = 1 NEXT_TELEMETRY_DISABLED = 1
# For details on how to populate a .env.local to run auth and get it to connect
# to an arbitrary Ente instance, see `apps/photos/.env`.

View file

@ -9,5 +9,5 @@ module.exports = {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: "./tsconfig.json", project: "./tsconfig.json",
}, },
ignorePatterns: [".eslintrc.js", "out"], ignorePatterns: [".eslintrc.js", "next.config.js", "out"],
}; };

View file

@ -3,9 +3,12 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@/build-config": "*",
"@/next": "*", "@/next": "*",
"@ente/accounts": "*", "@ente/accounts": "*",
"@ente/eslint-config": "*", "@ente/eslint-config": "*",
"@ente/shared": "*" "@ente/shared": "*",
"jssha": "~3.3.1",
"otpauth": "^9"
} }
} }

View file

@ -1,23 +0,0 @@
import { Button } from "@mui/material";
import { t } from "i18next";
export const AuthFooter = () => {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<p>{t("AUTH_DOWNLOAD_MOBILE_APP")}</p>
<a
href="https://github.com/ente-io/ente/tree/main/auth#-download"
download
>
<Button color="accent">{t("DOWNLOAD")}</Button>
</a>
</div>
);
};

View file

@ -1,35 +0,0 @@
import { HorizontalFlex } from "@ente/shared/components/Container";
import { EnteLogo } from "@ente/shared/components/EnteLogo";
import NavbarBase from "@ente/shared/components/Navbar/base";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import LogoutOutlined from "@mui/icons-material/LogoutOutlined";
import MoreHoriz from "@mui/icons-material/MoreHoriz";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import React from "react";
export default function AuthNavbar() {
const { isMobile, logout } = React.useContext(AppContext);
return (
<NavbarBase isMobile={isMobile}>
<HorizontalFlex flex={1} justifyContent={"center"}>
<EnteLogo />
</HorizontalFlex>
<HorizontalFlex position={"absolute"} right="24px">
<OverflowMenu
ariaControls={"auth-options"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenuOption
color="critical"
startIcon={<LogoutOutlined />}
onClick={logout}
>
{t("LOGOUT")}
</OverflowMenuOption>
</OverflowMenu>
</HorizontalFlex>
</NavbarBase>
);
}

View file

@ -1,237 +0,0 @@
import { ButtonBase, Snackbar } from "@mui/material";
import { t } from "i18next";
import { HOTP, TOTP } from "otpauth";
import { useEffect, useState } from "react";
import { Code } from "types/code";
import TimerProgress from "./TimerProgress";
const TOTPDisplay = ({ issuer, account, code, nextCode, period }) => {
return (
<div
style={{
backgroundColor: "rgba(40, 40, 40, 0.6)",
borderRadius: "4px",
overflow: "hidden",
}}
>
<TimerProgress period={period ?? Code.defaultPeriod} />
<div
style={{
padding: "12px 20px 0px 20px",
display: "flex",
alignItems: "flex-start",
minWidth: "320px",
minHeight: "120px",
justifyContent: "space-between",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
minWidth: "200px",
}}
>
<p
style={{
fontWeight: "bold",
margin: "0px",
fontSize: "14px",
textAlign: "left",
}}
>
{issuer}
</p>
<p
style={{
marginTop: "0px",
marginBottom: "8px",
textAlign: "left",
fontSize: "12px",
maxWidth: "200px",
minHeight: "16px",
color: "grey",
}}
>
{account}
</p>
<p
style={{
margin: "0px",
marginBottom: "1rem",
fontSize: "24px",
fontWeight: "bold",
textAlign: "left",
}}
>
{code}
</p>
</div>
<div style={{ flex: 1 }} />
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
minWidth: "120px",
textAlign: "right",
marginTop: "auto",
marginBottom: "1rem",
}}
>
<p
style={{
fontWeight: "bold",
marginBottom: "0px",
fontSize: "10px",
marginTop: "auto",
textAlign: "right",
color: "grey",
}}
>
{t("AUTH_NEXT")}
</p>
<p
style={{
fontSize: "14px",
fontWeight: "bold",
marginBottom: "0px",
marginTop: "auto",
textAlign: "right",
color: "grey",
}}
>
{nextCode}
</p>
</div>
</div>
</div>
);
};
function BadCodeInfo({ codeInfo, codeErr }) {
const [showRawData, setShowRawData] = useState(false);
return (
<div className="code-info">
<div>{codeInfo.title}</div>
<div>{codeErr}</div>
<div>
{showRawData ? (
<div onClick={() => setShowRawData(false)}>
{codeInfo.rawData ?? "no raw data"}
</div>
) : (
<div onClick={() => setShowRawData(true)}>Show rawData</div>
)}
</div>
</div>
);
}
interface OTPDisplayProps {
codeInfo: Code;
}
const OTPDisplay = (props: OTPDisplayProps) => {
const { codeInfo } = props;
const [code, setCode] = useState("");
const [nextCode, setNextCode] = useState("");
const [codeErr, setCodeErr] = useState("");
const [hasCopied, setHasCopied] = useState(false);
const generateCodes = () => {
try {
const currentTime = new Date().getTime();
if (codeInfo.type.toLowerCase() === "totp") {
const totp = new TOTP({
secret: codeInfo.secret,
algorithm: codeInfo.algorithm ?? Code.defaultAlgo,
period: codeInfo.period ?? Code.defaultPeriod,
digits: codeInfo.digits ?? Code.defaultDigits,
});
setCode(totp.generate());
setNextCode(
totp.generate({
timestamp: currentTime + codeInfo.period * 1000,
}),
);
} else if (codeInfo.type.toLowerCase() === "hotp") {
const hotp = new HOTP({
secret: codeInfo.secret,
counter: 0,
algorithm: codeInfo.algorithm,
});
setCode(hotp.generate());
setNextCode(hotp.generate({ counter: 1 }));
}
} catch (err) {
setCodeErr(err.message);
}
};
const copyCode = () => {
navigator.clipboard.writeText(code);
setHasCopied(true);
setTimeout(() => {
setHasCopied(false);
}, 2000);
};
useEffect(() => {
// this is to set the initial code and nextCode on component mount
generateCodes();
const codeType = codeInfo.type;
const codePeriodInMs = codeInfo.period * 1000;
const timeToNextCode =
codePeriodInMs - (new Date().getTime() % codePeriodInMs);
const intervalId = null;
// wait until we are at the start of the next code period,
// and then start the interval loop
setTimeout(() => {
// we need to call generateCodes() once before the interval loop
// to set the initial code and nextCode
generateCodes();
codeType.toLowerCase() === "totp" ||
codeType.toLowerCase() === "hotp"
? setInterval(() => {
generateCodes();
}, codePeriodInMs)
: null;
}, timeToNextCode);
return () => {
if (intervalId) clearInterval(intervalId);
};
}, [codeInfo]);
return (
<div style={{ padding: "8px" }}>
{codeErr === "" ? (
<ButtonBase
component="div"
onClick={() => {
copyCode();
}}
>
<TOTPDisplay
period={codeInfo.period}
issuer={codeInfo.issuer}
account={codeInfo.account}
code={code}
nextCode={nextCode}
/>
<Snackbar
open={hasCopied}
message="Code copied to clipboard"
/>
</ButtonBase>
) : (
<BadCodeInfo codeInfo={codeInfo} codeErr={codeErr} />
)}
</div>
);
};
export default OTPDisplay;

View file

@ -1,41 +0,0 @@
import { useEffect, useState } from "react";
const TimerProgress = ({ period }) => {
const [progress, setProgress] = useState(0);
const [ticker, setTicker] = useState(null);
const microSecondsInPeriod = period * 1000000;
const startTicker = () => {
const ticker = setInterval(() => {
updateTimeRemaining();
}, 10);
setTicker(ticker);
};
const updateTimeRemaining = () => {
const timeRemaining =
microSecondsInPeriod -
((new Date().getTime() * 1000) % microSecondsInPeriod);
setProgress(timeRemaining / microSecondsInPeriod);
};
useEffect(() => {
startTicker();
return () => clearInterval(ticker);
}, []);
const color = progress > 0.4 ? "green" : "orange";
return (
<div
style={{
borderTopLeftRadius: "3px",
width: `${progress * 100}%`,
height: "3px",
backgroundColor: color,
}}
/>
);
};
export default TimerProgress;

View file

@ -1,9 +1,3 @@
import { APPS } from "@ente/shared/apps/constants"; import Page from "@ente/shared/next/pages/404";
import NotFoundPage from "@ente/shared/next/pages/404";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function NotFound() { export default Page;
const appContext = useContext(AppContext);
return <NotFoundPage appContext={appContext} appName={APPS.AUTH} />;
}

View file

@ -4,6 +4,8 @@ import {
logStartupBanner, logStartupBanner,
logUnhandledErrorsAndRejections, logUnhandledErrorsAndRejections,
} from "@/next/log-web"; } from "@/next/log-web";
import type { AppName, BaseAppContextT } from "@/next/types/app";
import { ensure } from "@/utils/ensure";
import { accountLogout } from "@ente/accounts/services/logout"; import { accountLogout } from "@ente/accounts/services/logout";
import { import {
APPS, APPS,
@ -12,45 +14,46 @@ import {
} from "@ente/shared/apps/constants"; } from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container"; import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import { import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
DialogBoxAttributesV2,
SetDialogBoxAttributesV2,
} from "@ente/shared/components/DialogBoxV2/types";
import EnteSpinner from "@ente/shared/components/EnteSpinner"; import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { MessageContainer } from "@ente/shared/components/MessageContainer"; import { MessageContainer } from "@ente/shared/components/MessageContainer";
import AppNavbar from "@ente/shared/components/Navbar/app"; import { AppNavbar } from "@ente/shared/components/Navbar/app";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { useLocalState } from "@ente/shared/hooks/useLocalState"; import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService"; import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { getTheme } from "@ente/shared/themes"; import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants"; import { THEME_COLOR } from "@ente/shared/themes/constants";
import { SetTheme } from "@ente/shared/themes/types";
import type { User } from "@ente/shared/user/types"; import type { User } from "@ente/shared/user/types";
import { CssBaseline, useMediaQuery } from "@mui/material"; import { CssBaseline, useMediaQuery } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from "@mui/material/styles";
import { t } from "i18next"; import { t } from "i18next";
import { AppProps } from "next/app"; import type { AppProps } from "next/app";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { createContext, useEffect, useRef, useState } from "react"; import { createContext, useContext, useEffect, useRef, useState } from "react";
import LoadingBar from "react-top-loading-bar"; import LoadingBar, { type LoadingBarRef } from "react-top-loading-bar";
import "../../public/css/global.css"; import "../../public/css/global.css";
type AppContextType = { /**
showNavBar: (show: boolean) => void; * Properties available via the {@link AppContext} to the Auth app's React tree.
*/
type AppContextT = BaseAppContextT & {
startLoading: () => void; startLoading: () => void;
finishLoading: () => void; finishLoading: () => void;
isMobile: boolean;
themeColor: THEME_COLOR; themeColor: THEME_COLOR;
setThemeColor: SetTheme; setThemeColor: (themeColor: THEME_COLOR) => void;
somethingWentWrong: () => void; somethingWentWrong: () => void;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
logout: () => void;
}; };
export const AppContext = createContext<AppContextType>(null); /** The React {@link Context} available to all pages. */
export const AppContext = createContext<AppContextT | undefined>(undefined);
/** Utility hook to reduce amount of boilerplate in account related pages. */
export const useAppContext = () => ensure(useContext(AppContext));
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
const appName: AppName = "auth";
const router = useRouter(); const router = useRouter();
const [isI18nReady, setIsI18nReady] = useState<boolean>(false); const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -58,10 +61,11 @@ export default function App({ Component, pageProps }: AppProps) {
typeof window !== "undefined" && !window.navigator.onLine, typeof window !== "undefined" && !window.navigator.onLine,
); );
const [showNavbar, setShowNavBar] = useState(false); const [showNavbar, setShowNavBar] = useState(false);
const isLoadingBarRunning = useRef(false); const isLoadingBarRunning = useRef<boolean>(false);
const loadingBar = useRef(null); const loadingBar = useRef<LoadingBarRef>(null);
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
useState<DialogBoxAttributesV2>(); DialogBoxAttributesV2 | undefined
>();
const [dialogBoxV2View, setDialogBoxV2View] = useState(false); const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
const isMobile = useMediaQuery("(max-width:428px)"); const isMobile = useMediaQuery("(max-width:428px)");
const [themeColor, setThemeColor] = useLocalState( const [themeColor, setThemeColor] = useLocalState(
@ -134,9 +138,23 @@ export default function App({ Component, pageProps }: AppProps) {
void accountLogout().then(() => router.push(PAGES.ROOT)); void accountLogout().then(() => router.push(PAGES.ROOT));
}; };
const appContext = {
appName,
logout,
showNavBar,
isMobile,
setDialogBoxAttributesV2,
startLoading,
finishLoading,
themeColor,
setThemeColor,
somethingWentWrong,
};
// TODO: Refactor this to have a fallback
const title = isI18nReady const title = isI18nReady
? t("TITLE", { context: APPS.AUTH }) ? t("title", { context: "auth" })
: APP_TITLES.get(APPS.AUTH); : APP_TITLES.get(APPS.AUTH) ?? "";
return ( return (
<> <>
@ -158,19 +176,7 @@ export default function App({ Component, pageProps }: AppProps) {
attributes={dialogBoxAttributeV2} attributes={dialogBoxAttributeV2}
/> />
<AppContext.Provider <AppContext.Provider value={appContext}>
value={{
showNavBar,
startLoading,
finishLoading,
isMobile,
themeColor,
setThemeColor,
somethingWentWrong,
setDialogBoxAttributesV2,
logout,
}}
>
{(loading || !isI18nReady) && ( {(loading || !isI18nReady) && (
<Overlay <Overlay
sx={(theme) => ({ sx={(theme) => ({

View file

@ -0,0 +1,414 @@
import { ensure } from "@/utils/ensure";
import {
HorizontalFlex,
VerticallyCentered,
} from "@ente/shared/components/Container";
import { EnteLogo } from "@ente/shared/components/EnteLogo";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import NavbarBase from "@ente/shared/components/Navbar/base";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages";
import { CustomError } from "@ente/shared/error";
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
import LogoutOutlined from "@mui/icons-material/LogoutOutlined";
import MoreHoriz from "@mui/icons-material/MoreHoriz";
import { Button, ButtonBase, Snackbar, TextField, styled } from "@mui/material";
import { t } from "i18next";
import { useRouter } from "next/router";
import { AppContext } from "pages/_app";
import React, { useContext, useEffect, useState } from "react";
import { generateOTPs, type Code } from "services/code";
import { getAuthCodes } from "services/remote";
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
const router = useRouter();
const [codes, setCodes] = useState<Code[]>([]);
const [hasFetched, setHasFetched] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
const fetchCodes = async () => {
try {
setCodes(await getAuthCodes());
} catch (e) {
if (
e instanceof Error &&
e.message == CustomError.KEY_MISSING
) {
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.AUTH);
router.push(PAGES.ROOT);
} else {
// do not log errors
}
}
setHasFetched(true);
};
void fetchCodes();
appContext.showNavBar(false);
}, []);
const lcSearch = searchTerm.toLowerCase();
const filteredCodes = codes.filter(
(code) =>
code.issuer?.toLowerCase().includes(lcSearch) ||
code.account?.toLowerCase().includes(lcSearch),
);
if (!hasFetched) {
return (
<VerticallyCentered>
<EnteSpinner />
</VerticallyCentered>
);
}
return (
<>
<AuthNavbar />
<div
style={{
maxWidth: "800px",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
margin: "0 auto",
}}
>
<div style={{ marginBottom: "1rem" }} />
{filteredCodes.length == 0 && searchTerm.length == 0 ? (
<></>
) : (
<TextField
id="search"
name="search"
label={t("SEARCH")}
onChange={(e) => setSearchTerm(e.target.value)}
variant="filled"
style={{ width: "350px" }}
value={searchTerm}
autoFocus
/>
)}
<div style={{ marginBottom: "1rem" }} />
<div
style={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "center",
}}
>
{filteredCodes.length == 0 ? (
<div
style={{
alignItems: "center",
display: "flex",
textAlign: "center",
marginTop: "32px",
}}
>
{searchTerm.length > 0 ? (
<p>{t("NO_RESULTS")}</p>
) : (
<></>
)}
</div>
) : (
filteredCodes.map((code) => (
<CodeDisplay key={code.id} code={code} />
))
)}
</div>
<Footer />
</div>
</>
);
};
export default Page;
const AuthNavbar: React.FC = () => {
const { isMobile, logout } = ensure(useContext(AppContext));
return (
<NavbarBase isMobile={isMobile}>
<HorizontalFlex flex={1} justifyContent={"center"}>
<EnteLogo />
</HorizontalFlex>
<HorizontalFlex position={"absolute"} right="24px">
<OverflowMenu
ariaControls={"auth-options"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenuOption
color="critical"
startIcon={<LogoutOutlined />}
onClick={logout}
>
{t("LOGOUT")}
</OverflowMenuOption>
</OverflowMenu>
</HorizontalFlex>
</NavbarBase>
);
};
interface CodeDisplayProps {
code: Code;
}
const CodeDisplay: React.FC<CodeDisplayProps> = ({ code }) => {
const [otp, setOTP] = useState("");
const [nextOTP, setNextOTP] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [hasCopied, setHasCopied] = useState(false);
const regen = () => {
try {
const [m, n] = generateOTPs(code);
setOTP(m);
setNextOTP(n);
} catch (e) {
setErrorMessage(e instanceof Error ? e.message : String(e));
}
};
const copyCode = () => {
navigator.clipboard.writeText(otp);
setHasCopied(true);
setTimeout(() => setHasCopied(false), 2000);
};
useEffect(() => {
// Generate to set the initial otp and nextOTP on component mount.
regen();
const periodMs = code.period * 1000;
const timeToNextCode = periodMs - (Date.now() % periodMs);
let interval: ReturnType<typeof setInterval> | undefined;
// Wait until we are at the start of the next code period, and then
// start the interval loop.
setTimeout(() => {
// We need to call regen() once before the interval loop to set the
// initial otp and nextOTP.
regen();
interval = setInterval(regen, periodMs);
}, timeToNextCode);
return () => interval && clearInterval(interval);
}, [code]);
return (
<div style={{ padding: "8px" }}>
{errorMessage ? (
<UnparseableCode {...{ code, errorMessage }} />
) : (
<ButtonBase component="div" onClick={copyCode}>
<OTPDisplay {...{ code, otp, nextOTP }} />
<Snackbar open={hasCopied} message={t("COPIED")} />
</ButtonBase>
)}
</div>
);
};
interface OTPDisplayProps {
code: Code;
otp: string;
nextOTP: string;
}
const OTPDisplay: React.FC<OTPDisplayProps> = ({ code, otp, nextOTP }) => {
return (
<div
style={{
backgroundColor: "rgba(40, 40, 40, 0.6)",
borderRadius: "4px",
overflow: "hidden",
}}
>
<CodeValidityBar code={code} />
<div
style={{
padding: "12px 20px 0px 20px",
display: "flex",
alignItems: "flex-start",
minWidth: "320px",
minHeight: "120px",
justifyContent: "space-between",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
minWidth: "200px",
}}
>
<p
style={{
fontWeight: "bold",
margin: "0px",
fontSize: "14px",
textAlign: "left",
}}
>
{code.issuer ?? ""}
</p>
<p
style={{
marginTop: "0px",
marginBottom: "8px",
textAlign: "left",
fontSize: "12px",
maxWidth: "200px",
minHeight: "16px",
color: "grey",
}}
>
{code.account ?? ""}
</p>
<p
style={{
margin: "0px",
marginBottom: "1rem",
fontSize: "24px",
fontWeight: "bold",
textAlign: "left",
}}
>
{otp}
</p>
</div>
<div style={{ flex: 1 }} />
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
minWidth: "120px",
textAlign: "right",
marginTop: "auto",
marginBottom: "1rem",
}}
>
<p
style={{
fontWeight: "bold",
marginBottom: "0px",
fontSize: "10px",
marginTop: "auto",
textAlign: "right",
color: "grey",
}}
>
{t("AUTH_NEXT")}
</p>
<p
style={{
fontSize: "14px",
fontWeight: "bold",
marginBottom: "0px",
marginTop: "auto",
textAlign: "right",
color: "grey",
}}
>
{nextOTP}
</p>
</div>
</div>
</div>
);
};
interface CodeValidityBarProps {
code: Code;
}
const CodeValidityBar: React.FC<CodeValidityBarProps> = ({ code }) => {
const [progress, setProgress] = useState(code.type == "hotp" ? 1 : 0);
useEffect(() => {
const advance = () => {
const us = code.period * 1e6;
const timeRemaining = us - ((Date.now() * 1000) % us);
setProgress(timeRemaining / us);
};
const ticker =
code.type == "hotp" ? undefined : setInterval(advance, 10);
return () => ticker && clearInterval(ticker);
}, [code]);
const color = progress > 0.4 ? "green" : "orange";
return (
<div
style={{
borderTopLeftRadius: "3px",
width: `${progress * 100}%`,
height: "3px",
backgroundColor: color,
}}
/>
);
};
interface UnparseableCodeProps {
code: Code;
errorMessage: string;
}
const UnparseableCode: React.FC<UnparseableCodeProps> = ({
code,
errorMessage,
}) => {
const [showRawData, setShowRawData] = useState(false);
return (
<div className="code-info">
<div>{code.issuer}</div>
<div>{errorMessage}</div>
<div>
{showRawData ? (
<div onClick={() => setShowRawData(false)}>
{code.uriString}
</div>
) : (
<div onClick={() => setShowRawData(true)}>Show rawData</div>
)}
</div>
</div>
);
};
const Footer: React.FC = () => {
return (
<Footer_>
<p>{t("AUTH_DOWNLOAD_MOBILE_APP")}</p>
<a
href="https://github.com/ente-io/ente/tree/main/auth#-download"
download
>
<Button color="accent">{t("DOWNLOAD")}</Button>
</a>
</Footer_>
);
};
const Footer_ = styled("div")`
margin-block-start: 2rem;
margin-block-end: 4rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;

View file

@ -1,129 +0,0 @@
import { VerticallyCentered } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages";
import { CustomError } from "@ente/shared/error";
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
import { TextField } from "@mui/material";
import { AuthFooter } from "components/AuthFooter";
import AuthNavbar from "components/Navbar";
import OTPDisplay from "components/OTPDisplay";
import { t } from "i18next";
import { useRouter } from "next/router";
import { AppContext } from "pages/_app";
import { useContext, useEffect, useState } from "react";
import { getAuthCodes } from "services";
const AuthenticatorCodesPage = () => {
const appContext = useContext(AppContext);
const router = useRouter();
const [codes, setCodes] = useState([]);
const [hasFetched, setHasFetched] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
const fetchCodes = async () => {
try {
const res = await getAuthCodes();
setCodes(res);
} catch (err) {
if (err.message === CustomError.KEY_MISSING) {
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.AUTH);
router.push(PAGES.ROOT);
} else {
// do not log errors
}
}
setHasFetched(true);
};
fetchCodes();
appContext.showNavBar(false);
}, []);
const filteredCodes = codes.filter(
(secret) =>
(secret.issuer ?? "")
.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
(secret.account ?? "")
.toLowerCase()
.includes(searchTerm.toLowerCase()),
);
if (!hasFetched) {
return (
<>
<VerticallyCentered>
<EnteSpinner></EnteSpinner>
</VerticallyCentered>
</>
);
}
return (
<>
<AuthNavbar />
<div
style={{
maxWidth: "800px",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
margin: "0 auto",
}}
>
<div style={{ marginBottom: "1rem" }} />
{filteredCodes.length === 0 && searchTerm.length === 0 ? (
<></>
) : (
<TextField
id="search"
name="search"
label={t("SEARCH")}
onChange={(e) => setSearchTerm(e.target.value)}
variant="filled"
style={{ width: "350px" }}
value={searchTerm}
autoFocus
/>
)}
<div style={{ marginBottom: "1rem" }} />
<div
style={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "center",
}}
>
{filteredCodes.length === 0 ? (
<div
style={{
alignItems: "center",
display: "flex",
textAlign: "center",
marginTop: "32px",
}}
>
{searchTerm.length !== 0 ? (
<p>{t("NO_RESULTS")}</p>
) : (
<div />
)}
</div>
) : (
filteredCodes.map((code) => (
<OTPDisplay codeInfo={code} key={code.id} />
))
)}
</div>
<div style={{ marginBottom: "2rem" }} />
<AuthFooter />
<div style={{ marginBottom: "4rem" }} />
</div>
</>
);
};
export default AuthenticatorCodesPage;

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/change-email";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -1,9 +0,0 @@
import ChangeEmailPage from "@ente/accounts/pages/change-email";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function ChangeEmail() {
const appContext = useContext(AppContext);
return <ChangeEmailPage appContext={appContext} appName={APPS.AUTH} />;
}

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