Compare commits

...

165 Commits

Author SHA1 Message Date
Vishnu Mohandas
b2cf6be5f5 [photos] v0.8.75 (#1312) 2024-04-04 14:50:54 +05:30
vishnukvmd
fce68ba1be [photos] v0.8.75 2024-04-04 14:50:22 +05:30
Manav Rathi
876c5800f9 [web] Convert payments app to Vite (#1311)
This is the culmination of the previous few PRs. The payments app now
uses vite, which is what we want to give out a real shot for the smaller
of our apps.

**Tested by**

Local setup, and subscribing to a plan.
2024-04-04 14:36:36 +05:30
Manav Rathi
608cb6c85e Prevent double rendering in strict mode
The double invocation during dev mode, while harmless, is confusing, so add an
additional small check to insure this only runs once.
2024-04-04 14:25:04 +05:30
Manav Rathi
712b99b8f3 Fix lint issue 2024-04-04 14:15:42 +05:30
Manav Rathi
935e47fbca Fix the payments eslint 2024-04-04 14:10:16 +05:30
Manav Rathi
fcb26d39f1 Don't need default export 2024-04-04 14:02:31 +05:30
Manav Rathi
ff6d0d32cf Remove unused 404 handler
The default appType == 'spa' in vite redirects all (unclaimed) paths to /index.html.

If needed, this can be disabled:
https://stackoverflow.com/questions/69701743/how-can-i-configure-vites-dev-server-to-give-404-errors/69711988#69711988
2024-04-04 13:58:30 +05:30
Vishnu Mohandas
52c47234fd [Photos] Allow for configuring a custom server (#1302)
## Description
Users can now tap on the onboarding screen 7 times to bring up a page
where they can configure the endpoint the app should be connecting to.

![photos-selfhost](https://github.com/ente-io/ente/assets/1161789/42fda09a-07e4-4c4e-a658-ec4a2d3f1848)

## Tests
- [x] Verified that production flows are working as expected
- [x] Verified that configuring the endpoint to a local instance lets
you
  - [x] Connect to that instance
  - [x] Create an account
  - [x] Upload a photo
  - [x] Logout and log back in
2024-04-04 13:41:26 +05:30
Manav Rathi
756050ae8c Fix compilation 2024-04-04 13:02:08 +05:30
Manav Rathi
a2d39a46be [server] nginx configuration improvements (#1310)
- Use keepalives
- Update deprecated http2 syntax
- Document how to check config

Refs:
-
https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#no-keepalives
- https://github.com/nginxinc/kubernetes-ingress/issues/4237
-
https://stackoverflow.com/questions/65944578/how-to-get-nginx-running-in-docker-to-reload-nginx-conf-configuration
2024-04-04 12:42:10 +05:30
Manav Rathi
407eca5414 [server] nginx configuration improvements
- Use keepalives
- Update deprecated http2 syntax
- Document how to check config

Refs:
- https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#no-keepalives
- https://github.com/nginxinc/kubernetes-ingress/issues/4237
- https://stackoverflow.com/questions/65944578/how-to-get-nginx-running-in-docker-to-reload-nginx-conf-configuration
2024-04-04 12:36:39 +05:30
Manav Rathi
87dc7d76ca Remove middleman package, to get it to compile 2024-04-04 11:36:20 +05:30
Ashil
8b643549fe [mobile][photos] Remove unused global keys (#1309) 2024-04-04 11:29:23 +05:30
Manav Rathi
4255e48abb Convert payments app to use Vite - Part 1
(Doesn't compile)
2024-04-04 11:10:34 +05:30
Manav Rathi
a8a5cc8b59 Inline spinner 2024-04-04 09:55:52 +05:30
Manav Rathi
949a42004f Remove the need for a separate page 2024-04-04 09:53:11 +05:30
Manav Rathi
cb94dd8b42 [web] Refactor payments (#1304)
This is a reduction in code generally before we give a shot to using
vite for Payments. Once that is done, will do an end-to-end test of the
payments pages on localhost.
2024-04-03 21:44:08 +05:30
Manav Rathi
56d500f4e8 Keep React in scope 2024-04-03 21:41:03 +05:30
Manav Rathi
7a41ba43a5 Another cleanup of billing-service 2024-04-03 21:32:38 +05:30
Manav Rathi
7a729183e2 Cleanup the pages 2024-04-03 20:03:02 +05:30
Manav Rathi
aa5422db6c Clean up CSS 2024-04-03 19:54:16 +05:30
Manav Rathi
c0fee7bc91 Clean up strings 2024-04-03 19:51:01 +05:30
Manav Rathi
1411ca6fad Continue refactoring 2024-04-03 19:46:15 +05:30
vishnukvmd
9d7a342aa9 Ensure widget is updated when endpoint changes 2024-04-03 17:34:45 +05:30
vishnukvmd
ee33a3229f Update landing page to provide an option to update the app's endpoint 2024-04-03 17:24:44 +05:30
vishnukvmd
54c4862e71 Add widget that renders the current endpoint app is connecting to 2024-04-03 17:24:26 +05:30
vishnukvmd
b97839adae Update strings 2024-04-03 17:24:01 +05:30
vishnukvmd
37c4295df9 Update NetworkClient when configuration changes 2024-04-03 17:23:54 +05:30
vishnukvmd
089be79688 Add API within Configuration to update the endpoint 2024-04-03 17:23:23 +05:30
vishnukvmd
0034d880f9 Initialize Configuration before Network 2024-04-03 17:23:00 +05:30
vishnukvmd
81bdc0fe73 Add strings 2024-04-03 17:22:34 +05:30
vishnukvmd
76dca4d819 Update iOS config 2024-04-03 17:22:22 +05:30
Manav Rathi
d0f1bbfca7 Refactor billing service 2024-04-03 17:22:00 +05:30
Manav Rathi
8a00f1b85f Remove custom errors indirection 2024-04-03 15:45:20 +05:30
Manav Rathi
f10f751a2f Inline local storage calls
The methods are trivial, and we cannot centralize the keys since they will be
different for different apps. So an abstraction for this is not beneficial.

Also move the next specific dev build check to @/next
2024-04-03 14:21:11 +05:30
Manav Rathi
d28daece8a ignorePatters seems to be inherited (as we wished) when extending 2024-04-03 13:48:41 +05:30
Manav Rathi
24bce96d71 Shorten 2024-04-03 13:44:30 +05:30
Manav Rathi
ad6dea2ecb prettier markdown 2024-04-03 13:39:44 +05:30
Manav Rathi
212dcfb88a Tinker 2024-04-03 13:32:05 +05:30
Manav Rathi
a689aca4a6 Remove the eslint stuff from docs
(trying to remove the eslint-disables throughout)
2024-04-03 13:26:46 +05:30
Manav Rathi
e2fd88bff0 Remove (possibly) unnecessary tsconfigRootDir
Let's see what breaks
2024-04-03 13:15:12 +05:30
Manav Rathi
764b6bf2f3 Move react setup to react specific file 2024-04-03 13:02:09 +05:30
Neeraj Gupta
2fe703df92 [server] Increase embedding fetch limit (#1300)
## Description

Also use different semaphore than existing diff API

## Tests
2024-04-03 12:38:34 +05:30
Manav Rathi
ca688d0d46 [server] Add a notice that minio is only for getting started (#1299)
From our Discord, someone commented

> if minio's cautions about Single-Node, Single-Drive are to be taken
seriously:
>
> "SNSD deployments use a zero-parity erasure coded backend that
provides no
added reliability or availability beyond what the underlying storage
volume
  implements. These deployments are best suited for local testing and
evaluation, or for small-scale data workloads that do not have
availability or
  performance requirements."

MinIO was never meant as a production replacement, it was only to make
it easy
for people to get started. So add a notice in the docs re this.
2024-04-03 10:04:20 +05:30
Manav Rathi
885308471f [server] Add a notice that minio is only for getting started
From our Discord, someone commented

> if minio's cautions about Single-Node, Single-Drive are to be taken seriously:
>
> "SNSD deployments use a zero-parity erasure coded backend that provides no
  added reliability or availability beyond what the underlying storage volume
  implements. These deployments are best suited for local testing and
  evaluation, or for small-scale data workloads that do not have availability or
  performance requirements."

MinIO was never meant as a production replacement, it was only to make it easy
for people to get started. So add a notice in the docs re this.
2024-04-03 10:01:48 +05:30
Vishnu Mohandas
125f7bfece Update export docs 2024-04-03 08:52:34 +05:30
Neeraj Gupta
011aee20d5 [server] Fix handling of redundant auth update (#1298)
## Description
When client tries to send update request with same data, the actual db
update doesn't happen. This was resulting in 5xx error.

## Tests
2024-04-03 07:13:44 +05:30
Neeraj Gupta
85778bcdaa [server] Fix handling of redundant auth update 2024-04-03 07:05:03 +05:30
Manav Rathi
43e97d225e [web] Restructure shared package layouts (#1294)
- Merge @/ui into @/next
- Restructure eslint config
2024-04-02 20:55:05 +05:30
Manav Rathi
b3a86874db yarn.lock 2024-04-02 20:47:35 +05:30
Manav Rathi
5c1ed5be8f Restructure eslint config 2024-04-02 20:44:57 +05:30
Manav Rathi
14fde54d87 Not really 2024-04-02 20:04:23 +05:30
Manav Rathi
26b35cec9e Merge @/ui into @/next 2024-04-02 19:58:42 +05:30
Manav Rathi
6213628aee [web] Prefer .local files for local only configuration (#1280)
Refs: https://vitejs.dev/guide/env-and-mode.html
2024-04-02 17:22:32 +05:30
Manav Rathi
a7625cd83d [web] Enable Russian (#1288)
The translation percentage of Russian in crowdin is now 100%, it's time
to enable it as an option in the UI.

A big thank you to the translators ❤️
2024-04-02 17:22:15 +05:30
Manav Rathi
cc90dd7ba5 [web] Enable Russian
The translation percentage of Russian in crowdin is now 100%, it's time to
enable it as an option in the UI.

A big thank you to the translators.
2024-04-02 17:16:33 +05:30
Manav Rathi
b3630f9543 [desktop] Prevent the desktop app from getting stuck on viewing openstreetmap info (#1287)
Open the link in a new tab. This prevents the desktop app from getting
into a state where the user cannot navigate back.

Tested on the web app and desktop app.
2024-04-02 17:10:16 +05:30
Manav Rathi
9cb289e002 [desktop] Prevent the desktop app from getting stuck on viewing openstreetmap info
Open the link in a new tab. This prevents the desktop app from getting into a
state where the user cannot navigate back.
2024-04-02 17:05:23 +05:30
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
Manav Rathi
b95fc54adb [web] Prefer .local files for local only configuration
Refs:
https://vitejs.dev/guide/env-and-mode.html
2024-04-01 19:59:02 +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
Manav Rathi
d22cf34a0e Remove nesting 2024-03-29 16:18:49 +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
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
238 changed files with 3907 additions and 5715 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

@@ -39,5 +39,5 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
projectName: ente
branch: deploy/payments
directory: web/apps/payments/out
directory: web/apps/payments/dist
wranglerVersion: "3"

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

@@ -98,13 +98,13 @@ class _CodeWidgetState extends State<CodeWidget> {
onSelected: () => _onShowQrPressed(null),
),
MenuItem(
label: 'Edit',
label: l10n.edit,
icon: Icons.edit,
onSelected: () => _onEditPressed(null),
),
const MenuDivider(),
MenuItem(
label: 'Delete',
label: l10n.delete,
value: "Delete",
icon: Icons.delete,
onSelected: () => _onDeletePressed(null),

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

@@ -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

@@ -73,7 +73,7 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder
}
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)

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,37 +11,60 @@ 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.
2. Open the side bar, and select the option to **export data**.
![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 complete.
<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**.
7. **Sync continuously** : You can utilize Continuous Sync to eliminate manual
exports each time new photos are added to Ente. This feature automatically
detects new files and runs exports accordingly, It also ensures that exported
data reflects the latest album states with new files, moves, and deletions.
<div align="center">
![Ente - Continuous sync](continuous-sync.webp)
![Ente - Rexport](export-5.png){width=400px}
</div>
### Sync continuously
You can switch on the toggle to **Sync continuously** to eliminate manual
exports each time new photos are added to Ente. This feature automatically
detects new files and runs exports accordingly. It also ensures that exported
data reflects the latest album states with new files, moves, and deletions.
![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!
Note that we also provide a [CLI
tool](https://github.com/ente-io/ente/tree/main/cli#export) to export your data.
Some more details are in this [FAQ entry](/photos/faq/export).
Please find more details [here](/photos/faq/export).

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -62,3 +62,12 @@ We can see this in the default configuration of nginx:
This is a [handy tool](https://nginx-playground.wizardzines.com) to check the
syntax of the configuration files. Alternatively, you can run `docker exec nginx
nginx -t` on the instance to ask nginx to check the configuration.
## Updating configuration
Nginx configuration files can be changed without needing to restart anything.
1. Update the configuration file at `/root/nginx/conf.d/museum.conf`
2. Verify that there are no errors in the configuration by using `sudo docker
exec nginx nginx -t`.
3. Ask nginx to reload the configuration `sudo systemctl reload nginx`.

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

@@ -152,6 +152,8 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.1.1):
- Flutter
- photo_manager (2.0.0):
- Flutter
- FlutterMacOS
@@ -247,6 +249,7 @@ DEPENDENCIES:
- open_mail_app (from `.symlinks/plugins/open_mail_app/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
@@ -353,6 +356,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
receive_sharing_intent:
@@ -429,6 +434,7 @@ SPEC CHECKSUMS:
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
@@ -454,4 +460,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: c1a8f198a245ed1f10e40b617efdb129b021b225
COCOAPODS: 1.14.3
COCOAPODS: 1.15.2

View File

@@ -16,6 +16,7 @@ import 'package:photos/db/files_db.dart';
import 'package:photos/db/memories_db.dart';
import 'package:photos/db/trash_db.dart';
import 'package:photos/db/upload_locks_db.dart';
import "package:photos/events/endpoint_updated_event.dart";
import 'package:photos/events/signed_in_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
import 'package:photos/models/key_attributes.dart';
@@ -69,6 +70,7 @@ class Configuration {
static const hasSelectedAllFoldersForBackupKey =
"has_selected_all_folders_for_backup";
static const anonymousUserIDKey = "anonymous_user_id";
static const endPointKey = "endpoint";
final kTempFolderDeletionTimeBuffer = const Duration(hours: 6).inMicroseconds;
@@ -390,7 +392,12 @@ class Configuration {
}
String getHttpEndpoint() {
return endpoint;
return _preferences.getString(endPointKey) ?? endpoint;
}
Future<void> setHttpEndpoint(String endpoint) async {
await _preferences.setString(endPointKey, endpoint);
Bus.instance.fire(EndpointUpdatedEvent());
}
String? getToken() {

View File

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

View File

@@ -1,14 +1,12 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:photos/core/configuration.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
class EnteRequestInterceptor extends Interceptor {
final SharedPreferences _preferences;
final String enteEndpoint;
EnteRequestInterceptor(this._preferences, this.enteEndpoint);
EnteRequestInterceptor(this.enteEndpoint);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
@@ -20,7 +18,7 @@ class EnteRequestInterceptor extends Interceptor {
}
// ignore: prefer_const_constructors
options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString());
final String? tokenValue = _preferences.getString(Configuration.tokenKey);
final String? tokenValue = Configuration.instance.getToken();
if (tokenValue != null) {
options.headers.putIfAbsent("X-Auth-Token", () => tokenValue);
}

View File

@@ -3,26 +3,21 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:fk_user_agent/fk_user_agent.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:photos/core/constants.dart';
import "package:photos/core/configuration.dart";
import "package:photos/core/event_bus.dart";
import 'package:photos/core/network/ente_interceptor.dart';
import 'package:shared_preferences/shared_preferences.dart';
import "package:photos/events/endpoint_updated_event.dart";
int kConnectTimeout = 15000;
class NetworkClient {
// apiEndpoint points to the Ente server's API endpoint
static const apiEndpoint = String.fromEnvironment(
"endpoint",
defaultValue: kDefaultProductionEndpoint,
);
late Dio _dio;
late Dio _enteDio;
Future<void> init() async {
await FkUserAgent.init();
final packageInfo = await PackageInfo.fromPlatform();
final preferences = await SharedPreferences.getInstance();
final endpoint = Configuration.instance.getHttpEndpoint();
_dio = Dio(
BaseOptions(
connectTimeout: kConnectTimeout,
@@ -35,7 +30,7 @@ class NetworkClient {
);
_enteDio = Dio(
BaseOptions(
baseUrl: apiEndpoint,
baseUrl: endpoint,
connectTimeout: kConnectTimeout,
headers: {
HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
@@ -44,7 +39,18 @@ class NetworkClient {
},
),
);
_enteDio.interceptors.add(EnteRequestInterceptor(preferences, apiEndpoint));
_setupInterceptors(endpoint);
Bus.instance.on<EndpointUpdatedEvent>().listen((event) {
final endpoint = Configuration.instance.getHttpEndpoint();
_enteDio.options.baseUrl = endpoint;
_setupInterceptors(endpoint);
});
}
void _setupInterceptors(String endpoint) {
_enteDio.interceptors.clear();
_enteDio.interceptors.add(EnteRequestInterceptor(endpoint));
}
NetworkClient._privateConstructor();

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

@@ -0,0 +1,3 @@
import "package:photos/events/event.dart";
class EndpointUpdatedEvent extends Event {}

View File

@@ -62,6 +62,8 @@ class MessageLookup extends MessageLookupByLibrary {
static String m13(provider) =>
"Please contact us at support@ente.io to manage your ${provider} subscription.";
static String m69(endpoint) => "Connected to ${endpoint}";
static String m14(count) =>
"${Intl.plural(count, one: 'Delete ${count} item', other: 'Delete ${count} items')}";
@@ -502,6 +504,7 @@ class MessageLookup extends MessageLookupByLibrary {
"currentUsageIs":
MessageLookupByLibrary.simpleMessage("Current usage is "),
"custom": MessageLookupByLibrary.simpleMessage("Custom"),
"customEndpoint": m69,
"darkTheme": MessageLookupByLibrary.simpleMessage("Dark"),
"dayToday": MessageLookupByLibrary.simpleMessage("Today"),
"dayYesterday": MessageLookupByLibrary.simpleMessage("Yesterday"),
@@ -562,6 +565,10 @@ class MessageLookup extends MessageLookupByLibrary {
"details": MessageLookupByLibrary.simpleMessage("Details"),
"devAccountChanged": MessageLookupByLibrary.simpleMessage(
"The developer account we use to publish ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable."),
"developerSettings":
MessageLookupByLibrary.simpleMessage("Developer settings"),
"developerSettingsWarning": MessageLookupByLibrary.simpleMessage(
"Are you sure that you want to modify Developer settings?"),
"deviceCodeHint":
MessageLookupByLibrary.simpleMessage("Enter the code"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
@@ -627,6 +634,8 @@ class MessageLookup extends MessageLookupByLibrary {
"encryption": MessageLookupByLibrary.simpleMessage("Encryption"),
"encryptionKeys":
MessageLookupByLibrary.simpleMessage("Encryption keys"),
"endpointUpdatedMessage": MessageLookupByLibrary.simpleMessage(
"Endpoint updated successfully"),
"endtoendEncryptedByDefault": MessageLookupByLibrary.simpleMessage(
"End-to-end encrypted by default"),
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant":
@@ -781,6 +790,10 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Install manually"),
"invalidEmailAddress":
MessageLookupByLibrary.simpleMessage("Invalid email address"),
"invalidEndpoint":
MessageLookupByLibrary.simpleMessage("Invalid endpoint"),
"invalidEndpointMessage": MessageLookupByLibrary.simpleMessage(
"Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again."),
"invalidKey": MessageLookupByLibrary.simpleMessage("Invalid key"),
"invalidRecoveryKey": MessageLookupByLibrary.simpleMessage(
"The recovery key you entered is not valid. Please make sure it contains 24 words, and check the spelling of each.\n\nIf you entered an older recovery code, make sure it is 64 characters long, and check each of them."),
@@ -1220,6 +1233,8 @@ class MessageLookup extends MessageLookupByLibrary {
"sendEmail": MessageLookupByLibrary.simpleMessage("Send email"),
"sendInvite": MessageLookupByLibrary.simpleMessage("Send invite"),
"sendLink": MessageLookupByLibrary.simpleMessage("Send link"),
"serverEndpoint":
MessageLookupByLibrary.simpleMessage("Server endpoint"),
"sessionExpired":
MessageLookupByLibrary.simpleMessage("Session expired"),
"setAPassword": MessageLookupByLibrary.simpleMessage("Set a password"),

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

@@ -8473,6 +8473,76 @@ class S {
args: [],
);
}
/// `Are you sure that you want to modify Developer settings?`
String get developerSettingsWarning {
return Intl.message(
'Are you sure that you want to modify Developer settings?',
name: 'developerSettingsWarning',
desc: '',
args: [],
);
}
/// `Developer settings`
String get developerSettings {
return Intl.message(
'Developer settings',
name: 'developerSettings',
desc: '',
args: [],
);
}
/// `Server endpoint`
String get serverEndpoint {
return Intl.message(
'Server endpoint',
name: 'serverEndpoint',
desc: '',
args: [],
);
}
/// `Invalid endpoint`
String get invalidEndpoint {
return Intl.message(
'Invalid endpoint',
name: 'invalidEndpoint',
desc: '',
args: [],
);
}
/// `Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.`
String get invalidEndpointMessage {
return Intl.message(
'Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.',
name: 'invalidEndpointMessage',
desc: '',
args: [],
);
}
/// `Endpoint updated successfully`
String get endpointUpdatedMessage {
return Intl.message(
'Endpoint updated successfully',
name: 'endpointUpdatedMessage',
desc: '',
args: [],
);
}
/// `Connected to {endpoint}`
String customEndpoint(Object endpoint) {
return Intl.message(
'Connected to $endpoint',
name: 'customEndpoint',
desc: '',
args: [endpoint],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -1203,5 +1203,12 @@
"descriptions": "Descriptions",
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption."
}
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"developerSettingsWarning": "Are you sure that you want to modify Developer settings?",
"developerSettings": "Developer settings",
"serverEndpoint": "Server endpoint",
"invalidEndpoint": "Invalid endpoint",
"invalidEndpointMessage": "Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.",
"endpointUpdatedMessage": "Endpoint updated successfully",
"customEndpoint": "Connected to {endpoint}"
}

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

@@ -189,8 +189,8 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
// Start workers asynchronously. No need to wait for them to start
Computer.shared().turnOn(workersCount: 4).ignore();
CryptoUtil.init();
await NetworkClient.instance.init();
await Configuration.instance.init();
await NetworkClient.instance.init();
await UserService.instance.init();
await EntityService.instance.init();
LocationService.instance.init(preferences);

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

@@ -19,7 +19,10 @@ import 'package:photos/ui/components/buttons/button_widget.dart';
import 'package:photos/ui/components/dialog_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/payment/subscription.dart';
import "package:photos/ui/settings/developer_settings_page.dart";
import "package:photos/ui/settings/developer_settings_widget.dart";
import "package:photos/ui/settings/language_picker.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/navigation_util.dart";
class LandingPageWidget extends StatefulWidget {
@@ -30,7 +33,10 @@ class LandingPageWidget extends StatefulWidget {
}
class _LandingPageWidgetState extends State<LandingPageWidget> {
static const kDeveloperModeTapCountThreshold = 7;
double _featureIndex = 0;
int _developerModeTapCount = 0;
@override
void initState() {
@@ -40,7 +46,35 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(body: _getBody(), resizeToAvoidBottomInset: false);
return Scaffold(
body: GestureDetector(
onTap: () async {
_developerModeTapCount++;
if (_developerModeTapCount >= kDeveloperModeTapCountThreshold) {
_developerModeTapCount = 0;
final result = await showChoiceDialog(
context,
title: S.of(context).developerSettings,
firstButtonLabel: S.of(context).yes,
body: S.of(context).developerSettingsWarning,
isDismissible: false,
);
if (result?.action == ButtonAction.first) {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const DeveloperSettingsPage();
},
),
);
setState(() {});
}
}
},
child: _getBody(),
),
resizeToAvoidBottomInset: false,
);
}
Widget _getBody() {
@@ -131,6 +165,9 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
),
),
),
// const DeveloperSettingsWidget() does not refresh when the endpoint is changed
// ignore: prefer_const_constructors
DeveloperSettingsWidget(),
const Padding(
padding: EdgeInsets.all(20),
),
@@ -195,7 +232,9 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
// No key
if (Configuration.instance.getKeyAttributes() == null) {
// Never had a key
page = const PasswordEntryPage(mode: PasswordEntryMode.set,);
page = const PasswordEntryPage(
mode: PasswordEntryMode.set,
);
} else if (Configuration.instance.getKey() == null) {
// Yet to decrypt the key
page = const PasswordReentryPage();
@@ -223,7 +262,9 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
// No key
if (Configuration.instance.getKeyAttributes() == null) {
// Never had a key
page = const PasswordEntryPage(mode: PasswordEntryMode.set,);
page = const PasswordEntryPage(
mode: PasswordEntryMode.set,
);
} else if (Configuration.instance.getKey() == null) {
// Yet to decrypt the key
page = const PasswordReentryPage();

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

@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import "package:photos/core/configuration.dart";
import "package:photos/core/network/network.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/ui/common/gradient_button.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/toast_util.dart";
class DeveloperSettingsPage extends StatefulWidget {
const DeveloperSettingsPage({super.key});
@override
State<DeveloperSettingsPage> createState() => _DeveloperSettingsPageState();
}
class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
final _logger = Logger('DeveloperSettingsPage');
final _urlController = TextEditingController();
@override
void dispose() {
_urlController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_logger.info(
"Current endpoint is: ${Configuration.instance.getHttpEndpoint()}",
);
return Scaffold(
appBar: AppBar(
title: Text(S.of(context).developerSettings),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _urlController,
decoration: InputDecoration(
labelText: S.of(context).serverEndpoint,
hintText: Configuration.instance.getHttpEndpoint(),
),
autofocus: true,
),
const SizedBox(height: 40),
GradientButton(
onTap: () async {
final url = _urlController.text;
_logger.info("Entered endpoint: $url");
try {
final uri = Uri.parse(url);
if ((uri.scheme == "http" || uri.scheme == "https")) {
await _ping(url);
await Configuration.instance.setHttpEndpoint(url);
showToast(context, S.of(context).endpointUpdatedMessage);
Navigator.of(context).pop();
} else {
throw const FormatException();
}
} catch (e) {
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).invalidEndpoint,
S.of(context).invalidEndpointMessage,
);
}
},
text: S.of(context).save,
),
],
),
),
);
}
Future<void> _ping(String endpoint) async {
try {
final response =
await NetworkClient.instance.getDio().get('$endpoint/ping');
if (response.data['message'] != 'pong') {
throw Exception('Invalid response');
}
} catch (e) {
throw Exception('Error occurred: $e');
}
}
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import "package:photos/core/configuration.dart";
import "package:photos/core/constants.dart";
import "package:photos/generated/l10n.dart";
class DeveloperSettingsWidget extends StatelessWidget {
const DeveloperSettingsWidget({super.key});
@override
Widget build(BuildContext context) {
final endpoint = Configuration.instance.getHttpEndpoint();
if (endpoint != kDefaultProductionEndpoint) {
final endpointURI = Uri.parse(endpoint);
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Text(
S
.of(context)
.customEndpoint("${endpointURI.host}:${endpointURI.port}"),
style: Theme.of(context).textTheme.bodySmall,
),
);
} else {
return const SizedBox.shrink();
}
}
}

View File

@@ -18,6 +18,7 @@ import 'package:photos/ui/settings/account_section_widget.dart';
import 'package:photos/ui/settings/app_version_widget.dart';
import 'package:photos/ui/settings/backup/backup_section_widget.dart';
import 'package:photos/ui/settings/debug_section_widget.dart';
import "package:photos/ui/settings/developer_settings_widget.dart";
import 'package:photos/ui/settings/general_section_widget.dart';
import 'package:photos/ui/settings/inherited_settings_state.dart';
import 'package:photos/ui/settings/security_section_widget.dart';
@@ -144,6 +145,7 @@ class SettingsPage extends StatelessWidget {
contents.addAll([sectionSpacing, const DebugSectionWidget()]);
}
contents.add(const AppVersionWidget());
contents.add(const DeveloperSettingsWidget());
contents.add(
const Padding(
padding: EdgeInsets.only(bottom: 60),

View File

@@ -80,7 +80,6 @@ class _HomeWidgetState extends State<HomeWidget> {
final _logger = Logger("HomeWidgetState");
final _selectedFiles = SelectedFiles();
final GlobalKey shareButtonKey = GlobalKey();
final PageController _pageController = PageController();
int _selectedTabIndex = 0;

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

@@ -86,7 +86,6 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
late Function() _selectedFilesListener;
String? _appBarTitle;
late CollectionActions collectionActions;
final GlobalKey shareButtonKey = GlobalKey();
bool isQuickLink = false;
late bool isInternalUser;
late GalleryType galleryType;

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.75+595
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

@@ -97,7 +97,7 @@ Overall, there are [three approaches](RUNNING.md) you can take:
* Run without Docker
Everything that you might needed to run museum is all in here, since this is the
setup we ourselves use in production.
code we ourselves use in production.
> [!TIP]
>

View File

@@ -44,6 +44,12 @@ Or interact with the MinIO S3 API
Or open the MinIO dashboard at <http://localhost:3201> (user: test/password: testtest).
> [!NOTE]
>
> While we've provided a MinIO based Docker compose file to make it easy for
> people to get started, if you're running it in production we recommend using
> an external S3.
> [!NOTE]
>
> If something seems amiss, ensure that Docker has read access to the parent

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

@@ -275,7 +275,9 @@ func (c *Controller) uploadObject(obj ente.EmbeddingObject, key string) (int, er
return len(embeddingObj), nil
}
var globalFetchSemaphore = make(chan struct{}, 300)
var globalDiffFetchSemaphore = make(chan struct{}, 300)
var globalFileFetchSemaphore = make(chan struct{}, 400)
func (c *Controller) getEmbeddingObjectsParallel(objectKeys []string) ([]ente.EmbeddingObject, error) {
var wg sync.WaitGroup
@@ -285,10 +287,10 @@ func (c *Controller) getEmbeddingObjectsParallel(objectKeys []string) ([]ente.Em
for i, objectKey := range objectKeys {
wg.Add(1)
globalFetchSemaphore <- struct{}{} // Acquire from global semaphore
globalDiffFetchSemaphore <- struct{}{} // Acquire from global semaphore
go func(i int, objectKey string) {
defer wg.Done()
defer func() { <-globalFetchSemaphore }() // Release back to global semaphore
defer func() { <-globalDiffFetchSemaphore }() // Release back to global semaphore
obj, err := c.getEmbeddingObject(objectKey, downloader)
if err != nil {
@@ -322,10 +324,10 @@ func (c *Controller) getEmbeddingObjectsParallelV2(userID int64, dbEmbeddingRows
for i, dbEmbeddingRow := range dbEmbeddingRows {
wg.Add(1)
globalFetchSemaphore <- struct{}{} // Acquire from global semaphore
globalFileFetchSemaphore <- struct{}{} // Acquire from global semaphore
go func(i int, dbEmbeddingRow ente.Embedding) {
defer wg.Done()
defer func() { <-globalFetchSemaphore }() // Release back to global semaphore
defer func() { <-globalFileFetchSemaphore }() // Release back to global semaphore
objectKey := c.getObjectKey(userID, dbEmbeddingRow.FileID, dbEmbeddingRow.Model)
obj, err := c.getEmbeddingObject(objectKey, downloader)
if err != nil {
@@ -373,8 +375,8 @@ func (c *Controller) _validateGetFileEmbeddingsRequest(ctx *gin.Context, userID
if len(req.FileIDs) == 0 {
return ente.NewBadRequestWithMessage("fileIDs are required")
}
if len(req.FileIDs) > 100 {
return ente.NewBadRequestWithMessage("fileIDs should be less than or equal to 100")
if len(req.FileIDs) > 200 {
return ente.NewBadRequestWithMessage("fileIDs should be less than or equal to 200")
}
if err := c.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{
ActorUserId: userID,

View File

@@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/ente-io/museum/ente"
model "github.com/ente-io/museum/ente/authenticator"
"github.com/ente-io/stacktrace"
@@ -43,7 +44,10 @@ func (r *Repository) Get(ctx context.Context, userID int64, id uuid.UUID) (model
)
err := row.Scan(&res.ID, &res.UserID, &res.EncryptedData, &res.Header, &res.IsDeleted, &res.CreatedAt, &res.UpdatedAt)
if err != nil {
return model.Entity{}, stacktrace.Propagate(err, "failed to getTotpEntry")
if errors.Is(err, sql.ErrNoRows) {
return model.Entity{}, &ente.ErrNotFoundError
}
return model.Entity{}, stacktrace.Propagate(err, "failed to auth entity with id=%s", id)
}
return res, nil
}
@@ -70,6 +74,16 @@ func (r *Repository) Update(ctx context.Context, userID int64, req model.UpdateE
return stacktrace.Propagate(err, "")
}
if affected != 1 {
dbEntity, dbEntityErr := r.Get(ctx, userID, req.ID)
if dbEntityErr != nil {
return stacktrace.Propagate(dbEntityErr, fmt.Sprintf("failed to get entity for update with id=%s", req.ID))
}
if dbEntity.IsDeleted {
return stacktrace.Propagate(ente.NewBadRequestWithMessage("entity is already deleted"), "")
} else if *dbEntity.EncryptedData == req.EncryptedData && *dbEntity.Header == req.Header {
logrus.WithField("id", req.ID).Info("entity is already updated")
return nil
}
return stacktrace.Propagate(errors.New("exactly one row should be updated"), "")
}
return nil

View File

@@ -62,7 +62,7 @@ To bring up an additional museum node:
sudo mkdir -p /root/museum/data/billing
sudo mv *.json /root/museum/data/billing/
* If not running behind Nginx, add the TLS credentials (otherwise add the to
* If not running behind Nginx, add the TLS credentials (otherwise add them to
Nginx)
sudo tee /root/museum/credentials/tls.cert

View File

@@ -4,11 +4,15 @@
upstream museum {
# https://nginx.org/en/docs/http/ngx_http_upstream_module.html
server host.docker.internal:8080 max_conns=50;
# Keep these many connections alive to upstream (requires HTTP/1.1)
keepalive 20;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;
@@ -16,6 +20,8 @@ server {
location / {
proxy_pass http://museum;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

4
web/.gitignore vendored
View File

@@ -8,9 +8,11 @@ node_modules/
.vscode/
# Local env files
.env
.env*.local
# Vite
dist
# Next.js
.next/
out/

View File

@@ -1,3 +1,2 @@
thirdparty/
public/
*.md

View File

@@ -1,5 +1,6 @@
{
"tabWidth": 4,
"proseWrap": "always",
"plugins": [
"prettier-plugin-organize-imports",
"prettier-plugin-packagejson"

View File

@@ -4,8 +4,8 @@ Source code for Ente's various web apps and supporting websites.
Live versions are at:
* Ente Photos: [web.ente.io](https://web.ente.io)
* Ente Auth: [auth.ente.io](https://auth.ente.io)
- Ente Photos: [web.ente.io](https://web.ente.io)
- Ente Auth: [auth.ente.io](https://auth.ente.io)
To know more about Ente, see [our main README](../README.md) or visit
[ente.io](https://ente.io).
@@ -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
@@ -49,17 +49,17 @@ For more details about development workflows, see [docs/dev](docs/dev.md).
As a brief overview, this directory contains the following apps:
* `apps/photos`: A fully functional web client for Ente Photos.
* `apps/auth`: A view only client for Ente Auth. Currently you can only view
your 2FA codes using this web app. For adding and editing your 2FA codes,
please use the Ente Auth [mobile/desktop app](../auth/README.md) instead.
- `apps/photos`: A fully functional web client for Ente Photos.
- `apps/auth`: A view only client for Ente Auth. Currently you can only view
your 2FA codes using this web app. For adding and editing your 2FA codes,
please use the Ente Auth [mobile/desktop app](../auth/README.md) instead.
These two are the public facing apps. There are other part of the code which are
accessed as features within the main apps, but in terms of code are
independently maintained and deployed:
* `apps/accounts`: Passkey support (Coming soon)
* `apps/cast`: Chromecast support (Coming soon)
- `apps/accounts`: Passkey support (Coming soon)
- `apps/cast`: Chromecast support (Coming soon)
> [!NOTE]
>
@@ -81,12 +81,12 @@ City coordinates from [Simple Maps](https://simplemaps.com/data/world-cities)
[![Crowdin](https://badges.crowdin.net/ente-photos-web/localized.svg)](https://crowdin.com/project/ente-photos-web)
If you're interested in helping out with translation, please visit our [Crowdin
project](https://crowdin.com/project/ente-photos-web) to get started. Thank you
for your support.
If you're interested in helping out with translation, please visit our
[Crowdin project](https://crowdin.com/project/ente-photos-web) to get started.
Thank you for your support.
If your language is not listed for translation, please [create a GitHub
issue](https://github.com/ente-io/ente/issues/new?title=Request+for+New+Language+Translation&body=Language+name%3A)
If your language is not listed for translation, please
[create a GitHub issue](https://github.com/ente-io/ente/issues/new?title=Request+for+New+Language+Translation&body=Language+name%3A)
to have it added.
## Contribute

View File

@@ -1,7 +1,5 @@
import { setupI18n } from "@/ui/i18n";
import { CacheProvider } from "@emotion/react";
import { setupI18n } from "@/next/i18n";
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 "@/next/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),
};
}

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