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:
- type: markdown
attributes:
value: >
Before opening a new issue, please ensure you are on the latest
version (it might've already been fixed), and that you've searched
for existing issues (please add you observations as a comment
there instead of creating a duplicate).
value: |
Before opening a new bug report, please ensure
1. you are on the latest version (it might've already been fixed),
2. you've searched for existing issues (please add your observations as a comment 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
attributes:
label: Description
@ -16,7 +17,8 @@ body:
Please describe the bug. If possible, also include the steps to
reproduce the behaviour, and the expected behaviour (sometimes
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:
required: true
- type: input

View file

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

View file

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

View file

@ -20,6 +20,8 @@
"codeIssuerHint": "発行者",
"codeSecretKeyHint": "秘密鍵",
"codeAccountHint": "アカウント (you@domain.com)",
"codeTagHint": "タグ",
"accountKeyType": "鍵の種類",
"sessionExpired": "セッションが失効しました",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
@ -77,6 +79,7 @@
"data": "データ",
"importCodes": "コードをインポート",
"importTypePlainText": "プレーンテキスト",
"importTypeEnteEncrypted": "Ente 暗号化されたエクスポート",
"passwordForDecryptingExport": "復号化用パスワード",
"passwordEmptyError": "パスワードは空欄にできません",
"importFromApp": "{appName} からコードをインポート",
@ -121,6 +124,7 @@
"suggestFeatures": "機能を提案",
"faq": "FAQ",
"faq_q_1": "Authはどのくらい安全ですか",
"faq_a_1": "Ente Authでバックアップされたコードはすべてエンドツーエンドで暗号化されて保存されます。つまり、コードにアクセスできるのはあなただけです。当社のアプリはオープンソースであり、暗号化技術は外部監査を受けています。",
"faq_q_2": "パソコンから私のコードにアクセスできますか?",
"faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。",
"faq_q_3": "コードを削除するにはどうすればいいですか?",
@ -154,6 +158,7 @@
}
}
},
"invalidQRCode": "QRコードが無効です",
"noRecoveryKeyTitle": "回復キーがありませんか?",
"enterEmailHint": "メールアドレスを入力してください",
"invalidEmailTitle": "メールアドレスが無効です",
@ -347,6 +352,7 @@
"deleteCodeAuthMessage": "コードを削除するためには認証が必要です",
"showQRAuthMessage": "QR コードを表示するためには認証が必要です",
"confirmAccountDeleteTitle": "アカウントの削除に同意",
"confirmAccountDeleteMessage": "このアカウントは他のEnteアプリも使用している場合はそれらにも紐づけされています。\nすべてのEnteアプリでアップロードされたデータは削除され、アカウントは完全に削除されます。",
"androidBiometricHint": "本人を確認する",
"@androidBiometricHint": {
"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": "無効なエンドポイントです",
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
"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",
"electron-log": "^5.1",
"electron-store": "^8.2",
"electron-updater": "^6.1",
"electron-updater": "^6.2",
"ffmpeg-static": "^5.2",
"html-entities": "^2.5",
"jpeg-js": "^0.4",

View file

@ -106,7 +106,7 @@ const handleRead = async (path: string) => {
res.headers.set("Content-Length", `${fileSize}`);
// 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}`);
}
return res;
@ -132,6 +132,13 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
// Close the zip handle when the underlying stream closes.
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, {
headers: {
// 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.
"Content-Type": "application/octet-stream",
"Content-Length": `${entry.size}`,
// 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
"X-Last-Modified-Ms": `${entry.time}`,
"X-Last-Modified-Ms": `${modifiedMs}`,
},
});
};

View file

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

View file

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

View file

@ -7,4 +7,5 @@ description:
# Migrating to/from Ente Auth
- [Migrating from Authy](authy/)
- [Importing codes from Steam](steam/)
- [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
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.
### 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
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`

View file

@ -427,7 +427,7 @@ SPEC CHECKSUMS:
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892
integration_test: 13825b8a9334a850581300559b8839134b124670
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
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/file_uploader.dart';
import 'package:photos/utils/validator_util.dart';
import "package:photos/utils/wakelock_util.dart";
import 'package:shared_preferences/shared_preferences.dart';
import "package:tuple/tuple.dart";
import 'package:uuid/uuid.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class Configuration {
Configuration._privateConstructor();
@ -585,7 +585,7 @@ class Configuration {
Future<void> setShouldKeepDeviceAwake(bool value) async {
await _preferences.setBool(keyShouldKeepDeviceAwake, value);
await WakelockPlus.toggle(enable: value);
await EnteWakeLock.toggle(enable: value);
}
Future<void> setShouldBackupVideos(bool value) async {

View file

@ -69,6 +69,8 @@ const galleryGridSpacing = 2.0;
const kSearchSectionLimit = 9;
const maxPickAssetLimit = 50;
const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
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/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_ml_result.dart";
import "package:photos/utils/ml_util.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`.
@ -33,6 +35,15 @@ class FaceMLDataDB {
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
static Future<SqliteDatabase>? _sqliteAsyncDBFuture;
@ -48,17 +59,42 @@ class FaceMLDataDB {
_logger.info("Opening sqlite_async access: DB path " + databaseDirectory);
final asyncDBConnection =
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;
}
Future<void> _onCreate(SqliteDatabase asyncDBConnection) async {
await asyncDBConnection.execute(createFacesTable);
await asyncDBConnection.execute(createFaceClustersTable);
await asyncDBConnection.execute(createClusterPersonTable);
await asyncDBConnection.execute(createClusterSummaryTable);
await asyncDBConnection.execute(createNotPersonFeedbackTable);
await asyncDBConnection.execute(fcClusterIDIndex);
Future<void> _migrate(
SqliteDatabase database,
) async {
final result = await database.execute('PRAGMA user_version');
final currentVersion = result[0]['user_version'] as int;
final toVersion = _migrationScripts.length;
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.
@ -193,10 +229,10 @@ class FaceMLDataDB {
final db = await instance.asyncDB;
await db.execute(deleteFacesTable);
await db.execute(dropClusterPersonTable);
await db.execute(dropClusterSummaryTable);
await db.execute(deletePersonTable);
await db.execute(dropNotPersonFeedbackTable);
await db.execute(deleteFaceClustersTable);
await db.execute(deleteClusterPersonTable);
await db.execute(deleteClusterSummaryTable);
await db.execute(deleteNotPersonFeedbackTable);
}
Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster(
@ -249,7 +285,7 @@ class FaceMLDataDB {
final List<int> fileId = [recentFileID];
int? avatarFileId;
if (avatarFaceId != null) {
avatarFileId = int.tryParse(avatarFaceId.split('_')[0]);
avatarFileId = tryGetFileIdFromFaceId(avatarFaceId);
if (avatarFileId != null) {
fileId.add(avatarFileId);
}
@ -401,8 +437,10 @@ class FaceMLDataDB {
final personID = map[personIdColumn] as String;
final clusterID = map[fcClusterID] as int;
final faceID = map[fcFaceId] as String;
result.putIfAbsent(personID, () => {}).putIfAbsent(clusterID, () => {})
.add(faceID);
result
.putIfAbsent(personID, () => {})
.putIfAbsent(clusterID, () => {})
.add(faceID);
}
return result;
}
@ -476,8 +514,7 @@ class FaceMLDataDB {
for (final map in maps) {
final clusterID = map[fcClusterID] as int;
final faceID = map[fcFaceId] as String;
final x = faceID.split('_').first;
final fileID = int.parse(x);
final fileID = getFileIdFromFaceId(faceID);
result[fileID] = (result[fileID] ?? {})..add(clusterID);
}
return result;
@ -665,19 +702,55 @@ class FaceMLDataDB {
return maps.first['count'] as int;
}
Future<int> getClusteredFaceCount() async {
Future<int> getClusteredOrFacelessFileCount() async {
final db = await instance.asyncDB;
final List<Map<String, dynamic>> maps = await db.getAll(
'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable',
final List<Map<String, dynamic>> clustered = await db.getAll(
'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 {
final int totalFaces = await getTotalFaceCount();
final int clusteredFaces = await getClusteredFaceCount();
Future<double> getClusteredToIndexableFilesRatio() async {
final int indexableFiles = (await getIndexableFileIDs()).length;
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([
@ -695,7 +768,7 @@ class FaceMLDataDB {
try {
final db = await instance.asyncDB;
await db.execute(dropFaceClustersTable);
await db.execute(deleteFaceClustersTable);
await db.execute(createFaceClustersTable);
await db.execute(fcClusterIDIndex);
} catch (e, s) {
@ -795,7 +868,7 @@ class FaceMLDataDB {
for (final map in maps) {
final clusterID = map[clusterIDColumn] as int;
final String faceID = map[fcFaceId] as String;
final fileID = int.parse(faceID.split('_').first);
final fileID = getFileIdFromFaceId(faceID);
result[fileID] = (result[fileID] ?? {})..add(clusterID);
}
return result;
@ -814,8 +887,8 @@ class FaceMLDataDB {
final Map<int, Set<int>> result = {};
for (final map in maps) {
final clusterID = map[fcClusterID] as int;
final faceId = map[fcFaceId] as String;
final fileID = int.parse(faceId.split("_").first);
final faceID = map[fcFaceId] as String;
final fileID = getFileIdFromFaceId(faceID);
result[fileID] = (result[fileID] ?? {})..add(clusterID);
}
return result;
@ -906,16 +979,15 @@ class FaceMLDataDB {
if (faces) {
await db.execute(deleteFacesTable);
await db.execute(createFacesTable);
await db.execute(dropFaceClustersTable);
await db.execute(deleteFaceClustersTable);
await db.execute(createFaceClustersTable);
await db.execute(fcClusterIDIndex);
}
await db.execute(deletePersonTable);
await db.execute(dropClusterPersonTable);
await db.execute(dropNotPersonFeedbackTable);
await db.execute(dropClusterSummaryTable);
await db.execute(dropFaceClustersTable);
await db.execute(deleteClusterPersonTable);
await db.execute(deleteNotPersonFeedbackTable);
await db.execute(deleteClusterSummaryTable);
await db.execute(deleteFaceClustersTable);
await db.execute(createClusterPersonTable);
await db.execute(createNotPersonFeedbackTable);
@ -933,9 +1005,8 @@ class FaceMLDataDB {
final db = await instance.asyncDB;
// Drop the tables
await db.execute(deletePersonTable);
await db.execute(dropClusterPersonTable);
await db.execute(dropNotPersonFeedbackTable);
await db.execute(deleteClusterPersonTable);
await db.execute(deleteNotPersonFeedbackTable);
// Recreate the tables
await db.execute(createClusterPersonTable);
@ -964,7 +1035,7 @@ class FaceMLDataDB {
final Map<String, int> faceIDToClusterID = {};
for (final row in faceIdsResult) {
final faceID = row[fcFaceId] as String;
if (fileIds.contains(faceID.split('_').first)) {
if (fileIds.contains(getFileIdFromFaceId(faceID))) {
maxClusterID += 1;
faceIDToClusterID[faceID] = maxClusterID;
}
@ -990,7 +1061,7 @@ class FaceMLDataDB {
final Map<String, int> faceIDToClusterID = {};
for (final row in faceIdsResult) {
final faceID = row[fcFaceId] as String;
if (fileIds.contains(faceID.split('_').first)) {
if (fileIds.contains(getFileIdFromFaceId(faceID))) {
maxClusterID += 1;
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
//##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
const fcClusterIDIndex =
'''CREATE INDEX IF NOT EXISTS idx_fcClusterID ON $faceClustersTable($fcClusterID);''';
const dropFaceClustersTable = 'DROP TABLE IF EXISTS $faceClustersTable';
const deleteFaceClustersTable = 'DELETE FROM $faceClustersTable';
//##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
const clusterPersonTable = 'cluster_person';
const personIdColumn = 'person_id';
@ -69,7 +63,7 @@ CREATE TABLE IF NOT EXISTS $clusterPersonTable (
PRIMARY KEY($personIdColumn, $clusterIDColumn)
);
''';
const dropClusterPersonTable = 'DROP TABLE IF EXISTS $clusterPersonTable';
const deleteClusterPersonTable = 'DELETE FROM $clusterPersonTable';
// End Clusters 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
@ -99,5 +93,5 @@ CREATE TABLE IF NOT EXISTS $notPersonFeedback (
PRIMARY KEY($personIdColumn, $clusterIDColumn)
);
''';
const dropNotPersonFeedbackTable = 'DROP TABLE IF EXISTS $notPersonFeedback';
const deleteNotPersonFeedbackTable = 'DELETE FROM $notPersonFeedback';
// 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."),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"longPressAnEmailToVerifyEndToEndEncryption":

View file

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

View file

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

View file

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

View file

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

View file

@ -773,6 +773,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Il codice che hai inserito non è corretto"),
"incorrectRecoveryKeyTitle":
MessageLookupByLibrary.simpleMessage("Chiave di recupero errata"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice":
MessageLookupByLibrary.simpleMessage("Dispositivo non sicuro"),
"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."),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"longPressAnEmailToVerifyEndToEndEncryption":

View file

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

View file

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

View file

@ -131,6 +131,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Kod jest nieprawidłowy"),
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
"Nieprawidłowy klucz odzyskiwania"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"invalidEmailAddress":
MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"),
"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";
static String m25(freeAmount, storageUnit) =>
"${freeAmount} ${storageUnit} grátis";
"${freeAmount} ${storageUnit} livre";
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>."),
"activeSessions":
MessageLookupByLibrary.simpleMessage("Sessões ativas"),
"addAName": MessageLookupByLibrary.simpleMessage("Adicione um nome"),
"addANewEmail":
MessageLookupByLibrary.simpleMessage("Adicionar um novo email"),
"addCollaborator":
@ -446,7 +447,7 @@ class MessageLookup extends MessageLookupByLibrary {
"clubByFileName": MessageLookupByLibrary.simpleMessage(
"Agrupar pelo nome de arquivo"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"),
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Código aplicado"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
@ -692,6 +693,8 @@ class MessageLookup extends MessageLookupByLibrary {
"enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Insira a senha para criptografar seus dados"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Insira o código de referência"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -717,9 +720,9 @@ class MessageLookup extends MessageLookupByLibrary {
"exportYourData":
MessageLookupByLibrary.simpleMessage("Exportar seus dados"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
MessageLookupByLibrary.simpleMessage("Reconhecimento facial"),
"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"),
"failedToApplyCode":
MessageLookupByLibrary.simpleMessage("Falha ao aplicar o código"),
@ -761,12 +764,15 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Arquivos excluídos"),
"filesSavedToGallery":
MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"),
"findPeopleByName": MessageLookupByLibrary.simpleMessage(
"Encontre pessoas rapidamente por nome"),
"flip": MessageLookupByLibrary.simpleMessage("Inverter"),
"forYourMemories":
MessageLookupByLibrary.simpleMessage("para suas memórias"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"foundFaces":
MessageLookupByLibrary.simpleMessage("Rostos encontrados"),
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
"Armazenamento gratuito reivindicado"),
"freeStorageOnReferralSuccess": m24,
@ -830,6 +836,8 @@ class MessageLookup extends MessageLookupByLibrary {
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
"Chave de recuperação incorreta"),
"indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice":
MessageLookupByLibrary.simpleMessage("Dispositivo não seguro"),
"installManually":
@ -1064,6 +1072,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"),
"pendingSync":
MessageLookupByLibrary.simpleMessage("Sincronização pendente"),
"people": MessageLookupByLibrary.simpleMessage("Pessoas"),
"peopleUsingYourCode":
MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"),
"permDeleteWarning": MessageLookupByLibrary.simpleMessage(
@ -1197,6 +1206,8 @@ class MessageLookup extends MessageLookupByLibrary {
"removeParticipant":
MessageLookupByLibrary.simpleMessage("Remover participante"),
"removeParticipantBody": m43,
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"),
"removePublicLink":
MessageLookupByLibrary.simpleMessage("Remover link público"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(
@ -1260,7 +1271,7 @@ class MessageLookup extends MessageLookupByLibrary {
"searchDatesEmptySection": MessageLookupByLibrary.simpleMessage(
"Pesquisar por data, mês ou ano"),
"searchFaceEmptySection": MessageLookupByLibrary.simpleMessage(
"Encontre todas as fotos de uma pessoa"),
"Pessoas serão exibidas aqui uma vez que a indexação é feita"),
"searchFileTypesAndNamesEmptySection":
MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"),
"searchHint1": MessageLookupByLibrary.simpleMessage(

View file

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

View file

@ -8793,6 +8793,16 @@ class S {
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> {

View file

@ -24,5 +24,6 @@
"faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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",
"location": "Local",
"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",
"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",
@ -1042,7 +1042,7 @@
"@storageUsageInfo": {
"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}",
"verifyIDLabel": "Verificar",
"fileInfoAddDescHint": "Adicionar descrição...",
@ -1171,6 +1171,7 @@
}
},
"faces": "Rostos",
"people": "Pessoas",
"contents": "Conteúdos",
"addNew": "Adicionar novo",
"@addNew": {
@ -1196,14 +1197,14 @@
"verifyPasskey": "Verificar chave de acesso",
"playOnTv": "Reproduzir álbum na TV",
"pair": "Parear",
"autoPair": "Pareamento automático",
"pairWithPin": "Parear com PIN",
"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.",
"deviceCodeHint": "Insira o código",
"joinDiscord": "Junte-se ao Discord",
"locations": "Locais",
"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}}",
"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.",
@ -1216,6 +1217,8 @@
"customEndpoint": "Conectado a {endpoint}",
"createCollaborativeLink": "Criar link colaborativo",
"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.",
"manualPairDesc": "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado.",
"connectToDevice": "Conectar ao dispositivo",
@ -1227,8 +1230,11 @@
"castIPMismatchTitle": "Falha ao transmitir álbum",
"castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.",
"pairingComplete": "Pareamento concluído",
"faceRecognition": "Face recognition",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces",
"clusteringProgress": "Clustering progress"
"autoPair": "Pareamento automático",
"pairWithPin": "Parear com PIN",
"faceRecognition": "Reconhecimento facial",
"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",
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"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/lock_screen.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/local_settings.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -180,6 +181,16 @@ void _headlessTaskHandler(HeadlessTask task) {
}
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;
_logger.info("Initializing... inBG =$isBackground via: $via");
final SharedPreferences preferences = await SharedPreferences.getInstance();
@ -235,17 +246,11 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
unawaited(SemanticSearchService.instance.init());
MachineLearningController.instance.init();
// Can not including existing tf/ml binaries as they are not being built
// from source.
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819
if (!UpdateService.instance.isFdroidFlavor()) {
// unawaited(ObjectDetectionService.instance.init());
if (flagService.faceSearchEnabled) {
unawaited(FaceMlService.instance.init());
} else {
if (LocalSettings.instance.isFaceIndexingEnabled) {
unawaited(LocalSettings.instance.toggleFaceIndexing());
}
if (flagService.faceSearchEnabled) {
unawaited(FaceMlService.instance.init());
} else {
if (LocalSettings.instance.isFaceIndexingEnabled) {
unawaited(LocalSettings.instance.toggleFaceIndexing());
}
}
PersonService.init(
@ -254,6 +259,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
preferences,
);
initComplete = true;
_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) {
faceInfos.sort((a, b) {
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!);
}
});
_sortFaceInfosOnCreationTime(faceInfos);
}
// 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) {
faceInfos.sort((a, b) {
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!);
}
});
_sortFaceInfosOnCreationTime(faceInfos);
}
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) {
faceInfos.sort((a, b) {
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!);
}
});
_sortFaceInfosOnCreationTime(faceInfos);
}
// Get the embeddings
@ -1027,3 +994,20 @@ class FaceClusteringService {
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';
}
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 CouldNotInitializeFaceDetector implements Exception {}

View file

@ -310,5 +310,9 @@ class FaceResultBuilder {
}
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:dart_ui_isolate/dart_ui_isolate.dart";
import "package:flutter/foundation.dart" show debugPrint, kDebugMode;
import "package:flutter/services.dart";
import "package:logging/logging.dart";
import "package:onnxruntime/onnxruntime.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/db/files_db.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/file_ml/file_ml.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/utils/file_util.dart";
import 'package:photos/utils/image_ml_isolate.dart';
@ -97,8 +98,9 @@ class FaceMlService {
bool _shouldSyncPeople = false;
bool _isSyncing = false;
final int _fileDownloadLimit = 10;
final int _fileDownloadLimit = 5;
final int _embeddingFetchLimit = 200;
final int _kForceClusteringFaceCount = 8000;
Future<void> init({bool initializeImageMlIsolate = false}) async {
if (LocalSettings.instance.isFaceIndexingEnabled == false) {
@ -109,6 +111,7 @@ class FaceMlService {
return;
}
_logger.info("init called");
_logStatus();
await _computer.compute(initOrtEnv);
try {
await FaceDetectionService.instance.init();
@ -152,8 +155,8 @@ class FaceMlService {
_logger.info(
"MLController allowed running ML, faces indexing starting",
);
unawaited(indexAndClusterAll());
}
unawaited(indexAndClusterAll());
} else {
_logger.info(
"MLController stopped running ML, faces indexing will be paused (unless it's fetching embeddings)",
@ -161,9 +164,16 @@ class FaceMlService {
pauseIndexingAndClustering();
}
});
if (Platform.isIOS &&
MachineLearningController.instance.isDeviceHealthy) {
_logger.info("Starting face indexing and clustering on iOS from init");
unawaited(indexAndClusterAll());
}
_listenIndexOnDiffSync();
_listenOnPeopleChangedSync();
_logger.info('init done');
});
}
@ -245,6 +255,7 @@ class FaceMlService {
}
/// The main execution function of the isolate.
@pragma('vm:entry-point')
static void _isolateMain(SendPort mainSendPort) async {
final receivePort = ReceivePort();
mainSendPort.send(receivePort.sendPort);
@ -287,10 +298,6 @@ class FaceMlService {
return _functionLock.synchronized(() async {
_resetInactivityTimer();
if (_shouldPauseIndexingAndClustering == false) {
return null;
}
final completer = Completer<dynamic>();
final answerPort = ReceivePort();
@ -360,16 +367,17 @@ class FaceMlService {
if (_cannotRunMLFunction()) return;
await sync(forceSync: _shouldSyncPeople);
await indexAllImages();
final indexingCompleteRatio = await _getIndexedDoneRatio();
if (indexingCompleteRatio < 0.95) {
final int unclusteredFacesCount =
await FaceMLDataDB.instance.getUnclusteredFaceCount();
if (unclusteredFacesCount > _kForceClusteringFaceCount) {
_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 indexAllImages();
await clusterAllImages();
}
void pauseIndexingAndClustering() {
@ -447,7 +455,8 @@ class FaceMlService {
if (LocalSettings.instance.remoteFetchEnabled) {
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
for (final f in chunk) {
fileIds.add(f.uploadedFileID!);
@ -512,12 +521,19 @@ class FaceMlService {
rethrow;
}
}
}
if (!await canUseHighBandwidth()) {
continue;
} else {
_logger.warning(
'Not fetching embeddings because user manually disabled it in debug options',
);
}
final smallerChunks = chunk.chunks(_fileDownloadLimit);
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) {
if (_shouldPauseIndexingAndClustering) {
_logger.info("indexAllImages() was paused, stopping");
@ -543,8 +559,9 @@ class FaceMlService {
stopwatch.stop();
_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) {
_logger.severe("indexAllImages failed", e, s);
} finally {
@ -584,8 +601,8 @@ class FaceMlService {
allFaceInfoForClustering.add(faceInfo);
}
}
// sort the embeddings based on file creation time, oldest first
allFaceInfoForClustering.sort((a, b) {
// sort the embeddings based on file creation time, newest first
allFaceInfoForClustering.sort((b, a) {
return fileIDToCreationTime[a.fileID]!
.compareTo(fileIDToCreationTime[b.fileID]!);
});
@ -758,6 +775,9 @@ class FaceMlService {
// disposeImageIsolateAfterUse: false,
);
if (result == null) {
_logger.severe(
"Failed to analyze image with uploadedFileID: ${enteFile.uploadedFileID}",
);
return false;
}
final List<Face> faces = [];
@ -834,13 +854,22 @@ class FaceMlService {
}
await FaceMLDataDB.instance.bulkInsertFaces(faces);
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) {
_logger.severe(
"Failed to analyze using FaceML for image with ID: ${enteFile.uploadedFileID}",
e,
s,
);
return true;
return false;
}
}
@ -877,6 +906,7 @@ class FaceMlService {
),
) as String?;
if (resultJsonString == null) {
_logger.severe('Analyzing image in isolate is giving back null');
return null;
}
result = FaceMlResult.fromJsonString(resultJsonString);
@ -993,7 +1023,16 @@ class FaceMlService {
final stopwatch = Stopwatch()..start();
File? file;
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 {
file = await getFile(enteFile, isOrigin: true);
// 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) {
if (_isIndexingOrClusteringRunning == false ||
_mlControllerStatus == false) {

View file

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

View file

@ -4,7 +4,6 @@ import "dart:io";
import "package:battery_info/battery_info_plugin.dart";
import "package:battery_info/model/android_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:photos/core/event_bus.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 kMinimumBatteryLevel = 20; // 20%
static const kDefaultInteractionTimeout =
kDebugMode ? Duration(seconds: 3) : Duration(seconds: 5);
static const kDefaultInteractionTimeout = Duration(seconds: 10);
static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"];
bool _isDeviceHealthy = true;
@ -28,7 +26,10 @@ class MachineLearningController {
bool _canRunML = false;
late Timer _userInteractionTimer;
bool get isDeviceHealthy => _isDeviceHealthy;
void init() {
_logger.info('init called');
if (Platform.isAndroid) {
_startInteractionTimer();
BatteryInfoPlugin()
@ -45,6 +46,7 @@ class MachineLearningController {
});
}
_fireControlEvent();
_logger.info('init done');
}
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/device_info.dart";
import "package:photos/utils/local_settings.dart";
import "package:photos/utils/ml_util.dart";
import "package:photos/utils/thumbnail_util.dart";
class SemanticSearchService {
@ -160,8 +161,7 @@ class SemanticSearchService {
}
Future<IndexStatus> getIndexStatus() async {
final indexableFileIDs = await FilesDB.instance
.getOwnedFileIDs(Configuration.instance.getUserID()!);
final indexableFileIDs = await getIndexableFileIDs();
return IndexStatus(
min(_cachedEmbeddings.length, indexableFileIDs.length),
(await _getFileIDsToBeIndexed()).length,
@ -222,8 +222,7 @@ class SemanticSearchService {
}
Future<List<int>> _getFileIDsToBeIndexed() async {
final uploadedFileIDs = await FilesDB.instance
.getOwnedFileIDs(Configuration.instance.getUserID()!);
final uploadedFileIDs = await getIndexableFileIDs();
final embeddedFileIDs =
await EmbeddingsDB.instance.getFileIDs(_currentModel);

View file

@ -754,15 +754,6 @@ class SearchService {
Future<List<GenericSearchResult>> getAllFace(int? limit) async {
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");
final Map<int, Set<int>> fileIdToClusterID =
await FaceMLDataDB.instance.getFileIdToClusterIds();

View file

@ -177,7 +177,7 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: FutureBuilder<double>(
future: FaceMLDataDB.instance.getClusteredToTotalFacesRatio(),
future: FaceMLDataDB.instance.getClusteredToIndexableFilesRatio(),
builder: (context, snapshot) {
if (snapshot.hasData) {
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/service_locator.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/semantic_search_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/utils/data_util.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");
@ -40,6 +43,7 @@ class MachineLearningSettingsPage extends StatefulWidget {
class _MachineLearningSettingsPageState
extends State<MachineLearningSettingsPage> {
late InitializationState _state;
final EnteWakeLock _wakeLock = EnteWakeLock();
late StreamSubscription<MLFrameworkInitializationUpdateEvent>
_eventSubscription;
@ -53,6 +57,7 @@ class _MachineLearningSettingsPageState
setState(() {});
});
_fetchState();
_wakeLock.enable();
}
void _fetchState() {
@ -63,6 +68,7 @@ class _MachineLearningSettingsPageState
void dispose() {
super.dispose();
_eventSubscription.cancel();
_wakeLock.disable();
}
@override
@ -83,8 +89,8 @@ class _MachineLearningSettingsPageState
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.pop(context);
Navigator.pop(context);
Navigator.pop(context);
if (Navigator.canPop(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 {
final indexedFiles = await FaceMLDataDB.instance
.getIndexedFileCount(minimumMlVersion: faceMlVersion);
final indexableFiles = (await FaceMlService.getIndexableFileIDs()).length;
final indexableFiles = (await getIndexableFileIDs()).length;
final showIndexedFiles = min(indexedFiles, indexableFiles);
final pendingFiles = max(indexableFiles - indexedFiles, 0);
final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount();
final clusteredFaces =
await FaceMLDataDB.instance.getClusteredFaceCount();
final clusteringDoneRatio = clusteredFaces / foundFaces;
final clusteringDoneRatio =
await FaceMLDataDB.instance.getClusteredToIndexableFilesRatio();
final bool deviceIsHealthy =
MachineLearningController.instance.isDeviceHealthy;
return (showIndexedFiles, pendingFiles, foundFaces, clusteringDoneRatio);
return (
showIndexedFiles,
pendingFiles,
clusteringDoneRatio,
deviceIsHealthy
);
} catch (e, s) {
_logger.severe('Error getting face recognition status', e, s);
rethrow;
@ -479,10 +490,17 @@ class FaceRecognitionStatusWidgetState
if (snapshot.hasData) {
final int indexedFiles = snapshot.data!.$1;
final int pendingFiles = snapshot.data!.$2;
final int foundFaces = snapshot.data!.$3;
final double clusteringDoneRatio = snapshot.data!.$4;
final double clusteringDoneRatio = snapshot.data!.$3;
final double clusteringPercentage =
(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(
children: [
@ -512,19 +530,6 @@ class FaceRecognitionStatusWidgetState
isGestureDetectorDisabled: true,
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(
captionedTextWidget: CaptionedTextWidget(
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/file_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:visibility_detector/visibility_detector.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class VideoWidget extends StatefulWidget {
final EnteFile file;
@ -45,7 +45,7 @@ class _VideoWidgetState extends State<VideoWidget> {
ChewieController? _chewieController;
final _progressNotifier = ValueNotifier<double?>(null);
bool _isPlaying = false;
bool _wakeLockEnabledHere = false;
final EnteWakeLock _wakeLock = EnteWakeLock();
@override
void initState() {
@ -126,13 +126,7 @@ class _VideoWidgetState extends State<VideoWidget> {
_chewieController?.dispose();
_progressNotifier.dispose();
if (_wakeLockEnabledHere) {
unawaited(
WakelockPlus.enabled.then((isEnabled) {
isEnabled ? WakelockPlus.disable() : null;
}),
);
}
_wakeLock.dispose();
super.dispose();
}
@ -257,17 +251,10 @@ class _VideoWidgetState extends State<VideoWidget> {
Future<void> _keepScreenAliveOnPlaying(bool isPlaying) async {
if (isPlaying) {
return 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;
}
});
_wakeLock.enable();
}
if (_wakeLockEnabledHere && !isPlaying) {
return WakelockPlus.disable();
if (!isPlaying) {
_wakeLock.disable();
}
}

View file

@ -5,6 +5,7 @@ import "package:flutter/material.dart";
import "package:flutter_animate/flutter_animate.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
import "package:photos/core/configuration.dart";
import "package:photos/core/constants.dart";
import "package:photos/db/files_db.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/l10n/l10n.dart";
@ -167,7 +168,14 @@ class AddPhotosPhotoWidget extends StatelessWidget {
Future<void> _onPickFromDeviceClicked(BuildContext context) async {
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) {
final ca = CollectionActions(
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 {

View file

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

View file

@ -161,43 +161,45 @@ class _ClusterPageState extends State<ClusterPage> {
),
),
showNamingBanner
? Dismissible(
key: const Key("namingBanner"),
direction: DismissDirection.horizontal,
onDismissed: (direction) {
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");
}
? SafeArea(
child: Dismissible(
key: const Key("namingBanner"),
direction: DismissDirection.horizontal,
onDismissed: (direction) {
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");
}
},
),
),
)
: const SizedBox.shrink(),

View file

@ -38,12 +38,17 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
.getClusterFilesForPersonID(widget.person.remoteID),
builder: (context, snapshot) {
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(
itemCount: keys.length,
itemBuilder: (context, index) {
final int clusterID = keys[index];
final List<EnteFile> files = snapshot.data![keys[index]]!;
final List<EnteFile> files = clusters[clusterID]!;
return InkWell(
onTap: () {
Navigator.of(context).push(
@ -93,34 +98,37 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"${snapshot.data![keys[index]]!.length} photos",
"${files.length} photos",
style: getEnteTextTheme(context).body,
),
GestureDetector(
onTap: () async {
try {
await PersonService.instance
.removeClusterToPerson(
personID: widget.person.remoteID,
clusterID: clusterID,
);
_logger.info(
"Removed cluster $clusterID from person ${widget.person.remoteID}",
);
Bus.instance.fire(PeopleChangedEvent());
setState(() {});
} catch (e) {
_logger.severe(
"removing cluster from person,",
e,
);
}
},
child: const Icon(
CupertinoIcons.minus_circled,
color: Colors.red,
),
),
(index != 0)
? GestureDetector(
onTap: () async {
try {
await PersonService.instance
.removeClusterToPerson(
personID: widget.person.remoteID,
clusterID: clusterID,
);
_logger.info(
"Removed cluster $clusterID from person ${widget.person.remoteID}",
);
Bus.instance
.fire(PeopleChangedEvent());
setState(() {});
} catch (e) {
_logger.severe(
"removing cluster from person,",
e,
);
}
},
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,
) {
int resultCount = 0;
final maxResultCount = _isYearValid(query) ? 13 : 12;
final maxResultCount = _isYearValid(query) ? 12 : 11;
final streamController = StreamController<List<SearchResult>>();
if (query.isEmpty) {
@ -260,10 +260,11 @@ class SearchWidgetState extends State<SearchWidget> {
onResultsReceived(locationResult);
},
);
_searchService.getAllFace(null).then(
(locationResult) {
(faceResult) {
final List<GenericSearchResult> filteredResults = [];
for (final result in locationResult) {
for (final result in faceResult) {
if (result.name().toLowerCase().contains(query.toLowerCase())) {
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/error-reporting/super_logging.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/dialog_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
@ -122,9 +123,28 @@ Future<void> _sendLogs(
}
}
Future<String> getZippedLogsFile(BuildContext context) async {
final dialog = createProgressDialog(context, S.of(context).preparingLogs);
await dialog.show();
Future<void> sendLogsForInit(
String toEmail,
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 logsDirectory = Directory(logsPath + "/logs");
final tempPath = (await getTemporaryDirectory()).path;
@ -134,7 +154,9 @@ Future<String> getZippedLogsFile(BuildContext context) async {
encoder.create(zipFilePath);
await encoder.addDirectory(logsDirectory);
encoder.close();
await dialog.hide();
if (context != null) {
await dialog.hide();
}
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"
description:
name: animated_list_plus
sha256: fe66f9c300d715254727fbdf050487844d17b013fec344fa28081d29bddbdf1a
sha256: fb3d7f1fbaf5af84907f3c739236bacda8bf32cbe1f118dd51510752883ff50c
url: "https://pub.dev"
source: hosted
version: "0.4.5"
version: "0.5.2"
animated_stack_widget:
dependency: transitive
description:
@ -971,10 +971,10 @@ packages:
dependency: "direct main"
description:
name: home_widget
sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3"
sha256: "2a0fdd6267ff975bd07bedf74686bd5577200f504f5de36527ac1b56bdbe68e3"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.6.0"
html:
dependency: transitive
description:
@ -1152,26 +1152,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.1"
like_button:
dependency: "direct main"
description:
@ -1368,10 +1368,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.12.0"
mgrs_dart:
dependency: transitive
description:
@ -2144,10 +2144,10 @@ packages:
dependency: "direct main"
description:
name: styled_text
sha256: f72928d1ebe8cb149e3b34a689cb1ddca696b808187cf40ac3a0bd183dff379c
sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "8.1.0"
sync_http:
dependency: transitive
description:
@ -2160,18 +2160,18 @@ packages:
dependency: "direct main"
description:
name: syncfusion_flutter_core
sha256: "9be1bb9bbdb42823439a18da71484f1964c14dbe1c255ab1b931932b12fa96e8"
sha256: "63108a33f9b0d89f7b6b56cce908b8e519fe433dbbe0efcf41ad3e8bb2081bd9"
url: "https://pub.dev"
source: hosted
version: "19.4.56"
version: "25.2.5"
syncfusion_flutter_sliders:
dependency: "direct main"
description:
name: syncfusion_flutter_sliders
sha256: "1f6a63ccab4180b544074b9264a20f01ee80b553de154192fe1d7b434089d3c2"
sha256: f27310bedc0e96e84054f0a70ac593d1a3c38397c158c5226ba86027ad77b2c1
url: "https://pub.dev"
source: hosted
version: "19.4.56"
version: "25.2.5"
synchronized:
dependency: "direct main"
description:
@ -2192,26 +2192,26 @@ packages:
dependency: "direct dev"
description:
name: test
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
url: "https://pub.dev"
source: hosted
version: "1.24.9"
version: "1.25.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
version: "0.7.0"
test_core:
dependency: transitive
description:
name: test_core
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
url: "https://pub.dev"
source: hosted
version: "0.5.9"
version: "0.6.0"
timezone:
dependency: transitive
description:
@ -2441,10 +2441,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
version: "14.2.1"
volume_controller:
dependency: transitive
description:
@ -2591,4 +2591,4 @@ packages:
version: "3.1.2"
sdks:
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
# 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
environment:
@ -21,7 +21,7 @@ environment:
dependencies:
adaptive_theme: ^3.1.0
animate_do: ^2.0.0
animated_list_plus: ^0.4.5
animated_list_plus: ^0.5.2
archive: ^3.1.2
background_fetch: ^1.2.1
battery_info: ^1.1.1
@ -93,13 +93,13 @@ dependencies:
fluttertoast: ^8.0.6
freezed_annotation: ^2.4.1
google_nav_bar: ^5.0.5
home_widget: ^0.5.0
home_widget: ^0.6.0
html_unescape: ^2.0.0
http: ^1.1.0
image: ^4.0.17
image_editor: ^1.3.0
in_app_purchase: ^3.0.7
intl: ^0.18.0
intl: ^0.19.0
json_annotation: ^4.8.0
latlong2: ^0.9.0
like_button: ^2.0.5
@ -152,9 +152,9 @@ dependencies:
sqlite3_flutter_libs: ^0.5.20
sqlite_async: ^0.6.1
step_progress_indicator: ^1.0.2
styled_text: ^7.0.0
syncfusion_flutter_core: ^19.2.49
syncfusion_flutter_sliders: ^19.2.49
styled_text: ^8.1.0
syncfusion_flutter_core: ^25.2.5
syncfusion_flutter_sliders: ^25.2.5
synchronized: ^3.1.0
tuple: ^2.0.0
uni_links: ^0.5.1
@ -177,6 +177,7 @@ dependency_overrides:
# 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
ffi: 2.1.0
intl: 0.18.1
video_player:
git:
url: https://github.com/ente-io/packages.git

View file

@ -95,6 +95,15 @@ db:
# Map of data centers
#
# 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:
# Override the primary and secondary hot storage. The commented out values
# 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 { setupI18n } from "@/next/i18n";
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 { accountLogout } from "@ente/accounts/services/logout";
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import {
DialogBoxAttributesV2,
SetDialogBoxAttributesV2,
} from "@ente/shared/components/DialogBoxV2/types";
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
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 HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
@ -22,25 +21,28 @@ import { ThemeProvider } from "@mui/material/styles";
import { t } from "i18next";
import { AppProps } from "next/app";
import { useRouter } from "next/router";
import { createContext, useEffect, useState } from "react";
import { createContext, useContext, useEffect, useState } from "react";
import "styles/global.css";
interface AppContextProps {
isMobile: boolean;
showNavBar: (show: boolean) => void;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
logout: () => void;
}
/** The accounts app has no extra properties on top of the base context. */
type AppContextT = BaseAppContextT;
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) {
const appName: AppName = "account";
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [showNavbar, setShowNavBar] = useState(false);
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
useState<DialogBoxAttributesV2>();
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
DialogBoxAttributesV2 | undefined
>();
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
@ -85,8 +87,17 @@ export default function App({ Component, pageProps }: AppProps) {
void accountLogout().then(() => router.push(PAGES.ROOT));
};
const appContext = {
appName,
logout,
showNavBar,
isMobile,
setDialogBoxAttributesV2,
};
// TODO: This string doesn't actually exist
const title = isI18nReady
? t("TITLE", { context: APPS.ACCOUNTS })
? t("title", { context: "accounts" })
: APP_TITLES.get(APPS.ACCOUNTS);
return (
@ -102,15 +113,7 @@ export default function App({ Component, pageProps }: AppProps) {
attributes={dialogBoxAttributeV2 as any}
/>
<AppContext.Provider
value={{
isMobile,
showNavBar,
setDialogBoxAttributesV2:
setDialogBoxAttributesV2 as any,
logout,
}}
>
<AppContext.Provider value={appContext}>
{!isI18nReady && (
<Overlay
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 RecoverPage from "@ente/accounts/pages/recover";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import RecoverPage from "@ente/accounts/pages/two-factor/recover";
import { useAppContext } from "../../_app";
export default function Recover() {
const appContext = useContext(AppContext);
return (
<RecoverPage
appContext={appContext}
appName={APPS.PHOTOS}
twoFactorType={TwoFactorType.PASSKEY}
/>
);
}
const Page = () => (
<RecoverPage
appContext={useAppContext()}
twoFactorType={TwoFactorType.PASSKEY}
/>
);
export default Page;

View file

@ -9,14 +9,8 @@ import { t } from "i18next";
import _sodium from "libsodium-wrappers";
import { useRouter } from "next/router";
import { AppContext } from "pages/_app";
import {
Dispatch,
SetStateAction,
createContext,
useContext,
useEffect,
useState,
} from "react";
import type { Dispatch, SetStateAction } from "react";
import { createContext, useContext, useEffect, useState } from "react";
import { Passkey } from "types/passkey";
import {
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
# 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,
project: "./tsconfig.json",
},
ignorePatterns: [".eslintrc.js", "out"],
ignorePatterns: [".eslintrc.js", "next.config.js", "out"],
};

View file

@ -3,9 +3,12 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@/build-config": "*",
"@/next": "*",
"@ente/accounts": "*",
"@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 NotFoundPage from "@ente/shared/next/pages/404";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import Page from "@ente/shared/next/pages/404";
export default function NotFound() {
const appContext = useContext(AppContext);
return <NotFoundPage appContext={appContext} appName={APPS.AUTH} />;
}
export default Page;

View file

@ -4,6 +4,8 @@ import {
logStartupBanner,
logUnhandledErrorsAndRejections,
} 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 {
APPS,
@ -12,45 +14,46 @@ import {
} from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import {
DialogBoxAttributesV2,
SetDialogBoxAttributesV2,
} from "@ente/shared/components/DialogBoxV2/types";
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
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 { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import { SetTheme } from "@ente/shared/themes/types";
import type { User } from "@ente/shared/user/types";
import { CssBaseline, useMediaQuery } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import { t } from "i18next";
import { AppProps } from "next/app";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { createContext, useEffect, useRef, useState } from "react";
import LoadingBar from "react-top-loading-bar";
import { createContext, useContext, useEffect, useRef, useState } from "react";
import LoadingBar, { type LoadingBarRef } from "react-top-loading-bar";
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;
finishLoading: () => void;
isMobile: boolean;
themeColor: THEME_COLOR;
setThemeColor: SetTheme;
setThemeColor: (themeColor: THEME_COLOR) => 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) {
const appName: AppName = "auth";
const router = useRouter();
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
@ -58,10 +61,11 @@ export default function App({ Component, pageProps }: AppProps) {
typeof window !== "undefined" && !window.navigator.onLine,
);
const [showNavbar, setShowNavBar] = useState(false);
const isLoadingBarRunning = useRef(false);
const loadingBar = useRef(null);
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
useState<DialogBoxAttributesV2>();
const isLoadingBarRunning = useRef<boolean>(false);
const loadingBar = useRef<LoadingBarRef>(null);
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
DialogBoxAttributesV2 | undefined
>();
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
const isMobile = useMediaQuery("(max-width:428px)");
const [themeColor, setThemeColor] = useLocalState(
@ -134,9 +138,23 @@ export default function App({ Component, pageProps }: AppProps) {
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
? t("TITLE", { context: APPS.AUTH })
: APP_TITLES.get(APPS.AUTH);
? t("title", { context: "auth" })
: APP_TITLES.get(APPS.AUTH) ?? "";
return (
<>
@ -158,19 +176,7 @@ export default function App({ Component, pageProps }: AppProps) {
attributes={dialogBoxAttributeV2}
/>
<AppContext.Provider
value={{
showNavBar,
startLoading,
finishLoading,
isMobile,
themeColor,
setThemeColor,
somethingWentWrong,
setDialogBoxAttributesV2,
logout,
}}
>
<AppContext.Provider value={appContext}>
{(loading || !isI18nReady) && (
<Overlay
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