Compare commits

...

118 Commits

Author SHA1 Message Date
Ashil
1800ad0a1f [Mobile][Photos] Bump up version to v0.8.74 (#1284) 2024-04-02 17:02:01 +05:30
Manav Rathi
3230b9275e [web] New translations (#1281)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2024-04-02 17:00:20 +05:30
Crowdin Bot
ce5627f04c New Crowdin translations by GitHub Action 2024-04-02 11:28:47 +00:00
Manav Rathi
8dd7c100af [web] Make the cast link clickable (#1286)
Tested locally.
2024-04-02 16:53:03 +05:30
Manav Rathi
2e7dcc6bc2 Make the cast link clickable 2024-04-02 16:51:27 +05:30
Manav Rathi
0e1bdfe07e Support arbitrary ReactNodes as title 2024-04-02 14:38:37 +05:30
Vishnu Mohandas
1e106d707f [mobile][photos] Fix colours in loading screen on light theme when viewing photos (#1283)
## Description

Before


https://github.com/ente-io/ente/assets/77285023/75304321-1fa4-4d61-9ad1-cc87ade62f92

After



https://github.com/ente-io/ente/assets/77285023/55b204b2-773c-42db-a03c-c9879f33548b
2024-04-02 14:01:41 +05:30
ashilkn
0053e814c8 nit: remove unnecessary clipping 2024-04-02 13:49:41 +05:30
ashilkn
53184da7fb fix: loading icon's color in light theme when viewing images 2024-04-02 13:49:05 +05:30
ashilkn
165bcb5c6e fix: white bg for loading state when viewing images, turned it to black.
when swiping on light theme, this comes up a 'white flash' on the right which looked odd. We use black bg when viewing images.
2024-04-02 13:47:57 +05:30
Vishnu Mohandas
d6316a1724 [mobile][photos] Prepare for release v0.8.73 (#1278) 2024-04-02 09:06:45 +05:30
ashilkn
bc0a453cbc Merge branch 'main' into prepare_for_release_v0.8.73 2024-04-01 18:41:24 +05:30
ashilkn
166e9ad1bf Update change log 2024-04-01 18:41:21 +05:30
ashilkn
841921a732 bump up version to v0.8.73 2024-04-01 18:38:17 +05:30
Vishnu Mohandas
769da989c4 [mobile][photos] Do not upload files if ACCESS_MEDIA_LOCATION is not granted (#1275)
## Tests

Tested with and without granting `ACCESS_MEDIA_LOCATION`.
2024-04-01 18:28:37 +05:30
Vishnu Mohandas
3e917bd855 [photos][mobile] Add support for viewing HEIC images with proXDR (#1171)
## Description

New Oneplus devices have a "ProXDR" feature when viewing images. These
images (when in HEIC format) decode fine on devices that supports
ProXDR, but fails to decode on other devices.

So if decoding fails, we convert it to a JPEG and use that image for
viewing it.

## Tests

- [x] Tries converting to jpeg only if decoding fails
- [x] If converting also fails, the behaviour remains the same as before
when decoding fails.
2024-04-01 18:28:28 +05:30
Manav Rathi
7f1730b56c [web] Fix and standardize MUI / emotion imports (#1277)
- Always import from `@mui/material`
- Component selector API doesn't work, live with it and document it
2024-04-01 17:18:05 +05:30
Manav Rathi
25e762ba57 Remove mui from list of transpiled packages
- I can't see this mentioned anywhere in the docs
- Removing it didn't break anything dev / preview
2024-04-01 17:13:06 +05:30
Manav Rathi
d5f294980e Remove use of emotion from payments 2024-04-01 17:08:54 +05:30
Manav Rathi
1e410a82f2 Remove stray use of @mui/system 2024-04-01 17:05:05 +05:30
ashilkn
d013519655 refactor 2024-04-01 16:59:42 +05:30
Manav Rathi
3b3d314f9c Remove stale import from styled-engine 2024-04-01 16:52:57 +05:30
ashilkn
855d362cca Merge branch 'main' into handle_proxdr_image_viewing 2024-04-01 16:43:19 +05:30
Manav Rathi
eced463f6f Investigate and explain why component selectors don't work with Next vanilla
Refs:
- https://github.com/mui/material-ui/issues/27380#issuecomment-928973157
- https://codesandbox.io/s/hopeful-browser-4q17t5?file=/README.md
- https://mui.com/system/styled/#how-to-use-components-selector-api
- https://github.com/vercel/next.js/issues/46973
2024-04-01 16:43:10 +05:30
Ashil
f8febe12df [mobile][photos] Reupload files with missing GPS data (#1263)
## Description

- Fixes corrupt files (missing GPS data) that were uploaded due to [this
issue](https://github.com/ente-io/ente/pull/1261)
- Refactor

## Tests

Tested and working
- Uploaded two file from a build that has missing permission for
`ACCESS_MEDIA_LOCATION` and GPS data is missing.
- Created a new build with changes in this PR.
- Deleted the file from device. 
- Remote file has GPS data when checked from file info.

---------

Co-authored-by: Neeraj Gupta <254676+ua741@users.noreply.github.com>
2024-04-01 16:41:33 +05:30
ashilkn
0c44d1b789 remove unneccesary check 2024-04-01 16:18:40 +05:30
ashilkn
f74af4199d only verify media location access if platform is android 2024-04-01 16:14:29 +05:30
Manav Rathi
9b27cac465 Remove @emotion/server
It is not mentioned as a dependency in the SSR page

> For v10 and above, SSR just works in Next.js.
>
> https://emotion.sh/docs/ssr

Tested by - yarn dev, yarn preview:photos
2024-04-01 16:05:17 +05:30
ashilkn
7b94c32bbf Do not upload files if ACCESS_MEDIA_LOCATION is not granted 2024-04-01 15:51:13 +05:30
Manav Rathi
881c94be05 [web] Remove bootstrap (#1274)
Fixes the issues we started seeing with broken CSS after removing the
emotion cache.
2024-04-01 15:47:02 +05:30
Manav Rathi
7248a226bc Remove bootstrap 2024-04-01 15:35:45 +05:30
Manav Rathi
8ae7ae2de9 Replace the Spinner in payments 2024-04-01 15:32:56 +05:30
Manav Rathi
28cf7d76d5 Even numbers don't seem to be kosher, only strings work 2024-04-01 15:13:43 +05:30
Manav Rathi
c2957238da Fix the date handling 2024-04-01 15:07:29 +05:30
Manav Rathi
f9a2ec774a Make it work with MUI components 2024-04-01 14:54:29 +05:30
Neeraj Gupta
548721e415 [mob]Ignore souceFileMissing error for iOS (#1273)
## Description

## Tests
2024-04-01 14:32:18 +05:30
Manav Rathi
0568cd03c9 Refactor somewhat
More to come
2024-04-01 13:07:36 +05:30
Manav Rathi
226f891e99 [docs] Update-export-faq (#1271) 2024-04-01 12:32:18 +05:30
Manav Rathi
9643dd645f Fix typo "intac" 2024-04-01 12:31:38 +05:30
Manav Rathi
7e897815a1 Remove stale VSCode settings
.vscode is already in the gitignore, not sure how this got added (perhaps some
bug in the github.dev web editor that Jishnu is using).
2024-04-01 12:18:24 +05:30
Manav Rathi
a9b92b9bfa [web] Remove emotion cache (#1272)
- Still doesn't work in dev mode
- Prepares ground for removing bootstrap
2024-04-01 12:14:50 +05:30
Jishnuraj9
489de9f8c2 export-general-subscription-update 2024-04-01 11:54:59 +05:30
Manav Rathi
39bc68390f Match the variable name 2024-04-01 10:49:28 +05:30
Manav Rathi
83dabfbdee Refactor 2024-04-01 10:23:54 +05:30
Manav Rathi
35f2a6944e Inline 2024-04-01 09:58:55 +05:30
Manav Rathi
fbe2996dcc [meta] Update READMEs to mention the new Auth Desktop app (#1269)
Fixes https://github.com/ente-io/ente/issues/1268
2024-04-01 09:28:39 +05:30
Manav Rathi
7f23b31bbc [meta] Update READMEs to mention the new Auth Desktop app
Fixes https://github.com/ente-io/ente/issues/1268
2024-04-01 09:26:07 +05:30
Jishnuraj9
f43e260434 update images of export 2024-04-01 08:04:24 +05:30
Manav Rathi
18698d35bb Replace in export progress 2024-03-31 21:43:24 +05:30
Manav Rathi
9e41b906a7 Swap progress bar 2024-03-31 21:38:26 +05:30
Manav Rathi
0f2181c09b Remove more legacy ml code 2024-03-31 18:41:15 +05:30
Manav Rathi
707e14702e Remove unused ML debug code 2024-03-31 18:34:00 +05:30
Manav Rathi
f3a0240f1d Remove more dead code
...that uses bootstrap instead of spending migration effort on it.
2024-03-31 18:23:05 +05:30
Manav Rathi
e84b989484 Remove unused code
Came across this when trying to migrate off bootstrap in the few remaining
places, this code is unused and just removing it instead of doing a migration of
it to mui.
2024-03-31 18:19:48 +05:30
Manav Rathi
86e4cffb8e Replace bootstrap buttons in fix time dialog 2024-03-31 18:16:57 +05:30
Manav Rathi
1d02fe4f32 Remove unused fix-large-thumbnail feature
This was disabled years ago. Specifically removing this now to reduce the amount
of work in removing bootstrap.
2024-03-31 18:07:17 +05:30
Manav Rathi
e5edeae370 Remove the bootstrap carousel 2024-03-31 18:01:38 +05:30
Manav Rathi
40a1da1ba7 Fine tune 2024-03-31 18:00:09 +05:30
Manav Rathi
5dfafa28c7 Almost there in terms of styling 2024-03-31 17:54:38 +05:30
Manav Rathi
d3df6b31ae Use actual contents 2024-03-31 17:31:33 +05:30
Manav Rathi
145850a66e Try using intrinsic size 2024-03-31 17:28:26 +05:30
Manav Rathi
6b56c28870 Mark vscode-styled-components optional 2024-03-31 17:07:48 +05:30
Manav Rathi
9ec68ecd3d Mention vscode-styled-components 2024-03-31 17:04:38 +05:30
Manav Rathi
8c127a6cec Animate 2024-03-31 16:47:10 +05:30
Manav Rathi
3890373d4a Try pure-react-carousel as a replacement of bootstrap's Carousel 2024-03-31 16:45:46 +05:30
Manav Rathi
ee1eb75bdf Extract component 2024-03-31 16:41:50 +05:30
Manav Rathi
14e99ea26a Fix ports 2024-03-30 20:59:05 +05:30
Manav Rathi
7183a8b493 [web] Remove emotion caches
This is no longer needed for emotion > 10

> For v10 and above, SSR just works in Next.js.
>
> https://emotion.sh/docs/ssr#nextjs

Tested with

- yarn dev:*
- yarn preview:*

This change screws up the CSS in places in dev mode though.
2024-03-30 20:56:38 +05:30
Prateek Sunal
e1ac5e7394 [FIX] Auth Desk (#1262)
## Description

- Don't hide but close app on exit
- Hide window option
- Fix translation of labels of context menu
2024-03-30 19:27:56 +05:30
Prateek Sunal
1391cff1f1 fix: translate labels of context menu 2024-03-30 19:13:30 +05:30
Prateek Sunal
204d4d048e chore: update podfile 2024-03-30 19:10:01 +05:30
Prateek Sunal
8dfd60df19 fix: close app on exit and add option to hide window 2024-03-30 19:02:05 +05:30
Manav Rathi
5810d2b762 Add yarn preview:* 2024-03-30 18:06:58 +05:30
Manav Rathi
f6abcafc83 Copy over fix into auth and accounts 2024-03-30 17:20:26 +05:30
Ashil
7950f1ec26 [mobile][photos] Explicitly ask for media location (#1261)
## Description

On bumping up photo_manager version, it introduced a breaking change
where we need to explicitly ask for ACCESS_MEDIA_LOCATION permission.

## Tests

Tested on android 13 and 14 devices.
2024-03-30 16:48:15 +05:30
github-actions[bot]
6974672f8c [mobile] New translations (#1239)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-app)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-03-30 16:15:32 +05:30
Neeraj Gupta
25cedd5e2f [server] Gracefully handle stale collection entry (#1260)
## Description

## Tests
2024-03-30 15:24:52 +05:30
Neeraj Gupta
a51a965fc8 [cli] Fix handling of delete file with stale colleciton entry (#1259)
## Description

## Tests
Tested locally
2024-03-30 14:14:03 +05:30
Vishnu Mohandas
fc5d8aeca6 [auth] Add new custom icons in auth (#1258)
Add new icons

- ConfigCat
- Mercado Livre
- SendGrid
- Habbo
2024-03-30 07:02:04 +05:30
ialexanderbrito
03fc59a8fb feat: add new custom icons in auth 2024-03-29 17:15:31 -03:00
Prateek Sunal
80a27f7e6f [FIX] libffi7 missing error in auth appimage (#1257)
Package libffi7 with appimage
2024-03-30 01:03:34 +05:30
Prateek Sunal
cc2c8e3e26 fix: libffi7 missing error in appimage 2024-03-30 00:54:16 +05:30
Manav Rathi
d52f873c92 [web] Remove dead code from cast (#1256) 2024-03-29 22:45:05 +05:30
Manav Rathi
911cdd9448 Remove more dead code from cast 2024-03-29 22:37:42 +05:30
Manav Rathi
b4699ecfcb Remove ElectronFile from cast 2024-03-29 22:26:50 +05:30
Manav Rathi
ded151241f Remove more dead code from cast 2024-03-29 22:22:03 +05:30
Manav Rathi
3dfc3a6dba Remove dead code from cast 2024-03-29 21:56:33 +05:30
Vishnu Mohandas
08b7fe6a49 [FIX] home widget count function and package source (#1252)
## Description

- Switched to pub.dev version of home_widget
- Replaced count home widget with getInstalledWidgets().

## Tests
2024-03-29 21:32:05 +05:30
Manav Rathi
7ef59bb4cc Short circuit unused code
isFileEligibleForCast filters out isRawFileFromFileName. Specifically, it
filters out HEIC files. So getRenderableImage is a no-op.
2024-03-29 21:01:17 +05:30
Manav Rathi
049a240916 Remove dead code from cast 2024-03-29 20:54:10 +05:30
Manav Rathi
3fdf5f1e46 Remove dead cast code 2024-03-29 20:12:57 +05:30
Prateek Sunal
d92fd25a78 fix: home widget count function 2024-03-29 19:46:13 +05:30
Manav Rathi
640b546d78 [docs] Update-export2 (#1249) 2024-03-29 17:24:07 +05:30
Manav Rathi
9e3fbce6c7 Center align the dialog images 2024-03-29 17:23:08 +05:30
Manav Rathi
e89eb48214 [web] Various minor fixes and improvements to cast (#1250)
- Use 3001 for sidecars
- Use the placeholder as the placeholder, not as the label
- Change slideshow time from 5s => 10s
- Remove the 123456 below the actual code
- Make the code copyable without inserting spaces in between
2024-03-29 17:14:45 +05:30
Manav Rathi
9440b967c8 Remove the extra spaces being inserted when we copy paste
Ref:

- https://github.com/facebook/react/issues/1643
- https://stackoverflow.com/questions/10837063/display-text-with-spaces-that-are-not-copied
2024-03-29 17:10:51 +05:30
Jishnuraj9
d6769fb1d5 Update image and text-export 2024-03-29 17:06:48 +05:30
Jishnuraj9
df377ebcf3 update image 2024-03-29 16:34:16 +05:30
Prateek Sunal
0114775e22 Bump auth to 2.0.50 build number (#1248)
## Description

## Tests
2024-03-29 16:30:55 +05:30
Prateek Sunal
31ffb124b6 chore: bump it again to 250 2024-03-29 16:27:09 +05:30
Prateek Sunal
f01583d168 chore: bump auth to 249 build number 2024-03-29 16:25:58 +05:30
Neeraj Gupta
f6dca2dfc9 [server] Drop locationTag table and related code (#1245)
## Description

## Tests
2024-03-29 16:25:39 +05:30
Prateek Sunal
45f9b47f24 [FEAT] Add Context menu to desktop right click (#1247)
## Description


![image](https://github.com/ente-io/ente/assets/41370460/099a90d9-3fb7-48cc-8177-79ea81c3edfc)
2024-03-29 16:24:19 +05:30
Prateek Sunal
cb7a2445e1 fix: qr image 2024-03-29 16:23:55 +05:30
Prateek Sunal
4830a69aad chore: remove lint 2024-03-29 16:23:04 +05:30
Prateek Sunal
f8fbedfe10 fix: use context menu on desktop 2024-03-29 16:21:30 +05:30
Manav Rathi
d22cf34a0e Remove nesting 2024-03-29 16:18:49 +05:30
Neeraj Gupta
ef250acad9 [server] Add person EntityType (#1246)
## Description

## Tests
2024-03-29 16:12:59 +05:30
Neeraj Gupta
459c4515a0 [server] Drop locationTag table and related code 2024-03-29 16:08:28 +05:30
Prateek Sunal
056e29a5f5 feat: context menu vert 2024-03-29 16:07:09 +05:30
Prateek Sunal
71e87ef7e9 feat: context menu horz 2024-03-29 16:07:01 +05:30
Manav Rathi
0d3662d9fe Remove the 123456 below the actual code
It prevents copy pasting (and doesn't look too good either)
2024-03-29 16:03:34 +05:30
Manav Rathi
70e5e9b13c [cast] Change slideshow time from 5s => 10s 2024-03-29 12:34:54 +05:30
Manav Rathi
949780d1e8 [cast] Use the placeholder as the placeholder, not as the label 2024-03-29 12:15:06 +05:30
Manav Rathi
a06a93e73d Use 3001 for sidecars 2024-03-29 11:58:42 +05:30
Neeraj Gupta
bc45db51a9 [cli] Improve logging for decryption error (#1242)
## Description
Related to #1237
## Tests
2024-03-29 11:08:45 +05:30
ashilkn
953824ca25 Refactor: reduce parameters and change name of function 2024-03-21 19:54:31 +05:30
ashilkn
7c05069dbd fix(viewing proXDR images): When codec fails to produce an image, try converting image to jpeg and use the jpeg file for viewing the image 2024-03-21 16:26:09 +05:30
170 changed files with 1917 additions and 4998 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -85,7 +85,7 @@ jobs:
- name: Install dependencies for desktop build
run: |
sudo apt-get update -y
sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev
sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi7
- name: Install appimagetool
run: |

View File

@@ -70,7 +70,7 @@ existing users will be grandfathered in.
[<img height="42" src=".github/assets/app-store-badge.svg">](https://apps.apple.com/app/id6444121398)
[<img height="42" src=".github/assets/play-store-badge.png">](https://play.google.com/store/apps/details?id=io.ente.auth)
[<img height="42" src=".github/assets/f-droid-badge.png">](https://f-droid.org/packages/io.ente.auth/)
[<img height="42" src=".github/assets/github-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
[<img height="42" src=".github/assets/desktop-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
[<img height="42" src=".github/assets/web-badge.svg">](https://auth.ente.io)
</div>

View File

@@ -31,14 +31,16 @@ You can alternatively install the build from PlayStore or F-Droid.
<img height="59" src="../.github/assets/app-store-badge.svg">
</a>
### Desktop
You can [**download**](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
a native desktop app from this repository's GitHub releases. The desktop app
works on Windows, Linux and macOS.
### Web
You can view your 2FA codes at [auth.ente.io](https://auth.ente.io). For adding
or managing your secrets, please use our mobile app.
### Desktop
A native desktop app is coming soon!
or managing your secrets, please use our mobile or desktop app.
## 🧑‍💻 Build from source

View File

@@ -36,7 +36,9 @@
},
{
"title": "BorgBase",
"altNames": ["borg"],
"altNames": [
"borg"
],
"slug": "BorgBase"
},
{
@@ -54,6 +56,9 @@
"slug": "cih",
"hex": "D14633"
},
{
"title": "ConfigCat"
},
{
"title": "Cloudflare"
},
@@ -67,7 +72,9 @@
},
{
"title": "DCS",
"altNames": ["Digital Combat Simulator"],
"altNames": [
"Digital Combat Simulator"
],
"slug": "dcs"
},
{
@@ -115,9 +122,14 @@
},
{
"title": "Gosuslugi",
"altNames": ["Госуслуги"],
"altNames": [
"Госуслуги"
],
"slug": "Gosuslugi"
},
{
"title": "Habbo"
},
{
"title": "Healthchecks.io",
"slug": "healthchecks"
@@ -180,13 +192,24 @@
},
{
"title": "Mastodon",
"altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"],
"altNames": [
"mstdn",
"fediscience",
"mathstodon",
"fosstodon"
],
"slug": "mastodon",
"hex": "6364FF"
},
{
"title": "Mercado Livre",
"slug": "mercado_livre"
},
{
"title": "Murena",
"altNames": ["eCloud"],
"altNames": [
"eCloud"
],
"slug": "ecloud"
},
{
@@ -284,6 +307,9 @@
"slug": "rust_language_forum",
"hex": "000000"
},
{
"title": "Sendgrid"
},
{
"title": "service-bw"
},
@@ -374,13 +400,18 @@
},
{
"title": "X",
"altNames": ["twitter"],
"altNames": [
"twitter"
],
"slug": "x"
},
{
"title": "Yandex",
"altNames": ["Ya", "Яндекс"],
"altNames": [
"Ya",
"Яндекс"
],
"slug": "Yandex"
}
]
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 68 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -46,7 +46,6 @@ class _AppState extends State<App> with WindowListener, TrayListener {
Future<void> initWindowManager() async {
windowManager.addListener(this);
await windowManager.setPreventClose(true);
}
Future<void> initTrayManager() async {
@@ -154,11 +153,6 @@ class _AppState extends State<App> with WindowListener, TrayListener {
};
}
@override
void onWindowClose() async {
await windowManager.hide();
}
@override
void onWindowResize() {
WindowListenerService.instance.onWindowResize().ignore();
@@ -187,11 +181,16 @@ class _AppState extends State<App> with WindowListener, TrayListener {
@override
void onTrayMenuItemClick(MenuItem menuItem) {
if (menuItem.key == 'show_window') {
windowManager.show();
} else if (menuItem.key == 'exit_app') {
windowManager.setPreventClose(false);
windowManager.close();
switch (menuItem.key) {
case 'hide_window':
windowManager.hide();
break;
case 'show_window':
windowManager.show();
break;
case 'exit_app':
windowManager.close();
break;
}
}
}

View File

@@ -43,6 +43,10 @@ Future<void> initSystemTray() async {
await trayManager.setIcon(path);
Menu menu = Menu(
items: [
MenuItem(
key: 'hide_window',
label: 'Hide Window',
),
MenuItem(
key: 'show_window',
label: 'Show Window',

View File

@@ -17,8 +17,8 @@ import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:logging/logging.dart';
import 'package:move_to_background/move_to_background.dart';
@@ -86,108 +86,122 @@ class _CodeWidgetState extends State<CodeWidget> {
final l10n = context.l10n;
return Container(
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
child: Slidable(
key: ValueKey(widget.code.hashCode),
endActionPane: ActionPane(
extentRatio: 0.60,
motion: const ScrollMotion(),
children: [
const SizedBox(
width: 4,
),
SlidableAction(
onPressed: _onShowQrPressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor:
Theme.of(context).colorScheme.inverseBackgroundColor,
icon: Icons.qr_code_2_outlined,
label: "QR",
padding: const EdgeInsets.only(left: 4, right: 0),
spacing: 8,
),
const SizedBox(
width: 4,
),
SlidableAction(
onPressed: _onEditPressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor:
Theme.of(context).colorScheme.inverseBackgroundColor,
icon: Icons.edit_outlined,
label: l10n.edit,
padding: const EdgeInsets.only(left: 4, right: 0),
spacing: 8,
),
const SizedBox(
width: 4,
),
SlidableAction(
onPressed: _onDeletePressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor: const Color(0xFFFE4A49),
icon: Icons.delete,
label: l10n.delete,
padding: const EdgeInsets.only(left: 0, right: 0),
spacing: 8,
),
],
),
child: Builder(
builder: (context) {
return RawGestureDetector(
gestures: {
PanGestureRecognizer:
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(
debugOwner: this,
// This recognizer accepts any button press made with a secondary button.
allowedButtonsFilter: (int buttons) =>
buttons & kSecondaryButton != 0,
child: Builder(
builder: (context) {
if (PlatformUtil.isDesktop()) {
return ContextMenuRegion(
contextMenu: ContextMenu(
entries: <ContextMenuEntry>[
MenuItem(
label: 'QR',
icon: Icons.qr_code_2_outlined,
onSelected: () => _onShowQrPressed(null),
),
(PanGestureRecognizer instance) {
instance
..dragStartBehavior = DragStartBehavior.down
..onEnd = (DragEndDetails details) {
Slidable.of(context)?.openEndActionPane();
};
},
),
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
child: Material(
color: Colors.transparent,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onTap: () {
_copyCurrentOTPToClipboard();
},
onDoubleTap: isMaskingEnabled
? () {
setState(
() {
_hideCode = !_hideCode;
},
);
}
: null,
onLongPress: () {
_copyCurrentOTPToClipboard();
},
child: _getCardContents(l10n),
),
MenuItem(
label: l10n.edit,
icon: Icons.edit,
onSelected: () => _onEditPressed(null),
),
),
const MenuDivider(),
MenuItem(
label: l10n.delete,
value: "Delete",
icon: Icons.delete,
onSelected: () => _onDeletePressed(null),
),
],
padding: const EdgeInsets.all(8.0),
),
child: _clippedCard(l10n),
);
},
}
return Slidable(
key: ValueKey(widget.code.hashCode),
endActionPane: ActionPane(
extentRatio: 0.60,
motion: const ScrollMotion(),
children: [
const SizedBox(
width: 4,
),
SlidableAction(
onPressed: _onShowQrPressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor:
Theme.of(context).colorScheme.inverseBackgroundColor,
icon: Icons.qr_code_2_outlined,
label: "QR",
padding: const EdgeInsets.only(left: 4, right: 0),
spacing: 8,
),
const SizedBox(
width: 4,
),
SlidableAction(
onPressed: _onEditPressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor:
Theme.of(context).colorScheme.inverseBackgroundColor,
icon: Icons.edit_outlined,
label: l10n.edit,
padding: const EdgeInsets.only(left: 4, right: 0),
spacing: 8,
),
const SizedBox(
width: 4,
),
SlidableAction(
onPressed: _onDeletePressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor: const Color(0xFFFE4A49),
icon: Icons.delete,
label: l10n.delete,
padding: const EdgeInsets.only(left: 0, right: 0),
spacing: 8,
),
],
),
child: Builder(
builder: (context) => _clippedCard(l10n),
),
);
},
),
);
}
Widget _clippedCard(AppLocalizations l10n) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
child: Material(
color: Colors.transparent,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onTap: () {
_copyCurrentOTPToClipboard();
},
onDoubleTap: isMaskingEnabled
? () {
setState(
() {
_hideCode = !_hideCode;
},
);
}
: null,
onLongPress: () {
_copyCurrentOTPToClipboard();
},
child: _getCardContents(l10n),
),
),
),
);

View File

@@ -23,4 +23,5 @@ startup_notify: false
#
# include:
# - libcurl.so.4
include: []
include:
- libffi.so.7

View File

@@ -63,7 +63,7 @@ PODS:
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- system_tray (0.0.1):
- tray_manager (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
@@ -91,7 +91,7 @@ DEPENDENCIES:
- sodium_libs (from `Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
- system_tray (from `Flutter/ephemeral/.symlinks/plugins/system_tray/macos`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
@@ -144,8 +144,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
sqlite3_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
system_tray:
:path: Flutter/ephemeral/.symlinks/plugins/system_tray/macos
tray_manager:
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
window_manager:
@@ -177,7 +177,7 @@ SPEC CHECKSUMS:
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 73b7fc691fdc43277614250e04d183740cb15078
sqlite3_flutter_libs: 06a05802529659a272beac4ee1350bfec294f386
system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8

View File

@@ -347,6 +347,14 @@ packages:
url: "https://github.com/ente-io/ente_crypto_dart.git"
source: git
version: "1.0.0"
equatable:
dependency: transitive
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
url: "https://pub.dev"
source: hosted
version: "2.0.5"
event_bus:
dependency: "direct main"
description:
@@ -440,6 +448,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.1.5"
flutter_context_menu:
dependency: "direct main"
description:
name: flutter_context_menu
sha256: "9f220a8fa0290c68e38000d6d62a0dc4555d490c15a5bd856a6e6d255d81b8dc"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
flutter_displaymode:
dependency: "direct main"
description:

View File

@@ -1,6 +1,6 @@
name: ente_auth
description: ente two-factor authenticator
version: 2.0.48+248
version: 2.0.50+250
publish_to: none
environment:
@@ -41,6 +41,7 @@ dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.0.1
flutter_context_menu: ^0.1.3
flutter_displaymode: ^0.6.0
flutter_email_sender: ^6.0.2
flutter_inappwebview: ^6.0.0

View File

@@ -18,6 +18,10 @@ type File struct {
Info *FileInfo `json:"info,omitempty"`
}
func (f File) IsRemovedFromAlbum() bool {
return f.IsDeleted || f.File.EncryptedData == "-"
}
// FileInfo has information about storage used by the file & it's metadata(future)
type FileInfo struct {
FileSize int64 `json:"fileSize,omitempty"`

View File

@@ -98,7 +98,8 @@ func DecryptChaChaBase64(data string, key []byte, nonce string) (string, []byte,
// Decode data from base64
dataBytes, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return "", nil, fmt.Errorf("invalid data: %v", err)
// safe to log the encrypted data
return "", nil, fmt.Errorf("invalid base64 data %s: %v", data, err)
}
// Decode nonce from base64
nonceBytes, err := base64.StdEncoding.DecodeString(nonce)

View File

@@ -15,7 +15,7 @@ import (
"strings"
)
var AppVersion = "0.1.12"
var AppVersion = "0.1.13"
func main() {
cliDBPath, err := GetCLIConfigPath()

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/ente-io/cli/internal/api"
eCrypto "github.com/ente-io/cli/internal/crypto"
"github.com/ente-io/cli/pkg/model"
@@ -41,7 +42,7 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder
if collection.MagicMetadata != nil {
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.MagicMetadata.Data, collectionKey, collection.MagicMetadata.Header)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to decrypt magic metadata for collection %d: %w", collection.ID, err)
}
err = json.Unmarshal(encodedJsonBytes, &album.PrivateMeta)
if err != nil {
@@ -51,28 +52,28 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder
if collection.PublicMagicMetadata != nil {
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.PublicMagicMetadata.Data, collectionKey, collection.PublicMagicMetadata.Header)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to decrypt public magic metadata for collection %d: %w", collection.ID, err)
}
err = json.Unmarshal(encodedJsonBytes, &album.PublicMeta)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to unmarshal public magic metadata for collection %d: %w", collection.ID, err)
}
}
if album.IsShared && collection.SharedMagicMetadata != nil {
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.SharedMagicMetadata.Data, collectionKey, collection.SharedMagicMetadata.Header)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to decrypt shared magic metadata for collection %d: %w", collection.ID, err)
}
err = json.Unmarshal(encodedJsonBytes, &album.SharedMeta)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to unmarshal shared magic metadata for collection %d: %w", collection.ID, err)
}
}
return &album, nil
}
func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file api.File, holder *secrets.KeyHolder) (*model.RemoteFile, error) {
if file.IsDeleted {
if file.IsRemovedFromAlbum() {
return nil, errors.New("file is deleted")
}
albumKey := album.AlbumKey.MustDecrypt(holder.DeviceKey)
@@ -99,7 +100,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap
if file.Metadata.DecryptionHeader != "" {
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.Metadata.EncryptedData, fileKey, file.Metadata.DecryptionHeader)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to decrypt metadata for file %d: %w", file.ID, err)
}
err = json.Unmarshal(encodedJsonBytes, &photoFile.Metadata)
if err != nil {
@@ -109,7 +110,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap
if file.MagicMetadata != nil {
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.MagicMetadata.Data, fileKey, file.MagicMetadata.Header)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to decrypt magic metadata for file %d: %w", file.ID, err)
}
err = json.Unmarshal(encodedJsonBytes, &photoFile.PrivateMetadata)
if err != nil {
@@ -119,7 +120,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap
if file.PubicMagicMetadata != nil {
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.PubicMagicMetadata.Data, fileKey, file.PubicMagicMetadata.Header)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to decrypt public magic metadata for file %d: %w", file.ID, err)
}
err = json.Unmarshal(encodedJsonBytes, &photoFile.PublicMetadata)
if err != nil {

View File

@@ -87,16 +87,16 @@ func (c *ClICtrl) fetchRemoteFiles(ctx context.Context) error {
if file.UpdationTime > maxUpdated {
maxUpdated = file.UpdationTime
}
if isFirstSync && file.IsDeleted {
if isFirstSync && file.IsRemovedFromAlbum() {
// on first sync, no need to sync delete markers
continue
}
albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsDeleted, SyncedLocally: false}
albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsRemovedFromAlbum(), SyncedLocally: false}
putErr := c.UpsertAlbumEntry(ctx, &albumEntry)
if putErr != nil {
return putErr
}
if file.IsDeleted {
if file.IsRemovedFromAlbum() {
continue
}
photoFile, err := mapper.MapApiFileToPhotoFile(ctx, album, file, c.KeyHolder)

View File

@@ -31,7 +31,7 @@ are built against `electron`'s packaged `node` version. We use
to rebuild those modules automatically after each `yarn install` by invoking it
in as the `postinstall` step in our package.json.
### lint and lint-fix
### lint, lint-fix
Use `yarn lint` to check that your code formatting is as expected, and that
there are no linter errors. Use `yarn lint-fix` to try and automatically fix the

View File

@@ -21,3 +21,9 @@ photos and videos are always available in normal directories and files.
"continuous" exports, where it will automatically export new items in the
background without you needing to run any other cron jobs. See
[migration/export](/photos/migration/export/) for more details.
## Does the exported data from Ente photos preserve the same folder and album structure as in the app?
When you export your data for local backup, it will maintain the exact structure
how you have set up within Ente. The exported data will reflect the same photos
and album structure intact.

View File

@@ -74,3 +74,29 @@ If you would like to fund the development of this project, please consider
## How do I pronounce ente?
It's like cafe 😊. kaf-_ay_. en-_tay_.
## Does Ente apply compression to uploaded photos?
Ente does not apply compression to uploaded photos. The file size of your photos in Ente will be similar to the original file sizes you have.
## Can I add photos from a shared album to albums that I created in Ente?
Currently, Ente does not support adding photos from a shared album to your personal albums. If you want to include photos from a shared album in your own albums, you will need to ask the owner of the photos to add them to your album.
## How do I ensure that the Ente desktop app stays up to date on my system?
Ente desktop includes an auto-update feature, ensuring that whenever updates are deployed, the app will automatically download and install them. You don't need to manually update the software.
## Can I sync a folder containing multiple subfolders, each representing an album?
Yes, when you drag and drop the folder onto the desktop app, the app will detect the multiple folders and prompt you to choose whether you want to create a single album or separate albums for each folder.
## What is the difference between **Magic** and **Content** search results on the desktop?
**Magic** is where you can search for long queries. Like, "baby in red dress", or "dog playing at the beach".
**Content** is where you can search for single-words. Like, "car" or "pizza".
## How do I identify which files experienced upload issues within the desktop app?
Check the sections within the upload progress bar for "Failed Uploads," "Ignored Uploads," and "Unsuccessful Uploads."

View File

@@ -156,3 +156,7 @@ Sorry, since we're building a business that does not involve monetization of
user data, we have to charge to remain sustainable.
We do offer a generous free trial for you to experience the product.
## Will I need to pay for Ente Auth after my Ente Photos free plan expires?
No, you will not need to pay for Ente Auth after your Ente Photos free plan expires. Ente Auth is completely free to use, and the expiration of your Ente Photos free plan will not impact your ability to access or use Ente Auth.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -11,26 +11,44 @@ videos you have uploaded to Ente.
1. Sign in to [our desktop app](https://ente.io/download/desktop), if you
haven't done so already.
![Ente - Sign in to export data](sign-in.png)
2. Open the side bar, and select the option to **export data**.
![Ente - Export data](export-1.png)
3. Select the destination folder and click on **start**.
3. Choose the destination folder by clicking on three dots icon.
![Ente - Select destination folder and start](export-2.png)
<div align="center">
4. Wait for the export to get completed.
![Ente - Select destination folder and start](export-2.png){width=400px}
![Ente - Export in progress](export-3.png)
</div>
5. Later on if you wish to sync newer files that were uploaded since the last
time you exported, simply select **export data** again and click on
**resync**.
4. Select the folder and then click on **Start**
![Ente - Rexport](export-4.png)
<div align="center">
![Ente - Export in progress](export-3.png){width=400px}
</div>
5. Wait for the export to get completed.
<div align="center">
![Ente - Rexport](export-4.png){width=400px}
</div>
6. In case your download gets interrupted, Ente will resume from where it left
off. Simply select **export data** again and click on **resync**.
off. Simply select **export data** again and click on **Resync**.
<div align="center">
![Ente - Rexport](export-5.png){width=400px}
</div>
7. **Sync continuously** : You can utilize Continuous Sync to eliminate manual
exports each time new photos are added to Ente. This feature automatically
@@ -39,6 +57,8 @@ videos you have uploaded to Ente.
![Ente - Continuous sync](continuous-sync.webp)
If you run into any issues during your data export, please reach out to
[support@ente.io](mailto:support@ente.io) and we will be happy to help you!

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -1,6 +1,6 @@
# CHANGELOG
## v0.8.72
## v0.8.73
### Added
* #### Share an Album to Multiple Contacts at Once
@@ -9,7 +9,7 @@
* #### Bug Fixes and Performance Improvements
Many a bugs were squashed in this release. If you run into any, please write to team@ente.io, or let us know on Discord! 🙏
Many a bugs were squashed in this release and have improved performance on app start. If you run into any bugs, please write to team@ente.io, or let us know on Discord! 🙏
## v0.8.67

View File

@@ -79,3 +79,5 @@ class LoginKeyDerivationError extends Error {}
class SrpSetupNotCompleteError extends Error {}
class SharingNotPermittedForFreeAccountsError extends Error {}
class NoMediaLocationAccessError extends Error {}

View File

@@ -15,6 +15,7 @@ class FileUpdationDB {
static const columnLocalID = 'local_id';
static const columnReason = 'reason';
static const livePhotoCheck = 'livePhotoCheck';
static const androidMissingGPS = 'androidMissingGPS';
static const modificationTimeUpdated = 'modificationTimeUpdated';

View File

@@ -1533,6 +1533,24 @@ class FilesDB {
return result;
}
Future<List<String>> getLocalFilesBackedUpWithoutLocation(int userId) async {
final db = await instance.database;
final rows = await db.query(
filesTable,
columns: [columnLocalID],
distinct: true,
where:
'$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) '
'AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)',
whereArgs: [userId],
);
final result = <String>[];
for (final row in rows) {
result.add(row[columnLocalID] as String);
}
return result;
}
// updateSizeForUploadIDs takes a map of upploadedFileID and fileSize and
// update the fileSize for the given uploadedFileID
Future<void> updateSizeForUploadIDs(

View File

@@ -766,6 +766,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!"),
"hearUsWhereTitle": MessageLookupByLibrary.simpleMessage(
"Como você ouviu sobre o Ente? (opcional)"),
"help": MessageLookupByLibrary.simpleMessage("Ajuda"),
"hidden": MessageLookupByLibrary.simpleMessage("Oculto"),
"hide": MessageLookupByLibrary.simpleMessage("Ocultar"),
"hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."),
@@ -1011,6 +1012,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Detalhes de pagamento"),
"paymentFailed":
MessageLookupByLibrary.simpleMessage("Falha no pagamento"),
"paymentFailedMessage": MessageLookupByLibrary.simpleMessage(
"Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"),
"paymentFailedTalkToProvider": m37,
"pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"),
"pendingSync":

View File

@@ -630,6 +630,7 @@ class MessageLookup extends MessageLookupByLibrary {
"我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!"),
"hearUsWhereTitle":
MessageLookupByLibrary.simpleMessage("您是如何知道Ente的 (可选的)"),
"help": MessageLookupByLibrary.simpleMessage("帮助"),
"hidden": MessageLookupByLibrary.simpleMessage("已隐藏"),
"hide": MessageLookupByLibrary.simpleMessage("隐藏"),
"hiding": MessageLookupByLibrary.simpleMessage("正在隐藏..."),
@@ -834,6 +835,8 @@ class MessageLookup extends MessageLookupByLibrary {
"我们不储存这个密码,所以如果忘记, <underline>我们将无法解密您的数据</underline>"),
"paymentDetails": MessageLookupByLibrary.simpleMessage("付款明细"),
"paymentFailed": MessageLookupByLibrary.simpleMessage("支付失败"),
"paymentFailedMessage": MessageLookupByLibrary.simpleMessage(
"不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!"),
"paymentFailedTalkToProvider": m37,
"pendingItems": MessageLookupByLibrary.simpleMessage("待处理项目"),
"pendingSync": MessageLookupByLibrary.simpleMessage("正在等待同步"),

View File

@@ -304,6 +304,7 @@
}
},
"faq": "Perguntas frequentes",
"help": "Ajuda",
"oopsSomethingWentWrong": "Ops! Algo deu errado",
"peopleUsingYourCode": "Pessoas que usam seu código",
"eligible": "elegível",
@@ -640,7 +641,7 @@
"thankYou": "Obrigado",
"failedToVerifyPaymentStatus": "Falha ao verificar status do pagamento",
"pleaseWaitForSometimeBeforeRetrying": "Por favor, aguarde algum tempo antes de tentar novamente",
"paymentFailedWithReason": "Infelizmente o seu pagamento falhou devido a {reason}",
"paymentFailedMessage": "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!",
"youAreOnAFamilyPlan": "Você está em um plano familiar!",
"contactFamilyAdmin": "Entre em contato com <green>{familyAdminEmail}</green> para gerenciar sua assinatura",
"leaveFamily": "Sair da família",

View File

@@ -304,6 +304,7 @@
}
},
"faq": "常见问题",
"help": "帮助",
"oopsSomethingWentWrong": "哎呀,似乎出了点问题",
"peopleUsingYourCode": "使用您的代码的人",
"eligible": "符合资格",
@@ -640,7 +641,7 @@
"thankYou": "非常感谢您",
"failedToVerifyPaymentStatus": "验证支付状态失败",
"pleaseWaitForSometimeBeforeRetrying": "请稍等片刻后再重试",
"paymentFailedWithReason": "很抱歉,您的支付因 {reason} 而失败",
"paymentFailedMessage": "不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!",
"youAreOnAFamilyPlan": "你在一个家庭计划中!",
"contactFamilyAdmin": "请联系 <green>{familyAdminEmail}</green> 来管理您的订阅",
"leaveFamily": "离开家庭计划",

View File

@@ -145,13 +145,7 @@ class HomeWidgetService {
}
Future<int> countHomeWidgets() async {
return await hw.HomeWidget.getWidgetCount(
name: 'SlideshowWidgetProvider',
androidName: 'SlideshowWidgetProvider',
qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider',
iOSName: 'SlideshowWidget',
) ??
0;
return (await hw.HomeWidget.getInstalledWidgets()).length;
}
Future<void> clearHomeWidget() async {

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:logging/logging.dart';
import "package:photos/core/errors.dart";
import 'package:photos/db/ignored_files_db.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/ignored_file.dart';
@@ -47,7 +48,10 @@ class IgnoredFilesService {
return false;
}
String? getUploadSkipReason(Map<String, String> idToReasonMap, EnteFile file) {
String? getUploadSkipReason(
Map<String, String> idToReasonMap,
EnteFile file,
) {
final id = _getIgnoreID(file.localID, file.deviceFolder, file.title);
if (id != null && id.isNotEmpty) {
return idToReasonMap[id];
@@ -100,6 +104,12 @@ class IgnoredFilesService {
for (IgnoredFile iFile in dbResult) {
final id = _idForIgnoredFile(iFile);
if (id != null) {
if (Platform.isIOS &&
iFile.reason == InvalidReason.sourceFileMissing.name) {
// ignoreSourceFileMissing error on iOS as the file fetch from iCloud might have failed,
// but the file might be available later
continue;
}
result[id] = iFile.reason;
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:core';
import 'dart:io';
import 'package:logging/logging.dart';
import "package:photo_manager/photo_manager.dart";
import "package:photos/core/configuration.dart";
import 'package:photos/core/errors.dart';
import 'package:photos/db/file_updation_db.dart';
@@ -25,6 +26,10 @@ class LocalFileUpdateService {
late Logger _logger;
final String _iosLivePhotoSizeMigrationDone = 'fm_ios_live_photo_check';
final String _doneLivePhotoImport = 'fm_import_ios_live_photo_check';
final String _androidMissingGPSImportDone =
'fm_android_missing_gps_import_done';
final String _androidMissingGPSCheckDone =
'fm_android_missing_gps_check_done';
static int twoHundredKb = 200 * 1024;
final List<String> _oldMigrationKeys = [
'fm_badCreationTime',
@@ -63,6 +68,9 @@ class LocalFileUpdateService {
if (!Platform.isAndroid) {
await _handleLivePhotosSizedCheck();
}
if (Platform.isAndroid) {
await _androidMissingGPSCheck();
}
} catch (e, s) {
_logger.severe('failed to perform migration', e, s);
} finally {
@@ -385,6 +393,131 @@ class LocalFileUpdateService {
await _prefs.setBool(_doneLivePhotoImport, true);
}
//#region Android Missing GPS specific methods ###
Future<void> _androidMissingGPSCheck() async {
if (_prefs.containsKey(_androidMissingGPSCheckDone)) {
return;
}
await _importAndroidBadGPSCandidate();
// singleRunLimit indicates number of files to check during single
// invocation of this method. The limit act as a crude way to limit the
// resource consumed by the method
const int singleRunLimit = 500;
final localIDsToProcess =
await _fileUpdationDB.getLocalIDsForPotentialReUpload(
singleRunLimit,
FileUpdationDB.androidMissingGPS,
);
if (localIDsToProcess.isNotEmpty) {
final chunksOf50 = localIDsToProcess.chunks(50);
for (final chunk in chunksOf50) {
final sTime = DateTime.now().microsecondsSinceEpoch;
final List<Future> futures = [];
final chunkOf10 = chunk.chunks(10);
for (final smallChunk in chunkOf10) {
futures.add(_checkForMissingGPS(smallChunk));
}
await Future.wait(futures);
final eTime = DateTime.now().microsecondsSinceEpoch;
final d = Duration(microseconds: eTime - sTime);
_logger.info(
'Performed missing GPS Location check for ${chunk.length} files '
'completed in ${d.inSeconds.toString()} secs',
);
}
} else {
_logger.info('Completed android missing GPS check');
await _prefs.setBool(_androidMissingGPSCheckDone, true);
}
}
Future<void> _checkForMissingGPS(List<String> localIDs) async {
try {
final List<EnteFile> localFiles =
await FilesDB.instance.getLocalFiles(localIDs);
final ownerID = Configuration.instance.getUserID()!;
final Set<String> localIDsWithFile = {};
final Set<String> reuploadCandidate = {};
final Set<String> processedIDs = {};
for (EnteFile file in localFiles) {
if (file.localID == null) continue;
// ignore files that are not uploaded or have different owner
if (!file.isUploaded || file.ownerID! != ownerID) {
processedIDs.add(file.localID!);
}
if (file.hasLocation) {
processedIDs.add(file.localID!);
}
}
for (EnteFile enteFile in localFiles) {
try {
if (enteFile.localID == null ||
processedIDs.contains(enteFile.localID!)) {
continue;
}
final localID = enteFile.localID!;
localIDsWithFile.add(localID);
final AssetEntity? entity = await AssetEntity.fromId(localID);
if (entity == null) {
processedIDs.add(localID);
} else {
final latLng = await entity.latlngAsync();
if ((latLng.longitude ?? 0) == 0 || (latLng.latitude ?? 0) == 0) {
processedIDs.add(localID);
} else {
reuploadCandidate.add(localID);
processedIDs.add(localID);
}
}
} catch (e, s) {
processedIDs.add(enteFile.localID!);
_logger.severe('lat/long check file ${enteFile.toString()}', e, s);
}
}
for (String id in localIDs) {
// if the file with given localID doesn't exist, consider it as done.
if (!localIDsWithFile.contains(id)) {
processedIDs.add(id);
}
}
await FileUpdationDB.instance.insertMultiple(
reuploadCandidate.toList(),
FileUpdationDB.modificationTimeUpdated,
);
await FileUpdationDB.instance.deleteByLocalIDs(
processedIDs.toList(),
FileUpdationDB.androidMissingGPS,
);
} catch (e, s) {
_logger.severe('error while checking missing GPS', e, s);
}
}
Future<void> _importAndroidBadGPSCandidate() async {
if (_prefs.containsKey(_androidMissingGPSImportDone)) {
return;
}
final sTime = DateTime.now().microsecondsSinceEpoch;
_logger.info('importing files without missing GPS');
final int ownerID = Configuration.instance.getUserID()!;
final fileLocalIDs =
await FilesDB.instance.getLocalFilesBackedUpWithoutLocation(ownerID);
await _fileUpdationDB.insertMultiple(
fileLocalIDs,
FileUpdationDB.androidMissingGPS,
);
final eTime = DateTime.now().microsecondsSinceEpoch;
final d = Duration(microseconds: eTime - sTime);
_logger.info(
'importing completed, total files count ${fileLocalIDs.length} and took ${d.inSeconds.toString()} seconds',
);
await _prefs.setBool(_androidMissingGPSImportDone, true);
}
//#endregion Android Missing GPS specific methods ###
Future<MediaUploadData> getUploadData(EnteFile file) async {
final mediaUploadData = await getUploadDataFromEnteFile(file);
// delete the file from app's internal cache if it was copied to app

View File

@@ -20,6 +20,7 @@ import 'package:photos/services/app_lifecycle_service.dart';
import "package:photos/services/ignored_files_service.dart";
import 'package:photos/services/local/local_sync_util.dart';
import "package:photos/utils/debouncer.dart";
import "package:photos/utils/photo_manager_util.dart";
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite/sqflite.dart';
import 'package:tuple/tuple.dart';
@@ -61,7 +62,7 @@ class LocalSyncService {
return;
}
if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
final permissionState = await PhotoManager.requestPermissionExtend();
final permissionState = await requestPhotoMangerPermissions();
if (permissionState != PermissionState.authorized) {
_logger.severe(
"sync requested with invalid permission",
@@ -213,6 +214,11 @@ class LocalSyncService {
_logger.warning('Invalid file received for ignoring: $file');
return;
}
if (Platform.isIOS && error.reason == InvalidReason.sourceFileMissing) {
// ignoreSourceFileMissing error on iOS as the file fetch from iCloud might have failed,
// but the file might be available later
return;
}
final ignored = IgnoredFile(
file.localID,
file.title,

View File

@@ -170,7 +170,8 @@ class RemoteSyncService {
e is NoActiveSubscriptionError ||
e is WiFiUnavailableError ||
e is StorageLimitExceededError ||
e is SyncStopRequestedError) {
e is SyncStopRequestedError ||
e is NoMediaLocationAccessError) {
_logger.warning("Error executing remote sync", e, s);
rethrow;
} else {
@@ -555,6 +556,7 @@ class RemoteSyncService {
final int toBeUploaded = filesToBeUploaded.length + updatedFileIDs.length;
if (toBeUploaded > 0) {
Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload));
await _uploader.verifyMediaLocationAccess();
await _uploader.checkNetworkForUpload();
// verify if files upload is allowed based on their subscription plan and
// storage limit. To avoid creating new endpoint, we are using

View File

@@ -120,6 +120,14 @@ class SyncService {
} on UnauthorizedError {
_logger.info("Logging user out");
Bus.instance.fire(TriggerLogoutEvent());
} on NoMediaLocationAccessError {
_logger.severe("Not uploading due to no media location access");
Bus.instance.fire(
SyncStatusUpdate(
SyncStatus.error,
error: NoMediaLocationAccessError(),
),
);
} catch (e) {
if (e is DioError) {
if (e.type == DioErrorType.connectTimeout ||

View File

@@ -16,7 +16,7 @@ class UpdateService {
static final UpdateService instance = UpdateService._privateConstructor();
static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
static const changeLogVersionKey = "update_change_log_key";
static const currentChangeLogVersion = 16;
static const currentChangeLogVersion = 17;
LatestVersionInfo? _latestVersion;
final _logger = Logger("UpdateService");

View File

@@ -10,6 +10,7 @@ import 'package:photos/ui/components/buttons/icon_button_widget.dart';
import "package:photos/ui/settings/backup/backup_folder_selection_page.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/navigation_util.dart";
import "package:photos/utils/photo_manager_util.dart";
class HomeHeaderWidget extends StatefulWidget {
final Widget centerWidget;
@@ -48,7 +49,7 @@ class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
onTap: () async {
try {
final PermissionState state =
await PhotoManager.requestPermissionExtend();
await requestPhotoMangerPermissions();
await LocalSyncService.instance.onUpdatePermission(state);
} on Exception catch (e) {
Logger("HomeHeaderWidget").severe(

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/services/sync_service.dart';
import "package:photos/utils/photo_manager_util.dart";
import "package:styled_text/styled_text.dart";
class GrantPermissionsWidget extends StatelessWidget {
@@ -91,7 +92,7 @@ class GrantPermissionsWidget extends StatelessWidget {
key: const ValueKey("grantPermissionButton"),
child: Text(S.of(context).grantPermission),
onPressed: () async {
final state = await PhotoManager.requestPermissionExtend();
final state = await requestPhotoMangerPermissions();
if (state == PermissionState.authorized ||
state == PermissionState.limited) {
await SyncService.instance.onPermissionGranted(state);

View File

@@ -128,8 +128,8 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
),
ChangeLogEntry(
"Bug Fixes and Performance Improvements",
'Many a bugs were squashed in this release.\n'
'\nIf you run into any, please write to team@ente.io, or let us know on Discord! 🙏',
'Many a bugs were squashed in this release and have improved performance on app start.\n'
'\nIf you run into any bugs, please write to team@ente.io, or let us know on Discord! 🙏',
),
]);

View File

@@ -140,6 +140,7 @@ class _DetailPageState extends State<DetailPage> {
),
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: false,
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
@@ -165,6 +166,7 @@ class _DetailPageState extends State<DetailPage> {
Widget _buildPageView(BuildContext context) {
return PageView.builder(
clipBehavior: Clip.none,
itemBuilder: (context, index) {
final file = _files![index];
_preloadFiles(index);

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import "package:flutter_image_compress/flutter_image_compress.dart";
import 'package:logging/logging.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
@@ -50,6 +51,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
bool _loadedLargeThumbnail = false;
bool _loadingFinalImage = false;
bool _loadedFinalImage = false;
bool _convertToSupportedFormat = false;
ValueChanged<PhotoViewScaleState>? _scaleStateChangedCallback;
bool _isZooming = false;
PhotoViewController _photoViewController = PhotoViewController();
@@ -57,6 +59,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
@override
void initState() {
super.initState();
_photo = widget.photo;
_logger = Logger("ZoomableImage");
_logger.info('initState for ${_photo.generatedID} with tag ${_photo.tag}');
@@ -68,7 +71,6 @@ class _ZoomableImageState extends State<ZoomableImage> {
debugPrint("isZooming = $_isZooming, currentState $value");
// _logger.info('is reakky zooming $_isZooming with state $value');
};
super.initState();
}
@override
@@ -133,7 +135,9 @@ class _ZoomableImageState extends State<ZoomableImage> {
height: screenRelativeImageHeight,
child: Hero(
tag: widget.tagPrefix! + _photo.tag,
child: const EnteLoadingWidget(),
child: const EnteLoadingWidget(
color: Colors.white,
),
),
),
);
@@ -141,7 +145,9 @@ class _ZoomableImageState extends State<ZoomableImage> {
),
);
} else {
content = const EnteLoadingWidget();
content = const EnteLoadingWidget(
color: Colors.white,
);
}
final GestureDragUpdateCallback? verticalDragCallback = _isZooming
@@ -194,11 +200,8 @@ class _ZoomableImageState extends State<ZoomableImage> {
_loadingFinalImage = true;
getFileFromServer(_photo).then((file) {
if (file != null) {
_onFinalImageLoaded(
Image.file(
file,
gaplessPlayback: true,
).image,
_onFileLoaded(
file,
);
} else {
_loadingFinalImage = false;
@@ -239,7 +242,9 @@ class _ZoomableImageState extends State<ZoomableImage> {
_isGIF(), // since on iOS GIFs playback only when origin-files are loaded
).then((file) {
if (file != null && file.existsSync()) {
_onFinalImageLoaded(Image.file(file).image);
_onFileLoaded(
file,
);
} else {
_logger.info("File was deleted " + _photo.toString());
if (_photo.uploadedFileID != null) {
@@ -277,24 +282,45 @@ class _ZoomableImageState extends State<ZoomableImage> {
}
}
void _onFinalImageLoaded(ImageProvider imageProvider) {
void _onFileLoaded(File file) {
final imageProvider = Image.file(
file,
gaplessPlayback: true,
).image;
if (mounted) {
precacheImage(imageProvider, context).then((value) async {
if (mounted) {
await _updatePhotoViewController(
previewImageProvider: _imageProvider,
finalImageProvider: imageProvider,
);
setState(() {
_imageProvider = imageProvider;
_loadedFinalImage = true;
_logger.info("Final image loaded");
});
precacheImage(
imageProvider,
context,
onError: (exception, _) async {
_logger
.info(exception.toString() + ". Filename: ${_photo.displayName}");
if (exception.toString().contains(
"Codec failed to produce an image, possibly due to invalid image data",
)) {
unawaited(_loadInSupportedFormat(file));
}
},
).then((value) {
if (mounted && !_loadedFinalImage && !_convertToSupportedFormat) {
_updateViewWithFinalImage(imageProvider);
}
});
}
}
Future<void> _updateViewWithFinalImage(ImageProvider imageProvider) async {
await _updatePhotoViewController(
previewImageProvider: _imageProvider,
finalImageProvider: imageProvider,
);
setState(() {
_imageProvider = imageProvider;
_loadedFinalImage = true;
_logger.info("Final image loaded");
});
}
Future<void> _updatePhotoViewController({
required ImageProvider? previewImageProvider,
required ImageProvider finalImageProvider,
@@ -348,4 +374,28 @@ class _ZoomableImageState extends State<ZoomableImage> {
}
bool _isGIF() => _photo.displayName.toLowerCase().endsWith(".gif");
Future<void> _loadInSupportedFormat(File file) async {
_logger.info("Compressing ${_photo.displayName} to viewable format");
_convertToSupportedFormat = true;
final compressedFile =
await FlutterImageCompress.compressWithFile(file.path);
if (compressedFile != null) {
final imageProvider = MemoryImage(compressedFile);
unawaited(
precacheImage(imageProvider, context).then((value) {
if (mounted) {
_updateViewWithFinalImage(imageProvider);
}
}),
);
} else {
_logger.severe(
"Failed to compress image ${_photo.displayName} to viewable format",
);
}
}
}

View File

@@ -21,6 +21,7 @@ import "package:photos/ui/components/models/button_type.dart";
import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/viewer/gallery/gallery.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/photo_manager_util.dart";
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
Future<dynamic> showAddPhotosSheet(
@@ -203,7 +204,7 @@ class AddPhotosPhotoWidget extends StatelessWidget {
}
} catch (e) {
if (e is StateError) {
final PermissionState ps = await PhotoManager.requestPermissionExtend();
final PermissionState ps = await requestPhotoMangerPermissions();
if (ps != PermissionState.authorized && ps != PermissionState.limited) {
await showChoiceDialog(
context,

View File

@@ -9,6 +9,7 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import "package:permission_handler/permission_handler.dart";
import 'package:photos/core/configuration.dart';
import "package:photos/core/constants.dart";
import 'package:photos/core/errors.dart';
@@ -363,6 +364,15 @@ class FileUploader {
}
}
Future<void> verifyMediaLocationAccess() async {
if (Platform.isAndroid) {
final bool hasPermission = await Permission.accessMediaLocation.isGranted;
if (!hasPermission) {
throw NoMediaLocationAccessError();
}
}
}
Future<EnteFile> forceUpload(EnteFile file, int collectionID) async {
_hasInitiatedForceUpload = true;
return _tryToUpload(file, collectionID, true);

View File

@@ -0,0 +1,12 @@
import "package:photo_manager/photo_manager.dart";
Future<PermissionState> requestPhotoMangerPermissions() {
return PhotoManager.requestPermissionExtend(
requestOption: const PermissionRequestOption(
androidPermission: AndroidPermission(
type: RequestType.common,
mediaLocation: true,
),
),
);
}

View File

@@ -933,12 +933,11 @@ packages:
home_widget:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: "49158ce4a517e87817dc84c6b96c00639281229a"
url: "https://github.com/ente-io/FlutterHomeWidget"
source: git
version: "0.4.1"
name: home_widget
sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
html:
dependency: transitive
description:
@@ -1539,6 +1538,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8"
url: "https://pub.dev"
source: hosted
version: "11.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e
url: "https://pub.dev"
source: hosted
version: "11.1.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
url: "https://pub.dev"
source: hosted
version: "9.1.4"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
url: "https://pub.dev"
source: hosted
version: "3.12.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
url: "https://pub.dev"
source: hosted
version: "0.1.3"
petitparser:
dependency: transitive
description:

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.72+592
version: 0.8.74+594
publish_to: none
environment:
@@ -91,10 +91,7 @@ dependencies:
fluttertoast: ^8.0.6
freezed_annotation: ^2.2.0
google_nav_bar: ^5.0.5
home_widget:
git:
url: https://github.com/ente-io/FlutterHomeWidget
ref: main
home_widget: ^0.5.0
html_unescape: ^2.0.0
http: ^1.1.0
image: ^4.0.17
@@ -133,6 +130,7 @@ dependencies:
path: #dart
path_provider: ^2.1.1
pedantic: ^1.9.2
permission_handler: ^11.0.1
photo_manager: ^2.8.1
photo_view: ^0.14.0
pinput: ^1.2.2

View File

@@ -37,7 +37,6 @@ import (
embeddingCtrl "github.com/ente-io/museum/pkg/controller/embedding"
"github.com/ente-io/museum/pkg/controller/family"
kexCtrl "github.com/ente-io/museum/pkg/controller/kex"
"github.com/ente-io/museum/pkg/controller/locationtag"
"github.com/ente-io/museum/pkg/controller/lock"
remoteStoreCtrl "github.com/ente-io/museum/pkg/controller/remotestore"
"github.com/ente-io/museum/pkg/controller/storagebonus"
@@ -50,7 +49,6 @@ import (
"github.com/ente-io/museum/pkg/repo/datacleanup"
"github.com/ente-io/museum/pkg/repo/embedding"
"github.com/ente-io/museum/pkg/repo/kex"
locationtagRepo "github.com/ente-io/museum/pkg/repo/locationtag"
"github.com/ente-io/museum/pkg/repo/passkey"
"github.com/ente-io/museum/pkg/repo/remotestore"
storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus"
@@ -150,7 +148,6 @@ func main() {
twoFactorRecoveryRepo := &two_factor_recovery.Repository{Db: db, SecretEncryptionKey: secretEncryptionKeyBytes}
billingRepo := &repo.BillingRepository{DB: db}
userEntityRepo := &userEntityRepo.Repository{DB: db}
locationTagRepository := &locationtagRepo.Repository{DB: db}
authRepo := &authenticatorRepo.Repository{DB: db}
remoteStoreRepository := &remotestore.Repository{DB: db}
dataCleanupRepository := &datacleanup.Repository{DB: db}
@@ -641,13 +638,6 @@ func main() {
privateAPI.DELETE("/user-entity/entity", userEntityHandler.DeleteEntity)
privateAPI.GET("/user-entity/entity/diff", userEntityHandler.GetDiff)
locationTagController := &locationtag.Controller{Repo: locationTagRepository}
locationTagHandler := &api.LocationTagHandler{Controller: locationTagController}
privateAPI.POST("/locationtag/create", locationTagHandler.Create)
privateAPI.POST("/locationtag/update", locationTagHandler.Update)
privateAPI.DELETE("/locationtag/delete", locationTagHandler.Delete)
privateAPI.GET("/locationtag/diff", locationTagHandler.GetDiff)
authenticatorController := &authenticatorCtrl.Controller{Repo: authRepo}
authenticatorHandler := &api.AuthenticatorHandler{Controller: authenticatorController}

View File

@@ -1,59 +0,0 @@
package ente
import (
"database/sql/driver"
"encoding/json"
"github.com/ente-io/stacktrace"
"github.com/google/uuid"
)
// LocationTag represents a location tag in the system. The location information
// is stored in an encrypted as Attributes
type LocationTag struct {
ID uuid.UUID `json:"id"`
OwnerID int64 `json:"ownerId,omitempty"`
EncryptedKey string `json:"encryptedKey" binding:"required"`
KeyDecryptionNonce string `json:"keyDecryptionNonce" binding:"required"`
Attributes LocationTagAttribute `json:"attributes" binding:"required"`
IsDeleted bool `json:"isDeleted"`
Provider string `json:"provider,omitempty"`
CreatedAt int64 `json:"createdAt,omitempty"` // utc epoch microseconds
UpdatedAt int64 `json:"updatedAt,omitempty"` // utc epoch microseconds
}
// LocationTagAttribute holds encrypted data about user's location tag.
type LocationTagAttribute struct {
Version int `json:"version,omitempty" binding:"required"`
EncryptedData string `json:"encryptedData,omitempty" binding:"required"`
DecryptionNonce string `json:"decryptionNonce,omitempty" binding:"required"`
}
// Value implements the driver.Valuer interface. This method
// simply returns the JSON-encoded representation of the struct.
func (la LocationTagAttribute) Value() (driver.Value, error) {
return json.Marshal(la)
}
// Scan implements the sql.Scanner interface. This method
// simply decodes a JSON-encoded value into the struct fields.
func (la *LocationTagAttribute) Scan(value interface{}) error {
b, ok := value.([]byte)
if !ok {
return stacktrace.NewError("type assertion to []byte failed")
}
return json.Unmarshal(b, &la)
}
// DeleteLocationTagRequest is request structure for deleting a location tag
type DeleteLocationTagRequest struct {
ID uuid.UUID `json:"id" binding:"required"`
OwnerID int64 // should be populated from req headers
}
// GetLocationTagDiffRequest is request struct for fetching locationTag changes
type GetLocationTagDiffRequest struct {
// SinceTime *int64. Pointer allows us to pass 0 value otherwise binding fails for zero Value.
SinceTime *int64 `form:"sinceTime" binding:"required"`
Limit int16 `form:"limit" binding:"required"`
OwnerID int64 // should be populated from req headers
}

View File

@@ -8,6 +8,7 @@ type EntityType string
const (
Location EntityType = "location"
Person EntityType = "person"
)
type EntityKey struct {

View File

@@ -0,0 +1 @@
-- no-op

View File

@@ -0,0 +1,2 @@
DROP TRIGGER IF EXISTS update_location_tag_updated_at ON location_tag;
DROP TABLE location_tag;

View File

@@ -1,88 +0,0 @@
package api
import (
"fmt"
"net/http"
"github.com/ente-io/museum/ente"
"github.com/ente-io/museum/pkg/controller/locationtag"
"github.com/ente-io/museum/pkg/utils/auth"
"github.com/ente-io/museum/pkg/utils/handler"
"github.com/ente-io/stacktrace"
"github.com/gin-gonic/gin"
)
// LocationTagHandler expose request handlers to all location tag requests
type LocationTagHandler struct {
Controller *locationtag.Controller
}
// Create handler for creating a new location tag
func (h *LocationTagHandler) Create(c *gin.Context) {
var request ente.LocationTag
if err := c.ShouldBindJSON(&request); err != nil {
handler.Error(c,
stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err)))
return
}
request.OwnerID = auth.GetUserID(c.Request.Header)
resp, err := h.Controller.Create(c, request)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, "Failed to create locationTag"))
return
}
c.JSON(http.StatusOK, resp)
}
// Update handler for updating location tag
func (h *LocationTagHandler) Update(c *gin.Context) {
var request ente.LocationTag
if err := c.ShouldBindJSON(&request); err != nil {
handler.Error(c,
stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err)))
return
}
request.OwnerID = auth.GetUserID(c.Request.Header)
resp, err := h.Controller.Update(c, request)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, "Failed to update locationTag"))
return
}
c.JSON(http.StatusOK, gin.H{"locationTag": resp})
}
// Delete handler for deleting location tag
func (h *LocationTagHandler) Delete(c *gin.Context) {
var request ente.DeleteLocationTagRequest
if err := c.ShouldBindJSON(&request); err != nil {
handler.Error(c,
stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err)))
return
}
request.OwnerID = auth.GetUserID(c.Request.Header)
_, err := h.Controller.Delete(c, request)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, "Failed to delete locationTag"))
return
}
c.Status(http.StatusOK)
}
// GetDiff handler for fetching diff of location tag changes
func (h *LocationTagHandler) GetDiff(c *gin.Context) {
var request ente.GetLocationTagDiffRequest
if err := c.ShouldBindQuery(&request); err != nil {
handler.Error(c,
stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err)))
return
}
request.OwnerID = auth.GetUserID(c.Request.Header)
locationTags, err := h.Controller.GetDiff(c, request)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, "Failed to fetch locationTag diff"))
return
}
c.JSON(http.StatusOK, gin.H{
"diff": locationTags,
})
}

View File

@@ -488,6 +488,15 @@ func (c *CollectionController) GetDiffV2(ctx *gin.Context, cID int64, userID int
if diff[idx].OwnerID != userID {
diff[idx].MagicMetadata = nil
}
if diff[idx].Metadata.EncryptedData == "-" && !diff[idx].IsDeleted {
// This indicates that the file is deleted, but we still have a stale entry in the collection
log.WithFields(log.Fields{
"file_id": diff[idx].ID,
"collection_id": cID,
"updated_at": diff[idx].UpdationTime,
}).Warning("stale collection_file found")
diff[idx].IsDeleted = true
}
}
return diff, hasMore, nil
}

View File

@@ -1,31 +0,0 @@
package locationtag
import (
"github.com/ente-io/museum/ente"
"github.com/ente-io/museum/pkg/repo/locationtag"
"github.com/gin-gonic/gin"
)
// Controller is interface for exposing business logic related to location tags
type Controller struct {
Repo *locationtag.Repository
}
// Create a new location tag in the system
func (c *Controller) Create(ctx *gin.Context, req ente.LocationTag) (ente.LocationTag, error) {
return c.Repo.Create(ctx, req)
}
func (c *Controller) Update(ctx *gin.Context, req ente.LocationTag) (ente.LocationTag, error) {
// todo: verify ownership before updating
panic("implement me")
}
// Delete the location tag for the given id and ownerId
func (c *Controller) Delete(ctx *gin.Context, req ente.DeleteLocationTagRequest) (bool, error) {
return c.Repo.Delete(ctx, req.ID.String(), req.OwnerID)
}
// GetDiff fetches the locationTags which have changed after the specified time
func (c *Controller) GetDiff(ctx *gin.Context, req ente.GetLocationTagDiffRequest) ([]ente.LocationTag, error) {
return c.Repo.GetDiff(ctx, req.OwnerID, *req.SinceTime, req.Limit)
}

View File

@@ -1,89 +0,0 @@
package locationtag
import (
"context"
"database/sql"
"fmt"
"github.com/ente-io/museum/ente"
"github.com/ente-io/stacktrace"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
// Repository defines the methods for inserting, updating and retrieving
// locationTag related entities from the underlying repository
type Repository struct {
DB *sql.DB
}
// Create inserts a new &{ente.LocationTag} entry
func (r *Repository) Create(ctx context.Context, locationTag ente.LocationTag) (ente.LocationTag, error) {
err := r.DB.QueryRow(`INSERT into location_tag(
id,
user_id,
encrypted_key,
key_decryption_nonce,
attributes) VALUES ($1,$2,$3,$4,$5) RETURNING id,created_at,updated_at`,
uuid.New(), //$1 id
locationTag.OwnerID, // $2 user_id
locationTag.EncryptedKey, // $3 encrypted_key
locationTag.KeyDecryptionNonce, // $4 key_decryption_nonce
locationTag.Attributes). // %5 attributes
Scan(&locationTag.ID, &locationTag.CreatedAt, &locationTag.UpdatedAt)
if err != nil {
return ente.LocationTag{}, stacktrace.Propagate(err, "Failed to create locationTag")
}
return locationTag, nil
}
// GetDiff returns the &{[]ente.LocationTag} which have been added or
// modified after the given sinceTime
func (r *Repository) GetDiff(ctx context.Context, ownerID int64, sinceTime int64, limit int16) ([]ente.LocationTag, error) {
rows, err := r.DB.Query(`SELECT
id, user_id, provider, encrypted_key, key_decryption_nonce,
attributes, is_deleted, created_at, updated_at
FROM location_tag
WHERE user_id = $1
and updated_at > $2
ORDER BY updated_at
LIMIT $3`,
ownerID, // $1
sinceTime, // %2
limit, // $3
)
if err != nil {
return nil, stacktrace.Propagate(err, "GetDiff query failed")
}
return convertRowsToLocationTags(rows)
}
func (r *Repository) Delete(ctx context.Context, id string, ownerID int64) (bool, error) {
_, err := r.DB.ExecContext(ctx,
`UPDATE location_tag SET is_deleted=$1, attributes=$2 where id=$3 and user_id = $4`,
true, `{}`, // $1 is_deleted, $2 attr
id, ownerID) // $3 tagId, $4 ownerID
if err != nil {
return false, stacktrace.Propagate(err, fmt.Sprintf("faield to delele tag with id=%s", id))
}
return true, nil
}
func convertRowsToLocationTags(rows *sql.Rows) ([]ente.LocationTag, error) {
defer func() {
if err := rows.Close(); err != nil {
logrus.Error(err)
}
}()
locationTags := make([]ente.LocationTag, 0)
for rows.Next() {
tag := ente.LocationTag{}
err := rows.Scan(
&tag.ID, &tag.OwnerID, &tag.Provider, &tag.EncryptedKey, &tag.KeyDecryptionNonce,
&tag.Attributes, &tag.IsDeleted, &tag.CreatedAt, &tag.UpdatedAt)
if err != nil {
return nil, stacktrace.Propagate(err, "Failed to convert rowToLocationTag")
}
locationTags = append(locationTags, tag)
}
return locationTags, nil
}

View File

@@ -32,7 +32,7 @@ yarn dev
That's it. The web app will automatically hot reload when you make changes.
If you're new to web development and unsure about how to get started, or are
If you're new to web development and unsure about how to get started, or are
facing some problems when running the above steps, see [docs/new](docs/new.md).
## Other apps

View File

@@ -1,7 +1,5 @@
import { setupI18n } from "@/ui/i18n";
import { CacheProvider } from "@emotion/react";
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
import { EnteAppProps } from "@ente/shared/apps/types";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import {
@@ -15,9 +13,9 @@ 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 createEmotionCache from "@ente/shared/themes/createEmotionCache";
import { CssBaseline, useMediaQuery } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import { AppProps } from "next/app";
import Head from "next/head";
import { useRouter } from "next/router";
import { createContext, useEffect, useState } from "react";
@@ -31,10 +29,7 @@ interface AppContextProps {
export const AppContext = createContext<AppContextProps>({} as AppContextProps);
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
export default function App(props: EnteAppProps) {
export default function App(props: AppProps) {
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [showNavbar, setShowNavBar] = useState(false);
@@ -54,11 +49,7 @@ export default function App(props: EnteAppProps) {
const router = useRouter();
const {
Component,
emotionCache = clientSideEmotionCache,
pageProps,
} = props;
const { Component, pageProps } = props;
const [themeColor] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK);
@@ -87,7 +78,7 @@ export default function App(props: EnteAppProps) {
// TODO: Localise APP_TITLES
return (
<CacheProvider value={emotionCache}>
<>
<Head>
<title>{APP_TITLES.get(APPS.ACCOUNTS)}</title>
<meta
@@ -128,9 +119,9 @@ export default function App(props: EnteAppProps) {
</Overlay>
)}
{showNavbar && <AppNavbar isMobile={isMobile} />}
<Component {...pageProps} />
{isI18nReady && <Component {...pageProps} />}
</AppContext.Provider>
</ThemeProvider>
</CacheProvider>
</>
);
}

View File

@@ -1,7 +1,3 @@
import DocumentPage, {
EnteDocumentProps,
} from "@ente/shared/next/pages/_document";
import DocumentPage from "@ente/shared/next/pages/_document";
export default function Document(props: EnteDocumentProps) {
return <DocumentPage {...props} />;
}
export default DocumentPage;

View File

@@ -150,21 +150,6 @@ body {
background-color: #51cd7c;
}
.carousel-inner {
padding-bottom: 50px !important;
}
.carousel-indicators li {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 12px;
}
.carousel-indicators .active {
background-color: #51cd7c;
}
div.otp-input input {
width: 36px !important;
height: 36px;

View File

@@ -1,7 +1,9 @@
import AppNavbar from "@ente/shared/components/Navbar/app";
import { t } from "i18next";
import { createContext, useEffect, useRef, useState } from "react";
import { setupI18n } from "@/ui/i18n";
import {
APPS,
APP_TITLES,
CLIENT_PACKAGE_NAMES,
} from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import {
@@ -10,32 +12,26 @@ import {
} 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 { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import {
clearLogsIfLocalStorageLimitExceeded,
logStartupMessage,
} from "@ente/shared/logging/web";
import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS } from "@ente/shared/storage/localStorage";
import { CssBaseline, useMediaQuery } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import Head from "next/head";
import { useRouter } from "next/router";
import LoadingBar from "react-top-loading-bar";
import { setupI18n } from "@/ui/i18n";
import { CacheProvider } from "@emotion/react";
import {
APP_TITLES,
APPS,
CLIENT_PACKAGE_NAMES,
} from "@ente/shared/apps/constants";
import { EnteAppProps } from "@ente/shared/apps/types";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import createEmotionCache from "@ente/shared/themes/createEmotionCache";
import { SetTheme } from "@ente/shared/themes/types";
import { CssBaseline, useMediaQuery } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import { t } from "i18next";
import { AppProps } from "next/app";
import Head from "next/head";
import { useRouter } from "next/router";
import { createContext, useEffect, useRef, useState } from "react";
import LoadingBar from "react-top-loading-bar";
import "../../public/css/global.css";
type AppContextType = {
@@ -51,15 +47,8 @@ type AppContextType = {
export const AppContext = createContext<AppContextType>(null);
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
export default function App(props: EnteAppProps) {
const {
Component,
emotionCache = clientSideEmotionCache,
pageProps,
} = props;
export default function App(props: AppProps) {
const { Component, pageProps } = props;
const router = useRouter();
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
@@ -141,7 +130,7 @@ export default function App(props: EnteAppProps) {
});
return (
<CacheProvider value={emotionCache}>
<>
<Head>
<title>
{isI18nReady
@@ -195,9 +184,11 @@ export default function App(props: EnteAppProps) {
<EnteSpinner />
</Overlay>
)}
<Component setLoading={setLoading} {...pageProps} />
{isI18nReady && (
<Component setLoading={setLoading} {...pageProps} />
)}
</AppContext.Provider>
</ThemeProvider>
</CacheProvider>
</>
);
}

View File

@@ -1,7 +1,3 @@
import DocumentPage, {
EnteDocumentProps,
} from "@ente/shared/next/pages/_document";
import DocumentPage from "@ente/shared/next/pages/_document";
export default function Document(props: EnteDocumentProps) {
return <DocumentPage {...props} />;
}
export default DocumentPage;

View File

@@ -1,3 +1,5 @@
import { styled } from "@mui/material";
const colourPool = [
"#87CEFA", // Light Blue
"#90EE90", // Light Green
@@ -23,44 +25,41 @@ const colourPool = [
export default function LargeType({ chars }: { chars: string[] }) {
return (
<table
style={{
fontSize: "4rem",
fontWeight: "bold",
fontFamily: "monospace",
display: "flex",
position: "relative",
}}
>
<Container style={{}}>
{chars.map((char, i) => (
<tr
<span
key={i}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: "0.5rem",
// alternating background
backgroundColor: i % 2 === 0 ? "#2e2e2e" : "#5e5e5e",
// varying colors
color: colourPool[i % colourPool.length],
}}
>
<span
style={{
color: colourPool[i % colourPool.length],
lineHeight: 1.2,
}}
>
{char}
</span>
<span
style={{
fontSize: "1rem",
}}
>
{i + 1}
</span>
</tr>
{char}
</span>
))}
</table>
</Container>
);
}
const Container = styled("div")`
font-size: 4rem;
font-weight: bold;
font-family: monospace;
line-height: 1.2;
/*
* - We want them to be spans so that when the text is copy pasted, there
* is no extra whitespace inserted.
*
* - But we also want them to have a block level padding.
*
* To achieve both these goals, make them inline-blocks
*/
span {
display: inline-block;
padding: 0.5rem;
}
`;

View File

@@ -0,0 +1,95 @@
import { SlideshowContext } from "pages/slideshow";
import { useContext, useEffect, useState } from "react";
export default function PhotoAuditorium({
url,
nextSlideUrl,
}: {
url: string;
nextSlideUrl: string;
}) {
const { showNextSlide } = useContext(SlideshowContext);
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
useEffect(() => {
let timeout: NodeJS.Timeout;
let timeout2: NodeJS.Timeout;
if (nextSlidePrerendered) {
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
const delayTime = Math.max(10000 - elapsedTime, 0);
if (elapsedTime >= 10000) {
setShowPreloadedNextSlide(true);
} else {
timeout = setTimeout(() => {
setShowPreloadedNextSlide(true);
}, delayTime);
}
if (showNextSlide) {
timeout2 = setTimeout(() => {
showNextSlide();
setNextSlidePrerendered(false);
setPrerenderTime(null);
setShowPreloadedNextSlide(false);
}, delayTime);
}
}
return () => {
if (timeout) clearTimeout(timeout);
if (timeout2) clearTimeout(timeout2);
};
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
return (
<div
style={{
width: "100vw",
height: "100vh",
backgroundImage: `url(${url})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundBlendMode: "multiply",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
>
<div
style={{
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backdropFilter: "blur(10px)",
}}
>
<img
src={url}
style={{
maxWidth: "100%",
maxHeight: "100%",
display: showPreloadedNextSlide ? "none" : "block",
}}
/>
<img
src={nextSlideUrl}
style={{
maxWidth: "100%",
maxHeight: "100%",
display: showPreloadedNextSlide ? "block" : "none",
}}
onLoad={() => {
setNextSlidePrerendered(true);
setPrerenderTime(Date.now());
}}
/>
</div>
</div>
);
}

View File

@@ -20,9 +20,9 @@ export default function PhotoAuditorium({
if (nextSlidePrerendered) {
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
const delayTime = Math.max(5000 - elapsedTime, 0);
const delayTime = Math.max(10000 - elapsedTime, 0);
if (elapsedTime >= 5000) {
if (elapsedTime >= 10000) {
setShowPreloadedNextSlide(true);
} else {
timeout = setTimeout(() => {

View File

@@ -1,30 +0,0 @@
import { useEffect, useState } from "react";
export default function TimerBar({ percentage }: { percentage: number }) {
const okColor = "#75C157";
const warningColor = "#FFC000";
const lateColor = "#FF0000";
const [backgroundColor, setBackgroundColor] = useState(okColor);
useEffect(() => {
if (percentage >= 40) {
setBackgroundColor(okColor);
} else if (percentage >= 20) {
setBackgroundColor(warningColor);
} else {
setBackgroundColor(lateColor);
}
}, [percentage]);
return (
<div
style={{
width: `${percentage}%`, // Set the width based on the time left
height: "10px", // Same as the border thickness
backgroundColor, // The color of the moving border
transition: "width 1s linear", // Smooth transition for the width change
}}
/>
);
}

View File

@@ -1,56 +0,0 @@
import { getAlbumsURL } from "@ente/shared/network/api";
import { runningInBrowser } from "@ente/shared/platform";
import { PAGES } from "constants/pages";
export enum APPS {
PHOTOS = "PHOTOS",
AUTH = "AUTH",
ALBUMS = "ALBUMS",
}
export const ALLOWED_APP_PAGES = new Map([
[APPS.ALBUMS, [PAGES.SHARED_ALBUMS, PAGES.ROOT]],
[
APPS.AUTH,
[
PAGES.ROOT,
PAGES.LOGIN,
PAGES.SIGNUP,
PAGES.VERIFY,
PAGES.CREDENTIALS,
PAGES.RECOVER,
PAGES.CHANGE_PASSWORD,
PAGES.GENERATE,
PAGES.AUTH,
PAGES.TWO_FACTOR_VERIFY,
PAGES.TWO_FACTOR_RECOVER,
],
],
]);
export const CLIENT_PACKAGE_NAMES = new Map([
[APPS.ALBUMS, "io.ente.albums.web"],
[APPS.PHOTOS, "io.ente.photos.web"],
[APPS.AUTH, "io.ente.auth.web"],
]);
export const getAppNameAndTitle = () => {
if (!runningInBrowser()) {
return {};
}
const currentURL = new URL(window.location.href);
const albumsURL = new URL(getAlbumsURL());
if (currentURL.origin === albumsURL.origin) {
return { name: APPS.ALBUMS, title: "ente Photos" };
} else {
return { name: APPS.PHOTOS, title: "ente Photos" };
}
};
export const getAppTitle = () => {
return getAppNameAndTitle().title;
};
export const getAppName = () => {
return getAppNameAndTitle().name;
};

View File

@@ -1,5 +0,0 @@
export enum CACHES {
THUMBS = "thumbs",
FACE_CROPS = "face-crops",
FILES = "files",
}

View File

@@ -1,14 +1,3 @@
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
export const MAX_EDITED_CREATION_TIME = new Date();
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
export const MAX_CAPTION_SIZE = 5000;
export const TYPE_HEIC = "heic";
export const TYPE_HEIF = "heif";
export const TYPE_JPEG = "jpeg";
export const TYPE_JPG = "jpg";
export enum FILE_TYPE {
IMAGE,
VIDEO,
@@ -29,15 +18,3 @@ export const RAW_FORMATS = [
"dng",
"tif",
];
export const SUPPORTED_RAW_FORMATS = [
"heic",
"rw2",
"tiff",
"arw",
"cr3",
"cr2",
"nef",
"psd",
"dng",
"tif",
];

View File

@@ -1,15 +0,0 @@
export const GAP_BTW_TILES = 4;
export const DATE_CONTAINER_HEIGHT = 48;
export const SIZE_AND_COUNT_CONTAINER_HEIGHT = 72;
export const IMAGE_CONTAINER_MAX_HEIGHT = 180;
export const IMAGE_CONTAINER_MAX_WIDTH = 180;
export const MIN_COLUMNS = 4;
export const SPACE_BTW_DATES = 44;
export const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
export enum PLAN_PERIOD {
MONTH = "month",
YEAR = "year",
}
export const SYNC_INTERVAL_IN_MICROSECONDS = 1000 * 60 * 5; // 5 minutes

View File

@@ -1,20 +0,0 @@
export enum PAGES {
CHANGE_EMAIL = "/change-email",
CHANGE_PASSWORD = "/change-password",
CREDENTIALS = "/credentials",
GALLERY = "/gallery",
GENERATE = "/generate",
LOGIN = "/login",
RECOVER = "/recover",
SIGNUP = "/signup",
TWO_FACTOR_SETUP = "/two-factor/setup",
TWO_FACTOR_VERIFY = "/two-factor/verify",
TWO_FACTOR_RECOVER = "/two-factor/recover",
VERIFY = "/verify",
ROOT = "/",
SHARED_ALBUMS = "/shared-albums",
// ML_DEBUG = '/ml-debug',
DEDUPLICATE = "/deduplicate",
// AUTH page is used to show (auth)enticator codes
AUTH = "/auth",
}

View File

@@ -1,11 +1,5 @@
import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants";
import { FILE_TYPE } from "constants/file";
import {
FileTypeInfo,
ImportSuggestion,
Location,
ParsedExtractedMetadata,
} from "types/upload";
import { FileTypeInfo } from "types/upload";
// list of format that were missed by type-detection for some files.
export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
@@ -45,98 +39,3 @@ export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
];
export const KNOWN_NON_MEDIA_FORMATS = ["xmp", "html", "txt"];
export const EXIFLESS_FORMATS = ["gif", "bmp"];
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
export const MULTIPART_PART_SIZE = 20 * 1024 * 1024;
export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE;
export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor(
MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE,
);
export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random();
export const NULL_LOCATION: Location = { latitude: null, longitude: null };
export enum UPLOAD_STAGES {
START,
READING_GOOGLE_METADATA_FILES,
EXTRACTING_METADATA,
UPLOADING,
CANCELLING,
FINISH,
}
export enum UPLOAD_STRATEGY {
SINGLE_COLLECTION,
COLLECTION_PER_FOLDER,
}
export enum UPLOAD_RESULT {
FAILED,
ALREADY_UPLOADED,
UNSUPPORTED,
BLOCKED,
TOO_LARGE,
LARGER_THAN_AVAILABLE_STORAGE,
UPLOADED,
UPLOADED_WITH_STATIC_THUMBNAIL,
ADDED_SYMLINK,
}
export enum PICKED_UPLOAD_TYPE {
FILES = "files",
FOLDERS = "folders",
ZIPS = "zips",
}
export const MAX_FILE_SIZE_SUPPORTED = 4 * 1024 * 1024 * 1024; // 4 GB
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB
export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
location: NULL_LOCATION,
creationTime: null,
width: null,
height: null,
};
export const A_SEC_IN_MICROSECONDS = 1e6;
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
rootFolderName: "",
hasNestedFolders: false,
hasRootLevelFileWithFolder: false,
};
export const BLACK_THUMBNAIL_BASE64 =
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB" +
"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ" +
"EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC" +
"ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF" +
"BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk" +
"6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL" +
"W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA" +
"AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY" +
"nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK" +
"kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD" +
"AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC" +
"gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" +
"KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK" +
"ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" +
"KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=";

View File

@@ -1,19 +0,0 @@
export const ENTE_WEBSITE_LINK = "https://ente.io";
export const ML_BLOG_LINK = "https://ente.io/blog/desktop-ml-beta";
export const FACE_SEARCH_PRIVACY_POLICY_LINK =
"https://ente.io/privacy#8-biometric-information-privacy-policy";
export const SUPPORT_EMAIL = "support@ente.io";
export const APP_DOWNLOAD_URL = "https://ente.io/download/desktop";
export const FEEDBACK_EMAIL = "feedback@ente.io";
export const DELETE_ACCOUNT_EMAIL = "account-deletion@ente.io";
export const WEB_ROADMAP_URL = "https://github.com/ente-io/ente/discussions";
export const DESKTOP_ROADMAP_URL =
"https://github.com/ente-io/ente/discussions";

View File

@@ -59,21 +59,12 @@ export default function Slideshow() {
}
};
const init = async () => {
try {
const castToken = window.localStorage.getItem("castToken");
setCastToken(castToken);
} catch (e) {
logError(e, "error during sync");
router.push("/");
}
};
useEffect(() => {
if (castToken) {
const intervalId = setInterval(() => {
syncCastFiles(castToken);
}, 5000);
}, 10000);
syncCastFiles(castToken);
return () => clearInterval(intervalId);
}
@@ -105,7 +96,20 @@ export default function Slideshow() {
const router = useRouter();
useEffect(() => {
init();
try {
const castToken = window.localStorage.getItem("castToken");
// Wait 2 seconds to ensure the green tick and the confirmation
// message remains visible for at least 2 seconds before we start
// the slideshow.
const timeoutId = setTimeout(() => {
setCastToken(castToken);
}, 2000);
return () => clearTimeout(timeoutId);
} catch (e) {
logError(e, "error during sync");
router.push("/");
}
}, []);
useEffect(() => {

View File

@@ -1,31 +0,0 @@
export enum MS_KEYS {
SRP_CONFIGURE_IN_PROGRESS = "srpConfigureInProgress",
REDIRECT_URL = "redirectUrl",
}
type StoreType = Map<Partial<MS_KEYS>, any>;
class InMemoryStore {
private store: StoreType = new Map();
get(key: MS_KEYS) {
return this.store.get(key);
}
set(key: MS_KEYS, value: any) {
this.store.set(key, value);
}
delete(key: MS_KEYS) {
this.store.delete(key);
}
has(key: MS_KEYS) {
return this.store.has(key);
}
clear() {
this.store.clear();
}
}
export default new InMemoryStore();

View File

@@ -1,25 +0,0 @@
import { LimitedCacheStorage } from "types/cache/index";
class cacheStorageFactory {
getCacheStorage(): LimitedCacheStorage {
return transformBrowserCacheStorageToLimitedCacheStorage(caches);
}
}
export const CacheStorageFactory = new cacheStorageFactory();
function transformBrowserCacheStorageToLimitedCacheStorage(
caches: CacheStorage,
): LimitedCacheStorage {
return {
async open(cacheName) {
const cache = await caches.open(cacheName);
return {
match: cache.match.bind(cache),
put: cache.put.bind(cache),
delete: cache.delete.bind(cache),
};
},
delete: caches.delete.bind(caches),
};
}

View File

@@ -1,33 +0,0 @@
import { logError } from "@ente/shared/sentry";
import { CacheStorageFactory } from "./cacheStorageFactory";
const SecurityError = "SecurityError";
const INSECURE_OPERATION = "The operation is insecure.";
async function openCache(cacheName: string) {
try {
return await CacheStorageFactory.getCacheStorage().open(cacheName);
} catch (e) {
// ignoring insecure operation error, as it is thrown in incognito mode in firefox
if (e.name === SecurityError && e.message === INSECURE_OPERATION) {
// no-op
} else {
// log and ignore, we don't want to break the caller flow, when cache is not available
logError(e, "openCache failed");
}
}
}
async function deleteCache(cacheName: string) {
try {
return await CacheStorageFactory.getCacheStorage().delete(cacheName);
} catch (e) {
// ignoring insecure operation error, as it is thrown in incognito mode in firefox
if (e.name === SecurityError && e.message === INSECURE_OPERATION) {
// no-op
} else {
// log and ignore, we don't want to break the caller flow, when cache is not available
logError(e, "deleteCache failed");
}
}
}
export const CacheStorageService = { open: openCache, delete: deleteCache };

View File

@@ -1,162 +1,14 @@
import { EnteFile } from "types/file";
import {
createTypedObjectURL,
generateStreamFromArrayBuffer,
getRenderableFileURL,
} from "utils/file";
import { CustomError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
import { getCastFileURL, getCastThumbnailURL } from "@ente/shared/network/api";
import { logError } from "@ente/shared/sentry";
import { CACHES } from "constants/cache";
import { getCastFileURL } from "@ente/shared/network/api";
import { FILE_TYPE } from "constants/file";
import { LimitedCache } from "types/cache";
import { EnteFile } from "types/file";
import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker";
import { CacheStorageService } from "./cache/cacheStorageService";
import { generateStreamFromArrayBuffer } from "utils/file";
class CastDownloadManager {
private fileObjectURLPromise = new Map<
string,
Promise<{ original: string[]; converted: string[] }>
>();
private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
private fileDownloadProgress = new Map<number, number>();
private progressUpdater: (value: Map<number, number>) => void;
setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) {
this.progressUpdater = progressUpdater;
}
private async getThumbnailCache() {
try {
const thumbnailCache = await CacheStorageService.open(
CACHES.THUMBS,
);
return thumbnailCache;
} catch (e) {
return null;
// ignore
}
}
public async getCachedThumbnail(
file: EnteFile,
thumbnailCache?: LimitedCache,
) {
try {
if (!thumbnailCache) {
thumbnailCache = await this.getThumbnailCache();
}
const cacheResp: Response = await thumbnailCache?.match(
file.id.toString(),
);
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
return null;
} catch (e) {
logError(e, "failed to get cached thumbnail");
throw e;
}
}
public async getThumbnail(file: EnteFile, castToken: string) {
try {
if (!this.thumbnailObjectURLPromise.has(file.id)) {
const downloadPromise = async () => {
const thumbnailCache = await this.getThumbnailCache();
const cachedThumb = await this.getCachedThumbnail(
file,
thumbnailCache,
);
if (cachedThumb) {
return cachedThumb;
}
const thumb = await this.downloadThumb(castToken, file);
const thumbBlob = new Blob([thumb]);
try {
await thumbnailCache?.put(
file.id.toString(),
new Response(thumbBlob),
);
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(thumbBlob);
};
this.thumbnailObjectURLPromise.set(file.id, downloadPromise());
}
return await this.thumbnailObjectURLPromise.get(file.id);
} catch (e) {
this.thumbnailObjectURLPromise.delete(file.id);
logError(e, "get castDownloadManager preview Failed");
throw e;
}
}
private downloadThumb = async (castToken: string, file: EnteFile) => {
const resp = await HTTPService.get(
getCastThumbnailURL(file.id),
null,
{
"X-Cast-Access-Token": castToken,
},
{ responseType: "arraybuffer" },
);
if (typeof resp.data === "undefined") {
throw Error(CustomError.REQUEST_FAILED);
}
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const decrypted = await cryptoWorker.decryptThumbnail(
new Uint8Array(resp.data),
await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
file.key,
);
return decrypted;
};
getFile = async (file: EnteFile, castToken: string, forPreview = false) => {
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
try {
const getFilePromise = async () => {
const fileStream = await this.downloadFile(castToken, file);
const fileBlob = await new Response(fileStream).blob();
if (forPreview) {
return await getRenderableFileURL(file, fileBlob);
} else {
const fileURL = await createTypedObjectURL(
fileBlob,
file.metadata.title,
);
return { converted: [fileURL], original: [fileURL] };
}
};
if (!this.fileObjectURLPromise.get(fileKey)) {
this.fileObjectURLPromise.set(fileKey, getFilePromise());
}
const fileURLs = await this.fileObjectURLPromise.get(fileKey);
return fileURLs;
} catch (e) {
this.fileObjectURLPromise.delete(fileKey);
logError(e, "castDownloadManager failed to get file");
throw e;
}
};
public async getCachedOriginalFile(file: EnteFile) {
return await this.fileObjectURLPromise.get(file.id.toString());
}
async downloadFile(castToken: string, file: EnteFile) {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const onDownloadProgress = this.trackDownloadProgress(file.id);
if (
file.metadata.fileType === FILE_TYPE.IMAGE ||
@@ -187,9 +39,6 @@ class CastDownloadManager {
});
const reader = resp.body.getReader();
const contentLength = +resp.headers.get("Content-Length");
let downloadedBytes = 0;
const stream = new ReadableStream({
async start(controller) {
const decryptionHeader = await cryptoWorker.fromB64(
@@ -208,11 +57,6 @@ class CastDownloadManager {
reader.read().then(async ({ done, value }) => {
// Is there more data to read?
if (!done) {
downloadedBytes += value.byteLength;
onDownloadProgress({
loaded: downloadedBytes,
total: contentLength,
});
const buffer = new Uint8Array(
data.byteLength + value.byteLength,
);
@@ -254,20 +98,6 @@ class CastDownloadManager {
});
return stream;
}
trackDownloadProgress = (fileID: number) => {
return (event: { loaded: number; total: number }) => {
if (event.loaded === event.total) {
this.fileDownloadProgress.delete(fileID);
} else {
this.fileDownloadProgress.set(
fileID,
Math.round((event.loaded * 100) / event.total),
);
}
this.progressUpdater(new Map(this.fileDownloadProgress));
};
};
}
export default new CastDownloadManager();

View File

@@ -1,12 +0,0 @@
import { EventEmitter } from "eventemitter3";
// When registering event handlers,
// handle errors to avoid unhandled rejection or propagation to emit call
export enum Events {
LOGOUT = "logout",
FILE_UPLOADED = "fileUploaded",
LOCAL_FILES_UPDATED = "localFilesUpdated",
}
export const eventBus = new EventEmitter<Events>();

View File

@@ -1,26 +1,19 @@
// import isElectron from 'is-electron';
// import { ElectronFFmpeg } from 'services/electron/ffmpeg';
import { ElectronFile } from "types/upload";
import ComlinkFFmpegWorker from "utils/comlink/ComlinkFFmpegWorker";
export interface IFFmpeg {
run: (
cmd: string[],
inputFile: File | ElectronFile,
inputFile: File,
outputFilename: string,
dontTimeout?: boolean,
) => Promise<File | ElectronFile>;
) => Promise<File>;
}
class FFmpegFactory {
private client: IFFmpeg;
async getFFmpegClient() {
if (!this.client) {
// if (isElectron()) {
// this.client = new ElectronFFmpeg();
// } else {
this.client = await ComlinkFFmpegWorker.getInstance();
// }
}
return this.client;
}

View File

@@ -4,10 +4,9 @@ import {
INPUT_PATH_PLACEHOLDER,
OUTPUT_PATH_PLACEHOLDER,
} from "constants/ffmpeg";
import { ElectronFile } from "types/upload";
import ffmpegFactory from "./ffmpegFactory";
export async function convertToMP4(file: File | ElectronFile) {
export async function convertToMP4(file: File) {
try {
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
return await ffmpegClient.run(

View File

@@ -1,14 +0,0 @@
import { logError } from "@ente/shared/sentry";
import WasmHEICConverterService from "./wasmHeicConverter/wasmHEICConverterService";
class HeicConversionService {
async convert(heicFileData: Blob): Promise<Blob> {
try {
return await WasmHEICConverterService.convert(heicFileData);
} catch (e) {
logError(e, "failed to convert heic file");
throw e;
}
}
}
export default new HeicConversionService();

View File

@@ -30,16 +30,3 @@ export const decodeLivePhoto = async (file: EnteFile, zipBlob: Blob) => {
}
return livePhoto;
};
export const encodeLivePhoto = async (livePhoto: LivePhoto) => {
const zip = new JSZip();
zip.file(
"image" + getFileExtensionWithDot(livePhoto.imageNameTitle),
livePhoto.image,
);
zip.file(
"video" + getFileExtensionWithDot(livePhoto.videoNameTitle),
livePhoto.video,
);
return await zip.generateAsync({ type: "uint8array" });
};

View File

@@ -1,10 +1,7 @@
import { logError } from "@ente/shared/sentry";
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
import { ElectronFile } from "types/upload";
export async function getUint8ArrayView(
file: Blob | ElectronFile,
): Promise<Uint8Array> {
export async function getUint8ArrayView(file: Blob): Promise<Uint8Array> {
try {
return new Uint8Array(await file.arrayBuffer());
} catch (e) {
@@ -14,45 +11,3 @@ export async function getUint8ArrayView(
throw e;
}
}
export function getFileStream(file: File, chunkSize: number) {
const fileChunkReader = fileChunkReaderMaker(file, chunkSize);
const stream = new ReadableStream<Uint8Array>({
async pull(controller: ReadableStreamDefaultController) {
const chunk = await fileChunkReader.next();
if (chunk.done) {
controller.close();
} else {
controller.enqueue(chunk.value);
}
},
});
const chunkCount = Math.ceil(file.size / chunkSize);
return {
stream,
chunkCount,
};
}
export async function getElectronFileStream(
file: ElectronFile,
chunkSize: number,
) {
const chunkCount = Math.ceil(file.size / chunkSize);
return {
stream: await file.stream(),
chunkCount,
};
}
async function* fileChunkReaderMaker(file: File, chunkSize: number) {
let offset = 0;
while (offset < file.size) {
const blob = file.slice(offset, chunkSize + offset);
const fileChunk = await getUint8ArrayView(blob);
yield fileChunk;
offset += chunkSize;
}
return null;
}

View File

@@ -6,37 +6,25 @@ import {
KNOWN_NON_MEDIA_FORMATS,
WHITELISTED_FILE_FORMATS,
} from "constants/upload";
import FileType, { FileTypeResult } from "file-type";
import { ElectronFile, FileTypeInfo } from "types/upload";
import FileType from "file-type";
import { FileTypeInfo } from "types/upload";
import { getFileExtension } from "utils/file";
import { getUint8ArrayView } from "./readerService";
function getFileSize(file: File | ElectronFile) {
return file.size;
}
const TYPE_VIDEO = "video";
const TYPE_IMAGE = "image";
const CHUNK_SIZE_FOR_TYPE_DETECTION = 4100;
export async function getFileType(
receivedFile: File | ElectronFile,
): Promise<FileTypeInfo> {
export async function getFileType(receivedFile: File): Promise<FileTypeInfo> {
try {
let fileType: FILE_TYPE;
let typeResult: FileTypeResult;
if (receivedFile instanceof File) {
typeResult = await extractFileType(receivedFile);
} else {
typeResult = await extractElectronFileType(receivedFile);
}
const typeResult = await extractFileType(receivedFile);
const mimTypeParts: string[] = typeResult.mime?.split("/");
if (mimTypeParts?.length !== 2) {
throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime));
}
switch (mimTypeParts[0]) {
case TYPE_IMAGE:
fileType = FILE_TYPE.IMAGE;
@@ -54,7 +42,7 @@ export async function getFileType(
};
} catch (e) {
const fileFormat = getFileExtension(receivedFile.name);
const fileSize = convertBytesToHumanReadable(getFileSize(receivedFile));
const fileSize = convertBytesToHumanReadable(receivedFile.size);
const whiteListedFormat = WHITELISTED_FILE_FORMATS.find(
(a) => a.exactType === fileFormat,
);
@@ -85,14 +73,6 @@ async function extractFileType(file: File) {
return getFileTypeFromBuffer(fileDataChunk);
}
async function extractElectronFileType(file: ElectronFile) {
const stream = await file.stream();
const reader = stream.getReader();
const { value: fileDataChunk } = await reader.read();
await reader.cancel();
return getFileTypeFromBuffer(fileDataChunk);
}
async function getFileTypeFromBuffer(buffer: Uint8Array) {
const result = await FileType.fromBuffer(buffer);
if (!result?.mime) {

View File

@@ -1,13 +0,0 @@
import * as HeicConvert from "heic-convert";
import { getUint8ArrayView } from "services/readerService";
export async function convertHEIC(
fileBlob: Blob,
format: string,
): Promise<Blob> {
const filedata = await getUint8ArrayView(fileBlob);
const result = await HeicConvert({ buffer: filedata, format });
const convertedFileData = new Uint8Array(result);
const convertedFileBlob = new Blob([convertedFileData]);
return convertedFileBlob;
}

View File

@@ -1,114 +0,0 @@
import { CustomError } from "@ente/shared/error";
import { addLogLine } from "@ente/shared/logging";
import { retryAsyncFunction } from "@ente/shared/promise";
import { logError } from "@ente/shared/sentry";
import QueueProcessor from "@ente/shared/utils/queueProcessor";
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
import { getDedicatedConvertWorker } from "utils/comlink/ComlinkConvertWorker";
import { ComlinkWorker } from "utils/comlink/comlinkWorker";
import { DedicatedConvertWorker } from "worker/convert.worker";
const WORKER_POOL_SIZE = 2;
const MAX_CONVERSION_IN_PARALLEL = 1;
const WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS = [100, 100];
const WAIT_TIME_IN_MICROSECONDS = 30 * 1000;
const BREATH_TIME_IN_MICROSECONDS = 1000;
const CONVERT_FORMAT = "JPEG";
class HEICConverter {
private convertProcessor = new QueueProcessor<Blob>(
MAX_CONVERSION_IN_PARALLEL,
);
private workerPool: ComlinkWorker<typeof DedicatedConvertWorker>[] = [];
private ready: Promise<void>;
constructor() {
this.ready = this.init();
}
private async init() {
this.workerPool = [];
for (let i = 0; i < WORKER_POOL_SIZE; i++) {
this.workerPool.push(getDedicatedConvertWorker());
}
}
async convert(fileBlob: Blob): Promise<Blob> {
await this.ready;
const response = this.convertProcessor.queueUpRequest(() =>
retryAsyncFunction<Blob>(async () => {
const convertWorker = this.workerPool.shift();
const worker = await convertWorker.remote;
try {
const convertedHEIC = await new Promise<Blob>(
(resolve, reject) => {
const main = async () => {
try {
const timeout = setTimeout(() => {
reject(Error("wait time exceeded"));
}, WAIT_TIME_IN_MICROSECONDS);
const startTime = Date.now();
const convertedHEIC =
await worker.convertHEIC(
fileBlob,
CONVERT_FORMAT,
);
addLogLine(
`originalFileSize:${convertBytesToHumanReadable(
fileBlob?.size,
)},convertedFileSize:${convertBytesToHumanReadable(
convertedHEIC?.size,
)}, heic conversion time: ${
Date.now() - startTime
}ms `,
);
clearTimeout(timeout);
resolve(convertedHEIC);
} catch (e) {
reject(e);
}
};
main();
},
);
if (!convertedHEIC || convertedHEIC?.size === 0) {
logError(
Error(`converted heic fileSize is Zero`),
"converted heic fileSize is Zero",
{
originalFileSize: convertBytesToHumanReadable(
fileBlob?.size ?? 0,
),
convertedFileSize: convertBytesToHumanReadable(
convertedHEIC?.size ?? 0,
),
},
);
}
await new Promise((resolve) => {
setTimeout(
() => resolve(null),
BREATH_TIME_IN_MICROSECONDS,
);
});
this.workerPool.push(convertWorker);
return convertedHEIC;
} catch (e) {
logError(e, "heic conversion failed");
convertWorker.terminate();
this.workerPool.push(getDedicatedConvertWorker());
throw e;
}
}, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS),
);
try {
return await response.promise;
} catch (e) {
if (e.message === CustomError.REQUEST_CANCELLED) {
// ignore
return null;
}
throw e;
}
}
}
export default new HEICConverter();

View File

@@ -1,10 +0,0 @@
export interface LimitedCacheStorage {
open: (cacheName: string) => Promise<LimitedCache>;
delete: (cacheName: string) => Promise<boolean>;
}
export interface LimitedCache {
match: (key: string) => Promise<Response>;
put: (key: string, data: Response) => Promise<void>;
delete: (key: string) => Promise<boolean>;
}

View File

@@ -1,5 +0,0 @@
export interface CastPayload {
collectionID: number;
collectionKey: string;
castToken: string;
}

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