Compare commits
165 Commits
auth-v2.0.
...
photos-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2cf6be5f5 | ||
|
|
fce68ba1be | ||
|
|
876c5800f9 | ||
|
|
608cb6c85e | ||
|
|
712b99b8f3 | ||
|
|
935e47fbca | ||
|
|
fcb26d39f1 | ||
|
|
ff6d0d32cf | ||
|
|
52c47234fd | ||
|
|
756050ae8c | ||
|
|
a2d39a46be | ||
|
|
407eca5414 | ||
|
|
87dc7d76ca | ||
|
|
8b643549fe | ||
|
|
4255e48abb | ||
|
|
a8a5cc8b59 | ||
|
|
949a42004f | ||
|
|
cb94dd8b42 | ||
|
|
56d500f4e8 | ||
|
|
7a41ba43a5 | ||
|
|
7a729183e2 | ||
|
|
aa5422db6c | ||
|
|
c0fee7bc91 | ||
|
|
1411ca6fad | ||
|
|
9d7a342aa9 | ||
|
|
ee33a3229f | ||
|
|
54c4862e71 | ||
|
|
b97839adae | ||
|
|
37c4295df9 | ||
|
|
089be79688 | ||
|
|
0034d880f9 | ||
|
|
81bdc0fe73 | ||
|
|
76dca4d819 | ||
|
|
d0f1bbfca7 | ||
|
|
8a00f1b85f | ||
|
|
f10f751a2f | ||
|
|
d28daece8a | ||
|
|
24bce96d71 | ||
|
|
ad6dea2ecb | ||
|
|
212dcfb88a | ||
|
|
a689aca4a6 | ||
|
|
e2fd88bff0 | ||
|
|
764b6bf2f3 | ||
|
|
2fe703df92 | ||
|
|
ca688d0d46 | ||
|
|
885308471f | ||
|
|
125f7bfece | ||
|
|
011aee20d5 | ||
|
|
85778bcdaa | ||
|
|
43e97d225e | ||
|
|
b3a86874db | ||
|
|
5c1ed5be8f | ||
|
|
14fde54d87 | ||
|
|
26b35cec9e | ||
|
|
6213628aee | ||
|
|
a7625cd83d | ||
|
|
cc90dd7ba5 | ||
|
|
b3630f9543 | ||
|
|
9cb289e002 | ||
|
|
1800ad0a1f | ||
|
|
3230b9275e | ||
|
|
ce5627f04c | ||
|
|
8dd7c100af | ||
|
|
2e7dcc6bc2 | ||
|
|
0e1bdfe07e | ||
|
|
1e106d707f | ||
|
|
0053e814c8 | ||
|
|
53184da7fb | ||
|
|
165bcb5c6e | ||
|
|
d6316a1724 | ||
|
|
b95fc54adb | ||
|
|
bc0a453cbc | ||
|
|
166e9ad1bf | ||
|
|
841921a732 | ||
|
|
769da989c4 | ||
|
|
3e917bd855 | ||
|
|
7f1730b56c | ||
|
|
25e762ba57 | ||
|
|
d5f294980e | ||
|
|
1e410a82f2 | ||
|
|
d013519655 | ||
|
|
3b3d314f9c | ||
|
|
855d362cca | ||
|
|
eced463f6f | ||
|
|
f8febe12df | ||
|
|
0c44d1b789 | ||
|
|
f74af4199d | ||
|
|
9b27cac465 | ||
|
|
7b94c32bbf | ||
|
|
881c94be05 | ||
|
|
7248a226bc | ||
|
|
8ae7ae2de9 | ||
|
|
28cf7d76d5 | ||
|
|
c2957238da | ||
|
|
f9a2ec774a | ||
|
|
548721e415 | ||
|
|
0568cd03c9 | ||
|
|
226f891e99 | ||
|
|
9643dd645f | ||
|
|
7e897815a1 | ||
|
|
a9b92b9bfa | ||
|
|
489de9f8c2 | ||
|
|
39bc68390f | ||
|
|
83dabfbdee | ||
|
|
35f2a6944e | ||
|
|
fbe2996dcc | ||
|
|
7f23b31bbc | ||
|
|
f43e260434 | ||
|
|
18698d35bb | ||
|
|
9e41b906a7 | ||
|
|
0f2181c09b | ||
|
|
707e14702e | ||
|
|
f3a0240f1d | ||
|
|
e84b989484 | ||
|
|
86e4cffb8e | ||
|
|
1d02fe4f32 | ||
|
|
e5edeae370 | ||
|
|
40a1da1ba7 | ||
|
|
5dfafa28c7 | ||
|
|
d3df6b31ae | ||
|
|
145850a66e | ||
|
|
6b56c28870 | ||
|
|
9ec68ecd3d | ||
|
|
8c127a6cec | ||
|
|
3890373d4a | ||
|
|
ee1eb75bdf | ||
|
|
14e99ea26a | ||
|
|
7183a8b493 | ||
|
|
e1ac5e7394 | ||
|
|
1391cff1f1 | ||
|
|
204d4d048e | ||
|
|
8dfd60df19 | ||
|
|
5810d2b762 | ||
|
|
f6abcafc83 | ||
|
|
7950f1ec26 | ||
|
|
6974672f8c | ||
|
|
25cedd5e2f | ||
|
|
a51a965fc8 | ||
|
|
fc5d8aeca6 | ||
|
|
03fc59a8fb | ||
|
|
80a27f7e6f | ||
|
|
cc2c8e3e26 | ||
|
|
d52f873c92 | ||
|
|
911cdd9448 | ||
|
|
b4699ecfcb | ||
|
|
ded151241f | ||
|
|
3dfc3a6dba | ||
|
|
08b7fe6a49 | ||
|
|
7ef59bb4cc | ||
|
|
049a240916 | ||
|
|
3fdf5f1e46 | ||
|
|
d92fd25a78 | ||
|
|
640b546d78 | ||
|
|
9e3fbce6c7 | ||
|
|
e89eb48214 | ||
|
|
9440b967c8 | ||
|
|
d6769fb1d5 | ||
|
|
df377ebcf3 | ||
|
|
d22cf34a0e | ||
|
|
0d3662d9fe | ||
|
|
70e5e9b13c | ||
|
|
949780d1e8 | ||
|
|
a06a93e73d | ||
|
|
953824ca25 | ||
|
|
7c05069dbd |
BIN
.github/assets/github-badge.png
vendored
|
Before Width: | Height: | Size: 12 KiB |
2
.github/workflows/auth-release.yml
vendored
@@ -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: |
|
||||
|
||||
2
.github/workflows/web-deploy-payments.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
9
auth/assets/custom-icons/icons/configcat.svg
Normal file
|
After Width: | Height: | Size: 68 KiB |
9
auth/assets/custom-icons/icons/habbo.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
9
auth/assets/custom-icons/icons/mercado_livre.svg
Normal file
|
After Width: | Height: | Size: 57 KiB |
9
auth/assets/custom-icons/icons/sendgrid.svg
Normal file
|
After Width: | Height: | Size: 59 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -23,4 +23,5 @@ startup_notify: false
|
||||
#
|
||||
# include:
|
||||
# - libcurl.so.4
|
||||
include: []
|
||||
include:
|
||||
- libffi.so.7
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
@@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 21 KiB |
BIN
docs/docs/photos/migration/export/export-5.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
@@ -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**.
|
||||

|
||||
|
||||
2. Open the side bar, and select the option to **Export Data**.
|
||||
|
||||

|
||||
|
||||
3. Select the destination folder and click on **start**.
|
||||
3. Choose the destination folder by clicking on three dots icon.
|
||||
|
||||

|
||||
<div align="center">
|
||||
|
||||
4. Wait for the export to get completed.
|
||||
{width=400px}
|
||||
|
||||

|
||||
</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**
|
||||
|
||||

|
||||
<div align="center">
|
||||
|
||||
{width=400px}
|
||||
|
||||
</div>
|
||||
|
||||
5. Wait for the export to complete.
|
||||
|
||||
<div align="center">
|
||||
|
||||
{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">
|
||||
|
||||

|
||||
{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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
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).
|
||||
|
||||
BIN
docs/docs/photos/migration/export/sign-in.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -79,3 +79,5 @@ class LoginKeyDerivationError extends Error {}
|
||||
class SrpSetupNotCompleteError extends Error {}
|
||||
|
||||
class SharingNotPermittedForFreeAccountsError extends Error {}
|
||||
|
||||
class NoMediaLocationAccessError extends Error {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
3
mobile/lib/events/endpoint_updated_event.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
import "package:photos/events/event.dart";
|
||||
|
||||
class EndpointUpdatedEvent extends Event {}
|
||||
15
mobile/lib/generated/intl/messages_en.dart
generated
@@ -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"),
|
||||
|
||||
3
mobile/lib/generated/intl/messages_pt.dart
generated
@@ -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":
|
||||
|
||||
3
mobile/lib/generated/intl/messages_zh.dart
generated
@@ -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("正在等待同步"),
|
||||
|
||||
70
mobile/lib/generated/l10n.dart
generated
@@ -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> {
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "离开家庭计划",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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! 🙏',
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
91
mobile/lib/ui/settings/developer_settings_page.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
27
mobile/lib/ui/settings/developer_settings_widget.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
12
mobile/lib/utils/photo_manager_util.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -8,9 +8,11 @@ node_modules/
|
||||
.vscode/
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# Vite
|
||||
dist
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
thirdparty/
|
||||
public/
|
||||
*.md
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always",
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports",
|
||||
"prettier-plugin-packagejson"
|
||||
|
||||
@@ -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)
|
||||
|
||||
[](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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
95
web/apps/cast/src/components/PhotoAuditorium.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum CACHES {
|
||||
THUMBS = "thumbs",
|
||||
FACE_CROPS = "face-crops",
|
||||
FILES = "files",
|
||||
}
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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=";
|
||||
|
||||
@@ -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";
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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();
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||