diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index fb9781e2db..3cd5333ac3 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -145,7 +145,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 patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff5 + 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 patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff5 xz-utils libarchive-tools sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu' - name: Install appimagetool @@ -157,10 +157,16 @@ jobs: - name: Build desktop app run: | flutter config --enable-linux-desktop - dart pub global activate flutter_distributor + # dart pub global activate flutter_distributor + dart pub global activate --source git https://github.com/prateekmedia/flutter_distributor --git-ref pacman --git-path packages/flutter_distributor + # Run below command if it is a beta or nightly + if [[ ${{ github.ref }} =~ beta|nightly ]]; then + flutter_distributor package --platform=linux --targets=pacman --skip-clean + mv dist/**/*-*-linux.pacman artifacts/ente-${{ github.ref_name }}-x86_64.pacman + fi flutter_distributor package --platform=linux --targets=rpm --skip-clean - flutter_distributor package --platform=linux --targets=appimage --skip-clean mv dist/**/*-*-linux.rpm artifacts/ente-${{ github.ref_name }}-x86_64.rpm + flutter_distributor package --platform=linux --targets=appimage --skip-clean mv dist/**/*-*-linux.AppImage artifacts/ente-${{ github.ref_name }}-x86_64.AppImage - name: Generate checksums diff --git a/.github/workflows/web-deploy-staging.yml b/.github/workflows/web-deploy-staging.yml index ca3a6142b2..4323ee34c4 100644 --- a/.github/workflows/web-deploy-staging.yml +++ b/.github/workflows/web-deploy-staging.yml @@ -1,5 +1,7 @@ name: "Deploy staging (web)" +# Builds the "staging/web" branch if it exists, "main" otherwise. + on: schedule: # Run everyday at ~3:00 PM IST @@ -18,9 +20,20 @@ jobs: working-directory: web steps: - - name: Checkout code + - name: Determine branch to build + id: select-branch + working-directory: ${{ github.workspace }} + run: | + if git ls-remote --exit-code --heads https://github.com/ente-io/ente refs/heads/staging/web; then + echo "branch=staging/web" >> $GITHUB_OUTPUT + else + echo "branch=main" >> $GITHUB_OUTPUT + fi + + - name: Checkout ${{ steps.select-branch.outputs.branch }} uses: actions/checkout@v4 with: + ref: ${{ steps.select-branch.outputs.branch }} submodules: recursive - name: Setup node and enable yarn caching diff --git a/auth/assets/custom-icons/_data/custom-icons.json b/auth/assets/custom-icons/_data/custom-icons.json index 2591c14ee4..82b5334970 100644 --- a/auth/assets/custom-icons/_data/custom-icons.json +++ b/auth/assets/custom-icons/_data/custom-icons.json @@ -39,11 +39,15 @@ { "title": "Bloom Host", "slug": "bloom_host", - "altNames": ["Bloom Host Billing"] + "altNames": [ + "Bloom Host Billing" + ] }, { "title": "BorgBase", - "altNames": ["borg"], + "altNames": [ + "borg" + ], "slug": "BorgBase" }, { @@ -67,6 +71,13 @@ { "title": "Cloudflare" }, + { + "title": "CloudAMQP" + }, + { + "title": "ConfigCat", + "slug": "configcat" + }, { "title": "CoinDCX" }, @@ -83,7 +94,9 @@ }, { "title": "DCS", - "altNames": ["Digital Combat Simulator"], + "altNames": [ + "Digital Combat Simulator" + ], "slug": "dcs" }, { @@ -143,7 +156,9 @@ }, { "title": "Gosuslugi", - "altNames": ["Госуслуги"], + "altNames": [ + "Госуслуги" + ], "slug": "Gosuslugi" }, { @@ -219,7 +234,11 @@ { "title": "Local", "slug": "local_wp", - "altNames": ["LocalWP", "Local WP", "Local Wordpress"] + "altNames": [ + "LocalWP", + "Local WP", + "Local Wordpress" + ] }, { "title": "Marketplace.tf", @@ -227,14 +246,23 @@ }, { "title": "Mastodon", - "altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"], + "altNames": [ + "mstdn", + "fediscience", + "mathstodon", + "fosstodon" + ], "slug": "mastodon", "hex": "6364FF" }, { "title": "Mercado Livre", "slug": "mercado_livre", - "altNames": ["Mercado Libre", "MercadoLibre", "MercadoLivre"] + "altNames": [ + "Mercado Libre", + "MercadoLibre", + "MercadoLivre" + ] }, { "title": "Microsoft" @@ -250,7 +278,9 @@ }, { "title": "Murena", - "altNames": ["eCloud"], + "altNames": [ + "eCloud" + ], "slug": "ecloud" }, { @@ -277,6 +307,10 @@ { "title": "Notion" }, + { + "title": "NuCommunity", + "slug": "nucommunity" + }, { "title": "NVIDIA" }, @@ -335,8 +369,16 @@ "slug": "real_debrid" }, { - "title": "Registro.br", - "slug": "registro_br" + "title": "Registro br", + "slug": "registro_br", + "altNames": [ + "Registro br", + "registrobr", + "Registro.br" + ] + }, + { + "title": "Render" }, { "title": "Revolt", @@ -355,6 +397,9 @@ "slug": "rust_language_forum", "hex": "000000" }, + { + "title": "Samsung" + }, { "title": "Sendgrid" }, @@ -396,7 +441,10 @@ }, { "title": "Techlore", - "altNames": ["Techlore Courses", "Techlore Forums"] + "altNames": [ + "Techlore Courses", + "Techlore Forums" + ] }, { "title": "Termius", @@ -426,6 +474,13 @@ "title": "Twingate", "hex": "858585" }, + { + "title": "Twitch", + "altNames": [ + "Twitch.tv", + "Twitch tv" + ] + }, { "title": "Ubisoft", "hex": "4285f4" @@ -460,23 +515,32 @@ { "title": "WorkOS", "slug": "workos", - "altNames": ["Work OS"] + "altNames": [ + "Work OS" + ] }, { "title": "X", - "altNames": ["twitter"], + "altNames": [ + "twitter" + ], "slug": "x" }, { "title": "Yandex", - "altNames": ["Ya", "Яндекс"], + "altNames": [ + "Ya", + "Яндекс" + ], "slug": "Yandex" }, { "title": "YNAB", - "altNames": ["You Need A Budget"], + "altNames": [ + "You Need A Budget" + ], "slug": "ynab", "hex": "3B5EDA" } ] -} +} \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/cloudamqp.svg b/auth/assets/custom-icons/icons/cloudamqp.svg new file mode 100644 index 0000000000..65ec3aebd2 --- /dev/null +++ b/auth/assets/custom-icons/icons/cloudamqp.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/hivelocity.svg b/auth/assets/custom-icons/icons/hivelocity.svg index b84451a4c2..81aa936583 100644 --- a/auth/assets/custom-icons/icons/hivelocity.svg +++ b/auth/assets/custom-icons/icons/hivelocity.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/auth/assets/custom-icons/icons/instagram.svg b/auth/assets/custom-icons/icons/instagram.svg index c068893628..65ca0e53d9 100644 --- a/auth/assets/custom-icons/icons/instagram.svg +++ b/auth/assets/custom-icons/icons/instagram.svg @@ -1,1562 +1 @@ - - + diff --git a/auth/assets/custom-icons/icons/kick.svg b/auth/assets/custom-icons/icons/kick.svg index 29122ec7e7..67b065a96c 100644 --- a/auth/assets/custom-icons/icons/kick.svg +++ b/auth/assets/custom-icons/icons/kick.svg @@ -1,3 +1,3 @@ - - + + diff --git a/auth/assets/custom-icons/icons/kraken.svg b/auth/assets/custom-icons/icons/kraken.svg index f38ee454c4..f46c926724 100644 --- a/auth/assets/custom-icons/icons/kraken.svg +++ b/auth/assets/custom-icons/icons/kraken.svg @@ -1,3 +1 @@ - - - + diff --git a/auth/assets/custom-icons/icons/kucoin.svg b/auth/assets/custom-icons/icons/kucoin.svg index 076ec5c4f5..1b67b54717 100644 --- a/auth/assets/custom-icons/icons/kucoin.svg +++ b/auth/assets/custom-icons/icons/kucoin.svg @@ -1,3 +1 @@ - - - + diff --git a/auth/assets/custom-icons/icons/nucommunity.svg b/auth/assets/custom-icons/icons/nucommunity.svg new file mode 100644 index 0000000000..29f040cbe7 --- /dev/null +++ b/auth/assets/custom-icons/icons/nucommunity.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/plutus.svg b/auth/assets/custom-icons/icons/plutus.svg index 6a88dc417a..4a5d52e88c 100644 --- a/auth/assets/custom-icons/icons/plutus.svg +++ b/auth/assets/custom-icons/icons/plutus.svg @@ -1,3 +1 @@ - - - + diff --git a/auth/assets/custom-icons/icons/registro_br.svg b/auth/assets/custom-icons/icons/registro_br.svg index a719a6b975..fa5b9cb810 100644 --- a/auth/assets/custom-icons/icons/registro_br.svg +++ b/auth/assets/custom-icons/icons/registro_br.svg @@ -1,83 +1,7 @@ - - - - - - image/svg+xml - - - - - - - - - - + + + + + + diff --git a/auth/assets/custom-icons/icons/render.svg b/auth/assets/custom-icons/icons/render.svg new file mode 100644 index 0000000000..68cdd931f5 --- /dev/null +++ b/auth/assets/custom-icons/icons/render.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/auth/assets/custom-icons/icons/samsung.svg b/auth/assets/custom-icons/icons/samsung.svg new file mode 100644 index 0000000000..1c622af77d --- /dev/null +++ b/auth/assets/custom-icons/icons/samsung.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/auth/assets/custom-icons/icons/standardnotes.svg b/auth/assets/custom-icons/icons/standardnotes.svg index 89fb3ed578..026cf96c1f 100644 --- a/auth/assets/custom-icons/icons/standardnotes.svg +++ b/auth/assets/custom-icons/icons/standardnotes.svg @@ -1,3 +1 @@ - - - + diff --git a/auth/assets/custom-icons/icons/twitch.svg b/auth/assets/custom-icons/icons/twitch.svg new file mode 100644 index 0000000000..c64a9b9450 --- /dev/null +++ b/auth/assets/custom-icons/icons/twitch.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/auth/docs/adding-icons.md b/auth/docs/adding-icons.md index 6a50fb9f77..2d4c4f2c0d 100644 --- a/auth/docs/adding-icons.md +++ b/auth/docs/adding-icons.md @@ -7,6 +7,15 @@ If you would like to add your own custom icon, please open a pull-request with the relevant SVG placed within `assets/custom-icons/icons` and add the corresponding entry within `assets/custom-icons/_data/custom-icons.json`. +Please be careful to upload small and optimized icon files. If your icon file +is over 50KB, it is likely not optimized. + +Note that the correspondence between the icon and the issuer is based on the name +of the issuer provided by the user, excluding spaces. Only the text before the +first dot "." or left parentheses "(" will be used for icon matching. +e.g. Issuer name provided: "github.com (Main account)" - Then "github" will be +used for matching. + This JSON file contains the following attributes: | Attribute | Usecase | Required | diff --git a/auth/lib/core/network.dart b/auth/lib/core/network.dart index c14c9e758b..0efa09fb5d 100644 --- a/auth/lib/core/network.dart +++ b/auth/lib/core/network.dart @@ -1,8 +1,10 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/event_bus.dart'; +import 'package:ente_auth/core/win_http_client.dart'; import 'package:ente_auth/events/endpoint_updated_event.dart'; import 'package:ente_auth/utils/package_info_util.dart'; import 'package:ente_auth/utils/platform_util.dart'; @@ -50,6 +52,19 @@ class Network { }, ), ); + if (Platform.isWindows) { + final customHttpClient = windowsHttpClient(); + _enteDio.httpClientAdapter = IOHttpClientAdapter( + createHttpClient: () { + return customHttpClient; + }, + ); + _dio.httpClientAdapter = IOHttpClientAdapter( + createHttpClient: () { + return customHttpClient; + }, + ); + } _setupInterceptors(endpoint); Bus.instance.on().listen((event) { diff --git a/auth/lib/core/win_http_client.dart b/auth/lib/core/win_http_client.dart new file mode 100644 index 0000000000..c5c6e0d360 --- /dev/null +++ b/auth/lib/core/win_http_client.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +/* +Reference from +https://github.com/realm/realm-dart/blob/main/packages/realm_dart/lib/src/handles/native/default_client.dart +https://github.com/realm/realm-dart/pull/1378 + */ +HttpClient windowsHttpClient() { + final logger = Logger("WindowsHttpClient"); + const isrgRootX1CertPEM = // The root certificate used by lets encrypt + ''' +subject=CN=ISRG Root X1,O=Internet Security Research Group,C=US +issuer=CN=DST Root CA X3,O=Digital Signature Trust Co. +-----BEGIN CERTIFICATE----- +MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC +ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL +wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D +LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK +4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5 +bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y +sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ +Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4 +FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc +SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql +PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND +TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1 +c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx ++tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB +ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu +b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E +U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu +MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC +5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW +9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG +WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O +he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC +Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 +-----END CERTIFICATE-----'''; + + if (Platform.isWindows) { + final context = SecurityContext(withTrustedRoots: true); + try { + logger.info('Adding certificate to trusted certificates'); + context.setTrustedCertificatesBytes( + const AsciiEncoder().convert(isrgRootX1CertPEM), + ); + logger.info("Certificate added to trusted certificates"); + return HttpClient(context: context); + } on TlsException catch (e) { + logger.warning( + "Error adding certificate to trusted certificates: ${e.osError?.message}", + ); + // certificate is already trusted. Nothing to do here + if (e.osError?.message.contains("CERT_ALREADY_IN_HASH_TABLE") != true) { + rethrow; + } else { + return HttpClient(); + } + } + } + throw UnsupportedError("This platform is not supported"); +} diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index 58ad079a0b..0c2f9a0de4 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -263,6 +263,8 @@ "exportLogs": "Export logs", "enterYourRecoveryKey": "Enter your recovery key", "tempErrorContactSupportIfPersists": "It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team.", + "networkHostLookUpErr": "Unable to connect to Ente, please check your network settings and contact support if the error persists.", + "networkConnectionRefusedErr": "Unable to connect to Ente, please retry after sometime. If the error persists, please contact support.", "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team.", "about": "About", "weAreOpenSource": "We are open source!", diff --git a/auth/lib/services/passkey_service.dart b/auth/lib/services/passkey_service.dart index 2be2bb1a02..4d7e9badd5 100644 --- a/auth/lib/services/passkey_service.dart +++ b/auth/lib/services/passkey_service.dart @@ -49,7 +49,7 @@ class PasskeyService { ); } catch (e) { Logger('PasskeyService').severe("failed to open passkey page", e); - showGenericErrorDialog(context: context).ignore(); + showGenericErrorDialog(context: context, error: e).ignore(); } } } diff --git a/auth/lib/services/user_service.dart b/auth/lib/services/user_service.dart index 182dee2d25..ca88796f15 100644 --- a/auth/lib/services/user_service.dart +++ b/auth/lib/services/user_service.dart @@ -101,7 +101,7 @@ class UserService { ); return; } - unawaited(showGenericErrorDialog(context: context)); + unawaited(showGenericErrorDialog(context: context, error: null)); } on DioException catch (e) { await dialog.hide(); _logger.info(e); @@ -114,12 +114,12 @@ class UserService { ), ); } else { - unawaited(showGenericErrorDialog(context: context)); + unawaited(showGenericErrorDialog(context: context, error: e)); } } catch (e) { await dialog.hide(); _logger.severe(e); - unawaited(showGenericErrorDialog(context: context)); + unawaited(showGenericErrorDialog(context: context, error: e)); } } @@ -227,7 +227,7 @@ class UserService { //to close and only then to show the error dialog. Future.delayed( const Duration(milliseconds: 150), - () => showGenericErrorDialog(context: context), + () => showGenericErrorDialog(context: context, error: e), ); rethrow; } @@ -248,7 +248,10 @@ class UserService { } } catch (e) { _logger.severe(e); - await showGenericErrorDialog(context: context); + await showGenericErrorDialog( + context: context, + error: e, + ); return null; } } diff --git a/auth/lib/ui/account/password_entry_page.dart b/auth/lib/ui/account/password_entry_page.dart index 9b1ce61812..b7e3e14694 100644 --- a/auth/lib/ui/account/password_entry_page.dart +++ b/auth/lib/ui/account/password_entry_page.dart @@ -412,7 +412,10 @@ class _PasswordEntryPageState extends State { _logger.severe(e, s); await dialog.hide(); // ignore: unawaited_futures - showGenericErrorDialog(context: context); + showGenericErrorDialog( + context: context, + error: e, + ); } } @@ -472,7 +475,10 @@ class _PasswordEntryPageState extends State { _logger.severe(e, s); await dialog.hide(); // ignore: unawaited_futures - showGenericErrorDialog(context: context); + showGenericErrorDialog( + context: context, + error: e, + ); } } @@ -500,7 +506,10 @@ class _PasswordEntryPageState extends State { ); } else { // ignore: unawaited_futures - showGenericErrorDialog(context: context); + showGenericErrorDialog( + context: context, + error: e, + ); } } } diff --git a/auth/lib/ui/account/verify_recovery_page.dart b/auth/lib/ui/account/verify_recovery_page.dart index 03ed81fdf0..bb4ebb068a 100644 --- a/auth/lib/ui/account/verify_recovery_page.dart +++ b/auth/lib/ui/account/verify_recovery_page.dart @@ -47,7 +47,10 @@ class _VerifyRecoveryPageState extends State { "Please check your internet connection and try again.", ); } else { - await showGenericErrorDialog(context: context); + await showGenericErrorDialog( + context: context, + error: e, + ); } return; } @@ -107,7 +110,10 @@ class _VerifyRecoveryPageState extends State { ); } catch (e) { // ignore: unawaited_futures - showGenericErrorDialog(context: context); + showGenericErrorDialog( + context: context, + error: e, + ); return; } } diff --git a/auth/lib/ui/components/buttons/button_widget.dart b/auth/lib/ui/components/buttons/button_widget.dart index 1932cc02bd..9e589cd7a0 100644 --- a/auth/lib/ui/components/buttons/button_widget.dart +++ b/auth/lib/ui/components/buttons/button_widget.dart @@ -485,7 +485,10 @@ class _ButtonChildWidgetState extends State { } else if (exception != null) { //This is to show the execution was unsuccessful if the dialog is manually //closed before the execution completes. - showGenericErrorDialog(context: context); + showGenericErrorDialog( + context: context, + error: exception, + ); } } diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index 81fd7ed586..ee78399944 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -56,7 +56,8 @@ class _HomePageState extends State { final scaffoldKey = GlobalKey(); final TextEditingController _textController = TextEditingController(); - final FocusNode searchInputFocusNode = FocusNode(); + final bool _autoFocusSearch = + PreferenceService.instance.shouldAutoFocusOnSearchBar(); bool _showSearchBox = false; String _searchText = ""; List? _allCodes; @@ -87,18 +88,7 @@ class _HomePageState extends State { _iconsChangedEvent = Bus.instance.on().listen((event) { setState(() {}); }); - _showSearchBox = PreferenceService.instance.shouldAutoFocusOnSearchBar(); - if (_showSearchBox) { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - // https://github.com/flutter/flutter/issues/20706#issuecomment-646328652 - FocusScope.of(context).unfocus(); - Timer(const Duration(milliseconds: 1), () { - FocusScope.of(context).requestFocus(searchInputFocusNode); - }); - }, - ); - } + _showSearchBox = _autoFocusSearch; } void _loadCodes() { @@ -240,8 +230,7 @@ class _HomePageState extends State { : TextField( autocorrect: false, enableSuggestions: false, - focusNode: searchInputFocusNode, - autofocus: _searchText.isEmpty, + autofocus: _autoFocusSearch, controller: _textController, onChanged: (val) { _searchText = val; @@ -460,7 +449,10 @@ class _HomePageState extends State { CodeStore.instance.addCode(newCode); _focusNewCode(newCode); } catch (e, s) { - showGenericErrorDialog(context: context); + showGenericErrorDialog( + context: context, + error: e, + ); _logger.severe("error while handling deeplink", e, s); } } diff --git a/auth/lib/ui/passkey_page.dart b/auth/lib/ui/passkey_page.dart index eab0b72f99..4181c474e9 100644 --- a/auth/lib/ui/passkey_page.dart +++ b/auth/lib/ui/passkey_page.dart @@ -69,7 +69,7 @@ class _PasskeyPageState extends State { return; } catch (e, s) { _logger.severe("failed to check status", e, s); - showGenericErrorDialog(context: context).ignore(); + showGenericErrorDialog(context: context, error: e).ignore(); return; } await UserService.instance.onPassKeyVerified(context, response); @@ -111,7 +111,7 @@ class _PasskeyPageState extends State { } } catch (e, s) { _logger.severe('passKey: failed to handle deeplink', e, s); - showGenericErrorDialog(context: context).ignore(); + showGenericErrorDialog(context: context, error: e).ignore(); } } @@ -169,7 +169,7 @@ class _PasskeyPageState extends State { await checkStatus(); } catch (e) { debugPrint('failed to check status %e'); - showGenericErrorDialog(context: context).ignore(); + showGenericErrorDialog(context: context, error: e).ignore(); } }, shouldSurfaceExecutionStates: true, diff --git a/auth/lib/ui/settings/account_section_widget.dart b/auth/lib/ui/settings/account_section_widget.dart index d51b2dd873..47e639c2e7 100644 --- a/auth/lib/ui/settings/account_section_widget.dart +++ b/auth/lib/ui/settings/account_section_widget.dart @@ -111,7 +111,10 @@ class AccountSectionWidget extends StatelessWidget { CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey()); } catch (e) { // ignore: unawaited_futures - showGenericErrorDialog(context: context); + showGenericErrorDialog( + context: context, + error: e, + ); return; } // ignore: unawaited_futures diff --git a/auth/lib/ui/settings/security_section_widget.dart b/auth/lib/ui/settings/security_section_widget.dart index 49672b0368..678a4ddfa1 100644 --- a/auth/lib/ui/settings/security_section_widget.dart +++ b/auth/lib/ui/settings/security_section_widget.dart @@ -182,7 +182,10 @@ class _SecuritySectionWidgetState extends State { PasskeyService.instance.openPasskeyPage(buildContext).ignore(); } catch (e, s) { _logger.severe("failed to open passkey page", e, s); - await showGenericErrorDialog(context: context); + await showGenericErrorDialog( + context: context, + error: e, + ); } } diff --git a/auth/lib/utils/dialog_util.dart b/auth/lib/utils/dialog_util.dart index d24608b783..24636bf889 100644 --- a/auth/lib/utils/dialog_util.dart +++ b/auth/lib/utils/dialog_util.dart @@ -13,6 +13,8 @@ import 'package:ente_auth/ui/components/components_constants.dart'; import 'package:ente_auth/ui/components/dialog_widget.dart'; import 'package:ente_auth/ui/components/models/button_result.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; +import 'package:ente_auth/utils/email_util.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; typedef DialogBuilder = DialogWidget Function(BuildContext context); @@ -69,22 +71,97 @@ Future showErrorDialogForException({ ); } +String parseErrorForUI( + BuildContext context, + String genericError, { + Object? error, + bool surfaceError = kDebugMode, +}) { + try { + if (error == null) { + return genericError; + } + if (error is DioException) { + final DioException dioError = error; + if (dioError.type == DioExceptionType.unknown) { + if (dioError.error.toString().contains('Failed host lookup')) { + return context.l10n.networkHostLookUpErr; + } else if (dioError.error.toString().contains('SocketException')) { + return context.l10n.networkConnectionRefusedErr; + } + } + } + // return generic error if the user is not internal and the error is not in debug mode + if (!kDebugMode) { + return genericError; + } + String errorInfo = ""; + if (error is DioException) { + final DioException dioError = error; + if (dioError.type == DioExceptionType.badResponse) { + if (dioError.response?.data["code"] != null) { + errorInfo = "Reason: " + dioError.response!.data["code"]; + } else { + errorInfo = "Reason: " + dioError.response!.data.toString(); + } + } else if (dioError.type == DioExceptionType.unknown) { + errorInfo = "Reason: " + dioError.error.toString(); + } else { + errorInfo = "Reason: " + dioError.type.toString(); + } + } else { + if (kDebugMode) { + errorInfo = error.toString(); + } else { + errorInfo = error.toString().split('Source stack')[0]; + } + } + if (errorInfo.isNotEmpty) { + return "$genericError\n\n$errorInfo"; + } + return genericError; + } catch (e) { + return genericError; + } +} + ///Will return null if dismissed by tapping outside Future showGenericErrorDialog({ required BuildContext context, bool isDismissible = true, + required Object? error, }) async { + final errorBody = parseErrorForUI( + context, + context.l10n.itLooksLikeSomethingWentWrongPleaseRetryAfterSome, + error: error, + ); + return showDialogWidget( context: context, title: context.l10n.error, icon: Icons.error_outline_outlined, - body: context.l10n.itLooksLikeSomethingWentWrongPleaseRetryAfterSome, + body: errorBody, isDismissible: isDismissible, - buttons: const [ + buttons: [ + ButtonWidget( + buttonType: ButtonType.primary, + labelText: context.l10n.ok, + buttonAction: ButtonAction.first, + isInAlert: true, + ), ButtonWidget( buttonType: ButtonType.secondary, - labelText: "OK", - isInAlert: true, + labelText: context.l10n.contactSupport, + buttonAction: ButtonAction.second, + onTap: () async { + await sendLogs( + context, + context.l10n.contactSupport, + "support@ente.io", + postShare: () {}, + ); + }, ), ], ); diff --git a/auth/linux/packaging/appimage/make_config.yaml b/auth/linux/packaging/appimage/make_config.yaml index 0b52ddf9e2..a41658fd17 100644 --- a/auth/linux/packaging/appimage/make_config.yaml +++ b/auth/linux/packaging/appimage/make_config.yaml @@ -1,6 +1,8 @@ display_name: Auth license: GPLv3 +metainfo: linux/packaging/ente_auth.appdata.xml + icon: assets/icons/auth-icon.png keywords: diff --git a/auth/linux/packaging/deb/make_config.yaml b/auth/linux/packaging/deb/make_config.yaml index 755024d2a8..7092994bd4 100644 --- a/auth/linux/packaging/deb/make_config.yaml +++ b/auth/linux/packaging/deb/make_config.yaml @@ -10,6 +10,8 @@ license: GPLv3 icon: assets/icons/auth-icon.png installed_size: 36000 +metainfo: linux/packaging/ente_auth.appdata.xml + dependencies: - libwebkit2gtk-4.0-37 - libsqlite3-0 diff --git a/auth/linux/packaging/ente_auth.appdata.xml b/auth/linux/packaging/ente_auth.appdata.xml new file mode 100644 index 0000000000..c218b5ce48 --- /dev/null +++ b/auth/linux/packaging/ente_auth.appdata.xml @@ -0,0 +1,31 @@ + + + ente_auth + CC0-1.0 + AGPL-3.0 + Ente Auth + Open source 2FA authenticator, with end-to-end encrypted backups + +

Auth provides end-to-end encrypted cloud backups so you don't have to worry about losing your tokens. Our cryptography has been externally audited.

+

Auth has an app for every platform. Mobile, desktop and web. Your codes sync across all your devices, end-to-end encrypted.

+

Auth also comes with Offline mode, tags, icons, pins, import/export and more

+
+ ente_auth.desktop + https://ente.io/auth + + + https://raw.githubusercontent.com/ente-io/ente/main/.github/assets/auth.png + + + + + + + ente_auth.desktop + + + + Ente.io Developers + + human@ente.io +
\ No newline at end of file diff --git a/auth/linux/packaging/pacman/make_config.yaml b/auth/linux/packaging/pacman/make_config.yaml new file mode 100644 index 0000000000..c27c1c703f --- /dev/null +++ b/auth/linux/packaging/pacman/make_config.yaml @@ -0,0 +1,58 @@ +display_name: Auth +package_name: auth +maintainer: + name: Ente.io Developers + email: human@ente.io +licenses: + - GPLv3 +icon: assets/icons/auth-icon.png +installed_size: 36000 + +metainfo: linux/packaging/ente_auth.appdata.xml + +dependencies: + - c-ares + - ffmpeg + - gtk3 + - http-parser + - libevent + - libvpx + - libxslt + - libxss + - minizip + - nss + - re2 + - snappy + - libnotify + - libappindicator-gtk3 + +keywords: + - Authentication + - 2FA + +generic_name: Ente Authentication + +categories: + - Utility + +supported_mime_type: + - x-scheme-handler/enteauth + +postinstall_scripts: + - gtk-update-icon-cache -q -t -f usr/share/icons/hicolor + - update-desktop-database -q + - if [ ! -e /usr/lib/libsodium.so.23 ]; then + - " ln -s /usr/lib/libsodium.so /usr/lib/libsodium.so.23" + - fi + +postupgrade_scripts: + - post_install + +postremove_scripts: + - gtk-update-icon-cache -q -t -f usr/share/icons/hicolor + - update-desktop-database -q + - if [ -e /usr/lib/libsodium.so.23 ]; then + - rm /usr/lib/libsodium.so.23 + - fi + +startup_notify: false \ No newline at end of file diff --git a/auth/linux/packaging/rpm/make_config.yaml b/auth/linux/packaging/rpm/make_config.yaml index 495f6482c6..c285b90b30 100644 --- a/auth/linux/packaging/rpm/make_config.yaml +++ b/auth/linux/packaging/rpm/make_config.yaml @@ -9,6 +9,8 @@ url: https://github.com/ente-io/ente display_name: Auth +metainfo: linux/packaging/ente_auth.appdata.xml + requires: - libsqlite3x - webkit2gtk4.0 diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 26cfb90996..211159b768 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 3.0.13+313 +version: 3.0.17+317 publish_to: none environment: diff --git a/cli/cmd/admin.go b/cli/cmd/admin.go index 8a2d7f006b..f56fea06eb 100644 --- a/cli/cmd/admin.go +++ b/cli/cmd/admin.go @@ -58,6 +58,27 @@ var _disable2faCmd = &cobra.Command{ }, } +var _disablePasskeyCmd = &cobra.Command{ + Use: "disable-passkey", + Short: "Disable passkey for a user", + RunE: func(cmd *cobra.Command, args []string) error { + recoverWithLog() + var flags = &model.AdminActionForUser{} + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if f.Name == "admin-user" { + flags.AdminEmail = f.Value.String() + } + if f.Name == "user" { + flags.UserEmail = f.Value.String() + } + }) + if flags.UserEmail == "" { + return fmt.Errorf("user email is required") + } + return ctrl.DisablePasskeys(context.Background(), *flags) + }, +} + var _deleteUser = &cobra.Command{ Use: "delete-user", Short: "Delete a user", @@ -130,11 +151,13 @@ func init() { _listUsers.Flags().StringP("admin-user", "a", "", "The email of the admin user. ") _disable2faCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. ") _disable2faCmd.Flags().StringP("user", "u", "", "The email of the user to disable 2FA for. (required)") + _disablePasskeyCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. ") + _disablePasskeyCmd.Flags().StringP("user", "u", "", "The email of the user to disable passkey for. (required)") _deleteUser.Flags().StringP("admin-user", "a", "", "The email of the admin user. ") _deleteUser.Flags().StringP("user", "u", "", "The email of the user to delete. (required)") _updateFreeUserStorage.Flags().StringP("admin-user", "a", "", "The email of the admin user.") _updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)") // add a flag with no value --no-limit _updateFreeUserStorage.Flags().String("no-limit", "True", "When true, sets 100TB as storage limit, and expiry to current date + 100 years") - _adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _updateFreeUserStorage, _listUsers, _deleteUser) + _adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _disablePasskeyCmd, _updateFreeUserStorage, _listUsers, _deleteUser) } diff --git a/cli/docs/generated/ente.md b/cli/docs/generated/ente.md index 4f85dd0980..c3f4a11338 100644 --- a/cli/docs/generated/ente.md +++ b/cli/docs/generated/ente.md @@ -25,4 +25,4 @@ ente [flags] * [ente export](ente_export.md) - Starts the export process * [ente version](ente_version.md) - Prints the current version -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_account.md b/cli/docs/generated/ente_account.md index 41c37b0547..d254b29685 100644 --- a/cli/docs/generated/ente_account.md +++ b/cli/docs/generated/ente_account.md @@ -16,4 +16,4 @@ Manage account settings * [ente account list](ente_account_list.md) - list configured accounts * [ente account update](ente_account_update.md) - Update an existing account's export directory -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_account_add.md b/cli/docs/generated/ente_account_add.md index 1e86ae12f7..26a314a9e3 100644 --- a/cli/docs/generated/ente_account_add.md +++ b/cli/docs/generated/ente_account_add.md @@ -20,4 +20,4 @@ ente account add [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_account_get-token.md b/cli/docs/generated/ente_account_get-token.md index 3d8814d7d1..40a21f1439 100644 --- a/cli/docs/generated/ente_account_get-token.md +++ b/cli/docs/generated/ente_account_get-token.md @@ -18,4 +18,4 @@ ente account get-token [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_account_list.md b/cli/docs/generated/ente_account_list.md index a7677eb855..3c9ded6737 100644 --- a/cli/docs/generated/ente_account_list.md +++ b/cli/docs/generated/ente_account_list.md @@ -16,4 +16,4 @@ ente account list [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_account_update.md b/cli/docs/generated/ente_account_update.md index 8d9c8d7e54..a1837529c3 100644 --- a/cli/docs/generated/ente_account_update.md +++ b/cli/docs/generated/ente_account_update.md @@ -19,4 +19,4 @@ ente account update [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_admin.md b/cli/docs/generated/ente_admin.md index 5ac72489d6..29f136f75b 100644 --- a/cli/docs/generated/ente_admin.md +++ b/cli/docs/generated/ente_admin.md @@ -17,8 +17,9 @@ Commands for admin actions like disable or enabling 2fa, bumping up the storage * [ente](ente.md) - CLI tool for exporting your photos from ente.io * [ente admin delete-user](ente_admin_delete-user.md) - Delete a user * [ente admin disable-2fa](ente_admin_disable-2fa.md) - Disable 2fa for a user +* [ente admin disable-passkey](ente_admin_disable-passkey.md) - Disable passkey for a user * [ente admin get-user-id](ente_admin_get-user-id.md) - Get user id * [ente admin list-users](ente_admin_list-users.md) - List all users * [ente admin update-subscription](ente_admin_update-subscription.md) - Update subscription for user -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_admin_delete-user.md b/cli/docs/generated/ente_admin_delete-user.md index a1d52a73d2..901430ff09 100644 --- a/cli/docs/generated/ente_admin_delete-user.md +++ b/cli/docs/generated/ente_admin_delete-user.md @@ -18,4 +18,4 @@ ente admin delete-user [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_admin_disable-2fa.md b/cli/docs/generated/ente_admin_disable-2fa.md index 23cd330800..40aedd38da 100644 --- a/cli/docs/generated/ente_admin_disable-2fa.md +++ b/cli/docs/generated/ente_admin_disable-2fa.md @@ -18,4 +18,4 @@ ente admin disable-2fa [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_admin_disable-passkey.md b/cli/docs/generated/ente_admin_disable-passkey.md new file mode 100644 index 0000000000..c315aeae04 --- /dev/null +++ b/cli/docs/generated/ente_admin_disable-passkey.md @@ -0,0 +1,21 @@ +## ente admin disable-passkey + +Disable passkey for a user + +``` +ente admin disable-passkey [flags] +``` + +### Options + +``` + -a, --admin-user string The email of the admin user. + -h, --help help for disable-passkey + -u, --user string The email of the user to disable passkey for. (required) +``` + +### SEE ALSO + +* [ente admin](ente_admin.md) - Commands for admin actions + +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_admin_get-user-id.md b/cli/docs/generated/ente_admin_get-user-id.md index 47d632abb6..3cbe42f2d7 100644 --- a/cli/docs/generated/ente_admin_get-user-id.md +++ b/cli/docs/generated/ente_admin_get-user-id.md @@ -18,4 +18,4 @@ ente admin get-user-id [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_admin_list-users.md b/cli/docs/generated/ente_admin_list-users.md index 635e8ec3cd..519dcaccd2 100644 --- a/cli/docs/generated/ente_admin_list-users.md +++ b/cli/docs/generated/ente_admin_list-users.md @@ -17,4 +17,4 @@ ente admin list-users [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_admin_update-subscription.md b/cli/docs/generated/ente_admin_update-subscription.md index d0fadcd2ba..341ccf5fe5 100644 --- a/cli/docs/generated/ente_admin_update-subscription.md +++ b/cli/docs/generated/ente_admin_update-subscription.md @@ -23,4 +23,4 @@ ente admin update-subscription [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_auth.md b/cli/docs/generated/ente_auth.md index e0e97d84fc..2fd67cb0f1 100644 --- a/cli/docs/generated/ente_auth.md +++ b/cli/docs/generated/ente_auth.md @@ -13,4 +13,4 @@ Authenticator commands * [ente](ente.md) - CLI tool for exporting your photos from ente.io * [ente auth decrypt](ente_auth_decrypt.md) - Decrypt authenticator export -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_auth_decrypt.md b/cli/docs/generated/ente_auth_decrypt.md index c9db6ea545..682ba1560e 100644 --- a/cli/docs/generated/ente_auth_decrypt.md +++ b/cli/docs/generated/ente_auth_decrypt.md @@ -16,4 +16,4 @@ ente auth decrypt [input] [output] [flags] * [ente auth](ente_auth.md) - Authenticator commands -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_export.md b/cli/docs/generated/ente_export.md index d809e06e46..e3f07bb3c3 100644 --- a/cli/docs/generated/ente_export.md +++ b/cli/docs/generated/ente_export.md @@ -16,4 +16,4 @@ ente export [flags] * [ente](ente.md) - CLI tool for exporting your photos from ente.io -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/docs/generated/ente_version.md b/cli/docs/generated/ente_version.md index 08f384b52f..e1a3d8163b 100644 --- a/cli/docs/generated/ente_version.md +++ b/cli/docs/generated/ente_version.md @@ -16,4 +16,4 @@ ente version [flags] * [ente](ente.md) - CLI tool for exporting your photos from ente.io -###### Auto generated by spf13/cobra on 6-May-2024 +###### Auto generated by spf13/cobra on 22-Jun-2024 diff --git a/cli/internal/api/admin.go b/cli/internal/api/admin.go index 9e0bcb90a7..3511876cd1 100644 --- a/cli/internal/api/admin.go +++ b/cli/internal/api/admin.go @@ -88,6 +88,29 @@ func (c *Client) Disable2Fa(ctx context.Context, userID int64) error { return nil } +func (c *Client) DisablePassKeyMFA(ctx context.Context, userID int64) error { + var res interface{} + + payload := map[string]interface{}{ + "userID": userID, + } + r, err := c.restClient.R(). + SetContext(ctx). + SetResult(&res). + SetBody(payload). + Post("/admin/user/disable-passkeys") + if err != nil { + return err + } + if r.IsError() { + return &ApiError{ + StatusCode: r.StatusCode(), + Message: r.String(), + } + } + return nil +} + func (c *Client) UpdateFreePlanSub(ctx context.Context, userDetails *models.UserDetails, storageInBytes int64, expiryTimeInMicro int64) error { var res interface{} if userDetails.Subscription.ProductID != "free" { diff --git a/cli/main.go b/cli/main.go index be14a8686c..5b65929aea 100644 --- a/cli/main.go +++ b/cli/main.go @@ -15,7 +15,7 @@ import ( "strings" ) -var AppVersion = "0.1.16" +var AppVersion = "0.1.17" func main() { cliDBPath, err := GetCLIConfigPath() diff --git a/cli/pkg/admin_actions.go b/cli/pkg/admin_actions.go index 0105cdc199..44af3c27a9 100644 --- a/cli/pkg/admin_actions.go +++ b/cli/pkg/admin_actions.go @@ -82,6 +82,27 @@ func (c *ClICtrl) Disable2FA(ctx context.Context, params model.AdminActionForUse return nil } +func (c *ClICtrl) DisablePasskeys(ctx context.Context, params model.AdminActionForUser) error { + accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail) + if err != nil { + return err + } + userDetails, err := c.Client.GetUserIdFromEmail(accountCtx, params.UserEmail) + if err != nil { + return err + } + err = c.Client.DisablePassKeyMFA(accountCtx, userDetails.User.ID) + if err != nil { + if apiErr, ok := err.(*api.ApiError); ok && apiErr.StatusCode == 400 && strings.Contains(apiErr.Message, "Token is too old") { + fmt.Printf("Error: Old admin token, please re-authenticate using `ente account add` \n") + return nil + } + return err + } + fmt.Println("Successfully disabled passkey for user") + return nil +} + func (c *ClICtrl) UpdateFreeStorage(ctx context.Context, params model.AdminActionForUser, noLimit bool) error { accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail) if err != nil { diff --git a/cli/pkg/store.go b/cli/pkg/store.go index aa866d5952..98121e80ac 100644 --- a/cli/pkg/store.go +++ b/cli/pkg/store.go @@ -12,9 +12,9 @@ import ( ) func GetDB(path string) (*bolt.DB, error) { - db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second}) + db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 5 * time.Second}) if err != nil { - log.Fatal(err) + log.Fatal(fmt.Sprintf("Failed to open db %s ", path), err) } return db, err } diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 50f69759da..bc5839370a 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -455,20 +455,51 @@ const allowExternalLinks = (webContents: WebContents) => /** * Allow uploading to arbitrary S3 buckets. * - * The files in the desktop app are served over the ente:// protocol. During - * testing or self-hosting, we might be using a S3 bucket that does not allow - * whitelisting a custom URI scheme. To avoid requiring the bucket to set an - * "Access-Control-Allow-Origin: *" or do a echo-back of `Origin`, we add a - * workaround here instead, intercepting the ACAO header and allowing `*`. + * The files in the desktop app are served over the ente:// protocol. When that + * is returned as the CORS allowed origin, "Access-Control-Allow-Origin: + * ente://app", CORS requests fail. + * + * Further, during testing or self-hosting, file uploads involve a redirection + * (This doesn't affect our production systems since we upload via a worker, + * See: [Note: Passing credentials for self-hosted file fetches]). + * + * In some cases, we might be using a S3 bucket that does not allow whitelisting + * a custom URI scheme. Echoing back the value of `Origin` (even if the bucket + * would allow us to) would also not work, since the browser sends `null` as the + * `Origin` for the redirected request (this is as per the CORS spec). So the + * only way in such cases would be to require the bucket to set an + * "Access-Control-Allow-Origin: *". + * + * To avoid these issues, we intercepting the ACAO header and set it to `*`. + * + * However, that cause problems with requests that use credentials since "*" is + * not a valid value in such cases. One such example is the HCaptcha requests + * made by Stripe when we initiate a payment within the desktop app: + * + * > Access to XMLHttpRequest at 'https://api2.hcaptcha.com/getcaptcha/xxx' from + * > origin 'https://newassets.hcaptcha.com' has been blocked by CORS policy: + * > The value of the 'Access-Control-Allow-Origin' header in the response must + * > not be the wildcard '*' when the request's credentials mode is 'include'. + * > The credentials mode of requests initiated by the XMLHttpRequest is + * > controlled by the withCredentials attribute. + * + * So we only do this workaround if there was either no ACAO specified in the + * response, or if the ACAO was "ente://app". */ const allowAllCORSOrigins = (webContents: WebContents) => webContents.session.webRequest.onHeadersReceived( ({ responseHeaders }, callback) => { const headers: NonNullable = {}; - for (const [key, value] of Object.entries(responseHeaders ?? {})) - if (key.toLowerCase() != "access-control-allow-origin") - headers[key] = value; + headers["Access-Control-Allow-Origin"] = ["*"]; + for (const [key, value] of Object.entries(responseHeaders ?? {})) + if (key.toLowerCase() == "access-control-allow-origin") { + headers["Access-Control-Allow-Origin"] = + value[0] == rendererURL ? ["*"] : value; + } else { + headers[key] = value; + } + callback({ responseHeaders: headers }); }, ); diff --git a/infra/staff/package.json b/infra/staff/package.json index 1ccba760e6..094e3cfb05 100644 --- a/infra/staff/package.json +++ b/infra/staff/package.json @@ -11,12 +11,15 @@ "preview": "vite preview" }, "dependencies": { + "date-fns": "^3.6.0", "react": "^18", + "react-datepicker": "^7.1.0", "react-dom": "^18", "react-toastify": "^10.0.5", "zod": "^3" }, "devDependencies": { + "@rollup/plugin-node-resolve": "^15.2.3", "@types/react": "^18", "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^7", @@ -29,7 +32,7 @@ "prettier": "^3", "prettier-plugin-organize-imports": "^3.2", "prettier-plugin-packagejson": "^2.5", - "typescript": "^5.4.5", + "typescript": "^5", "vite": "^5.2" }, "packageManager": "yarn@1.22.22" diff --git a/infra/staff/src/App.css b/infra/staff/src/App.css index 4e7c66ce7a..e080f6b3b2 100644 --- a/infra/staff/src/App.css +++ b/infra/staff/src/App.css @@ -19,6 +19,20 @@ border-radius: 5px; margin-top: 20px; } +#submitbtn { + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + background-color: #009879; + color: white; + border: none; + border-radius: 5px; + margin-top: 20px; +} + +#submitbtn:hover { + background-color: #007c6c; +} .fetch-button-container button:hover { background-color: #007c6c; @@ -122,3 +136,321 @@ button { .dropdown-menu button:hover { background-color: #f0f0f0; } +.modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #ffffff; + padding: 20px; + border: 1px solid #ccc; + border-radius: 5px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + z-index: 1000; + max-width: 80%; + max-height: 80%; + overflow: auto; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 10px; + border-bottom: 1px solid #ccc; +} + +.modal-header .close-btn { + cursor: pointer; + color: #777; + font-size: 20px; +} + +.modal-content { + margin-top: 10px; +} + +/* Styles for draggable modal */ +.modal.draggable { + cursor: move; +} + +.modal.draggable .modal-header { + cursor: move; +} +.popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: lightgreen; + padding: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + z-index: 1000; +} + +.popup-content { + display: flex; + flex-direction: column; +} + +.popup-content div { + margin-bottom: 10px; +} +:root { + --popup-bg-color-light: #fff; + --popup-bg-color-dark: #2c2c2c; + --popup-border-color-light: #ccc; + --popup-border-color-dark: #444; + --popup-text-color-light: #000; + --popup-text-color-dark: #fff; + --popup-shadow-light: rgba(0, 0, 0, 0.1); + --popup-shadow-dark: rgba(255, 255, 255, 0.1); +} + +.update-subscription-popup { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + transform: translate(-50%, -50%); + background-color: var(--popup-bg-color-light); + border: 1px solid var(--popup-border-color-light); + padding: 20px; + z-index: 1000; + box-shadow: 0px 0px 10px var(--popup-shadow-light); +} + +.popup-content { + display: flex; + flex-direction: column; +} + +.close-button { + align-self: flex-end; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--popup-text-color-light); +} + +.popup-content h2 { + margin-top: 0; +} + +.popup-content form label { + display: block; + margin-bottom: 10px; +} + +.popup-content form input, +.popup-content form select { + width: 100%; + padding: 8px; + margin-top: 5px; + background-color: var(--popup-bg-color-light); + color: var(--popup-text-color-light); + border: 1px solid var(--popup-border-color-light); +} + +.popup-content form button { + padding: 10px 15px; + margin-top: 10px; + cursor: pointer; + background-color: var(--popup-bg-color-light); + color: var(--popup-text-color-light); + border: 1px solid var(--popup-border-color-light); +} + +.custom-select { + position: relative; + width: 100%; +} + +.custom-select select { + width: 100%; + padding: 8px; + cursor: pointer; + background-color: var(--popup-bg-color-light); + color: var(--popup-text-color-light); + border: 1px solid var(--popup-border-color-light); + appearance: none; +} + +.custom-select::after { + content: "\25BC"; + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + pointer-events: none; +} + +.message { + margin-top: 10px; + padding: 10px; + border-radius: 5px; +} + +.message.error { + background-color: #f8d7da; + color: #721c24; +} + +.message.success { + background-color: #d4edda; + color: #155724; +} + +@media (prefers-color-scheme: dark) { + .update-subscription-popup { + background-color: var(--popup-bg-color-dark); + border-color: var(--popup-border-color-dark); + color: var(--popup-text-color-dark); + box-shadow: 0px 0px 10px var(--popup-shadow-dark); + } + + .close-button { + color: var(--popup-text-color-dark); + } + + .popup-content form input, + .popup-content form select { + background-color: var(--popup-bg-color-dark); + color: var(--popup-text-color-dark); + border: 1px solid var(--popup-border-color-dark); + } + + .popup-content form button { + background-color: var(--popup-bg-color-dark); + color: var(--popup-text-color-dark); + border: 1px solid var(--popup-border-color-dark); + } +} +:root { + --popup-bg-color-light: #fff; + --popup-bg-color-dark: #2c2c2c; + --popup-border-color-light: #ccc; + --popup-border-color-dark: #444; + --popup-text-color-light: #000; + --popup-text-color-dark: #fff; + --popup-shadow-light: rgba(0, 0, 0, 0.1); + --popup-shadow-dark: rgba(255, 255, 255, 0.1); +} + +.update-subscription-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--popup-bg-color-light); + border: 1px solid var(--popup-border-color-light); + padding: 20px; + z-index: 1000; + box-shadow: 0px 0px 10px var(--popup-shadow-light); +} + +.popup-content { + display: flex; + flex-direction: column; +} + +.close-button { + align-self: flex-end; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--popup-text-color-light); +} + +.popup-content h2 { + margin-top: 0; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 5px; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 8px; + background-color: var(--popup-bg-color-light); + color: var(--popup-text-color-light); + border: 1px solid var(--popup-border-color-light); + border-radius: 5px; + margin-top: 5px; +} + +.custom-select { + position: relative; + width: 100%; +} + +.custom-select select { + width: 100%; + padding: 8px; + cursor: pointer; + background-color: var(--popup-bg-color-light); + color: var(--popup-text-color-light); + border: 1px solid var(--popup-border-color-light); + appearance: none; +} + +.custom-select::after { + content: "\25BC"; + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + pointer-events: none; +} + +.message { + margin-top: 10px; + padding: 10px; + border-radius: 5px; +} + +.message.error { + background-color: #f8d7da; + color: #721c24; +} + +.message.success { + background-color: #d4edda; + color: #155724; +} + +@media (prefers-color-scheme: dark) { + .update-subscription-popup { + background-color: var(--popup-bg-color-dark); + border-color: var(--popup-border-color-dark); + color: var(--popup-text-color-dark); + box-shadow: 0px 0px 10px var(--popup-shadow-dark); + } + + .close-button { + color: var(--popup-text-color-dark); + } + + .form-group input, + .form-group select { + background-color: var(--popup-bg-color-dark); + color: var(--popup-text-color-dark); + border: 1px solid var(--popup-border-color-dark); + } + + .form-group button { + background-color: var(--popup-bg-color-dark); + color: var(--popup-text-color-dark); + border: 1px solid var(--popup-border-color-dark); + } +} diff --git a/infra/staff/src/App.tsx b/infra/staff/src/App.tsx index e27c24ccbf..aaac53273d 100644 --- a/infra/staff/src/App.tsx +++ b/infra/staff/src/App.tsx @@ -15,7 +15,7 @@ export const App: React.FC = () => { const [email, setEmail] = useState(""); const [userData, setUserData] = useState(null); const [error, setError] = useState(null); - const [isDataFetched, setIsDataFetched] = useState(false); // Track if data has been fetched successfully + const [isDataFetched, setIsDataFetched] = useState(false); useEffect(() => { const storedToken = localStorage.getItem("token"); @@ -46,11 +46,11 @@ export const App: React.FC = () => { console.log("API Response:", userDataResponse); setUserData(userDataResponse); setError(null); - setIsDataFetched(true); // Set to true when data is successfully fetched + setIsDataFetched(true); } catch (error) { console.error("Error fetching data:", error); setError((error as Error).message); - setIsDataFetched(false); // Set to false if there's an error fetching data + setIsDataFetched(false); } }; @@ -113,7 +113,7 @@ export const App: React.FC = () => { displayValue = value; } } else if (typeof value === "object" && value !== null) { - displayValue = JSON.stringify(value, null, 2); // Pretty print JSON + displayValue = JSON.stringify(value, null, 2); } else if (value === null) { displayValue = "null"; } else if ( @@ -124,7 +124,7 @@ export const App: React.FC = () => { } else if (typeof value === "undefined") { displayValue = "undefined"; } else { - displayValue = value as string; // Fallback for any other types + displayValue = value as string; } return ( @@ -157,7 +157,7 @@ export const App: React.FC = () => { const handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === "Enter") { - event.preventDefault(); // Prevent form submission + event.preventDefault(); fetchData().catch((error: unknown) => console.error("Fetch data error:", error), ); diff --git a/infra/staff/src/components/Sidebar.tsx b/infra/staff/src/components/Sidebar.tsx index 8e60b98f7c..a16aebf238 100644 --- a/infra/staff/src/components/Sidebar.tsx +++ b/infra/staff/src/components/Sidebar.tsx @@ -1,6 +1,7 @@ -import React, { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import "../App.css"; import { apiOrigin } from "../services/support"; +import UpdateSubscription from "./UpdateSubscription"; // Import the UpdateSubscription component interface SidebarProps { token: string; @@ -12,18 +13,36 @@ interface UserData { ID: string; }; } +interface ActionResponse { + success?: boolean; + message?: string; +} export const Sidebar: React.FC = ({ token, email }) => { - const [, /*userId*/ setUserId] = useState(null); + const [userId, setUserId] = useState(null); const [error, setError] = useState(null); const [message, setMessage] = useState(null); const [dropdownVisible, setDropdownVisible] = useState(false); + const [showUpdateSubscription, setShowUpdateSubscription] = + useState(false); // State to control UpdateSubscription popup - interface ApiResponse { - data: { - userId: string; + const dropdownRef = useRef(null); + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); }; - } + }, []); + + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setDropdownVisible(false); + } + }; const fetchData = async (): Promise => { if (!email || !token) { @@ -32,9 +51,7 @@ export const Sidebar: React.FC = ({ token, email }) => { } try { - const url = `${apiOrigin}/admin/user?email=${encodeURIComponent( - email, - )}&token=${encodeURIComponent(token)}`; + const url = `${apiOrigin}/admin/user?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`; const response = await fetch(url); if (!response.ok) { throw new Error("Network response was not ok"); @@ -70,9 +87,7 @@ export const Sidebar: React.FC = ({ token, email }) => { Closefamily: "/admin/user/close-family", }; - const url = `${apiOrigin}${actionUrls[action]}?id=${encodeURIComponent( - userId, - )}&token=${encodeURIComponent(token)}`; + const url = `${apiOrigin}${actionUrls[action]}?id=${encodeURIComponent(userId)}&token=${encodeURIComponent(token)}`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -85,7 +100,7 @@ export const Sidebar: React.FC = ({ token, email }) => { ); } - const result = (await response.json()) as ApiResponse; + const result = (await response.json()) as ActionResponse; console.log("API Response:", result); setMessage(`${action} completed successfully`); @@ -109,8 +124,55 @@ export const Sidebar: React.FC = ({ token, email }) => { } }; + const deleteUser = async () => { + try { + const url = `${apiOrigin}/admin/user/delete?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`; + const response = await fetch(url, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error( + `Network response was not ok: ${response.status}`, + ); + } + + setMessage("Delete Account completed successfully"); + setError(null); + setTimeout(() => { + setMessage(null); + }, 1000); + setDropdownVisible(false); + } catch (error) { + console.error(`Error deleting account:`, error); + setError( + error instanceof Error && typeof error.message === "string" + ? error.message + : "An unexpected error occurred", + ); + + setTimeout(() => { + setError(null); + }, 1000); + setMessage(null); + } + }; + const handleActionClick = async (action: string) => { try { + if (action === "UpdateSubscription") { + const fetchedUserId = await fetchData(); + if (fetchedUserId) { + setShowUpdateSubscription(true); + } + return; + } + + if (action === "DeleteAccount") { + await deleteUser(); + return; + } + const fetchedUserId = await fetchData(); if (!fetchedUserId) { throw new Error("Incorrect email id or token"); @@ -140,6 +202,8 @@ export const Sidebar: React.FC = ({ token, email }) => { { value: "Disable2FA", label: "Disable 2FA" }, { value: "Closefamily", label: "Close Family" }, { value: "DisablePasskeys", label: "Disable Passkeys" }, + { value: "DeleteAccount", label: "Delete Account" }, + { value: "UpdateSubscription", label: "Update Subscription" }, // New option added here ]; return ( @@ -149,7 +213,7 @@ export const Sidebar: React.FC = ({ token, email }) => { MORE {dropdownVisible && ( -
+
    {dropdownOptions.map((option) => (
  • @@ -178,6 +242,13 @@ export const Sidebar: React.FC = ({ token, email }) => { {error ? `Error: ${error}` : `Success: ${message}`}
)} + {showUpdateSubscription && userId && ( + setShowUpdateSubscription(false)} + /> + )}
); }; diff --git a/infra/staff/src/components/UpdateSubscription.tsx b/infra/staff/src/components/UpdateSubscription.tsx new file mode 100644 index 0000000000..292d2947a5 --- /dev/null +++ b/infra/staff/src/components/UpdateSubscription.tsx @@ -0,0 +1,178 @@ +import React, { useEffect, useState } from "react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import "../App.css"; +interface UpdateSubscriptionProps { + token: string; + userId: string; + onClose: () => void; +} + +export const UpdateSubscription: React.FC = ({ + token, + userId, + onClose, +}) => { + const [expiryTime, setExpiryTime] = useState(null); + const [productId, setProductId] = useState("50gb_monthly"); + const [paymentProvider, setPaymentProvider] = useState("bitpay"); + const [transactionId, setTransactionId] = useState(""); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + const [storage, setStorage] = useState(""); + + useEffect(() => { + if (productId === "50gb_yearly" || productId === "50gb_monthly") { + setStorage(50 * 1024 * 1024 * 1024); + } else if ( + productId === "200gb_yearly" || + productId === "200gb_monthly" + ) { + setStorage(200 * 1024 * 1024 * 1024); + } else if ( + productId === "500gb_yearly" || + productId === "500gb_monthly" + ) { + setStorage(500 * 1024 * 1024 * 1024); + } else if ( + productId === "2000gb_yearly" || + productId === "2000gb_monthly" + ) { + setStorage(2000 * 1024 * 1024 * 1024); + } else { + setStorage(""); + } + }, [productId]); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + const expiryTimeTimestamp = expiryTime + ? expiryTime.getTime() * 1000 + : ""; + + const url = `http://localhost:8080/admin/user/subscription`; + const body = { + userId, + storage, + expiryTime: expiryTimeTimestamp, + productId, + paymentProvider, + transactionId, + }; + + try { + const response = await fetch(url, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-AUTH-TOKEN": token, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error( + `Network response was not ok: ${response.status}`, + ); + } + + setMessage("Subscription updated successfully"); + setError(null); + setTimeout(() => { + setMessage(null); + onClose(); + }, 1000); + } catch (error) { + console.error("Error updating subscription:", error); + setError( + error instanceof Error && typeof error.message === "string" + ? error.message + : "An unexpected error occurred", + ); + setTimeout(() => { + setError(null); + }, 1000); + } + }; + + const handleSubmitWrapper = (event: React.FormEvent) => { + handleSubmit(event).catch((error: unknown) => { + console.error("Error in handleSubmit:", error); + }); + }; + + return ( +
+
+ +

Update Subscription

+
+
+ + setExpiryTime(date)} + dateFormat="dd/MM/yyyy" + showYearDropdown + scrollableYearDropdown + yearDropdownItemNumber={15} + /> +
+
+ + +
+
+ + +
+
+ + setTransactionId(e.target.value)} + /> +
+ +
+ {(error ?? message) && ( +
+ {error ? `Error: ${error}` : `Success: ${message}`} +
+ )} +
+
+ ); +}; + +export default UpdateSubscription; diff --git a/infra/staff/yarn.lock b/infra/staff/yarn.lock index 928775dcf0..6e982d4676 100644 --- a/infra/staff/yarn.lock +++ b/infra/staff/yarn.lock @@ -23,7 +23,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz" integrity sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.24.5": +"@babel/core@^7.24.5": version "7.24.6" resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz" integrity sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ== @@ -208,11 +208,121 @@ "@babel/helper-validator-identifier" "^7.24.6" to-fast-properties "^2.0.0" +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + "@esbuild/darwin-x64@0.20.2": version "0.20.2" resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz" integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" @@ -245,6 +355,42 @@ resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@floating-ui/core@^1.0.0": + version "1.6.2" + resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz" + integrity sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg== + dependencies: + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/dom@^1.0.0": + version "1.6.5" + resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz" + integrity sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw== + dependencies: + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/react-dom@^2.1.0": + version "2.1.0" + resolved "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz" + integrity sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA== + dependencies: + "@floating-ui/dom" "^1.0.0" + +"@floating-ui/react@^0.26.2": + version "0.26.17" + resolved "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.17.tgz" + integrity sha512-ESD+jYWwqwVzaIgIhExrArdsCL1rOAzryG/Sjlu8yaD3Mtqi3uVyhbE2V7jD58Mo52qbzKz2eUY/Xgh5I86FCQ== + dependencies: + "@floating-ui/react-dom" "^2.1.0" + "@floating-ui/utils" "^0.2.0" + tabbable "^6.0.0" + +"@floating-ui/utils@^0.2.0": + version "0.2.2" + resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz" + integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz" @@ -304,7 +450,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -322,11 +468,107 @@ resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@rollup/plugin-node-resolve@^15.2.3": + version "15.2.3" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" + integrity sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-builtin-module "^3.2.1" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/pluginutils@^5.0.1": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" + integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + +"@rollup/rollup-android-arm-eabi@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" + integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ== + +"@rollup/rollup-android-arm64@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203" + integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA== + +"@rollup/rollup-darwin-arm64@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096" + integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w== + "@rollup/rollup-darwin-x64@4.18.0": version "4.18.0" resolved "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz" integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA== +"@rollup/rollup-linux-arm-gnueabihf@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8" + integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA== + +"@rollup/rollup-linux-arm-musleabihf@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549" + integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A== + +"@rollup/rollup-linux-arm64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577" + integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw== + +"@rollup/rollup-linux-arm64-musl@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c" + integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ== + +"@rollup/rollup-linux-powerpc64le-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf" + integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA== + +"@rollup/rollup-linux-riscv64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9" + integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg== + +"@rollup/rollup-linux-s390x-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec" + integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg== + +"@rollup/rollup-linux-x64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942" + integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w== + +"@rollup/rollup-linux-x64-musl@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d" + integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg== + +"@rollup/rollup-win32-arm64-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf" + integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA== + +"@rollup/rollup-win32-ia32-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54" + integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg== + +"@rollup/rollup-win32-x64-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" + integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== + "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" @@ -360,7 +602,7 @@ dependencies: "@babel/types" "^7.20.7" -"@types/estree@1.0.5": +"@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== @@ -385,6 +627,11 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + "@typescript-eslint/eslint-plugin@^7": version "7.11.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz" @@ -400,7 +647,7 @@ natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@^7", "@typescript-eslint/parser@^7.0.0": +"@typescript-eslint/parser@^7": version "7.11.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz" integrity sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg== @@ -487,7 +734,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: +acorn@^8.9.0: version "8.11.3" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== @@ -652,7 +899,7 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -browserslist@^4.22.2, "browserslist@>= 4.21.0": +browserslist@^4.22.2: version "4.23.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz" integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== @@ -662,6 +909,11 @@ browserslist@^4.22.2, "browserslist@>= 4.21.0": node-releases "^2.0.14" update-browserslist-db "^1.0.13" +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz" @@ -719,16 +971,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -780,6 +1032,11 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +date-fns@^3.3.1, date-fns@^3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.5" resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" @@ -792,6 +1049,11 @@ deep-is@^0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" @@ -1053,7 +1315,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -"eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^8, eslint@^8.56.0, eslint@>=7: +eslint@^8: version "8.57.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz" integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== @@ -1125,6 +1387,11 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" @@ -1468,6 +1735,13 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-builtin-module@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" @@ -1525,6 +1799,11 @@ is-map@^2.0.3: resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + is-negative-zero@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz" @@ -1735,21 +2014,7 @@ micromatch@^4.0.4: braces "^3.0.3" picomatch "^2.3.1" -minimatch@^3.0.5: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.1.2: +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -1952,12 +2217,12 @@ prettier-plugin-packagejson@^2.5: sort-package-json "2.10.0" synckit "0.9.0" -prettier@^3, "prettier@>= 1.16.0", prettier@>=2.0: +prettier@^3: version "3.3.0" resolved "https://registry.npmjs.org/prettier/-/prettier-3.3.0.tgz" integrity sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g== -prop-types@^15.8.1: +prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -1976,7 +2241,18 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -react-dom@^18, react-dom@>=18: +react-datepicker@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.1.0.tgz" + integrity sha512-Z91n5ybhmzI+YChj1ZG7ntPPOmHR2Dh4jbIl+mNgKXKoxyzUQBh7M3eQaFOwrBCVdKy5vsj370/ocQlGu1qsGA== + dependencies: + "@floating-ui/react" "^0.26.2" + clsx "^2.1.0" + date-fns "^3.3.1" + prop-types "^15.7.2" + react-onclickoutside "^6.13.0" + +react-dom@^18: version "18.3.1" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -1989,6 +2265,11 @@ react-is@^16.13.1: resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-onclickoutside@^6.13.0: + version "6.13.1" + resolved "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz" + integrity sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w== + react-refresh@^0.14.2: version "0.14.2" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz" @@ -2001,7 +2282,7 @@ react-toastify@^10.0.5: dependencies: clsx "^2.1.0" -react@^18, react@^18.3.1, react@>=18: +react@^18: version "18.3.1" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -2036,6 +2317,15 @@ resolve-from@^4.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.5: version "2.0.0-next.5" resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz" @@ -2288,6 +2578,11 @@ synckit@0.9.0: "@pkgr/core" "^0.1.0" tslib "^2.6.2" +tabbable@^6.0.0: + version "6.2.0" + resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" @@ -2371,7 +2666,7 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" -typescript@^5.4.5, typescript@>=2.9, typescript@>=4.2.0: +typescript@^5: version "5.4.5" resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== @@ -2401,7 +2696,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -"vite@^4.2.0 || ^5.0.0", vite@^5.2: +vite@^5.2: version "5.2.12" resolved "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz" integrity sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA== diff --git a/infra/workers/README.md b/infra/workers/README.md index 5ddd8fc393..dd4da132d5 100644 --- a/infra/workers/README.md +++ b/infra/workers/README.md @@ -3,24 +3,26 @@ Source code for our [Cloudflare Workers](https://developers.cloudflare.com/workers/). -Each worker is a self contained directory with its each `package.json`. +Workers are organized as Yarn workspaces sharing a common `package.json` and +base `tsconfig`. They can however be deployed individually. ## Deploying -- Switch to a worker directory, e.g. `cd github-discord-notifier`. +Install dependencies with `yarn`. -- Install dependencies (if needed) with `yarn` +> If you have previously deployed, then you will have an old `yarn.lock`. In +> this case it is safe to delete and recreate using `rm yarn.lock && yarn`. - > If you have previously deployed, then you will have an old `yarn.lock`. In - > this case it is safe to delete and recreate using `rm yarn.lock && yarn`. +Then, to deploy an individual worker -- Login into wrangler (if needed) using `yarn wrangler login` +- Login into wrangler (if needed) using + `yarn workspace health-check wrangler login` -- Deploy! `yarn wrangler deploy` +- Deploy! `yarn workspace health-check wrangler deploy` Wrangler is the CLI provided by Cloudflare to manage workers. Apart from deploying, it also allows us to stream logs from running workers by using -`yarn wrangler tail`. +`yarn workspace wrangler tail`. ## Creating a new worker diff --git a/infra/workers/cast-albums/package.json b/infra/workers/cast-albums/package.json index 63995e4592..8cfb5ffc8b 100644 --- a/infra/workers/cast-albums/package.json +++ b/infra/workers/cast-albums/package.json @@ -1,10 +1,5 @@ { "name": "cast-albums", - "private": true, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3" - }, - "packageManager": "yarn@1.22.22" + "version": "0.0.0", + "private": true } diff --git a/infra/workers/files/package.json b/infra/workers/files/package.json index 4ddcb5f853..11decac65e 100644 --- a/infra/workers/files/package.json +++ b/infra/workers/files/package.json @@ -1,10 +1,5 @@ { "name": "files", - "private": true, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3" - }, - "packageManager": "yarn@1.22.22" + "version": "0.0.0", + "private": true } diff --git a/infra/workers/github-discord-notifier/package.json b/infra/workers/github-discord-notifier/package.json index 133c633fbb..84476af12c 100644 --- a/infra/workers/github-discord-notifier/package.json +++ b/infra/workers/github-discord-notifier/package.json @@ -1,10 +1,5 @@ { "name": "github-discord-notifier", - "private": true, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3" - }, - "packageManager": "yarn@1.22.22" + "version": "0.0.0", + "private": true } diff --git a/infra/workers/health-check/package.json b/infra/workers/health-check/package.json index 73802a826b..2f3126e72c 100644 --- a/infra/workers/health-check/package.json +++ b/infra/workers/health-check/package.json @@ -1,10 +1,5 @@ { "name": "health-check", - "private": true, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3" - }, - "packageManager": "yarn@1.22.22" + "version": "0.0.0", + "private": true } diff --git a/infra/workers/package.json b/infra/workers/package.json new file mode 100644 index 0000000000..f754c98504 --- /dev/null +++ b/infra/workers/package.json @@ -0,0 +1,13 @@ +{ + "name": "workers", + "private": true, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240614.0", + "typescript": "^5", + "wrangler": "^3" + }, + "workspaces": [ + "*" + ], + "packageManager": "yarn@1.22.22" +} diff --git a/infra/workers/public-albums/package.json b/infra/workers/public-albums/package.json index 946f42689f..f3e54c8d64 100644 --- a/infra/workers/public-albums/package.json +++ b/infra/workers/public-albums/package.json @@ -1,10 +1,5 @@ { "name": "public-albums", - "private": true, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3" - }, - "packageManager": "yarn@1.22.22" + "version": "0.0.0", + "private": true } diff --git a/infra/workers/sentry-reporter/package.json b/infra/workers/sentry-reporter/package.json index 4ddcb5f853..be9f3ca908 100644 --- a/infra/workers/sentry-reporter/package.json +++ b/infra/workers/sentry-reporter/package.json @@ -1,10 +1,5 @@ { - "name": "files", - "private": true, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3" - }, - "packageManager": "yarn@1.22.22" + "name": "sentry-reporter", + "version": "0.0.0", + "private": true } diff --git a/infra/workers/tail/package.json b/infra/workers/tail/package.json index 2ec6898e4f..733bd57b0e 100644 --- a/infra/workers/tail/package.json +++ b/infra/workers/tail/package.json @@ -1,10 +1,5 @@ { "name": "tail", - "private": true, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3" - }, - "packageManager": "yarn@1.22.22" + "version": "0.0.0", + "private": true } diff --git a/infra/workers/thumbnails/package.json b/infra/workers/thumbnails/package.json index e5107655bc..73e529c402 100644 --- a/infra/workers/thumbnails/package.json +++ b/infra/workers/thumbnails/package.json @@ -1,10 +1,5 @@ { "name": "thumbnails", - "private": true, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3" - }, - "packageManager": "yarn@1.22.22" + "version": "0.0.0", + "private": true } diff --git a/infra/workers/uploader/package.json b/infra/workers/uploader/package.json index e22b4eb1fc..d7d955eea4 100644 --- a/infra/workers/uploader/package.json +++ b/infra/workers/uploader/package.json @@ -1,10 +1,5 @@ { "name": "uploader", - "private": true, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3" - }, - "packageManager": "yarn@1.22.22" + "version": "0.0.0", + "private": true } diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 8463f904c4..fd11b0a5b1 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -35,9 +35,9 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.24.0): + - FirebaseCoreInternal (10.28.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.24.0): + - FirebaseInstallations (10.28.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -83,29 +83,29 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.13.0): + - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/Environment (7.13.3): - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.0): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/Network (7.13.0): + - GoogleUtilities/Network (7.13.3): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.13.0)": + - "GoogleUtilities/NSData+zlib (7.13.3)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.0) - - GoogleUtilities/Reachability (7.13.0): + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (7.13.0): + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy - home_widget (0.0.1): @@ -157,12 +157,12 @@ PODS: - nanopb/encode (2.30910.0) - onnxruntime (0.0.1): - Flutter - - onnxruntime-objc (= 1.15.1) - - onnxruntime-c (1.15.1) - - onnxruntime-objc (1.15.1): - - onnxruntime-objc/Core (= 1.15.1) - - onnxruntime-objc/Core (1.15.1): - - onnxruntime-c (= 1.15.1) + - onnxruntime-objc (= 1.18.0) + - onnxruntime-c (1.18.0) + - onnxruntime-objc (1.18.0): + - onnxruntime-objc/Core (= 1.18.0) + - onnxruntime-objc/Core (1.18.0): + - onnxruntime-c (= 1.18.0) - open_mail_app (0.0.1): - Flutter - OrderedSet (5.0.0) @@ -181,9 +181,9 @@ PODS: - Flutter - screen_brightness_ios (0.1.0): - Flutter - - SDWebImage (5.19.1): - - SDWebImage/Core (= 5.19.1) - - SDWebImage/Core (5.19.1) + - SDWebImage (5.19.2): + - SDWebImage/Core (= 5.19.2) + - SDWebImage/Core (5.19.2) - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) @@ -427,8 +427,8 @@ SPEC CHECKSUMS: firebase_core: 66b99b4fb4e5d7cc4e88d4c195fe986681f3466a firebase_messaging: 0eb0425d28b4f4af147cdd4adcaf7c0100df28ed FirebaseCore: 11dc8a16dfb7c5e3c3f45ba0e191a33ac4f50894 - FirebaseCoreInternal: bcb5acffd4ea05e12a783ecf835f2210ce3dc6af - FirebaseInstallations: 8f581fca6478a50705d2bd2abd66d306e0f5736e + FirebaseCoreInternal: 58d07f1362fddeb0feb6a857d1d1d1c5e558e698 + FirebaseInstallations: 60c1d3bc1beef809fd1ad1189a8057a040c59f2e FirebaseMessaging: 4d52717dd820707cc4eadec5eb981b4832ec8d5d fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 @@ -441,7 +441,7 @@ SPEC CHECKSUMS: flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 @@ -458,9 +458,9 @@ SPEC CHECKSUMS: motionphoto: d4a432b8c8f22fb3ad966258597c0103c9c5ff16 move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d nanopb: 438bc412db1928dac798aa6fd75726007be04262 - onnxruntime: e9346181d75b8dea8733bdae512a22c298962e00 - onnxruntime-c: ebdcfd8650bcbd10121c125262f99dea681b92a3 - onnxruntime-objc: ae7acec7a3d03eaf072d340afed7a35635c1c2a6 + onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997 + onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c + onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b open_mail_app: 794172f6a22cd16319d3ddaf45e945b2f74952b0 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 @@ -470,7 +470,7 @@ SPEC CHECKSUMS: PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 receive_sharing_intent: 6837b01768e567fe8562182397bf43d63d8c6437 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 - SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb + SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 Sentry: ebc12276bd17613a114ab359074096b6b3725203 sentry_flutter: 88ebea3f595b0bc16acc5bedacafe6d60c12dcd5 diff --git a/mobile/lib/core/error-reporting/super_logging.dart b/mobile/lib/core/error-reporting/super_logging.dart index cc9c3122c3..6659636368 100644 --- a/mobile/lib/core/error-reporting/super_logging.dart +++ b/mobile/lib/core/error-reporting/super_logging.dart @@ -5,6 +5,7 @@ import 'dart:collection'; import 'dart:core'; import 'dart:io'; +import "package:dio/dio.dart"; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; @@ -14,7 +15,9 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photos/core/error-reporting/tunneled_transport.dart'; +import "package:photos/core/errors.dart"; import 'package:photos/models/typedefs.dart'; +import "package:photos/services/machine_learning/face_ml/face_ml_exceptions.dart"; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -230,12 +233,25 @@ class SuperLogging { StackTrace? stack, ) async { try { + if (error is DioError) { + return; + } + if (error is CouldNotRetrieveAnyFileData || + error is StorageLimitExceededError || + error is WiFiUnavailableError || + error is InvalidFileError || + error is NoActiveSubscriptionError) { + if (kDebugMode) { + $.info('Not sending error to sentry: $error'); + } + return; + } await Sentry.captureException( error, stackTrace: stack, ); } catch (e) { - $.info('Sending report to sentry.io failed: $e'); + $.info('Sending report to sentry failed: $e'); $.info('Original error: $error'); } } diff --git a/mobile/lib/db/embeddings_db.dart b/mobile/lib/db/embeddings_db.dart index b5a6111f45..4da5c056a5 100644 --- a/mobile/lib/db/embeddings_db.dart +++ b/mobile/lib/db/embeddings_db.dart @@ -123,7 +123,9 @@ class EmbeddingsDB { List _convertToEmbeddings(List> results) { final List embeddings = []; for (final result in results) { - embeddings.add(_getEmbeddingFromRow(result)); + final embedding = _getEmbeddingFromRow(result); + if (embedding.isEmpty) continue; + embeddings.add(embedding); } return embeddings; } diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index e23a5c762b..7c6b7947db 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -1808,6 +1808,9 @@ class FilesDB { } Future> getUploadedFiles(List uploadedIDs) async { + if (uploadedIDs.isEmpty) { + return []; + } final db = await instance.sqliteAsyncDB; String inParam = ""; for (final id in uploadedIDs) { diff --git a/mobile/lib/events/people_changed_event.dart b/mobile/lib/events/people_changed_event.dart index 51f4eaeefe..47e00b2462 100644 --- a/mobile/lib/events/people_changed_event.dart +++ b/mobile/lib/events/people_changed_event.dart @@ -19,4 +19,5 @@ class PeopleChangedEvent extends Event { enum PeopleEventType { defaultType, removedFilesFromCluster, + syncDone, } \ No newline at end of file diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index cba068c51b..db0f52d82b 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -333,7 +333,7 @@ Future _sync(String caller) async { await SyncService.instance.sync(); } catch (e, s) { if (!isHandledSyncError(e)) { - _logger.severe("Sync error", e, s); + _logger.warning("Sync error", e, s); } } } diff --git a/mobile/lib/models/embedding.dart b/mobile/lib/models/embedding.dart index c8f742caa9..91ac9a0213 100644 --- a/mobile/lib/models/embedding.dart +++ b/mobile/lib/models/embedding.dart @@ -6,6 +6,8 @@ class Embedding { final List embedding; int? updationTime; + bool get isEmpty => embedding.isEmpty; + Embedding({ required this.fileID, required this.model, @@ -13,6 +15,14 @@ class Embedding { this.updationTime, }); + factory Embedding.empty(int fileID, Model model) { + return Embedding( + fileID: fileID, + model: model, + embedding: [], + ); + } + static List decodeEmbedding(String embedding) { return List.from(jsonDecode(embedding) as List); } diff --git a/mobile/lib/services/collections_service.dart b/mobile/lib/services/collections_service.dart index 3631d00535..3510597bb1 100644 --- a/mobile/lib/services/collections_service.dart +++ b/mobile/lib/services/collections_service.dart @@ -729,7 +729,7 @@ class CollectionsService { collection.setName(newName); sync().ignore(); } catch (e, s) { - _logger.severe("failed to rename collection", e, s); + _logger.warning("failed to rename collection", e, s); rethrow; } } diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart index e4620f6676..1dd9c11b50 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart @@ -194,6 +194,7 @@ class FaceMlService { void _listenOnPeopleChangedSync() { Bus.instance.on().listen((event) { + if (event.type == PeopleEventType.syncDone) return; _shouldSyncPeople = true; }); } @@ -367,7 +368,7 @@ class FaceMlService { _isSyncing = true; if (forceSync) { await PersonService.instance.reconcileClusters(); - Bus.instance.fire(PeopleChangedEvent()); + Bus.instance.fire(PeopleChangedEvent(type: PeopleEventType.syncDone)); _shouldSyncPeople = false; } _isSyncing = false; @@ -924,7 +925,7 @@ class FaceMlService { await _getImagePathForML(enteFile, typeOfData: FileDataForML.fileData); if (filePath == null) { - _logger.severe( + _logger.warning( "Failed to get any data for enteFile with uploadedFileID ${enteFile.uploadedFileID} since its file path is null", ); throw CouldNotRetrieveAnyFileData(); diff --git a/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart b/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart index 6ca2c33dc9..34ae264b92 100644 --- a/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart +++ b/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart @@ -139,6 +139,7 @@ class ClusterFeedbackService { PersonEntity p, ) async { try { + _logger.info('removeFilesFromPerson called'); // Get the relevant faces to be removed final faceIDs = await FaceMLDataDB.instance .getFaceIDsForPerson(p.remoteID) @@ -150,6 +151,13 @@ class ClusterFeedbackService { final embeddings = await FaceMLDataDB.instance.getFaceEmbeddingMapForFaces(faceIDs); + if (faceIDs.isEmpty || embeddings.isEmpty) { + _logger.severe( + 'No faces or embeddings found for person ${p.remoteID} that match the given files', + ); + return; + } + final fileIDToCreationTime = await FilesDB.instance.getFileIDToCreationTime(); @@ -161,7 +169,7 @@ class ClusterFeedbackService { distanceThreshold: 0.20, ); if (clusterResult.isEmpty) { - _logger.warning('No clusters found or something went wrong'); + _logger.severe('No clusters found or something went wrong'); return; } final newFaceIdToClusterID = clusterResult.newFaceIdToCluster; @@ -179,7 +187,12 @@ class ClusterFeedbackService { await FaceMLDataDB.instance .bulkCaptureNotPersonFeedback(notClusterIdToPersonId); + // Update remote so new sync does not undo this change + await PersonService.instance + .removeFilesFromPerson(person: p, faceIDs: faceIDs.toSet()); + Bus.instance.fire(PeopleChangedEvent()); + _logger.info('removeFilesFromPerson done'); return; } catch (e, s) { _logger.severe("Error in removeFilesFromPerson", e, s); @@ -191,6 +204,7 @@ class ClusterFeedbackService { List files, int clusterID, ) async { + _logger.info('removeFilesFromCluster called'); try { // Get the relevant faces to be removed final faceIDs = await FaceMLDataDB.instance @@ -203,6 +217,13 @@ class ClusterFeedbackService { final embeddings = await FaceMLDataDB.instance.getFaceEmbeddingMapForFaces(faceIDs); + if (faceIDs.isEmpty || embeddings.isEmpty) { + _logger.severe( + 'No faces or embeddings found for cluster $clusterID that match the given files', + ); + return; + } + final fileIDToCreationTime = await FilesDB.instance.getFileIDToCreationTime(); @@ -214,6 +235,7 @@ class ClusterFeedbackService { distanceThreshold: 0.20, ); if (clusterResult.isEmpty) { + _logger.severe('No clusters found or something went wrong'); return; } final newFaceIdToClusterID = clusterResult.newFaceIdToCluster; @@ -230,13 +252,7 @@ class ClusterFeedbackService { source: "$clusterID", ), ); - // Bus.instance.fire( - // LocalPhotosUpdatedEvent( - // files, - // type: EventType.peopleClusterChanged, - // source: "$clusterID", - // ), - // ); + _logger.info('removeFilesFromCluster done'); return; } catch (e, s) { _logger.severe("Error in removeFilesFromCluster", e, s); @@ -673,7 +689,7 @@ class ClusterFeedbackService { .map((clusterID) => allClusterIdsToCountMap[clusterID] ?? 0) .reduce((value, element) => min(value, element)); final checkSizes = [100, 20, kMinimumClusterSizeSearchResult, 10, 5, 1]; - late Map clusterAvgBigClusters; + Map clusterAvgBigClusters = {}; final List<(int, double)> suggestionsMean = []; for (final minimumSize in checkSizes.toSet()) { if (smallestPersonClusterSize >= diff --git a/mobile/lib/services/machine_learning/face_ml/person/person_service.dart b/mobile/lib/services/machine_learning/face_ml/person/person_service.dart index 682deaff0c..34f8c6b34c 100644 --- a/mobile/lib/services/machine_learning/face_ml/person/person_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/person/person_service.dart @@ -201,6 +201,38 @@ class PersonService { personData.logStats(); } + Future removeFilesFromPerson({ + required PersonEntity person, + required Set faceIDs, + }) async { + final personData = person.data; + final List emptiedClusters = []; + for (final cluster in personData.assigned!) { + cluster.faces.removeWhere((faceID) => faceIDs.contains(faceID)); + if (cluster.faces.isEmpty) { + emptiedClusters.add(cluster.id); + } + } + + // Safety check to make sure we haven't created an empty cluster now, if so delete it + for (final emptyClusterID in emptiedClusters) { + personData.assigned! + .removeWhere((element) => element.id != emptyClusterID); + await faceMLDataDB.removeClusterToPerson( + personID: person.remoteID, + clusterID: emptyClusterID, + ); + } + + + await entityService.addOrUpdate( + EntityType.person, + json.encode(personData.toJson()), + id: person.remoteID, + ); + personData.logStats(); + } + Future deletePerson(String personID, {bool onlyMapping = false}) async { if (onlyMapping) { final PersonEntity? entity = await getPerson(personID); diff --git a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart index 1de02434e3..44fc961f3f 100644 --- a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart +++ b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart @@ -3,6 +3,7 @@ import "dart:collection"; import "dart:math" show min; import "package:computer/computer.dart"; +import "package:flutter/services.dart"; import "package:logging/logging.dart"; import "package:photos/core/cache/lru_map.dart"; import "package:photos/core/configuration.dart"; @@ -204,6 +205,9 @@ class SemanticSearchService { await _frameworkInitialization.future; _logger.info("Attempting backfill for image embeddings"); final fileIDs = await _getFileIDsToBeIndexed(); + if (fileIDs.isEmpty) { + return; + } final files = await FilesDB.instance.getUploadedFiles(fileIDs); _logger.info(files.length.toString() + " to be embedded"); // await _cacheThumbnails(files); @@ -211,20 +215,6 @@ class SemanticSearchService { unawaited(_pollQueue()); } - Future _cacheThumbnails(List files) async { - int counter = 0; - const batchSize = 100; - for (var i = 0; i < files.length;) { - final futures = []; - for (var j = 0; j < batchSize && i < files.length; j++, i++) { - futures.add(getThumbnail(files[i])); - } - await Future.wait(futures); - counter += futures.length; - _logger.info("$counter/${files.length} thumbnails cached"); - } - } - Future> _getFileIDsToBeIndexed() async { final uploadedFileIDs = await getIndexableFileIDs(); final embeddedFileIDs = @@ -387,6 +377,21 @@ class SemanticSearchService { file, embedding, ); + } on FormatException catch (e, _) { + _logger.severe( + "Could not get embedding for $file because FormatException occured, storing empty result locally", + e, + ); + final embedding = Embedding.empty(file.uploadedFileID!, _currentModel); + await EmbeddingsDB.instance.put(embedding); + } on PlatformException catch (e, s) { + _logger.severe( + "Could not get thumbnail for $file due to PlatformException related to thumbnails, storing empty result locally", + e, + s, + ); + final embedding = Embedding.empty(file.uploadedFileID!, _currentModel); + await EmbeddingsDB.instance.put(embedding); } catch (e, s) { _logger.severe(e, s); } diff --git a/mobile/lib/services/user_service.dart b/mobile/lib/services/user_service.dart index a6cb656206..4c20429b08 100644 --- a/mobile/lib/services/user_service.dart +++ b/mobile/lib/services/user_service.dart @@ -276,7 +276,7 @@ class UserService { throw Exception("delete action failed"); } } catch (e) { - _logger.severe(e); + _logger.warning(e); await showGenericErrorDialog(context: context, error: e); return null; } @@ -304,7 +304,7 @@ class UserService { throw Exception("delete action failed"); } } catch (e) { - _logger.severe(e); + _logger.warning(e); rethrow; } } @@ -329,7 +329,7 @@ class UserService { } rethrow; } catch (e, s) { - _logger.severe("unexpected error", e, s); + _logger.warning("unexpected error", e, s); rethrow; } } @@ -366,7 +366,7 @@ class UserService { Bus.instance.fire(AccountConfiguredEvent()); } } catch (e) { - _logger.severe(e); + _logger.warning(e); await dialog.hide(); await showGenericErrorDialog(context: context, error: e); } @@ -448,7 +448,7 @@ class UserService { } } catch (e) { await dialog.hide(); - _logger.severe(e); + _logger.warning(e); // ignore: unawaited_futures showErrorDialog( context, @@ -519,7 +519,7 @@ class UserService { } } catch (e) { await dialog.hide(); - _logger.severe(e); + _logger.warning(e); // ignore: unawaited_futures showErrorDialog( context, diff --git a/mobile/lib/ui/home/home_gallery_widget.dart b/mobile/lib/ui/home/home_gallery_widget.dart index 5d9f9c09dc..7de93e71e7 100644 --- a/mobile/lib/ui/home/home_gallery_widget.dart +++ b/mobile/lib/ui/home/home_gallery_widget.dart @@ -13,6 +13,7 @@ import 'package:photos/services/collections_service.dart'; import "package:photos/services/filter/db_filters.dart"; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class HomeGalleryWidget extends StatelessWidget { final Widget? header; @@ -84,13 +85,15 @@ class HomeGalleryWidget extends StatelessWidget { reloadDebounceTime: const Duration(seconds: 2), reloadDebounceExecutionInterval: const Duration(seconds: 5), ); - return Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - FileSelectionOverlayBar(GalleryType.homepage, selectedFiles), - ], + return SelectionState( + selectedFiles: selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + FileSelectionOverlayBar(GalleryType.homepage, selectedFiles), + ], + ), ); - // return gallery; } } diff --git a/mobile/lib/ui/map/map_pull_up_gallery.dart b/mobile/lib/ui/map/map_pull_up_gallery.dart index c88d4f81ce..f52439759e 100644 --- a/mobile/lib/ui/map/map_pull_up_gallery.dart +++ b/mobile/lib/ui/map/map_pull_up_gallery.dart @@ -15,6 +15,7 @@ import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/viewer/actions/file_selection_overlay_bar.dart"; import "package:photos/ui/viewer/gallery/gallery.dart"; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class MapPullUpGallery extends StatefulWidget { final StreamController> visibleImages; @@ -48,33 +49,37 @@ class _MapPullUpGalleryState extends State { Widget? cachedScrollableContent; return DeferredPointerHandler( - child: Stack( - alignment: Alignment.bottomCenter, - clipBehavior: Clip.none, - children: [ - DraggableScrollableSheet( - expand: false, - initialChildSize: initialChildSize, - minChildSize: initialChildSize, - maxChildSize: 0.8, - snap: true, - snapSizes: const [0.5], - builder: (context, scrollController) { - //Must use cached widget here to avoid rebuilds when DraggableScrollableSheet - //is snapped to it's initialChildSize - cachedScrollableContent ??= - cacheScrollableContent(scrollController, context, logger); - return cachedScrollableContent!; - }, - ), - DeferPointer( - child: FileSelectionOverlayBar( - GalleryType.searchResults, - _selectedFiles, - backgroundColor: getEnteColorScheme(context).backgroundElevated2, + child: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + children: [ + DraggableScrollableSheet( + expand: false, + initialChildSize: initialChildSize, + minChildSize: initialChildSize, + maxChildSize: 0.8, + snap: true, + snapSizes: const [0.5], + builder: (context, scrollController) { + //Must use cached widget here to avoid rebuilds when DraggableScrollableSheet + //is snapped to it's initialChildSize + cachedScrollableContent ??= + cacheScrollableContent(scrollController, context, logger); + return cachedScrollableContent!; + }, ), - ), - ], + DeferPointer( + child: FileSelectionOverlayBar( + GalleryType.searchResults, + _selectedFiles, + backgroundColor: + getEnteColorScheme(context).backgroundElevated2, + ), + ), + ], + ), ), ); } diff --git a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart index c760d88f3e..7fc020cf5d 100644 --- a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -695,7 +695,6 @@ class _FileSelectionActionsWidgetState widget.person!, ); } - Bus.instance.fire(PeopleChangedEvent()); } widget.selectedFiles.clearAll(); if (mounted) { @@ -738,7 +737,6 @@ class _FileSelectionActionsWidgetState widget.clusterID!, ); } - Bus.instance.fire(PeopleChangedEvent()); } widget.selectedFiles.clearAll(); if (mounted) { diff --git a/mobile/lib/ui/viewer/actions/file_selection_overlay_bar.dart b/mobile/lib/ui/viewer/actions/file_selection_overlay_bar.dart index 8e2260c74d..608286809c 100644 --- a/mobile/lib/ui/viewer/actions/file_selection_overlay_bar.dart +++ b/mobile/lib/ui/viewer/actions/file_selection_overlay_bar.dart @@ -3,8 +3,11 @@ import "package:photos/face/model/person.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/gallery_type.dart'; import 'package:photos/models/selected_files.dart'; +import "package:photos/service_locator.dart"; import "package:photos/theme/effects.dart"; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/components/bottom_action_bar/bottom_action_bar_widget.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class FileSelectionOverlayBar extends StatefulWidget { final GalleryType galleryType; @@ -51,38 +54,53 @@ class _FileSelectionOverlayBarState extends State { '$runtimeType building with ${widget.selectedFiles.files.length}', ); - return Container( - decoration: BoxDecoration( - boxShadow: shadowFloatFaintLight, - ), - child: ValueListenableBuilder( - valueListenable: _hasSelectedFilesNotifier, - builder: (context, value, child) { - return AnimatedCrossFade( - firstCurve: Curves.easeInOutExpo, - secondCurve: Curves.easeInOutExpo, - sizeCurve: Curves.easeInOutExpo, - crossFadeState: _hasSelectedFilesNotifier.value - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 400), - firstChild: BottomActionBarWidget( - selectedFiles: widget.selectedFiles, - galleryType: widget.galleryType, - collection: widget.collection, - person: widget.person, - clusterID: widget.clusterID, - onCancel: () { - if (widget.selectedFiles.files.isNotEmpty) { - widget.selectedFiles.clearAll(); - } - }, - backgroundColor: widget.backgroundColor, - ), - secondChild: const SizedBox(width: double.infinity), - ); - }, - ), + return ValueListenableBuilder( + valueListenable: _hasSelectedFilesNotifier, + builder: (context, value, child) { + return AnimatedCrossFade( + firstCurve: Curves.easeInOutExpo, + secondCurve: Curves.easeInOutExpo, + sizeCurve: Curves.easeInOutExpo, + crossFadeState: _hasSelectedFilesNotifier.value + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 400), + firstChild: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + flagService.internalUser + ? Padding( + padding: const EdgeInsets.only(right: 4), + child: SelectAllButton( + backgroundColor: widget.backgroundColor, + ), + ) + : const SizedBox.shrink(), + if (flagService.internalUser) const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + boxShadow: shadowFloatFaintLight, + ), + child: BottomActionBarWidget( + selectedFiles: widget.selectedFiles, + galleryType: widget.galleryType, + collection: widget.collection, + person: widget.person, + clusterID: widget.clusterID, + onCancel: () { + if (widget.selectedFiles.files.isNotEmpty) { + widget.selectedFiles.clearAll(); + } + }, + backgroundColor: widget.backgroundColor, + ), + ), + ], + ), + secondChild: const SizedBox(width: double.infinity), + ); + }, ); } @@ -90,3 +108,83 @@ class _FileSelectionOverlayBarState extends State { _hasSelectedFilesNotifier.value = widget.selectedFiles.files.isNotEmpty; } } + +class SelectAllButton extends StatefulWidget { + final Color? backgroundColor; + const SelectAllButton({super.key, required this.backgroundColor}); + + @override + State createState() => _SelectAllButtonState(); +} + +class _SelectAllButtonState extends State { + bool _allSelected = false; + @override + Widget build(BuildContext context) { + final selectionState = SelectionState.of(context); + assert( + selectionState != null, + "SelectionState not found in context, SelectionState should be an ancestor of FileSelectionOverlayBar", + ); + final colorScheme = getEnteColorScheme(context); + return GestureDetector( + onTap: () { + setState(() { + if (_allSelected) { + selectionState.selectedFiles.clearAll(); + } else { + selectionState.selectedFiles + .selectAll(selectionState.allGalleryFiles!.toSet()); + } + _allSelected = !_allSelected; + }); + }, + child: Padding( + padding: const EdgeInsets.only(top: 8), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: widget.backgroundColor ?? colorScheme.backgroundElevated2, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, -1), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "All", + style: getEnteTextTheme(context).miniMuted, + ), + const SizedBox(width: 4), + ListenableBuilder( + listenable: selectionState!.selectedFiles, + builder: (context, _) { + if (selectionState.selectedFiles.files.length == + selectionState.allGalleryFiles?.length) { + _allSelected = true; + } else { + _allSelected = false; + } + return Icon( + _allSelected + ? Icons.check_circle + : Icons.check_circle_outline, + color: _allSelected ? null : colorScheme.strokeMuted, + size: 18, + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/ui/viewer/gallery/archive_page.dart b/mobile/lib/ui/viewer/gallery/archive_page.dart index 1d6f544def..4f0a9502f6 100644 --- a/mobile/lib/ui/viewer/gallery/archive_page.dart +++ b/mobile/lib/ui/viewer/gallery/archive_page.dart @@ -15,6 +15,7 @@ import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import "package:photos/ui/viewer/gallery/empty_state.dart"; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class ArchivePage extends StatelessWidget { final String tagPrefix; @@ -86,15 +87,18 @@ class ArchivePage extends StatelessWidget { _selectedFiles, ), ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - FileSelectionOverlayBar( - overlayType, - _selectedFiles, - ), - ], + body: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + FileSelectionOverlayBar( + overlayType, + _selectedFiles, + ), + ], + ), ), ); } diff --git a/mobile/lib/ui/viewer/gallery/collection_page.dart b/mobile/lib/ui/viewer/gallery/collection_page.dart index 97183282b6..e0288f238b 100644 --- a/mobile/lib/ui/viewer/gallery/collection_page.dart +++ b/mobile/lib/ui/viewer/gallery/collection_page.dart @@ -16,6 +16,7 @@ import "package:photos/ui/viewer/gallery/empty_album_state.dart"; import 'package:photos/ui/viewer/gallery/empty_state.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class CollectionPage extends StatelessWidget { final CollectionWithThumbnail c; @@ -98,16 +99,19 @@ class CollectionPage extends StatelessWidget { collection: c.collection, ), ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - FileSelectionOverlayBar( - galleryType, - _selectedFiles, - collection: c.collection, - ), - ], + body: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + FileSelectionOverlayBar( + galleryType, + _selectedFiles, + collection: c.collection, + ), + ], + ), ), ); } diff --git a/mobile/lib/ui/viewer/gallery/device_folder_page.dart b/mobile/lib/ui/viewer/gallery/device_folder_page.dart index 42d7b80a61..b1a604ed02 100644 --- a/mobile/lib/ui/viewer/gallery/device_folder_page.dart +++ b/mobile/lib/ui/viewer/gallery/device_folder_page.dart @@ -22,6 +22,7 @@ import 'package:photos/ui/components/toggle_switch_widget.dart'; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class DeviceFolderPage extends StatelessWidget { final DeviceCollection deviceCollection; @@ -66,15 +67,18 @@ class DeviceFolderPage extends StatelessWidget { deviceCollection: deviceCollection, ), ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - FileSelectionOverlayBar( - GalleryType.localFolder, - _selectedFiles, - ), - ], + body: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + FileSelectionOverlayBar( + GalleryType.localFolder, + _selectedFiles, + ), + ], + ), ), ); } diff --git a/mobile/lib/ui/viewer/gallery/gallery.dart b/mobile/lib/ui/viewer/gallery/gallery.dart index b255c5c375..3155617060 100644 --- a/mobile/lib/ui/viewer/gallery/gallery.dart +++ b/mobile/lib/ui/viewer/gallery/gallery.dart @@ -16,6 +16,7 @@ import "package:photos/ui/viewer/gallery/component/group/type.dart"; import "package:photos/ui/viewer/gallery/component/multiple_groups_gallery_view.dart"; import 'package:photos/ui/viewer/gallery/empty_state.dart'; import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart"; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; import "package:photos/utils/debouncer.dart"; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -107,6 +108,7 @@ class GalleryState extends State { final _forceReloadEventSubscriptions = >[]; late String _logTag; bool _sortOrderAsc = false; + List _allFiles = []; @override void initState() { @@ -213,6 +215,8 @@ class GalleryState extends State { // group files into multiple groups and returns `true` if it resulted in a // gallery reload bool _onFilesLoaded(List files) { + _allFiles = files; + final updatedGroupedFiles = widget.enableFileGrouping && widget.groupType.timeGrouping() ? _groupBasedOnTime(files) @@ -246,6 +250,7 @@ class GalleryState extends State { @override Widget build(BuildContext context) { _logger.finest("Building Gallery ${widget.tagPrefix}"); + SelectionState.of(context)?.allGalleryFiles = _allFiles; if (!_hasLoadedFiles) { return widget.loadingWidget; } diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index c62d1f7389..5edd31984a 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -179,7 +179,7 @@ class _GalleryAppBarWidgetState extends State { setState(() {}); } } catch (e, s) { - _logger.severe("Failed to rename album", e, s); + _logger.warning("Failed to rename album", e, s); rethrow; } }, @@ -610,7 +610,7 @@ class _GalleryAppBarWidgetState extends State { await dialog.hide(); Navigator.of(context).pop(); } catch (e, s) { - _logger.severe("failed to trash collection", e, s); + _logger.warning("failed to trash collection", e, s); await dialog.hide(); await showGenericErrorDialog(context: context, error: e); } diff --git a/mobile/lib/ui/viewer/gallery/hidden_page.dart b/mobile/lib/ui/viewer/gallery/hidden_page.dart index f9e3525fda..1517b07460 100644 --- a/mobile/lib/ui/viewer/gallery/hidden_page.dart +++ b/mobile/lib/ui/viewer/gallery/hidden_page.dart @@ -19,6 +19,7 @@ import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/empty_hidden_widget.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class HiddenPage extends StatefulWidget { final String tagPrefix; @@ -139,15 +140,18 @@ class _HiddenPageState extends State { _selectedFiles, ), ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - FileSelectionOverlayBar( - widget.overlayType, - _selectedFiles, - ), - ], + body: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + FileSelectionOverlayBar( + widget.overlayType, + _selectedFiles, + ), + ], + ), ), ); } diff --git a/mobile/lib/ui/viewer/gallery/large_files_page.dart b/mobile/lib/ui/viewer/gallery/large_files_page.dart index 6b71ecba76..8668dfccae 100644 --- a/mobile/lib/ui/viewer/gallery/large_files_page.dart +++ b/mobile/lib/ui/viewer/gallery/large_files_page.dart @@ -13,6 +13,7 @@ import "package:photos/services/search_service.dart"; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import "package:photos/ui/viewer/gallery/component/group/type.dart"; import 'package:photos/ui/viewer/gallery/gallery.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class LargeFilesPagePage extends StatelessWidget { final String tagPrefix; @@ -84,15 +85,18 @@ class LargeFilesPagePage extends StatelessWidget { ), ), ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - FileSelectionOverlayBar( - overlayType, - _selectedFiles, - ), - ], + body: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + FileSelectionOverlayBar( + overlayType, + _selectedFiles, + ), + ], + ), ), ); } diff --git a/mobile/lib/ui/viewer/gallery/state/selection_state.dart b/mobile/lib/ui/viewer/gallery/state/selection_state.dart new file mode 100644 index 0000000000..ef97c884b1 --- /dev/null +++ b/mobile/lib/ui/viewer/gallery/state/selection_state.dart @@ -0,0 +1,42 @@ +import "package:flutter/material.dart"; +import "package:logging/logging.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/models/selected_files.dart"; + +///This is an inherited widget that needs to be wrapped around Gallery and +///FileSelectionOverlayBar to make select all work. +// ignore: must_be_immutable +class SelectionState extends InheritedWidget { + final SelectedFiles selectedFiles; + + ///Should be assigned later in gallery when files are loaded. + ///Note: EnteFiles in this list should be references of the same EnteFiles + ///that are grouped in gallery, so that when files are added/deleted, + ///both lists are in sync. + List? allGalleryFiles; + + SelectionState({ + Key? key, + required this.selectedFiles, + required Widget child, + }) : super(key: key, child: child); + + static SelectionState? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + static SelectionState? of(BuildContext context) { + final SelectionState? result = maybeOf(context); + if (result == null) { + Logger("SelectionState").warning( + "No SelectionState found in context. Ignore this if file selection is disabled in the gallery used.", + ); + } + return result; + } + + @override + bool updateShouldNotify(covariant InheritedWidget oldWidget) { + return false; + } +} diff --git a/mobile/lib/ui/viewer/gallery/trash_page.dart b/mobile/lib/ui/viewer/gallery/trash_page.dart index 2f2965eea9..d247245b36 100644 --- a/mobile/lib/ui/viewer/gallery/trash_page.dart +++ b/mobile/lib/ui/viewer/gallery/trash_page.dart @@ -13,6 +13,7 @@ import 'package:photos/ui/common/bottom_shadow.dart'; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; import 'package:photos/utils/delete_file_util.dart'; class TrashPage extends StatelessWidget { @@ -65,32 +66,35 @@ class TrashPage extends StatelessWidget { _selectedFiles, ), ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - const BottomShadowWidget( - offsetDy: 20, - ), - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - height: filesAreSelected ? 0 : 80, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: filesAreSelected ? 0.0 : 1.0, - curve: Curves.easeIn, - child: IgnorePointer( - ignoring: filesAreSelected, - child: const SafeArea( - minimum: EdgeInsets.only(bottom: 6), - child: BottomButtonsWidget(), + body: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + const BottomShadowWidget( + offsetDy: 20, + ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: filesAreSelected ? 0 : 80, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: filesAreSelected ? 0.0 : 1.0, + curve: Curves.easeIn, + child: IgnorePointer( + ignoring: filesAreSelected, + child: const SafeArea( + minimum: EdgeInsets.only(bottom: 6), + child: BottomButtonsWidget(), + ), ), ), ), - ), - FileSelectionOverlayBar(GalleryType.trash, _selectedFiles), - ], + FileSelectionOverlayBar(GalleryType.trash, _selectedFiles), + ], + ), ), ); } diff --git a/mobile/lib/ui/viewer/gallery/uncategorized_page.dart b/mobile/lib/ui/viewer/gallery/uncategorized_page.dart index 265a614a8e..7d49d2f3e5 100644 --- a/mobile/lib/ui/viewer/gallery/uncategorized_page.dart +++ b/mobile/lib/ui/viewer/gallery/uncategorized_page.dart @@ -13,6 +13,7 @@ import 'package:photos/services/ignored_files_service.dart'; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class UnCategorizedPage extends StatelessWidget { final String tagPrefix; @@ -82,15 +83,18 @@ class UnCategorizedPage extends StatelessWidget { collection: collection, ), ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - FileSelectionOverlayBar( - overlayType, - _selectedFiles, - ), - ], + body: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + FileSelectionOverlayBar( + overlayType, + _selectedFiles, + ), + ], + ), ), ); } diff --git a/mobile/lib/ui/viewer/location/location_screen.dart b/mobile/lib/ui/viewer/location/location_screen.dart index 55975dd3fa..dda1d5022a 100644 --- a/mobile/lib/ui/viewer/location/location_screen.dart +++ b/mobile/lib/ui/viewer/location/location_screen.dart @@ -25,6 +25,7 @@ import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/components/title_bar_widget.dart"; import "package:photos/ui/viewer/actions/file_selection_overlay_bar.dart"; import "package:photos/ui/viewer/gallery/gallery.dart"; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; import "package:photos/ui/viewer/location/edit_location_sheet.dart"; import "package:photos/utils/dialog_util.dart"; @@ -231,40 +232,43 @@ class _LocationGalleryWidgetState extends State { key: ValueKey("$centerPoint$selectedRadius"), builder: (context, snapshot) { if (snapshot.hasData) { - return Stack( - alignment: Alignment.bottomCenter, - children: [ - Gallery( - loadingWidget: Column( - children: [ - galleryHeaderWidget, - EnteLoadingWidget( - color: getEnteColorScheme(context).strokeMuted, - ), - ], + return SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Gallery( + loadingWidget: Column( + children: [ + galleryHeaderWidget, + EnteLoadingWidget( + color: getEnteColorScheme(context).strokeMuted, + ), + ], + ), + header: galleryHeaderWidget, + asyncLoader: ( + creationStartTime, + creationEndTime, { + limit, + asc, + }) async { + return snapshot.data as FileLoadResult; + }, + reloadEvent: Bus.instance.on(), + removalEventTypes: const { + EventType.deletedFromRemote, + EventType.deletedFromEverywhere, + }, + selectedFiles: _selectedFiles, + tagPrefix: widget.tagPrefix, ), - header: galleryHeaderWidget, - asyncLoader: ( - creationStartTime, - creationEndTime, { - limit, - asc, - }) async { - return snapshot.data as FileLoadResult; - }, - reloadEvent: Bus.instance.on(), - removalEventTypes: const { - EventType.deletedFromRemote, - EventType.deletedFromEverywhere, - }, - selectedFiles: _selectedFiles, - tagPrefix: widget.tagPrefix, - ), - FileSelectionOverlayBar( - GalleryType.locationTag, - _selectedFiles, - ), - ], + FileSelectionOverlayBar( + GalleryType.locationTag, + _selectedFiles, + ), + ], + ), ); } else { return Column( diff --git a/mobile/lib/ui/viewer/people/cluster_page.dart b/mobile/lib/ui/viewer/people/cluster_page.dart index 285804f543..efca41ecab 100644 --- a/mobile/lib/ui/viewer/people/cluster_page.dart +++ b/mobile/lib/ui/viewer/people/cluster_page.dart @@ -15,6 +15,7 @@ import 'package:photos/models/selected_files.dart'; import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart"; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; import "package:photos/ui/viewer/people/add_person_action_sheet.dart"; import "package:photos/ui/viewer/people/cluster_app_bar.dart"; import "package:photos/ui/viewer/people/people_banner.dart"; @@ -57,7 +58,8 @@ class _ClusterPageState extends State { late final StreamSubscription _filesUpdatedEvent; late final StreamSubscription _peopleChangedEvent; - bool get showNamingBanner => (!userDismissedNamingBanner && widget.showNamingBanner); + bool get showNamingBanner => + (!userDismissedNamingBanner && widget.showNamingBanner); bool userDismissedNamingBanner = false; @@ -66,7 +68,8 @@ class _ClusterPageState extends State { super.initState(); ClusterFeedbackService.setLastViewedClusterID(widget.clusterID); files = widget.searchResult; - _filesUpdatedEvent = Bus.instance.on().listen((event) { + _filesUpdatedEvent = + Bus.instance.on().listen((event) { if (event.type == EventType.deletedFromDevice || event.type == EventType.deletedFromEverywhere || event.type == EventType.deletedFromRemote || @@ -111,7 +114,8 @@ class _ClusterPageState extends State { final result = files .where( (file) => - file.creationTime! >= creationStartTime && file.creationTime! <= creationEndTime, + file.creationTime! >= creationStartTime && + file.creationTime! <= creationEndTime, ) .toList(); return Future.value( @@ -148,16 +152,19 @@ class _ClusterPageState extends State { body: Column( children: [ Expanded( - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - FileSelectionOverlayBar( - ClusterPage.overlayType, - _selectedFiles, - clusterID: widget.clusterID, - ), - ], + child: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + FileSelectionOverlayBar( + ClusterPage.overlayType, + _selectedFiles, + clusterID: widget.clusterID, + ), + ], + ), ), ), showNamingBanner @@ -185,7 +192,8 @@ class _ClusterPageState extends State { context, clusterID: widget.clusterID, ); - if (result != null && result is (PersonEntity, EnteFile)) { + if (result != null && + result is (PersonEntity, EnteFile)) { Navigator.pop(context); // ignore: unawaited_futures routeToPage(context, PeoplePage(person: result.$1)); diff --git a/mobile/lib/ui/viewer/people/people_page.dart b/mobile/lib/ui/viewer/people/people_page.dart index 8b399ced0d..f857943ea3 100644 --- a/mobile/lib/ui/viewer/people/people_page.dart +++ b/mobile/lib/ui/viewer/people/people_page.dart @@ -17,6 +17,7 @@ import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedba import "package:photos/services/search_service.dart"; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; import "package:photos/ui/viewer/people/people_app_bar.dart"; import "package:photos/ui/viewer/people/people_banner.dart"; import "package:photos/ui/viewer/people/person_cluster_suggestion.dart"; @@ -127,44 +128,48 @@ class _PeoplePageState extends State { return Column( children: [ Expanded( - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - Gallery( - asyncLoader: ( - creationStartTime, - creationEndTime, { - limit, - asc, - }) async { - final result = await loadPersonFiles(); - return Future.value( - FileLoadResult( - result, - false, - ), - ); - }, - reloadEvent: Bus.instance.on(), - forceReloadEvents: [ - Bus.instance.on(), - ], - removalEventTypes: const { - EventType.deletedFromRemote, - EventType.deletedFromEverywhere, - EventType.hide, - }, - tagPrefix: widget.tagPrefix + widget.tagPrefix, - selectedFiles: _selectedFiles, - initialFiles: - personFiles.isNotEmpty ? [personFiles.first] : [], - ), - FileSelectionOverlayBar( - PeoplePage.overlayType, - _selectedFiles, - person: widget.person, - ), - ], + child: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Gallery( + asyncLoader: ( + creationStartTime, + creationEndTime, { + limit, + asc, + }) async { + final result = await loadPersonFiles(); + return Future.value( + FileLoadResult( + result, + false, + ), + ); + }, + reloadEvent: + Bus.instance.on(), + forceReloadEvents: [ + Bus.instance.on(), + ], + removalEventTypes: const { + EventType.deletedFromRemote, + EventType.deletedFromEverywhere, + EventType.hide, + }, + tagPrefix: widget.tagPrefix + widget.tagPrefix, + selectedFiles: _selectedFiles, + initialFiles: + personFiles.isNotEmpty ? [personFiles.first] : [], + ), + FileSelectionOverlayBar( + PeoplePage.overlayType, + _selectedFiles, + person: widget.person, + ), + ], + ), ), ), showSuggestionBanner diff --git a/mobile/lib/ui/viewer/search/result/search_result_page.dart b/mobile/lib/ui/viewer/search/result/search_result_page.dart index 8687afe01c..218f069860 100644 --- a/mobile/lib/ui/viewer/search/result/search_result_page.dart +++ b/mobile/lib/ui/viewer/search/result/search_result_page.dart @@ -12,6 +12,7 @@ import 'package:photos/models/selected_files.dart'; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; +import "package:photos/ui/viewer/gallery/state/selection_state.dart"; class SearchResultPage extends StatefulWidget { final SearchResult searchResult; @@ -99,15 +100,18 @@ class _SearchResultPageState extends State { _selectedFiles, ), ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - FileSelectionOverlayBar( - SearchResultPage.overlayType, - _selectedFiles, - ), - ], + body: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + gallery, + FileSelectionOverlayBar( + SearchResultPage.overlayType, + _selectedFiles, + ), + ], + ), ), ); } diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 9b1b37fb4d..a5750f6b7a 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -286,7 +286,7 @@ class FileUploader { kFileUploadTimeout, onTimeout: () { final message = "Upload timed out for file " + file.toString(); - _logger.severe(message); + _logger.warning(message); throw TimeoutException(message); }, ); @@ -1184,7 +1184,7 @@ class FileUploader { clearQueue(error); throw error; } else { - _logger.severe("Could not fetch upload URLs", e, s); + _logger.warning("Could not fetch upload URLs", e, s); } } rethrow; diff --git a/mobile/lib/utils/file_uploader_util.dart b/mobile/lib/utils/file_uploader_util.dart index 1455ee0e9b..833c55b30d 100644 --- a/mobile/lib/utils/file_uploader_util.dart +++ b/mobile/lib/utils/file_uploader_util.dart @@ -346,7 +346,7 @@ Future _getMediaUploadDataFromAppCache(EnteFile file) async { width: dimensions?['width'], ); } catch (e, s) { - _logger.severe("failed to generate thumbnail", e, s); + _logger.warning("failed to generate thumbnail", e, s); throw InvalidFileError( "thumbnail failed for appCache fileType: ${file.fileType.toString()}", InvalidReason.thumbnailMissing, diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 169dc0c245..9beaf2f2f3 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1610,8 +1610,8 @@ packages: dependency: "direct main" description: path: "." - ref: "5f26aef45ed9f5e563c26f90c1e21b3339ed906d" - resolved-ref: "5f26aef45ed9f5e563c26f90c1e21b3339ed906d" + ref: ente_onnxruntime + resolved-ref: fb9393e36013790938b5bc995a4dca15fed3c944 url: "https://github.com/ente-io/onnxruntime.git" source: git version: "1.1.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 999204d3a2..ce76349f15 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -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.9.1+901 +version: 0.9.3+903 publish_to: none environment: @@ -127,7 +127,7 @@ dependencies: onnxruntime: git: url: https://github.com/ente-io/onnxruntime.git - ref: 5f26aef45ed9f5e563c26f90c1e21b3339ed906d + ref: ente_onnxruntime open_mail_app: ^0.4.5 package_info_plus: ^4.1.0 page_transition: ^2.0.2 diff --git a/server/compose.yaml b/server/compose.yaml index a7d5a2c39e..f20ebe2b8f 100644 --- a/server/compose.yaml +++ b/server/compose.yaml @@ -49,9 +49,8 @@ services: "-U", "pguser" ] - interval: 1s - timeout: 5s - retries: 20 + start_period: 40s + start_interval: 1s volumes: - postgres-data:/var/lib/postgresql/data networks: diff --git a/server/configurations/local.yaml b/server/configurations/local.yaml index fff43906c7..f392663c0e 100644 --- a/server/configurations/local.yaml +++ b/server/configurations/local.yaml @@ -317,7 +317,7 @@ internal: replication: enabled: false # The Cloudflare worker to use to download files from the primary hot - # bucket. Must be specified if replication is enabled. + # bucket. If this isn't specified, files will be downloaded directly. worker-url: # Number of go routines to spawn for replication # This is not related to the worker-url above. diff --git a/server/ente/user.go b/server/ente/user.go index 387d2627b3..79c7cf96bd 100644 --- a/server/ente/user.go +++ b/server/ente/user.go @@ -9,9 +9,6 @@ const ( EmailChangedTemplate = "email_changed.html" EmailChangedSubject = "Email address updated" - // OTTEmailSubject is the subject of the OTT mail - OTTEmailSubject = "ente Verification Code" - ChangeEmailOTTPurpose = "change" ) diff --git a/server/mail-templates/ott_auth.html b/server/mail-templates/ott_auth.html index 19b9826d64..9c89cb12ab 100644 --- a/server/mail-templates/ott_auth.html +++ b/server/mail-templates/ott_auth.html @@ -159,7 +159,7 @@ - +
Paste this code into the app to verify your email address
Use this code to verify your email address
@@ -188,7 +188,7 @@
- +
Please respond to this email if you are facing any issues
ente.io
@@ -212,4 +212,4 @@ - \ No newline at end of file + diff --git a/server/mail-templates/ott_photos.html b/server/mail-templates/ott_photos.html index 5774eb4f5e..1c7e63d946 100644 --- a/server/mail-templates/ott_photos.html +++ b/server/mail-templates/ott_photos.html @@ -159,7 +159,7 @@ - +
Paste this code into the app to verify your email address
Use this code to verify your email address
@@ -188,7 +188,7 @@
- +
Please respond to this email if you are facing any issues
ente.io
@@ -212,4 +212,4 @@ - \ No newline at end of file + diff --git a/server/pkg/controller/replication3.go b/server/pkg/controller/replication3.go index ec949cf4c3..4fad173ea2 100644 --- a/server/pkg/controller/replication3.go +++ b/server/pkg/controller/replication3.go @@ -87,10 +87,11 @@ func (c *ReplicationController3) StartReplication() error { workerURL := viper.GetString("replication.worker-url") if workerURL == "" { - return fmt.Errorf("replication.worker-url was not defined") + log.Infof("replication.worker-url was not defined, files will downloaded directly during replication") + } else { + log.Infof("Worker URL to download objects for replication v3 is: %s", workerURL) } c.workerURL = workerURL - log.Infof("Worker URL to download objects for replication v3 is: %s", workerURL) c.createMetrics() err := c.createTemporaryStorage() @@ -414,7 +415,7 @@ func (c *ReplicationController3) downloadFromB2ViaWorker(objectKey string, file q.Add("src", presignedEncodedURL) request.URL.RawQuery = q.Encode() - if c.S3Config.AreLocalBuckets() { + if c.S3Config.AreLocalBuckets() || c.workerURL == "" { originalURL := request.URL request, err = http.NewRequest("GET", presignedURL, nil) if err != nil { diff --git a/server/pkg/controller/user/userauth.go b/server/pkg/controller/user/userauth.go index 5d9664e997..5548dd23ad 100644 --- a/server/pkg/controller/user/userauth.go +++ b/server/pkg/controller/user/userauth.go @@ -302,8 +302,9 @@ func emailOTT(c *gin.Context, to string, ott string, client string, purpose stri inlineImage["content"] = "iVBORw0KGgoAAAANSUhEUgAAALAAAACwCAYAAACvt+ReAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABHlSURBVHgB7Z1tjFxV/ce/U7ZGxa3bEsrflr+d5o+x5W9tN/ZJAu60PFi1ursm9SnSbuWFwZIAMTFAom6jKdEX0iYg+AK7SzRqfNHWmABa2qmokW61iwq7QA23L7oiaLvsQhG3MJ7vnXOXu7t3Zu7MfTrn3PNpTnd3Zpru7nznN9/fwzm3AEtoKpVKUXzwr2VidchVlA/zvg5ivMY6LZbjrUKh4MASigIscxBCpQDXyLVafiyitjCTYBhVQR+Tnw8LYY/DMgMrYEwLtgdVoXbjrWiqGsNyHUJV0A5yTm4FLERbQlW0XagKV0coZkbog0LMZeSQXAnYJ9odSNcOpIEjVlmswTyJ2XgBGy7aWjioinmfEPMwDMZIAUtP24eqny0h31DAFPIADMQoAUvh3irWbchPtA2Lg2pU3m1S8meEgGV9th9Vm2BpzAAMEbLWApbC3Q9rE1plAJoLeR40hMIVa0B8+jyseKPQJ9bz4ne5XwYD7dAqAluPmzj9qCZ82nT8tBGwEC9LYfdA3S6ZKTio2ooBaIDyArY+NzMGoIE/VtoDC/HSLpyEFW8W9Il1UjwH34TCKBmBbdRVjrJYO1WMxspFYBt1laSEajTug2IoI2BWGMRikrYXtsKgInxOWG67R1aDlEAJCyEtw1HYCoMuOGJtUsFSZB6BZXmMlqEIiy4UUbUUPciYTAUsM9wDsJZBR/icHci6SpGZhZB+9zZYTGCvsBO3IwNSF7BMABh1S7CYxEFUS22ptqFTFbBM1viDrobBjIyM4Pjx4zhz5oz79dKlS7F+/XqsXLkShsPh+d40k7vUBJyHSgNFe++997ofg6CQb7nlFvT29sJgHKRYoUhFwKaLd2JiAnfddRcOHz4c6vEUMIVMQRuKg5REnLiATRfv6Ogodu3aNW0XwkLxPvTQQ1bEEUm0jGa6eGkVbrzxxqbFS/hvtm/f7vplQymKdTTpQfnEBCyrDUzYijCQwcFBV4CTk5NoFYqYduLAgQMwlCKqteLE6vyJWQjxTfNZybxTkwRM1LjihJ6Yy1DKwkpsQgIkEoFlk8JI8e7Zsyd28ZIkXhQKUZKaiJ3YI7BsLfbDMFhpuPvuuxN/u6ctYUXDUPpFJN6NGIlVwHK4wzhDR/Hu2LEjtYTruuuucyP9ggULYCBsdBxETMQmYJltcqrMqMEcr1rQSqUhCuza0VIYWGZjq7kzrvJaLAKWWaZxI5FZidfD4Fqxg6qII89NxJXE0fcWYRBsUPT09GQmXpL1CyhBiqhqJjKRBSz3SRk1FvnYY4+5DYooNd648GrFBjY8bhPaiaybSBbCRN/LKsOdd94JFWEVxLBBoMh+OKqA2SYuwRB0qMUa2PCI1ORo2ULIem8JhqBLI8HAhkcpipVoKQKbZh1Yc2W2rxOGReKWrUSrAh6AIYdJ0+/qOkxDP0xfbAgtWYmmBSyrDvuhOWl315KCDQ9OxhnStWu6S9eUgE1pWLA0xbdfU0pTBjU8HDTZ4Gg2ieO5ZUVojImD5AY1PIposqcQOgLLxO15aIzBnS0XQyJxUwldMxG4HxrD1rDJ4iVe167WrmhNoE0N3WYOFYF1j758QrnxUoXWcFoY0LVbHiYKh43A/dAUlsii7l3TEZYHNW94hKp0NYzAOkdflpcMqpO2hOYNj4ZROEwE7oeGMPrkXbxE89ZzQy9cNwLrGn0N3yDZEhp37RbWqws3isD90Iykdg3rDnMBipgdSM2oWxduFIEZfYvQBJ3nGtJCw1oxo+/yWlG4ZgSWMw9FaACjiuEn3MSGhs0c1oX7at1Zz0LcCg3gE2HCUE6aaCji7lp3BFoIEX3XoDq0ozSmt4aThhNsLDVqcvA2T7osz76xVgRWfpOmFW90NLNegUeV1YrASidvVrzxo0HDY1xE4IWzb5wTgYV4S1BYvCqc12AiGtTOO6Q2ZxBkIfqgKBSvKuc1mIgGIp5jI+ZYCFXtA30amxRWvMmjcNdujo2YIWAZoo9CMZI8bKS9vR3bd2x3nzTdBsH5jsTy4X333he7peJlwRiNFdxrN6MaMVvAe6FY/TfJtzUKdvChQe33krmD7D29sb87KXpC5j4h4Okq2WwP3AWFSNqT7bpllxEnP/JnYDMnbhjdFaz2zNDotIDl5NkaKEIaCcWKFStgCj29yVzRQcGS5Rr/RWP8EThX4iUmXfo1yXcSBXdyT79a5wXdmCV2HFJNvJkTHj2rANPB1i/gTC/APTVxwRWubmeU5Qm2nu+44w48dWQELw2ddZ+zjJge7nGrENJTnENGnD/zGo7d9IT7Kn/lQ2fxsyd+gnOVs0iakVGzJthWrkjWEn1kQxd6Orbh/NGp6duWbL4Ma762Eu9c8g6kjLtTwxNwCRnWfw9v+y3Gn5lZAjr5xnEcefPRRIVsBRwO1spv+sCX8Z5ni5ianJpz/6VrF6HrhxuQMm49uE1+kVkC9+R3R+aIl3RetN5daQjZUpvuDb24+vy1eP2PU+CfIF46IezE5AXMb29DilCz0wIuIQPGn5nAcz9y6j6GIl4+7woceeNR/OlNrU+c0YrFCy7Dje/9EhaeXIzXawjXz0tD/3LtRIq4OZsn4GXIgKfvPxXqcR2FRfh02+exufJRPHjhPhuNE+ZjV27Fxn904aJnw0fU/0ymntC5riFTC/HyaHM7ZCnkr87/urUVCfH/l6/CDee34pJTi6EBRf7VJjtwWmFtRby8vfAObOnYig+/1oWpVxrbBUXgfHCREbgIDbG2Ih6uvrwL17+2FRdNttVM0hRGXwF7WFvxFix3hZ1IY5LGAHD5i5mkP3Ghv4A9rK2o7jJuJGCK/Kq2LmyetyWwpqsZ5giYeLai613XYuDcD6ytmAXtwsfbevDmGHS0C0Eso4CNuUysxyWvLHZtxdDFv8eRC48ERiVDruoTis6Vndh80RYsfHYx3oRRLKSA342M+M8rydYO1716Fd5XWYEj8+baCr6VmgZHKv1zu/wZt733C3j/C6tMsAtBvDvTCDw1kfwv1bMVH7y4E4fGf54LW0Hhdm/sxeoX1uP1Z6ZMsQtBFI20EEFc8eoKfGPZHjwy/ks8KpaJMAJvve6Twi581J0Ye91c4U6TGwGTV8dewzW4Fqvmd+JfG8ZgEgXx59NXbsM/fzzh/pw5oSNXAvagrej45SKM/O8pXPHFYtpTVLHDeeoTX/+LOxWWMzr0fuYiwmGi04fOYOXNV2BZt367kxl1n77/OTz3o9OmJmkNybWACd9uveh1pRByBjsLWuKfQ2fd7ztHdiGQ3AvYg5GYolA9Gl+YuICnHzjVcI46L1gB+/CiMcW89turlIvGp4RVoO3Jq10Iotmr1ecC2omHtxzDSMiB+6RhkvabLx13t19Z8c6EAh6HJRBGu0eEkJsdvI8Lblvni+jhjx3LY4UhDOO0EBRw7kppYaGtOPyZ37u+OM0kzyZpoRi3HjgkaSV5jLonvvFnjB15EZaGuAJ2YNBIZZIkXXKzSVrTuAJ+GZamYDTmoogZkaPCczH+/J0R63Ob52WbxEXAS/LOt+hTvSTtsW2/s+JtjXOehbC0CG0FS27NJnk2SYuF01bAMRE2ybNJWqw4VsAx4iV5Y0f/gdUBJzbaJC12rICTgNGVy0vybJKWGE5boVBwKpWKbWYkgDeuaX1uMlC73iyEA0siWPEmxjD/8gT8JCwWvTjNvzwBD8Ni0Ysy/7ICtujKDAthBZwA4ziHn1YGxS/3BCyx42rWnUbj1V5EJcKBHeqJjXLl1/gDfoN/iz+jlb+6Iu4pfFaUehbCEplhapaf+HdkHIIlMg7+hgcq9wiD9itXvP7b91b2uMK2RGbaMbQF3WhpHoq1XPmViLqP130chT1cGcKWwqewAh+ApSWmg61fwAfF2g9L09AePFI5NCPi1sPzxmuwFqXCDdZWNM/cCCx9MO9Q5qLfqkMhHqz8zLUHrUDhO5W/uSKmmC2hoP91vC9mbyk6BivghjDS/qHy+HSSFgXvRUBr0Ve42UbjxhzzfzF7W/1BWOpSTdK+NydJiwqF7CV5/4ZtP9dhhkZnRGBee9YO9gRDsdLnJl3T9ZI8aysCcahR/w1Bu5IHxboVlmloF+KOuPXwe2ub5M2gPPuGIAEzRFsBC17AmIi6v2g5SYuKl+RtLHwEG3E1LG5wncEcAVsb8VaSxqibNYzGtC5MGHOe5M2xD6TW2WiDyCn+JE0lvCSP7wj8PIeUg26sdTJP7myEF+lG8RRUhp0+zlbkMMnbF3RjoICljSiLT0vIAWknaVHJYZLH5kXgqEO9s9HYby7BYCgA1l2zStKiwiRvuHJCPEk3CCFfD4PZV+uOeucDD8DQU3uqNd1fYKDygLbi9cN3D/rjF/B3GAiTt4Fad9YUsJy3rKl8XaHHZZLWaGpMN2gr+HPRWhiW5JXr3dnoeNW9Yn0TBhB18EYXDBwQ2l3vzrqXGJBRWPuSGn0uo5Pp4vXwXqwcrNc8Gg/4J8+CCHONjH5oyvQuCI0qDHHCTqLmu0B2N3pAwxPa5ck9ZSRQkZi/YH4iF/wOuzsiL2g6INQw+pKwlxjYKdbziJm3vastdgHrVtNNCw1rxw2jLwklYBmF6YV3QFGyHrzRBSZ5o5WnsBHXxFo7vjjeyy2Eir6kmYu89IvVDcWGfFQavNEFDsx7tuJzhZ34H7wHihEq+pLQFzqUrwil6sKqDt7oQpy1444VCxATu8NGX9LsZbZYF6aNKCJD8lLTTYuoteP57fPFiuWKbQ6qGgtNU/+r3Ll8u/j0ADLCf+KNJT78m0t5glAR/xf633a8vx0xsds7cScsTV8rWfwHHLUsIwaWbL4s9GNrnXhjiRcKmTMizdiKS9ddghgYqDfzUIsCWkBE4aL4cBIRE7qXhs7i2E1P1H2MrelmS5jDV7oe3CBEvAgRYNTtbMb7erRkXGRZjZniPYgAf+hL1y4KvHZEnGcvWFrHG9mkkD++tBtvG3v7jPtZPosoXrKvFfGSliKwhxDxUUTs0M2Owm+0X8DjE0etcBXlqqXX4BNLujF1ouJ+vaz7cqz71ipEoCzEuwktElXARcRgJR7eUsaLS/6Onw/91FYWNOH6dTe4g/RX3bw+SgRu2Tp4NJ3E+ZH/ceiicy0++OD73OTMilcfLl7yTqz99qqo9uH2KOIlkSKwh4jErN1F3gR6ZuwM7v/+/Tg+dBxjY2OwqMe6devwlZu/4n6MCH3vbYhIXAKmhaCVKCImhoaGMPrMKCYnJ2HJhvb2dnd5bN68GQvaY+m4Oahah8hb1mIRMInLD1uMJ7Lv9RPJA/uR39BOWCz12RmXeElsAiaySxc5qbMYy26pkdiIzUL4iSupsxhFLEnbbBIRMImjyWExhoNCvL1IgCQFzGSOIraXLMg3vA53KY6KQxCxemA/8hvmq86BJa84YvUkJV6SWAT2kOU1RuIiLHnCEWtTnBWHIBIXMLEizh0OUhAvSUXAxIo4NzhISbwkMQ88G/kDcWzOXtLWXJiwpSZekpqAiU/E9np05lFGtdrgIEVSFTBhRiprgsYd3Zpj2KTYlGS1oRapC9hDdmVs21l/difRYQtLaklcLURy1yM+7IedYtMNRtudcc82NEvmAia2QqEdDlJO1mqRmYXwI38RnbC+WAf4HHWqIF6iRAT2I6JxH6rb9a2lUAtaBvrdpo5+ShrlBEykpaAvLsGiAmXEPIgeF0pYiNnwFyXPCmCVwshLfWkCf/e3yxKZAwVRMgL7kdG4Hwofrm0oZSgadf0oGYH9yGjch+p+OweWpHHE6lU56vpRPgL7kUPyLJobce06xfAubLk3i45aq2glYA9rK2JnAE2ejK4KWgrYwwo5MmVo4HProbwHrofPHy+HAVcUTZEBsZbr4nProXUEno0vInfBtqVno6XHbYRRAvYjO3o8myLvu6LLYh1C9Qh/42rqxgrYQwiZAmblIk9R2btIO89jKMNgjBewHyHmkvjQBzPFnBvR+smVgP1IMXMWmWLW1WZwf+Ex5Ey0fnIrYD8y+aOIKejVUFfQDqp+lsI9aKKnbRYr4ABkx2+NXJ7dSFPUFKaDqlCflB+HrWDnYgXcBDJS+9cyVOeWg1YQ43hrus7xfX1afu0u3WuzafJf05durhLhbZAAAAAASUVORK5CYII=" } inlineImages = append(inlineImages, inlineImage) - err := emailUtil.SendTemplatedEmail([]string{to}, "ente", "verify@ente.io", - ente.OTTEmailSubject, templateName, map[string]interface{}{ + subject := fmt.Sprintf("Email verification code: %s", ott) + err := emailUtil.SendTemplatedEmail([]string{to}, "Ente", "verify@ente.io", + subject, templateName, map[string]interface{}{ "VerificationCode": ott, }, inlineImages) if err != nil { diff --git a/web/apps/accounts/src/services/passkey.ts b/web/apps/accounts/src/services/passkey.ts index 4f4ab55099..b30155e483 100644 --- a/web/apps/accounts/src/services/passkey.ts +++ b/web/apps/accounts/src/services/passkey.ts @@ -1,4 +1,5 @@ import { isDevBuild } from "@/next/env"; +import { apiOrigin } from "@/next/origins"; import { clientPackageName } from "@/next/types/app"; import { TwoFactorAuthorizationResponse } from "@/next/types/credentials"; import { ensure } from "@/utils/ensure"; @@ -8,7 +9,6 @@ import { toB64URLSafeNoPadding, toB64URLSafeNoPaddingString, } from "@ente/shared/crypto/internal/libsodium"; -import { apiOrigin } from "@ente/shared/network/api"; import { z } from "zod"; /** Return true if the user's browser supports WebAuthn (Passkeys). */ @@ -343,8 +343,28 @@ const authenticatorAttestationResponse = (credential: Credential) => { * Return `true` if the given {@link redirectURL} (obtained from the redirect * query parameter passed around during the passkey verification flow) is one of * the whitelisted URLs that we allow redirecting to on success. + * + * This check is likely not necessary but we've only kept it just to be on the + * safer side. However, this gets in the way of people who are self hosting + * Ente. So only do this check if we're running on our production servers (or + * localhost). */ export const isWhitelistedRedirect = (redirectURL: URL) => + shouldRestrictToWhitelistedRedirect() + ? _isWhitelistedRedirect(redirectURL) + : true; + +export const shouldRestrictToWhitelistedRedirect = () => { + // host includes port, hostname is sans port + const hostname = new URL(window.location.origin).hostname; + return ( + hostname.endsWith("localhost") || + hostname.endsWith(".ente.io") || + hostname.endsWith(".ente.sh") + ); +}; + +const _isWhitelistedRedirect = (redirectURL: URL) => (isDevBuild && redirectURL.hostname.endsWith("localhost")) || redirectURL.host.endsWith(".ente.io") || redirectURL.host.endsWith(".ente.sh") || diff --git a/web/apps/auth/src/services/remote.ts b/web/apps/auth/src/services/remote.ts index 9885e0b75b..0202d997e3 100644 --- a/web/apps/auth/src/services/remote.ts +++ b/web/apps/auth/src/services/remote.ts @@ -1,15 +1,13 @@ import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { ApiError, CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import { getActualKey } from "@ente/shared/user"; import { HttpStatusCode } from "axios"; import { codeFromURIString, type Code } from "services/code"; -const ENDPOINT = getEndpoint(); - export const getAuthCodes = async (): Promise => { const masterKey = await getActualKey(); try { @@ -83,7 +81,7 @@ interface AuthKey { export const getAuthKey = async (): Promise => { try { const resp = await HTTPService.get( - `${ENDPOINT}/authenticator/key`, + `${apiOrigin()}/authenticator/key`, {}, { "X-Auth-Token": getToken(), @@ -110,7 +108,7 @@ export const getDiff = async ( ): Promise => { try { const resp = await HTTPService.get( - `${ENDPOINT}/authenticator/entity/diff`, + `${apiOrigin()}/authenticator/entity/diff`, { sinceTime, limit, diff --git a/web/apps/cast/src/services/render.ts b/web/apps/cast/src/services/render.ts index d70bc8f343..18c4fa928f 100644 --- a/web/apps/cast/src/services/render.ts +++ b/web/apps/cast/src/services/render.ts @@ -11,13 +11,13 @@ import { heicToJPEG } from "@/media/heic-convert"; import { decodeLivePhoto } from "@/media/live-photo"; import { nameAndExtension } from "@/next/file"; import log from "@/next/log"; +import { apiOrigin, customAPIOrigin } from "@/next/origins"; import { shuffled } from "@/utils/array"; import { ensure } from "@/utils/ensure"; import { wait } from "@/utils/promise"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { ApiError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { apiOrigin, customAPIOrigin } from "@ente/shared/network/api"; import type { AxiosResponse } from "axios"; import type { CastData } from "services/cast-data"; import { detectMediaMIMEType } from "services/detect-type"; diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 9c07cc7aa6..77a63ec007 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -21,7 +21,6 @@ "exifr": "^7.1.3", "fast-srp-hap": "^2.0.4", "ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm", - "formik": "^2.1.5", "hdbscan": "0.0.1-alpha.5", "idb": "^8", "leaflet": "^1.9.4", diff --git a/web/apps/photos/src/components/Sidebar/index.tsx b/web/apps/photos/src/components/Sidebar/index.tsx index 32665b7120..7ec6f70559 100644 --- a/web/apps/photos/src/components/Sidebar/index.tsx +++ b/web/apps/photos/src/components/Sidebar/index.tsx @@ -1,5 +1,6 @@ import log from "@/next/log"; import { savedLogs } from "@/next/log-web"; +import { customAPIHost } from "@/next/origins"; import { openAccountsManagePasskeysPage } from "@ente/accounts/services/passkey"; import { SpaceBetweenFlex } from "@ente/shared/components/Container"; import { EnteLogo } from "@ente/shared/components/EnteLogo"; @@ -685,6 +686,8 @@ const DebugSection: React.FC = () => { electron?.appVersion().then((v) => setAppVersion(v)); }); + const host = customAPIHost(); + const confirmLogDownload = () => appContext.setDialogMessage({ title: t("DOWNLOAD_LOGS"), @@ -707,21 +710,6 @@ const DebugSection: React.FC = () => { return ( <> - - {appVersion && ( - - {appVersion} - - )} {isInternalUserViaEmailCheck() && ( { label={"Test Upload"} /> )} + + + {appVersion && ( + {appVersion} + )} + {host && {host}} + ); }; diff --git a/web/apps/photos/src/constants/billing.ts b/web/apps/photos/src/constants/billing.ts deleted file mode 100644 index f66263eda4..0000000000 --- a/web/apps/photos/src/constants/billing.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { getPaymentsURL } from "@ente/shared/network/api"; - -export const getDesktopRedirectURL = () => - `${getPaymentsURL()}/desktop-redirect`; diff --git a/web/apps/photos/src/pages/index.tsx b/web/apps/photos/src/pages/index.tsx index 8abad43975..ec37ca3bf8 100644 --- a/web/apps/photos/src/pages/index.tsx +++ b/web/apps/photos/src/pages/index.tsx @@ -1,19 +1,21 @@ +import { DevSettings } from "@/new/photos/components/DevSettings"; import log from "@/next/log"; +import { albumsAppOrigin, customAPIHost } from "@/next/origins"; import { Login } from "@ente/accounts/components/Login"; import { SignUp } from "@ente/accounts/components/SignUp"; import { EnteLogo } from "@ente/shared/components/EnteLogo"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; import { saveKeyInSessionStore } from "@ente/shared/crypto/helpers"; -import { getAlbumsURL } from "@ente/shared/network/api"; import localForage from "@ente/shared/storage/localForage"; -import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; +import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; -import { getKey, SESSION_KEYS } from "@ente/shared/storage/sessionStorage"; +import { SESSION_KEYS, getKey } from "@ente/shared/storage/sessionStorage"; import { + Box, Button, - styled, Typography, + styled, type TypographyProps, } from "@mui/material"; import { t } from "i18next"; @@ -26,14 +28,20 @@ import { useAppContext } from "./_app"; export default function LandingPage() { const { appName, showNavBar, setDialogMessage } = useAppContext(); - const router = useRouter(); + const [loading, setLoading] = useState(true); const [showLogin, setShowLogin] = useState(true); + // This is kept as state because it can change as a result of user action + // while we're on this page (there currently isn't an event listener we can + // attach to for observing changes to local storage by the same window). + const [host, setHost] = useState(customAPIHost()); + + const router = useRouter(); useEffect(() => { showNavBar(false); const currentURL = new URL(window.location.href); - const albumsURL = new URL(getAlbumsURL()); + const albumsURL = new URL(albumsAppOrigin()); currentURL.pathname = router.pathname; if ( currentURL.host === albumsURL.host && @@ -45,6 +53,8 @@ export default function LandingPage() { } }, []); + const handleMaybeChangeHost = () => setHost(customAPIHost()); + const handleAlbumsRedirect = async (currentURL: URL) => { const end = currentURL.hash.lastIndexOf("&"); const hash = currentURL.hash.slice(1, end !== -1 ? end : undefined); @@ -107,7 +117,7 @@ export default function LandingPage() { const redirectToLoginPage = () => router.push(PAGES.LOGIN); return ( - + {loading ? ( ) : ( @@ -129,23 +139,81 @@ export default function LandingPage() { + {showLogin ? ( - + ) : ( - + )} )} - + ); } -const Container = styled("div")` +interface TappableContainerProps { + /** + * Called when the user closes the dialog to set a custom server. + * + * This is our chance to re-read the value of the custom API origin from + * local storage since the user might've changed it. + */ + onMaybeChangeHost: () => void; +} + +const TappableContainer: React.FC< + React.PropsWithChildren +> = ({ onMaybeChangeHost, children }) => { + // [Note: Configuring custom server] + // + // Allow the user to tap 7 times anywhere on the onboarding screen to bring + // up a page where they can configure the endpoint that the app should + // connect to. + // + // See: https://help.ente.io/self-hosting/guides/custom-server/ + const [tapCount, setTapCount] = useState(0); + const [showDevSettings, setShowDevSettings] = useState(false); + + const handleClick: React.MouseEventHandler = (event) => { + // Don't allow this when running on (e.g.) web.ente.io. + if (!shouldAllowChangingAPIOrigin()) return; + + // Ignore clicks on buttons when counting up towards 7. + if (event.target instanceof HTMLButtonElement) return; + + // Ignore clicks when the dialog is already open. + if (showDevSettings) return; + + // Otherwise increase the tap count, + setTapCount(tapCount + 1); + // And show the dev settings dialog when it reaches 7. + if (tapCount + 1 == 7) { + setTapCount(0); + setShowDevSettings(true); + } + }; + + const handleClose = () => { + setShowDevSettings(false); + onMaybeChangeHost(); + }; + + return ( + + <> + + {children} + + + ); +}; + +const TappableContainer_ = styled("div")` display: flex; flex: 1; align-items: center; @@ -157,6 +225,15 @@ const Container = styled("div")` } `; +/** + * Disable the ability to set the custom server when we're running on our own + * production deployment. + */ +const shouldAllowChangingAPIOrigin = () => { + const hostname = new URL(window.location.origin).hostname; + return !(hostname.endsWith(".ente.io") || hostname.endsWith(".ente.sh")); +}; + const SlideContainer = styled("div")` flex: 1; display: flex; @@ -174,6 +251,35 @@ const Logo_ = styled("div")` margin-block-end: 64px; `; +const MobileBox = styled("div")` + display: none; + + @media (max-width: 1024px) { + max-width: 375px; + width: 100%; + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; + } +`; + +interface MobileBoxFooterProps { + host: string | undefined; +} + +const MobileBoxFooter: React.FC = ({ host }) => { + return ( + + {host && ( + + {host} + + )} + + ); +}; + const DesktopBox = styled("div")` flex: 1; height: 100%; @@ -188,19 +294,6 @@ const DesktopBox = styled("div")` } `; -const MobileBox = styled("div")` - display: none; - - @media (max-width: 1024px) { - max-width: 375px; - width: 100%; - padding: 12px; - display: flex; - flex-direction: column; - gap: 8px; - } -`; - const SideBox = styled("div")` display: flex; flex-direction: column; diff --git a/web/apps/photos/src/services/billingService.ts b/web/apps/photos/src/services/billingService.ts index d68938eb5e..67004874a0 100644 --- a/web/apps/photos/src/services/billingService.ts +++ b/web/apps/photos/src/services/billingService.ts @@ -1,19 +1,16 @@ import log from "@/next/log"; +import { apiOrigin, paymentsAppOrigin } from "@/next/origins"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint, getPaymentsURL } from "@ente/shared/network/api"; import { LS_KEYS, removeData, setData, } from "@ente/shared/storage/localStorage"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; -import { getDesktopRedirectURL } from "constants/billing"; import isElectron from "is-electron"; import { Plan, Subscription } from "types/billing"; import { getPaymentToken } from "./userService"; -const ENDPOINT = getEndpoint(); - enum PaymentActionType { Buy = "buy", Update = "update", @@ -36,11 +33,11 @@ class billingService { let response; if (!token) { response = await HTTPService.get( - `${ENDPOINT}/billing/plans/v2`, + `${apiOrigin()}/billing/plans/v2`, ); } else { response = await HTTPService.get( - `${ENDPOINT}/billing/user-plans`, + `${apiOrigin()}/billing/user-plans`, null, { "X-Auth-Token": getToken(), @@ -56,7 +53,7 @@ class billingService { public async syncSubscription() { try { const response = await HTTPService.get( - `${ENDPOINT}/billing/subscription`, + `${apiOrigin()}/billing/subscription`, null, { "X-Auth-Token": getToken(), @@ -100,7 +97,7 @@ class billingService { public async cancelSubscription() { try { const response = await HTTPService.post( - `${ENDPOINT}/billing/stripe/cancel-subscription`, + `${apiOrigin()}/billing/stripe/cancel-subscription`, null, null, { @@ -118,7 +115,7 @@ class billingService { public async activateSubscription() { try { const response = await HTTPService.post( - `${ENDPOINT}/billing/stripe/activate-subscription`, + `${apiOrigin()}/billing/stripe/activate-subscription`, null, null, { @@ -142,7 +139,7 @@ class billingService { return; } const response = await HTTPService.post( - `${ENDPOINT}/billing/verify-subscription`, + `${apiOrigin()}/billing/verify-subscription`, { paymentProvider: "stripe", productID: null, @@ -167,9 +164,14 @@ class billingService { return; } try { - await HTTPService.delete(`${ENDPOINT}/family/leave`, null, null, { - "X-Auth-Token": getToken(), - }); + await HTTPService.delete( + `${apiOrigin()}/family/leave`, + null, + null, + { + "X-Auth-Token": getToken(), + }, + ); removeData(LS_KEYS.FAMILY_DATA); } catch (e) { log.error("/family/leave failed", e); @@ -184,7 +186,7 @@ class billingService { ) { try { const redirectURL = this.getRedirectURL(); - window.location.href = `${getPaymentsURL()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&redirectURL=${redirectURL}`; + window.location.href = `${paymentsAppOrigin()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&redirectURL=${redirectURL}`; } catch (e) { log.error("unable to get payments url", e); throw e; @@ -195,7 +197,7 @@ class billingService { try { const redirectURL = this.getRedirectURL(); const response = await HTTPService.get( - `${ENDPOINT}/billing/stripe/customer-portal`, + `${apiOrigin()}/billing/stripe/customer-portal`, { redirectURL }, { "X-Auth-Token": getToken(), @@ -210,7 +212,7 @@ class billingService { public getRedirectURL() { if (isElectron()) { - return getDesktopRedirectURL(); + return `${paymentsAppOrigin()}/desktop-redirect`; } else { return `${window.location.origin}/gallery`; } diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index 18d43d7447..485ea7cb76 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -1,11 +1,10 @@ -import { getEndpoint } from "@ente/shared/network/api"; -import localForage from "@ente/shared/storage/localForage"; -import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; - import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; +import localForage from "@ente/shared/storage/localForage"; +import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import { getActualKey } from "@ente/shared/user"; import type { User } from "@ente/shared/user/types"; @@ -77,7 +76,6 @@ import { import { getLocalFiles } from "./fileService"; import { getPublicKey } from "./userService"; -const ENDPOINT = getEndpoint(); const COLLECTION_TABLE = "collections"; const COLLECTION_UPDATION_TIME = "collection-updation-time"; const HIDDEN_COLLECTION_IDS = "hidden-collection-ids"; @@ -183,7 +181,7 @@ const getCollections = async ( ): Promise => { try { const resp = await HTTPService.get( - `${ENDPOINT}/collections/v2`, + `${apiOrigin()}/collections/v2`, { sinceTime, }, @@ -330,7 +328,7 @@ export const getCollection = async ( return; } const resp = await HTTPService.get( - `${ENDPOINT}/collections/${collectionID}`, + `${apiOrigin()}/collections/${collectionID}`, null, { "X-Auth-Token": token }, ); @@ -474,7 +472,7 @@ const postCollection = async ( ): Promise => { try { const response = await HTTPService.post( - `${ENDPOINT}/collections`, + `${apiOrigin()}/collections`, collectionData, null, { "X-Auth-Token": token }, @@ -529,7 +527,7 @@ export const addToCollection = async ( files: fileKeysEncryptedWithNewCollection, }; await HTTPService.post( - `${ENDPOINT}/collections/add-files`, + `${apiOrigin()}/collections/add-files`, requestBody, null, { @@ -559,7 +557,7 @@ export const restoreToCollection = async ( files: fileKeysEncryptedWithNewCollection, }; await HTTPService.post( - `${ENDPOINT}/collections/restore-files`, + `${apiOrigin()}/collections/restore-files`, requestBody, null, { @@ -590,7 +588,7 @@ export const moveToCollection = async ( files: fileKeysEncryptedWithNewCollection, }; await HTTPService.post( - `${ENDPOINT}/collections/move-files`, + `${apiOrigin()}/collections/move-files`, requestBody, null, { @@ -736,7 +734,7 @@ export const removeNonUserFiles = async ( }; await HTTPService.post( - `${ENDPOINT}/collections/v3/remove-files`, + `${apiOrigin()}/collections/v3/remove-files`, request, null, { "X-Auth-Token": token }, @@ -763,7 +761,7 @@ export const deleteCollection = async ( const token = getToken(); await HTTPService.delete( - `${ENDPOINT}/collections/v3/${collectionID}`, + `${apiOrigin()}/collections/v3/${collectionID}`, null, { collectionID, keepFiles }, { "X-Auth-Token": token }, @@ -779,7 +777,7 @@ export const leaveSharedAlbum = async (collectionID: number) => { const token = getToken(); await HTTPService.post( - `${ENDPOINT}/collections/leave/${collectionID}`, + `${apiOrigin()}/collections/leave/${collectionID}`, null, null, { "X-Auth-Token": token }, @@ -817,7 +815,7 @@ export const updateCollectionMagicMetadata = async ( }; await HTTPService.put( - `${ENDPOINT}/collections/magic-metadata`, + `${apiOrigin()}/collections/magic-metadata`, reqBody, null, { @@ -861,7 +859,7 @@ export const updateSharedCollectionMagicMetadata = async ( }; await HTTPService.put( - `${ENDPOINT}/collections/sharee-magic-metadata`, + `${apiOrigin()}/collections/sharee-magic-metadata`, reqBody, null, { @@ -905,7 +903,7 @@ export const updatePublicCollectionMagicMetadata = async ( }; await HTTPService.put( - `${ENDPOINT}/collections/public-magic-metadata`, + `${apiOrigin()}/collections/public-magic-metadata`, reqBody, null, { @@ -940,7 +938,7 @@ export const renameCollection = async ( nameDecryptionNonce, }; await HTTPService.post( - `${ENDPOINT}/collections/rename`, + `${apiOrigin()}/collections/rename`, collectionRenameRequest, null, { @@ -969,7 +967,7 @@ export const shareCollection = async ( encryptedKey, }; await HTTPService.post( - `${ENDPOINT}/collections/share`, + `${apiOrigin()}/collections/share`, shareCollectionRequest, null, { @@ -993,7 +991,7 @@ export const unshareCollection = async ( email: withUserEmail, }; await HTTPService.post( - `${ENDPOINT}/collections/unshare`, + `${apiOrigin()}/collections/unshare`, shareCollectionRequest, null, { @@ -1015,7 +1013,7 @@ export const createShareableURL = async (collection: Collection) => { collectionID: collection.id, }; const resp = await HTTPService.post( - `${ENDPOINT}/collections/share-url`, + `${apiOrigin()}/collections/share-url`, createPublicAccessTokenRequest, null, { @@ -1036,7 +1034,7 @@ export const deleteShareableURL = async (collection: Collection) => { return null; } await HTTPService.delete( - `${ENDPOINT}/collections/share-url/${collection.id}`, + `${apiOrigin()}/collections/share-url/${collection.id}`, null, null, { @@ -1058,7 +1056,7 @@ export const updateShareableURL = async ( return null; } const res = await HTTPService.put( - `${ENDPOINT}/collections/share-url`, + `${apiOrigin()}/collections/share-url`, request, null, { diff --git a/web/apps/photos/src/services/deduplicationService.ts b/web/apps/photos/src/services/deduplicationService.ts index 1683e554c4..b17d9f4f0b 100644 --- a/web/apps/photos/src/services/deduplicationService.ts +++ b/web/apps/photos/src/services/deduplicationService.ts @@ -2,13 +2,11 @@ import { hasFileHash } from "@/media/file"; import { FILE_TYPE } from "@/media/file-type"; import type { Metadata } from "@/media/types/file"; import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import { EnteFile } from "types/file"; -const ENDPOINT = getEndpoint(); - interface DuplicatesResponse { duplicates: Array<{ fileIDs: number[]; @@ -148,7 +146,7 @@ function groupDupesByFileHashes(dupe: Duplicate) { async function fetchDuplicateFileIDs() { try { const response = await HTTPService.get( - `${ENDPOINT}/files/duplicates`, + `${apiOrigin()}/files/duplicates`, null, { "X-Auth-Token": getToken(), diff --git a/web/apps/photos/src/services/download/clients/photos.ts b/web/apps/photos/src/services/download/clients/photos.ts index 9f4ecc6079..ac88fa0324 100644 --- a/web/apps/photos/src/services/download/clients/photos.ts +++ b/web/apps/photos/src/services/download/clients/photos.ts @@ -1,6 +1,6 @@ +import { customAPIOrigin } from "@/next/origins"; import { CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { customAPIOrigin } from "@ente/shared/network/api"; import { retryAsyncFunction } from "@ente/shared/utils"; import { DownloadClient } from "services/download"; import { EnteFile } from "types/file"; diff --git a/web/apps/photos/src/services/download/clients/publicAlbums.ts b/web/apps/photos/src/services/download/clients/publicAlbums.ts index 4db8d2cf45..4a15407e59 100644 --- a/web/apps/photos/src/services/download/clients/publicAlbums.ts +++ b/web/apps/photos/src/services/download/clients/publicAlbums.ts @@ -1,6 +1,6 @@ +import { customAPIOrigin } from "@/next/origins"; import { CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { customAPIOrigin } from "@ente/shared/network/api"; import { retryAsyncFunction } from "@ente/shared/utils"; import { DownloadClient } from "services/download"; import { EnteFile } from "types/file"; diff --git a/web/apps/photos/src/services/embeddingService.ts b/web/apps/photos/src/services/embeddingService.ts index fb77609258..04c5089f9e 100644 --- a/web/apps/photos/src/services/embeddingService.ts +++ b/web/apps/photos/src/services/embeddingService.ts @@ -1,10 +1,10 @@ import { inWorker } from "@/next/env"; import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import { workerBridge } from "@/next/worker/worker-bridge"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; import localForage from "@ente/shared/storage/localForage"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import type { @@ -285,7 +285,7 @@ export const getEmbeddingsDiff = async ( return; } const response = await HTTPService.get( - `${getEndpoint()}/embeddings/diff`, + `${apiOrigin()}/embeddings/diff`, { sinceTime, limit: DIFF_LIMIT, @@ -314,7 +314,7 @@ export const putEmbedding = async ( throw Error(CustomError.TOKEN_MISSING); } const resp = await HTTPService.put( - `${getEndpoint()}/embeddings`, + `${apiOrigin()}/embeddings`, putEmbeddingReq, null, { diff --git a/web/apps/photos/src/services/entityService.ts b/web/apps/photos/src/services/entityService.ts index 31607110ed..67f6275a03 100644 --- a/web/apps/photos/src/services/entityService.ts +++ b/web/apps/photos/src/services/entityService.ts @@ -1,7 +1,7 @@ import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; import localForage from "@ente/shared/storage/localForage"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import { getActualKey } from "@ente/shared/user"; @@ -15,8 +15,6 @@ import { } from "types/entity"; import { getLatestVersionEntities } from "utils/entity"; -const ENDPOINT = getEndpoint(); - const DIFF_LIMIT = 500; const ENTITY_TABLES: Record = { @@ -60,7 +58,7 @@ const getEntityKey = async (type: EntityType) => { return; } const resp = await HTTPService.get( - `${ENDPOINT}/user-entity/key`, + `${apiOrigin()}/user-entity/key`, { type, }, @@ -175,7 +173,7 @@ const getEntityDiff = async ( return; } const resp = await HTTPService.get( - `${ENDPOINT}/user-entity/entity/diff`, + `${apiOrigin()}/user-entity/entity/diff`, { sinceTime: time, type, diff --git a/web/apps/photos/src/services/fileService.ts b/web/apps/photos/src/services/fileService.ts index a3aa90ab04..ebabc9dbfc 100644 --- a/web/apps/photos/src/services/fileService.ts +++ b/web/apps/photos/src/services/fileService.ts @@ -1,8 +1,8 @@ import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { Events, eventBus } from "@ente/shared/events"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; import localForage from "@ente/shared/storage/localForage"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import { REQUEST_BATCH_SIZE } from "constants/api"; @@ -28,7 +28,6 @@ import { setCollectionLastSyncTime, } from "./collectionService"; -const ENDPOINT = getEndpoint(); const FILES_TABLE = "files"; const HIDDEN_FILES_TABLE = "hidden-files"; @@ -118,7 +117,7 @@ export const getFiles = async ( break; } resp = await HTTPService.get( - `${ENDPOINT}/collections/v2/diff`, + `${apiOrigin()}/collections/v2/diff`, { collectionID: collection.id, sinceTime: time, @@ -187,7 +186,7 @@ export const trashFiles = async (filesToTrash: EnteFile[]) => { })), }; await HTTPService.post( - `${ENDPOINT}/files/trash`, + `${apiOrigin()}/files/trash`, trashRequest, null, { @@ -211,7 +210,7 @@ export const deleteFromTrash = async (filesToDelete: number[]) => { for (const batch of batchedFilesToDelete) { await HTTPService.post( - `${ENDPOINT}/trash/delete`, + `${apiOrigin()}/trash/delete`, { fileIDs: batch }, null, { @@ -253,9 +252,14 @@ export const updateFileMagicMetadata = async ( }, }); } - await HTTPService.put(`${ENDPOINT}/files/magic-metadata`, reqBody, null, { - "X-Auth-Token": token, - }); + await HTTPService.put( + `${apiOrigin()}/files/magic-metadata`, + reqBody, + null, + { + "X-Auth-Token": token, + }, + ); return fileWithUpdatedMagicMetadataList.map( ({ file, updatedMagicMetadata }): EnteFile => ({ ...file, @@ -296,7 +300,7 @@ export const updateFilePublicMagicMetadata = async ( }); } await HTTPService.put( - `${ENDPOINT}/files/public-magic-metadata`, + `${apiOrigin()}/files/public-magic-metadata`, reqBody, null, { diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index 2cf42254f5..9dced45610 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -1,15 +1,14 @@ import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { CustomError, parseSharingErrorCodes } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; import localForage from "@ente/shared/storage/localForage"; import { Collection, CollectionPublicMagicMetadata } from "types/collection"; import { EncryptedEnteFile, EnteFile } from "types/file"; import { LocalSavedPublicCollectionFiles } from "types/publicCollection"; import { decryptFile, mergeMetadata, sortFiles } from "utils/file"; -const ENDPOINT = getEndpoint(); const PUBLIC_COLLECTION_FILES_TABLE = "public-collection-files"; const PUBLIC_COLLECTIONS_TABLE = "public-collections"; const PUBLIC_REFERRAL_CODE = "public-referral-code"; @@ -253,7 +252,7 @@ const getPublicFiles = async ( break; } resp = await HTTPService.get( - `${ENDPOINT}/public-collection/diff`, + `${apiOrigin()}/public-collection/diff`, { sinceTime: time, }, @@ -308,7 +307,7 @@ export const getPublicCollection = async ( return; } const resp = await HTTPService.get( - `${ENDPOINT}/public-collection/info`, + `${apiOrigin()}/public-collection/info`, null, { "Cache-Control": "no-cache", "X-Auth-Access-Token": token }, ); @@ -358,7 +357,7 @@ export const verifyPublicCollectionPassword = async ( ): Promise => { try { const resp = await HTTPService.post( - `${ENDPOINT}/public-collection/verify-password`, + `${apiOrigin()}/public-collection/verify-password`, { passHash: passwordHash }, null, { "Cache-Control": "no-cache", "X-Auth-Access-Token": token }, diff --git a/web/apps/photos/src/services/trashService.ts b/web/apps/photos/src/services/trashService.ts index 7088bc0860..f367304550 100644 --- a/web/apps/photos/src/services/trashService.ts +++ b/web/apps/photos/src/services/trashService.ts @@ -1,6 +1,6 @@ import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; import localForage from "@ente/shared/storage/localForage"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import { Collection } from "types/collection"; @@ -14,8 +14,6 @@ const TRASH = "file-trash"; const TRASH_TIME = "trash-time"; const DELETED_COLLECTION = "deleted-collection"; -const ENDPOINT = getEndpoint(); - async function getLocalTrash() { const trash = (await localForage.getItem(TRASH)) || []; return trash; @@ -91,7 +89,7 @@ export const updateTrash = async ( break; } resp = await HTTPService.get( - `${ENDPOINT}/trash/v2/diff`, + `${apiOrigin()}/trash/v2/diff`, { sinceTime: time, }, @@ -160,7 +158,7 @@ export const emptyTrash = async () => { const lastUpdatedAt = await getLastSyncTime(); await HTTPService.post( - `${ENDPOINT}/trash/empty`, + `${apiOrigin()}/trash/empty`, { lastUpdatedAt }, null, { diff --git a/web/apps/photos/src/services/upload/publicUploadHttpClient.ts b/web/apps/photos/src/services/upload/publicUploadHttpClient.ts index 8f18a1638b..22ed45e2f3 100644 --- a/web/apps/photos/src/services/upload/publicUploadHttpClient.ts +++ b/web/apps/photos/src/services/upload/publicUploadHttpClient.ts @@ -1,13 +1,11 @@ import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import { CustomError, handleUploadError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; import { EnteFile } from "types/file"; import { retryHTTPCall } from "./uploadHttpClient"; import { MultipartUploadURLs, UploadFile, UploadURL } from "./uploadService"; -const ENDPOINT = getEndpoint(); - const MAX_URL_REQUESTS = 50; class PublicUploadHttpClient { @@ -25,7 +23,7 @@ class PublicUploadHttpClient { const response = await retryHTTPCall( () => HTTPService.post( - `${ENDPOINT}/public-collection/file`, + `${apiOrigin()}/public-collection/file`, uploadFile, null, { @@ -57,7 +55,7 @@ class PublicUploadHttpClient { throw Error(CustomError.TOKEN_MISSING); } this.uploadURLFetchInProgress = HTTPService.get( - `${ENDPOINT}/public-collection/upload-urls`, + `${apiOrigin()}/public-collection/upload-urls`, { count: Math.min(MAX_URL_REQUESTS, count * 2), }, @@ -93,7 +91,7 @@ class PublicUploadHttpClient { throw Error(CustomError.TOKEN_MISSING); } const response = await HTTPService.get( - `${ENDPOINT}/public-collection/multipart-upload-urls`, + `${apiOrigin()}/public-collection/multipart-upload-urls`, { count, }, diff --git a/web/apps/photos/src/services/upload/uploadHttpClient.ts b/web/apps/photos/src/services/upload/uploadHttpClient.ts index c23a58b520..6841c0c1e8 100644 --- a/web/apps/photos/src/services/upload/uploadHttpClient.ts +++ b/web/apps/photos/src/services/upload/uploadHttpClient.ts @@ -1,15 +1,12 @@ import log from "@/next/log"; +import { apiOrigin, uploaderOrigin } from "@/next/origins"; import { wait } from "@/utils/promise"; import { CustomError, handleUploadError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint, getUploadEndpoint } from "@ente/shared/network/api"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import { EnteFile } from "types/file"; import { MultipartUploadURLs, UploadFile, UploadURL } from "./uploadService"; -const ENDPOINT = getEndpoint(); -const UPLOAD_ENDPOINT = getUploadEndpoint(); - const MAX_URL_REQUESTS = 50; class UploadHttpClient { @@ -23,7 +20,7 @@ class UploadHttpClient { } const response = await retryHTTPCall( () => - HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, { + HTTPService.post(`${apiOrigin()}/files`, uploadFile, null, { "X-Auth-Token": token, }), handleUploadError, @@ -44,7 +41,7 @@ class UploadHttpClient { return; } this.uploadURLFetchInProgress = HTTPService.get( - `${ENDPOINT}/files/upload-urls`, + `${apiOrigin()}/files/upload-urls`, { count: Math.min(MAX_URL_REQUESTS, count * 2), }, @@ -74,7 +71,7 @@ class UploadHttpClient { return; } const response = await HTTPService.get( - `${ENDPOINT}/files/multipart-upload-urls`, + `${apiOrigin()}/files/multipart-upload-urls`, { count, }, @@ -122,7 +119,7 @@ class UploadHttpClient { try { await retryHTTPCall(() => HTTPService.put( - `${UPLOAD_ENDPOINT}/file-upload`, + `${uploaderOrigin()}/file-upload`, file, null, { @@ -178,7 +175,7 @@ class UploadHttpClient { try { const response = await retryHTTPCall(async () => { const resp = await HTTPService.put( - `${UPLOAD_ENDPOINT}/multipart-upload`, + `${uploaderOrigin()}/multipart-upload`, filePart, null, { @@ -219,7 +216,7 @@ class UploadHttpClient { try { await retryHTTPCall(() => HTTPService.post( - `${UPLOAD_ENDPOINT}/multipart-complete`, + `${uploaderOrigin()}/multipart-complete`, reqBody, null, { diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index eadb4ffc85..150b85ba69 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -520,6 +520,11 @@ class UploadManager { while (this.itemsToBeUploaded.length > 0) { this.abortIfCancelled(); + if (shouldWaitForMemoryPressureToEase()) { + await wait(2000); + continue; + } + const clusteredItem = this.itemsToBeUploaded.pop(); const { localID, collectionID } = clusteredItem; const collection = this.collections.get(collectionID); @@ -999,3 +1004,45 @@ const uploadItemSize = async (uploadItem: UploadItem): Promise => { return ensureElectron().pathOrZipItemSize(uploadItem); return uploadItem.file.size; }; + +/** + * [Note: Memory pressure when uploading video files] + * + * Some users have reported that their app runs out of memory when the app tries + * to upload multiple large videos simultaneously. For example, 4 parallel + * uploads of 4 700 MB videos. + * + * I am unable to reproduce this: tested on macOS, with videos up to 3.8 G + * uploaded in parallel. The memory usage remains constant (as expected, + * hovering around 2 G), since we don't pull the entire videos in memory and + * instead do a streaming disk read + encryption + upload. + * + * The JavaScript heap for the renderer process (when we're running in the + * context of our desktop app) is limited to 4 GB. See + * https://www.electronjs.org/blog/v8-memory-cage. + * + * Perhaps there is some distinct memory usage pattern on some systems that + * causes this limit to be reached. + * + * So as a safety check, this function returns true whenever we exceed some + * memory usage high water mark. If so, then the uploader should wait for the + * memory usage to come down before initiating a new upload. + */ +const shouldWaitForMemoryPressureToEase = () => { + if (!globalThis.electron) return false; + // performance.memory is deprecated in general as a Web standard, and is + // also not available in the DOM types provided by TypeScript. However, it + // is the method recommended by the Electron team (see the link about the V8 + // memory cage). The embedded Chromium supports it fine though, we just need + // to goad TypeScript to accept the type. + const heapSizeInBytes = (performance as any).memory.usedJSHeapSize; + const heapSizeInGB = heapSizeInBytes / (1024 * 1024 * 1024); + // 4 GB is the hard limit. Let us keep a lot of margin since uploads get + // triggered in parallel so if we're unlucky they all might get trigger when + // the memory usage is relatively low. + if (heapSizeInGB < 2.5) return false; + log.info( + `Memory usage (${heapSizeInGB} GB) exceeds the high water mark, pausing new uploads`, + ); + return true; +}; diff --git a/web/apps/photos/src/services/userService.ts b/web/apps/photos/src/services/userService.ts index 6ec018a368..6ae112635a 100644 --- a/web/apps/photos/src/services/userService.ts +++ b/web/apps/photos/src/services/userService.ts @@ -1,8 +1,8 @@ import log from "@/next/log"; +import { apiOrigin, customAPIOrigin, familyAppOrigin } from "@/next/origins"; import { putAttributes } from "@ente/accounts/api/user"; import { ApiError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint, getFamilyPortalURL } from "@ente/shared/network/api"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { getToken, @@ -17,15 +17,13 @@ import { } from "types/user"; import { getLocalFamilyData, isPartOfFamily } from "utils/user/family"; -const ENDPOINT = getEndpoint(); - const HAS_SET_KEYS = "hasSetKeys"; export const getPublicKey = async (email: string) => { const token = getToken(); const resp = await HTTPService.get( - `${ENDPOINT}/users/public-key`, + `${apiOrigin()}/users/public-key`, { email }, { "X-Auth-Token": token, @@ -38,7 +36,7 @@ export const getPaymentToken = async () => { const token = getToken(); const resp = await HTTPService.get( - `${ENDPOINT}/users/payment-token`, + `${apiOrigin()}/users/payment-token`, null, { "X-Auth-Token": token, @@ -52,7 +50,7 @@ export const getFamiliesToken = async () => { const token = getToken(); const resp = await HTTPService.get( - `${ENDPOINT}/users/families-token`, + `${apiOrigin()}/users/families-token`, null, { "X-Auth-Token": token, @@ -70,7 +68,7 @@ export const getRoadmapRedirectURL = async () => { const token = getToken(); const resp = await HTTPService.get( - `${ENDPOINT}/users/roadmap/v2`, + `${apiOrigin()}/users/roadmap/v2`, null, { "X-Auth-Token": token, @@ -86,7 +84,7 @@ export const getRoadmapRedirectURL = async () => { export const isTokenValid = async (token: string) => { try { const resp = await HTTPService.get( - `${ENDPOINT}/users/session-validity/v2`, + `${apiOrigin()}/users/session-validity/v2`, null, { "X-Auth-Token": token, @@ -125,7 +123,7 @@ export const isTokenValid = async (token: string) => { export const getTwoFactorStatus = async () => { const resp = await HTTPService.get( - `${ENDPOINT}/users/two-factor/status`, + `${apiOrigin()}/users/two-factor/status`, null, { "X-Auth-Token": getToken(), @@ -139,7 +137,7 @@ export const getUserDetailsV2 = async (): Promise => { const token = getToken(); const resp = await HTTPService.get( - `${ENDPOINT}/users/details/v2`, + `${apiOrigin()}/users/details/v2`, null, { "X-Auth-Token": token, @@ -156,7 +154,7 @@ export const getFamilyPortalRedirectURL = async () => { try { const jwtToken = await getFamiliesToken(); const isFamilyCreated = isPartOfFamily(getLocalFamilyData()); - return `${getFamilyPortalURL()}?token=${jwtToken}&isFamilyCreated=${isFamilyCreated}&redirectURL=${ + return `${familyAppOrigin()}?token=${jwtToken}&isFamilyCreated=${isFamilyCreated}&redirectURL=${ window.location.origin }/gallery`; } catch (e) { @@ -170,7 +168,7 @@ export const getAccountDeleteChallenge = async () => { const token = getToken(); const resp = await HTTPService.get( - `${ENDPOINT}/users/delete-challenge`, + `${apiOrigin()}/users/delete-challenge`, null, { "X-Auth-Token": token, @@ -195,7 +193,7 @@ export const deleteAccount = async ( } await HTTPService.delete( - `${ENDPOINT}/users/delete`, + `${apiOrigin()}/users/delete`, { challenge, reason, feedback }, null, { @@ -213,7 +211,7 @@ export const getFaceSearchEnabledStatus = async () => { const token = getToken(); const resp: AxiosResponse = await HTTPService.get( - `${ENDPOINT}/remote-store`, + `${apiOrigin()}/remote-store`, { key: "faceSearchEnabled", defaultValue: false, @@ -233,7 +231,7 @@ export const updateFaceSearchEnabledStatus = async (newStatus: boolean) => { try { const token = getToken(); await HTTPService.post( - `${ENDPOINT}/remote-store/update`, + `${apiOrigin()}/remote-store/update`, { key: "faceSearchEnabled", value: newStatus.toString(), @@ -264,7 +262,7 @@ export const getMapEnabledStatus = async () => { const token = getToken(); const resp: AxiosResponse = await HTTPService.get( - `${ENDPOINT}/remote-store`, + `${apiOrigin()}/remote-store`, { key: "mapEnabled", defaultValue: false, @@ -284,7 +282,7 @@ export const updateMapEnabledStatus = async (newStatus: boolean) => { try { const token = getToken(); await HTTPService.post( - `${ENDPOINT}/remote-store/update`, + `${apiOrigin()}/remote-store/update`, { key: "mapEnabled", value: newStatus.toString(), @@ -314,10 +312,13 @@ export const updateMapEnabledStatus = async (newStatus: boolean) => { * rename this to say getUseDirectUpload). */ export async function getDisableCFUploadProxyFlag(): Promise { - // If NEXT_PUBLIC_ENTE_ENDPOINT is set, that means we're not running a - // production deployment. Disable the Cloudflare upload proxy, and instead - // just directly use the upload URLs that museum gives us. - if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT) return true; + // If a custom origin is set, that means we're not running a production + // deployment (maybe we're running locally, or being self-hosted). + // + // In such cases, disable the Cloudflare upload proxy (which won't work for + // self-hosters), and instead just directly use the upload URLs that museum + // gives us. + if (customAPIOrigin()) return true; try { const featureFlags = ( diff --git a/web/docs/dependencies.md b/web/docs/dependencies.md index a0eee351fa..56b6712e00 100644 --- a/web/docs/dependencies.md +++ b/web/docs/dependencies.md @@ -46,9 +46,7 @@ The root `package.json` also has a convenience dev dependency: - [concurrently](https://github.com/open-cli-tools/concurrently) for spawning parallel tasks when we invoke various yarn scripts. -## Utils - -### Crypto +## Crypto We use [libsodium](https://libsodium.gitbook.io/doc/) for encryption, key generation etc. Specifically, we use its WebAssembly and JS wrappers made using @@ -70,6 +68,27 @@ builds (See this [issue](https://github.com/jedisct1/libsodium.js/issues/326)). Updating it is not a big problem, it is just a pending chore - we want to test a bit more exhaustively when changing the crypto layer. +## Meta frameworks + +### Next.js + +[Next.js](https://nextjs.org) ("next") provides the meta framework for both the +Photos and the Auth app, and also for some of the sidecar apps like accounts and +cast. + +We use a limited subset of Next. The main thing we get out of it is a reasonable +set of defaults for bundling our app into a static export which we can then +deploy to our webserver. In addition, the Next.js page router is convenient. +Apart from this, while we use a few tidbits from Next.js here and there, overall +our apps are regular React SPAs, and are not particularly tied to Next. + +### Vite + +For some of our newer code, we have started to use [Vite](https://vitejs.dev). +It is more lower level than Next, but the bells and whistles it doesn't have are +the bells and whistles (and the accompanying complexity) that we don't need in +some cases. + ## UI ### React @@ -77,13 +96,21 @@ bit more exhaustively when changing the crypto layer. [React](https://react.dev) ("react") is our core framework. It also has a sibling "react-dom" package that renders JSX to the DOM. -### MUI and Emotion +### MUI and Material Icons -We use [MUI](https://mui.com) ("@mui/material"), which is a React component -library, to get a base set of components. +We use [MUI](https://mui.com)'s + +- [@mui/material](https://mui.com/material-ui/getting-started/installation/), + which is a React component library, to get a base set of components; and + +- [@mui/material-icons](https://mui.com/material-ui/material-icons/). which + provides Material icons exported as React components (a `SvgIcon`). + +### Emotion MUI uses [Emotion](https://emotion.sh/) (a styled-component variant) as its -preferred CSS-in-JS library. +preferred CSS-in-JS library, and we use the same in our code too to reduce +moving parts. Emotion itself comes in many parts, of which we need the following: @@ -133,28 +160,13 @@ with Next.js. For more details, see [translations.md](translations.md). -## Meta frameworks +### Others -### Next.js +- [formik](https://github.com/jaredpalmer/formik) provides an easier to use + abstraction for dealing with form state, validation and submission states + when using React. -[Next.js](https://nextjs.org) ("next") provides the meta framework for both the -Photos and the Auth app, and also for some of the sidecar apps like accounts and -cast. - -We use a limited subset of Next. The main thing we get out of it is a reasonable -set of defaults for bundling our app into a static export which we can then -deploy to our webserver. In addition, the Next.js page router is convenient. -Apart from this, while we use a few tidbits from Next.js here and there, overall -our apps are regular React SPAs, and are not particularly tied to Next. - -### Vite - -For some of our newer code, we have started to use [Vite](https://vitejs.dev). -It is more lower level than Next, but the bells and whistles it doesn't have are -the bells and whistles (and the accompanying complexity) that we don't need in -some cases. - -## General +## Infrastructure - [comlink](https://github.com/GoogleChromeLabs/comlink) provides a minimal layer on top of Web Workers to make them more easier to use. @@ -182,6 +194,8 @@ some cases. ## Photos app specific +### General + - [react-dropzone](https://github.com/react-dropzone/react-dropzone/) is a React hook to create a drag-and-drop input zone. @@ -189,7 +203,7 @@ some cases. for converting arbitrary strings into strings that are suitable for being used as filenames. -## Face search +### Face search - [transformation-matrix](https://github.com/chrvadala/transformation-matrix) is used for performing 2D affine transformations using transformation diff --git a/web/packages/accounts/api/srp.ts b/web/packages/accounts/api/srp.ts index a9af2c15e1..d221309f05 100644 --- a/web/packages/accounts/api/srp.ts +++ b/web/packages/accounts/api/srp.ts @@ -1,7 +1,5 @@ import log from "@/next/log"; -import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; - +import { apiOrigin } from "@/next/origins"; import type { CompleteSRPSetupRequest, CompleteSRPSetupResponse, @@ -15,17 +13,19 @@ import type { UpdateSRPAndKeysResponse, } from "@ente/accounts/types/srp"; import { ApiError, CustomError } from "@ente/shared/error"; +import HTTPService from "@ente/shared/network/HTTPService"; import { HttpStatusCode } from "axios"; -const ENDPOINT = getEndpoint(); - export const getSRPAttributes = async ( email: string, ): Promise => { try { - const resp = await HTTPService.get(`${ENDPOINT}/users/srp/attributes`, { - email, - }); + const resp = await HTTPService.get( + `${apiOrigin()}/users/srp/attributes`, + { + email, + }, + ); return (resp.data as GetSRPAttributesResponse).attributes; } catch (e) { log.error("failed to get SRP attributes", e); @@ -39,7 +39,7 @@ export const startSRPSetup = async ( ): Promise => { try { const resp = await HTTPService.post( - `${ENDPOINT}/users/srp/setup`, + `${apiOrigin()}/users/srp/setup`, setupSRPRequest, undefined, { @@ -60,7 +60,7 @@ export const completeSRPSetup = async ( ) => { try { const resp = await HTTPService.post( - `${ENDPOINT}/users/srp/complete`, + `${apiOrigin()}/users/srp/complete`, completeSRPSetupRequest, undefined, { @@ -77,7 +77,7 @@ export const completeSRPSetup = async ( export const createSRPSession = async (srpUserID: string, srpA: string) => { try { const resp = await HTTPService.post( - `${ENDPOINT}/users/srp/create-session`, + `${apiOrigin()}/users/srp/create-session`, { srpUserID, srpA, @@ -97,7 +97,7 @@ export const verifySRPSession = async ( ) => { try { const resp = await HTTPService.post( - `${ENDPOINT}/users/srp/verify-session`, + `${apiOrigin()}/users/srp/verify-session`, { sessionID, srpUserID, @@ -125,7 +125,7 @@ export const updateSRPAndKeys = async ( ): Promise => { try { const resp = await HTTPService.post( - `${ENDPOINT}/users/srp/update`, + `${apiOrigin()}/users/srp/update`, updateSRPAndKeyRequest, undefined, { diff --git a/web/packages/accounts/api/user.ts b/web/packages/accounts/api/user.ts index 1060bb6ebb..3303778730 100644 --- a/web/packages/accounts/api/user.ts +++ b/web/packages/accounts/api/user.ts @@ -1,3 +1,4 @@ +import { apiOrigin } from "@/next/origins"; import type { AppName } from "@/next/types/app"; import type { RecoveryKey, @@ -9,15 +10,12 @@ import type { import type { B64EncryptionResult } from "@ente/shared/crypto/types"; import { ApiError, CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import type { KeyAttributes } from "@ente/shared/user/types"; import { HttpStatusCode } from "axios"; -const ENDPOINT = getEndpoint(); - export const sendOtt = (appName: AppName, email: string) => { - return HTTPService.post(`${ENDPOINT}/users/ott`, { + return HTTPService.post(`${apiOrigin()}/users/ott`, { email, client: appName == "auth" ? "totp" : "web", }); @@ -25,7 +23,7 @@ export const sendOtt = (appName: AppName, email: string) => { export const verifyOtt = (email: string, ott: string, referral: string) => { const cleanedReferral = `web:${referral?.trim() || ""}`; - return HTTPService.post(`${ENDPOINT}/users/verify-email`, { + return HTTPService.post(`${apiOrigin()}/users/verify-email`, { email, ott, source: cleanedReferral, @@ -34,7 +32,7 @@ export const verifyOtt = (email: string, ott: string, referral: string) => { export const putAttributes = (token: string, keyAttributes: KeyAttributes) => HTTPService.put( - `${ENDPOINT}/users/attributes`, + `${apiOrigin()}/users/attributes`, { keyAttributes }, undefined, { @@ -45,7 +43,7 @@ export const putAttributes = (token: string, keyAttributes: KeyAttributes) => export const logout = async () => { try { const token = getToken(); - await HTTPService.post(`${ENDPOINT}/users/logout`, null, undefined, { + await HTTPService.post(`${apiOrigin()}/users/logout`, null, undefined, { "X-Auth-Token": token, }); } catch (e) { @@ -65,10 +63,13 @@ export const logout = async () => { }; export const verifyTwoFactor = async (code: string, sessionID: string) => { - const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/verify`, { - code, - sessionID, - }); + const resp = await HTTPService.post( + `${apiOrigin()}/users/two-factor/verify`, + { + code, + sessionID, + }, + ); return resp.data as UserVerificationResponse; }; @@ -79,10 +80,13 @@ export const recoverTwoFactor = async ( sessionID: string, twoFactorType: TwoFactorType, ) => { - const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/recover`, { - sessionID, - twoFactorType, - }); + const resp = await HTTPService.get( + `${apiOrigin()}/users/two-factor/recover`, + { + sessionID, + twoFactorType, + }, + ); return resp.data as TwoFactorRecoveryResponse; }; @@ -91,17 +95,20 @@ export const removeTwoFactor = async ( secret: string, twoFactorType: TwoFactorType, ) => { - const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, { - sessionID, - secret, - twoFactorType, - }); + const resp = await HTTPService.post( + `${apiOrigin()}/users/two-factor/remove`, + { + sessionID, + secret, + twoFactorType, + }, + ); return resp.data as TwoFactorVerificationResponse; }; export const changeEmail = async (email: string, ott: string) => { await HTTPService.post( - `${ENDPOINT}/users/change-email`, + `${apiOrigin()}/users/change-email`, { email, ott, @@ -114,7 +121,7 @@ export const changeEmail = async (email: string, ott: string) => { }; export const sendOTTForEmailChange = async (email: string) => { - await HTTPService.post(`${ENDPOINT}/users/ott`, { + await HTTPService.post(`${apiOrigin()}/users/ott`, { email, client: "web", purpose: "change", @@ -123,7 +130,7 @@ export const sendOTTForEmailChange = async (email: string) => { export const setupTwoFactor = async () => { const resp = await HTTPService.post( - `${ENDPOINT}/users/two-factor/setup`, + `${apiOrigin()}/users/two-factor/setup`, null, undefined, { @@ -138,7 +145,7 @@ export const enableTwoFactor = async ( recoveryEncryptedTwoFactorSecret: B64EncryptionResult, ) => { await HTTPService.post( - `${ENDPOINT}/users/two-factor/enable`, + `${apiOrigin()}/users/two-factor/enable`, { code, encryptedTwoFactorSecret: @@ -154,13 +161,18 @@ export const enableTwoFactor = async ( }; export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) => - HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, undefined, { - "X-Auth-Token": token, - }); + HTTPService.put( + `${apiOrigin()}/users/recovery-key`, + recoveryKey, + undefined, + { + "X-Auth-Token": token, + }, + ); export const disableTwoFactor = async () => { await HTTPService.post( - `${ENDPOINT}/users/two-factor/disable`, + `${apiOrigin()}/users/two-factor/disable`, null, undefined, { diff --git a/web/packages/accounts/components/Login.tsx b/web/packages/accounts/components/Login.tsx index 50cebd56ee..8d31b2bc4b 100644 --- a/web/packages/accounts/components/Login.tsx +++ b/web/packages/accounts/components/Login.tsx @@ -7,7 +7,7 @@ import SingleInputForm, { type SingleInputFormProps, } from "@ente/shared/components/SingleInputForm"; import { LS_KEYS, setData } from "@ente/shared/storage/localStorage"; -import { Input } from "@mui/material"; +import { Input, Stack, Typography } from "@mui/material"; import { t } from "i18next"; import { useRouter } from "next/router"; import { getSRPAttributes } from "../api/srp"; @@ -17,9 +17,11 @@ import { PAGES } from "../constants/pages"; interface LoginProps { signUp: () => void; appName: AppName; + /** Reactive value of {@link customAPIHost}. */ + host: string | undefined; } -export function Login({ appName, signUp }: LoginProps) { +export function Login({ appName, signUp, host }: LoginProps) { const router = useRouter(); const loginUser: SingleInputFormProps["callback"] = async ( @@ -63,7 +65,17 @@ export function Login({ appName, signUp }: LoginProps) { /> - {t("NO_ACCOUNT")} + + {t("NO_ACCOUNT")} + + + {host ?? "" /* prevent layout shift with a minHeight */} + + ); diff --git a/web/packages/accounts/components/SignUp.tsx b/web/packages/accounts/components/SignUp.tsx index 9e0086075b..7d021812d9 100644 --- a/web/packages/accounts/components/SignUp.tsx +++ b/web/packages/accounts/components/SignUp.tsx @@ -31,6 +31,7 @@ import { IconButton, InputAdornment, Link, + Stack, TextField, Tooltip, Typography, @@ -53,9 +54,11 @@ interface SignUpProps { router: NextRouter; login: () => void; appName: AppName; + /** Reactive value of {@link customAPIHost}. */ + host: string | undefined; } -export function SignUp({ router, appName, login }: SignUpProps) { +export function SignUp({ router, appName, login, host }: SignUpProps) { const [acceptTerms, setAcceptTerms] = useState(false); const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); @@ -310,7 +313,19 @@ export function SignUp({ router, appName, login }: SignUpProps) { - {t("ACCOUNT_EXISTS")} + + + {t("ACCOUNT_EXISTS")} + + + + {host ?? "" /* prevent layout shift with a minHeight */} + + ); diff --git a/web/packages/accounts/pages/credentials.tsx b/web/packages/accounts/pages/credentials.tsx index b44bc4e103..312eba6b4b 100644 --- a/web/packages/accounts/pages/credentials.tsx +++ b/web/packages/accounts/pages/credentials.tsx @@ -1,13 +1,11 @@ -import { isDevBuild } from "@/next/env"; import log from "@/next/log"; import { ensure } from "@/utils/ensure"; import { VerticallyCentered } from "@ente/shared/components/Container"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import FormPaper from "@ente/shared/components/Form/FormPaper"; -import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer"; import LinkButton from "@ente/shared/components/LinkButton"; import { - ConnectionDetails, + LoginFlowFormFooter, PasswordHeader, VerifyingPasskey, } from "@ente/shared/components/LoginComponents"; @@ -42,6 +40,7 @@ import { setKey, } from "@ente/shared/storage/sessionStorage"; import type { KeyAttributes, User } from "@ente/shared/user/types"; +import { Stack } from "@mui/material"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -321,16 +320,16 @@ const Page: React.FC = ({ appContext }) => { srpAttributes={srpAttributes} /> - - router.push(PAGES.RECOVER)}> - {t("FORGOT_PASSWORD")} - - - {t("CHANGE_EMAIL")} - - - - {isDevBuild && } + + + router.push(PAGES.RECOVER)}> + {t("FORGOT_PASSWORD")} + + + {t("CHANGE_EMAIL")} + + + ); diff --git a/web/packages/accounts/pages/login.tsx b/web/packages/accounts/pages/login.tsx index ce9c8915f7..b6ea5ed207 100644 --- a/web/packages/accounts/pages/login.tsx +++ b/web/packages/accounts/pages/login.tsx @@ -1,3 +1,4 @@ +import { customAPIHost } from "@/next/origins"; import { VerticallyCentered } from "@ente/shared/components/Container"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import FormPaper from "@ente/shared/components/Form/FormPaper"; @@ -15,6 +16,8 @@ const Page: React.FC = ({ appContext }) => { const router = useRouter(); + const host = customAPIHost(); + useEffect(() => { const user = getData(LS_KEYS.USER); if (user?.email) { @@ -24,7 +27,7 @@ const Page: React.FC = ({ appContext }) => { showNavBar(true); }, []); - const register = () => { + const signUp = () => { router.push(PAGES.SIGNUP); }; @@ -35,7 +38,7 @@ const Page: React.FC = ({ appContext }) => { ) : ( - + ); diff --git a/web/packages/accounts/pages/signup.tsx b/web/packages/accounts/pages/signup.tsx index ce7e14e568..c55a2a13e0 100644 --- a/web/packages/accounts/pages/signup.tsx +++ b/web/packages/accounts/pages/signup.tsx @@ -1,3 +1,4 @@ +import { customAPIHost } from "@/next/origins"; import { PAGES } from "@ente/accounts/constants/pages"; import { LS_KEYS, getData } from "@ente/shared//storage/localStorage"; import { VerticallyCentered } from "@ente/shared/components/Container"; @@ -15,6 +16,8 @@ const Page: React.FC = ({ appContext }) => { const router = useRouter(); + const host = customAPIHost(); + useEffect(() => { const user = getData(LS_KEYS.USER); if (user?.email) { @@ -34,7 +37,7 @@ const Page: React.FC = ({ appContext }) => { ) : ( - + )} diff --git a/web/packages/accounts/pages/verify.tsx b/web/packages/accounts/pages/verify.tsx index c6e6954d12..9f84964904 100644 --- a/web/packages/accounts/pages/verify.tsx +++ b/web/packages/accounts/pages/verify.tsx @@ -3,10 +3,12 @@ import type { UserVerificationResponse } from "@ente/accounts/types/user"; import { VerticallyCentered } from "@ente/shared/components/Container"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import FormPaper from "@ente/shared/components/Form/FormPaper"; -import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer"; import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title"; import LinkButton from "@ente/shared/components/LinkButton"; -import { VerifyingPasskey } from "@ente/shared/components/LoginComponents"; +import { + LoginFlowFormFooter, + VerifyingPasskey, +} from "@ente/shared/components/LoginComponents"; import SingleInputForm, { type SingleInputFormProps, } from "@ente/shared/components/SingleInputForm"; @@ -20,7 +22,7 @@ import { } from "@ente/shared/storage/localStorage/helpers"; import { clearKeys } from "@ente/shared/storage/sessionStorage"; import type { KeyAttributes, User } from "@ente/shared/user/types"; -import { Box, Typography } from "@mui/material"; +import { Box, Stack, Typography } from "@mui/material"; import { HttpStatusCode } from "axios"; import { t } from "i18next"; import { useRouter } from "next/router"; @@ -225,18 +227,20 @@ const Page: React.FC = ({ appContext }) => { callback={onSubmit} /> - - {resend === 0 && ( - - {t("RESEND_MAIL")} + + + {resend === 0 && ( + + {t("RESEND_MAIL")} + + )} + {resend === 1 && {t("SENDING")}} + {resend === 2 && {t("SENT")}} + + {t("CHANGE_EMAIL")} - )} - {resend === 1 && {t("SENDING")}} - {resend === 2 && {t("SENT")}} - - {t("CHANGE_EMAIL")} - - + + ); diff --git a/web/packages/accounts/services/passkey.ts b/web/packages/accounts/services/passkey.ts index f16ceeaae9..56b0e78ba4 100644 --- a/web/packages/accounts/services/passkey.ts +++ b/web/packages/accounts/services/passkey.ts @@ -1,5 +1,6 @@ import { clientPackageHeaderIfPresent } from "@/next/http"; import log from "@/next/log"; +import { accountsAppOrigin, apiOrigin } from "@/next/origins"; import type { AppName } from "@/next/types/app"; import { clientPackageName } from "@/next/types/app"; import { TwoFactorAuthorizationResponse } from "@/next/types/credentials"; @@ -12,7 +13,6 @@ import { } from "@ente/shared/crypto/internal/libsodium"; import { CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { accountsAppURL, apiOrigin } from "@ente/shared/network/api"; import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore"; import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; @@ -48,7 +48,7 @@ export const passkeyVerificationRedirectURL = ( redirect, ...recoverOption, }); - return `${accountsAppURL()}/passkeys/verify?${params.toString()}`; + return `${accountsAppOrigin()}/passkeys/verify?${params.toString()}`; }; interface OpenPasskeyVerificationURLOptions { @@ -131,7 +131,7 @@ export const openAccountsManagePasskeysPage = async () => { const token = await getAccountsToken(); const params = new URLSearchParams({ token }); - window.open(`${accountsAppURL()}/passkeys?${params.toString()}`); + window.open(`${accountsAppOrigin()}/passkeys?${params.toString()}`); }; export const isPasskeyRecoveryEnabled = async () => { diff --git a/web/packages/new/package.json b/web/packages/new/package.json index 72c80268f6..8ce5399055 100644 --- a/web/packages/new/package.json +++ b/web/packages/new/package.json @@ -6,6 +6,7 @@ "@/next": "*", "@/utils": "*", "@ente/shared": "*", + "formik": "^2.4", "zod": "^3" }, "devDependencies": {} diff --git a/web/packages/new/photos/components/DevSettings.tsx b/web/packages/new/photos/components/DevSettings.tsx new file mode 100644 index 0000000000..0f37e60c8c --- /dev/null +++ b/web/packages/new/photos/components/DevSettings.tsx @@ -0,0 +1,182 @@ +import log from "@/next/log"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + InputAdornment, + Link, + TextField, + useMediaQuery, + type ModalProps, +} from "@mui/material"; +import { useFormik } from "formik"; +import { t } from "i18next"; +import React from "react"; +import { z } from "zod"; +import { FocusVisibleButton } from "./FocusVisibleButton"; +import { SlideTransition } from "./SlideTransition"; + +interface DevSettingsProps { + /** If `true`, then the dialog is shown. */ + open: boolean; + /** Called when the dialog wants to be closed. */ + onClose: () => void; +} + +/** + * A dialog allowing the user to set the API origin that the app connects to. + * See: [Note: Configuring custom server]. + */ +export const DevSettings: React.FC = ({ open, onClose }) => { + const fullScreen = useMediaQuery("(max-width: 428px)"); + + const handleDialogClose: ModalProps["onClose"] = (_, reason: string) => { + // Don't close on backdrop clicks. + if (reason != "backdropClick") onClose(); + }; + + const form = useFormik({ + initialValues: { + apiOrigin: localStorage.getItem("apiOrigin") ?? "", + }, + validate: ({ apiOrigin }) => { + try { + apiOrigin && new URL(apiOrigin); + } catch { + return { apiOrigin: "Invalid endpoint" }; + } + return {}; + }, + onSubmit: async (values, { setSubmitting, setErrors }) => { + try { + await updateAPIOrigin(values.apiOrigin); + } catch (e) { + // The person using this functionality is likely a developer and + // might be helped more by the original error instead of a + // friendlier but less specific message. + setErrors({ + apiOrigin: e instanceof Error ? e.message : String(e), + }); + return; + } + + setSubmitting(false); + onClose(); + }, + }); + + return ( + +
+ {t("developer_settings")} + + + + + + + + + ), + }} + /> + + + + {t("save")} + + + {t("CANCEL")} + + +
+
+ ); +}; + +/** + * Save {@link origin} to local storage after verifying it with a ping. + * + * The given {@link origin} will be verifying by making an API call to the + * `/ping` endpoint. If that succeeds, then it will be saved to local storage, + * and all subsequent API calls will use it as the {@link apiOrigin}. + * + * See: [Note: Configuring custom server]. + * + * @param origin The new API origin to use. Pass an empty string to clear the + * previously saved API origin (if any). + */ +const updateAPIOrigin = async (origin: string) => { + if (!origin) { + localStorage.removeItem("apiOrigin"); + return; + } + + const url = `${origin}/ping`; + const res = await fetch(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`); + try { + PingResponse.parse(await res.json()); + } catch (e) { + log.error("Invalid response", e); + throw new Error("Invalid response"); + } + + localStorage.setItem("apiOrigin", origin); +}; + +const PingResponse = z.object({ + message: z.enum(["pong"]), +}); diff --git a/web/packages/new/photos/components/FocusVisibleButton.tsx b/web/packages/new/photos/components/FocusVisibleButton.tsx new file mode 100644 index 0000000000..306b62e173 --- /dev/null +++ b/web/packages/new/photos/components/FocusVisibleButton.tsx @@ -0,0 +1,10 @@ +import { Button, styled } from "@mui/material"; + +/** A MUI {@link Button} that shows a keyboard focus indicator. */ +export const FocusVisibleButton = styled(Button)` + /* Show an outline when the button gains keyboard focus, e.g. when the user + tabs to it. */ + &.Mui-focusVisible { + outline: 1px solid #aaa; + } +`; diff --git a/web/packages/new/photos/components/SlideTransition.tsx b/web/packages/new/photos/components/SlideTransition.tsx new file mode 100644 index 0000000000..d5d2fda5c0 --- /dev/null +++ b/web/packages/new/photos/components/SlideTransition.tsx @@ -0,0 +1,16 @@ +import Slide from "@mui/material/Slide"; +import type { TransitionProps } from "@mui/material/transitions"; +import React from "react"; + +/** + * A React component that can be passed as the `TransitionComponent` props to a + * MUI {@link Dialog} to get it to use a slide transition (default is fade). + */ +export const SlideTransition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref, +) { + return ; +}); diff --git a/web/packages/new/photos/components/WhatsNew.tsx b/web/packages/new/photos/components/WhatsNew.tsx index 9a98ee5e48..f6b02b1f3a 100644 --- a/web/packages/new/photos/components/WhatsNew.tsx +++ b/web/packages/new/photos/components/WhatsNew.tsx @@ -1,6 +1,6 @@ +import { ut } from "@/next/i18n"; import ArrowForward from "@mui/icons-material/ArrowForward"; import { - Button, Dialog, DialogActions, DialogContent, @@ -10,21 +10,21 @@ import { styled, useMediaQuery, } from "@mui/material"; -import Slide from "@mui/material/Slide"; -import type { TransitionProps } from "@mui/material/transitions"; import React, { useEffect } from "react"; import { didShowWhatsNew } from "../services/changelog"; +import { FocusVisibleButton } from "./FocusVisibleButton"; +import { SlideTransition } from "./SlideTransition"; interface WhatsNewProps { /** If `true`, then the dialog is shown. */ open: boolean; - /** Callback to invoke when the dialog wants to be closed. */ + /** Called when the dialog wants to be closed. */ onClose: () => void; } /** - * Show a dialog showing a short summary of interesting-for-the-user things - * since the last time this dialog was shown. + * A dialog showing a short summary of interesting-for-the-user things since the + * last time this dialog was shown. */ export const WhatsNew: React.FC = ({ open, onClose }) => { const fullScreen = useMediaQuery("(max-width: 428px)"); @@ -39,36 +39,27 @@ export const WhatsNew: React.FC = ({ open, onClose }) => { TransitionComponent={SlideTransition} maxWidth="xs" > - {"What's new"} + {ut("What's new")} - } > - {"Continue"} - + {ut("Continue")} + ); }; -const SlideTransition = React.forwardRef(function Transition( - props: TransitionProps & { - children: React.ReactElement; - }, - ref: React.Ref, -) { - return ; -}); - const ChangelogContent: React.FC = () => { // NOTE: Remember to update changelogVersion when changing the content // below. @@ -78,16 +69,19 @@ const ChangelogContent: React.FC = () => {
  • - Support for Passkeys + {ut("Support for Passkeys")} - Passkeys can now be used as a second factor authentication - mechanism. + {ut( + "Passkeys can now be used as a second factor authentication mechanism.", + )}
  • - Window size + {ut("Window size")} - {"The app's window will remember its size and position."} + {ut( + "The app's window will remember its size and position.", + )}
  • @@ -102,14 +96,6 @@ const StyledUL = styled("ul")` } `; -const StyledButton = styled(Button)` - /* Show an outline when the button gains keyboard focus, e.g. when the user - tabs to it. */ - &.Mui-focusVisible { - outline: 1px solid #aaa; - } -`; - const ButtonContents = styled("div")` /* Make the button text fill the entire space so the endIcon shows at the trailing edge of the button. */ diff --git a/web/packages/new/photos/services/feature-flags.ts b/web/packages/new/photos/services/feature-flags.ts index ab7787b75e..8bef610f88 100644 --- a/web/packages/new/photos/services/feature-flags.ts +++ b/web/packages/new/photos/services/feature-flags.ts @@ -2,8 +2,8 @@ import { isDevBuild } from "@/next/env"; import { authenticatedRequestHeaders } from "@/next/http"; import { localUser } from "@/next/local-user"; import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import { nullToUndefined } from "@/utils/transform"; -import { apiOrigin } from "@ente/shared/network/api"; import { z } from "zod"; let _fetchTimeout: ReturnType | undefined; diff --git a/web/packages/next/i18n.ts b/web/packages/next/i18n.ts index cdc60e27ca..da63bc8c51 100644 --- a/web/packages/next/i18n.ts +++ b/web/packages/next/i18n.ts @@ -261,3 +261,15 @@ export const setLocaleInUse = async (locale: SupportedLocale) => { localStorage.setItem("locale", locale); return i18n.changeLanguage(locale); }; + +/** + * A no-op marker for strings that, for various reasons, are not translated. + * + * This function does nothing, it just returns back the passed it string + * verbatim. It is only kept as a way for us to keep track of strings that are + * not translated (and for some reason, are currently not meant to be), but + * still are user visible. + * + * It is the sibling of the {@link t} function provided by i18next. + */ +export const ut = (s: string) => s; diff --git a/web/packages/next/locales/ar-SA/translation.json b/web/packages/next/locales/ar-SA/translation.json index 0e2bc528c4..a644dd4a17 100644 --- a/web/packages/next/locales/ar-SA/translation.json +++ b/web/packages/next/locales/ar-SA/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/bg-BG/translation.json b/web/packages/next/locales/bg-BG/translation.json index 006c6adb7f..2c2c947fde 100644 --- a/web/packages/next/locales/bg-BG/translation.json +++ b/web/packages/next/locales/bg-BG/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/de-DE/translation.json b/web/packages/next/locales/de-DE/translation.json index dcbb5e5239..def17d0466 100644 --- a/web/packages/next/locales/de-DE/translation.json +++ b/web/packages/next/locales/de-DE/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "Sie werden zurück zur App weitergeleitet.", "redirect_again": "", "autogenerated_first_album_name": "Mein erstes Album", - "autogenerated_default_album_name": "Neues Album" + "autogenerated_default_album_name": "Neues Album", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/en-US/translation.json b/web/packages/next/locales/en-US/translation.json index 683c1f33f2..6b7313972c 100644 --- a/web/packages/next/locales/en-US/translation.json +++ b/web/packages/next/locales/en-US/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "You can close this window after the app opens.", "redirect_again": "Redirect again", "autogenerated_first_album_name": "My First Album", - "autogenerated_default_album_name": "New Album" + "autogenerated_default_album_name": "New Album", + "developer_settings": "Developer settings", + "server_endpoint": "Server endpoint", + "more_information": "More information", + "save": "Save" } diff --git a/web/packages/next/locales/es-ES/translation.json b/web/packages/next/locales/es-ES/translation.json index d0a3657fa7..c4bad10b3d 100644 --- a/web/packages/next/locales/es-ES/translation.json +++ b/web/packages/next/locales/es-ES/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/fa-IR/translation.json b/web/packages/next/locales/fa-IR/translation.json index f43f14fa12..a06d4e9acc 100644 --- a/web/packages/next/locales/fa-IR/translation.json +++ b/web/packages/next/locales/fa-IR/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/fi-FI/translation.json b/web/packages/next/locales/fi-FI/translation.json index 0e2bc528c4..a644dd4a17 100644 --- a/web/packages/next/locales/fi-FI/translation.json +++ b/web/packages/next/locales/fi-FI/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/fr-FR/translation.json b/web/packages/next/locales/fr-FR/translation.json index f4d2880a34..d56afd6168 100644 --- a/web/packages/next/locales/fr-FR/translation.json +++ b/web/packages/next/locales/fr-FR/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "Vous pouvez fermer cette fenêtre après l'ouverture de l'application.", "redirect_again": "Rediriger à nouveau", "autogenerated_first_album_name": "Mon premier album", - "autogenerated_default_album_name": "Nouvel album" + "autogenerated_default_album_name": "Nouvel album", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/gu-IN/translation.json b/web/packages/next/locales/gu-IN/translation.json new file mode 100644 index 0000000000..a644dd4a17 --- /dev/null +++ b/web/packages/next/locales/gu-IN/translation.json @@ -0,0 +1,647 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "password": "", + "link_password_description": "", + "unlock": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "title_photos": "", + "title_auth": "", + "title_accounts": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "delete_account": "", + "delete_account_manually_message": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "update_subscription_title": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE_COLLECTION": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "link_password_lock": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "SHARED_USING": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "NO_DUPLICATES_FOUND": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "free_plan_description": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "AUTHENTICATE": "", + "UPLOADED_TO_SINGLE_COLLECTION": "", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "", + "NEVERMIND": "", + "UPDATE_AVAILABLE": "", + "UPDATE_INSTALLABLE_MESSAGE": "", + "INSTALL_NOW": "", + "INSTALL_ON_NEXT_LAUNCH": "", + "UPDATE_AVAILABLE_MESSAGE": "", + "DOWNLOAD_AND_INSTALL": "", + "IGNORE_THIS_VERSION": "", + "TODAY": "", + "YESTERDAY": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "CONTINUOUS_EXPORT": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "delete_account_reason_label": "", + "delete_account_reason_placeholder": "", + "delete_reason": { + "missing_feature": "", + "behaviour": "", + "found_another_service": "", + "not_listed": "" + }, + "delete_account_feedback_label": "", + "delete_account_feedback_placeholder": "", + "delete_account_confirm_checkbox_label": "", + "delete_account_confirm": "", + "delete_account_confirm_message": "", + "feedback_required": "", + "feedback_required_found_another_service": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "editor": { + "crop": "" + }, + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_DESC": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_DESC": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "passkeys": "", + "passkey_fetch_failed": "", + "manage_passkey": "", + "delete_passkey": "", + "delete_passkey_confirmation": "", + "rename_passkey": "", + "add_passkey": "", + "enter_passkey_name": "", + "passkeys_description": "", + "CREATED_AT": "", + "passkey_add_failed": "", + "passkey_login_failed": "", + "passkey_login_invalid_url": "", + "passkey_login_already_claimed_session": "", + "passkey_login_generic_error": "", + "passkey_login_credential_hint": "", + "passkeys_not_supported": "", + "try_again": "", + "check_status": "", + "passkey_login_instructions": "", + "passkey_login": "", + "passkey": "", + "passkey_verify_description": "", + "waiting_for_verification": "", + "verification_still_pending": "", + "passkey_verified": "", + "redirecting_back_to_app": "", + "redirect_close_instructions": "", + "redirect_again": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" +} diff --git a/web/packages/next/locales/hi-IN/translation.json b/web/packages/next/locales/hi-IN/translation.json new file mode 100644 index 0000000000..a644dd4a17 --- /dev/null +++ b/web/packages/next/locales/hi-IN/translation.json @@ -0,0 +1,647 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "password": "", + "link_password_description": "", + "unlock": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "title_photos": "", + "title_auth": "", + "title_accounts": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "delete_account": "", + "delete_account_manually_message": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "update_subscription_title": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE_COLLECTION": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "link_password_lock": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "SHARED_USING": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "NO_DUPLICATES_FOUND": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "free_plan_description": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "AUTHENTICATE": "", + "UPLOADED_TO_SINGLE_COLLECTION": "", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "", + "NEVERMIND": "", + "UPDATE_AVAILABLE": "", + "UPDATE_INSTALLABLE_MESSAGE": "", + "INSTALL_NOW": "", + "INSTALL_ON_NEXT_LAUNCH": "", + "UPDATE_AVAILABLE_MESSAGE": "", + "DOWNLOAD_AND_INSTALL": "", + "IGNORE_THIS_VERSION": "", + "TODAY": "", + "YESTERDAY": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "CONTINUOUS_EXPORT": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "delete_account_reason_label": "", + "delete_account_reason_placeholder": "", + "delete_reason": { + "missing_feature": "", + "behaviour": "", + "found_another_service": "", + "not_listed": "" + }, + "delete_account_feedback_label": "", + "delete_account_feedback_placeholder": "", + "delete_account_confirm_checkbox_label": "", + "delete_account_confirm": "", + "delete_account_confirm_message": "", + "feedback_required": "", + "feedback_required_found_another_service": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "editor": { + "crop": "" + }, + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_DESC": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_DESC": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "passkeys": "", + "passkey_fetch_failed": "", + "manage_passkey": "", + "delete_passkey": "", + "delete_passkey_confirmation": "", + "rename_passkey": "", + "add_passkey": "", + "enter_passkey_name": "", + "passkeys_description": "", + "CREATED_AT": "", + "passkey_add_failed": "", + "passkey_login_failed": "", + "passkey_login_invalid_url": "", + "passkey_login_already_claimed_session": "", + "passkey_login_generic_error": "", + "passkey_login_credential_hint": "", + "passkeys_not_supported": "", + "try_again": "", + "check_status": "", + "passkey_login_instructions": "", + "passkey_login": "", + "passkey": "", + "passkey_verify_description": "", + "waiting_for_verification": "", + "verification_still_pending": "", + "passkey_verified": "", + "redirecting_back_to_app": "", + "redirect_close_instructions": "", + "redirect_again": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" +} diff --git a/web/packages/next/locales/id-ID/translation.json b/web/packages/next/locales/id-ID/translation.json index 81644e4b49..31e2a5c34b 100644 --- a/web/packages/next/locales/id-ID/translation.json +++ b/web/packages/next/locales/id-ID/translation.json @@ -2,7 +2,7 @@ "HERO_SLIDE_1_TITLE": "
    Cadangan pribadi
    untuk kenanganmu
    ", "HERO_SLIDE_1": "Dirancang dengan enkripsi ujung ke ujung", "HERO_SLIDE_2_TITLE": "
    Tersimpan aman
    di tempat pengungsian
    ", - "HERO_SLIDE_2": "Dirancang untuk waktu lebih lama", + "HERO_SLIDE_2": "Dibuat untuk melestarikan", "HERO_SLIDE_3_TITLE": "
    Tersedia
    di mana saja
    ", "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Masuk", @@ -83,7 +83,7 @@ "ZOOM_IN_OUT": "Perbesar/perkecil", "PREVIOUS": "Sebelumnya (←)", "NEXT": "Berikutnya (→)", - "title_photos": "Ente Photos", + "title_photos": "Ente Foto", "title_auth": "Ente Auth", "title_accounts": "Akun Ente", "UPLOAD_FIRST_PHOTO": "Unggah foto pertama Anda", @@ -639,5 +639,9 @@ "redirect_close_instructions": "Kamu dapat menutup jendela ini setelah app terbuka.", "redirect_again": "Arahkan ulang", "autogenerated_first_album_name": "Album Pertamaku", - "autogenerated_default_album_name": "Album Baru" + "autogenerated_default_album_name": "Album Baru", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/is-IS/translation.json b/web/packages/next/locales/is-IS/translation.json index 8fb66d78da..edd188c1ab 100644 --- a/web/packages/next/locales/is-IS/translation.json +++ b/web/packages/next/locales/is-IS/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/it-IT/translation.json b/web/packages/next/locales/it-IT/translation.json index e54860c7a7..a8c4761772 100644 --- a/web/packages/next/locales/it-IT/translation.json +++ b/web/packages/next/locales/it-IT/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/ko-KR/translation.json b/web/packages/next/locales/ko-KR/translation.json index 652b58c9ae..e21508e42e 100644 --- a/web/packages/next/locales/ko-KR/translation.json +++ b/web/packages/next/locales/ko-KR/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/nl-NL/translation.json b/web/packages/next/locales/nl-NL/translation.json index 5a60f309c0..d07b556994 100644 --- a/web/packages/next/locales/nl-NL/translation.json +++ b/web/packages/next/locales/nl-NL/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "Je kunt dit venster sluiten na het openen van de app.", "redirect_again": "Opnieuw doorverwijzen", "autogenerated_first_album_name": "Mijn eerste album", - "autogenerated_default_album_name": "Nieuw album" + "autogenerated_default_album_name": "Nieuw album", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/pt-BR/translation.json b/web/packages/next/locales/pt-BR/translation.json index ea48fd57c3..ad7189208b 100644 --- a/web/packages/next/locales/pt-BR/translation.json +++ b/web/packages/next/locales/pt-BR/translation.json @@ -536,7 +536,7 @@ "delete_account_feedback_placeholder": "Comentários", "delete_account_confirm_checkbox_label": "Sim, desejo excluir permanentemente esta conta e todos os seus dados", "delete_account_confirm": "Confirmar exclusão da conta", - "delete_account_confirm_message": "", + "delete_account_confirm_message": "

    Essa conta está vinculada a outros aplicativos Ente, se você usa algum.

    Seus dados enviados, em todos os aplicativos Ente, serão agendados para exclusão e sua conta será excluída permanentemente.

    ", "feedback_required": "Por favor, ajude-nos com esta informação", "feedback_required_found_another_service": "O que o outro serviço faz melhor?", "RECOVER_TWO_FACTOR": "Recuperar dois fatores", @@ -639,5 +639,9 @@ "redirect_close_instructions": "Você pode fechar esta janela após a aplicação ser aberta.", "redirect_again": "Redirecionar novamente", "autogenerated_first_album_name": "Meu Primeiro Álbum", - "autogenerated_default_album_name": "Novo Álbum" + "autogenerated_default_album_name": "Novo Álbum", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/pt-PT/translation.json b/web/packages/next/locales/pt-PT/translation.json index d24d54dab8..907d9da5e2 100644 --- a/web/packages/next/locales/pt-PT/translation.json +++ b/web/packages/next/locales/pt-PT/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/ru-RU/translation.json b/web/packages/next/locales/ru-RU/translation.json index a56c7c879f..ee556f47d9 100644 --- a/web/packages/next/locales/ru-RU/translation.json +++ b/web/packages/next/locales/ru-RU/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "Вы можете закрыть это окно после открытия приложения.", "redirect_again": "Перенаправить снова", "autogenerated_first_album_name": "Мой первый альбом", - "autogenerated_default_album_name": "Новый альбом" + "autogenerated_default_album_name": "Новый альбом", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/sv-SE/translation.json b/web/packages/next/locales/sv-SE/translation.json index 871ac0fe3f..d66ec7cff4 100644 --- a/web/packages/next/locales/sv-SE/translation.json +++ b/web/packages/next/locales/sv-SE/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/te-IN/translation.json b/web/packages/next/locales/te-IN/translation.json index 0e2bc528c4..a644dd4a17 100644 --- a/web/packages/next/locales/te-IN/translation.json +++ b/web/packages/next/locales/te-IN/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/th-TH/translation.json b/web/packages/next/locales/th-TH/translation.json index 0e2bc528c4..a644dd4a17 100644 --- a/web/packages/next/locales/th-TH/translation.json +++ b/web/packages/next/locales/th-TH/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/ti-ER/translation.json b/web/packages/next/locales/ti-ER/translation.json new file mode 100644 index 0000000000..a644dd4a17 --- /dev/null +++ b/web/packages/next/locales/ti-ER/translation.json @@ -0,0 +1,647 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "password": "", + "link_password_description": "", + "unlock": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "title_photos": "", + "title_auth": "", + "title_accounts": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "delete_account": "", + "delete_account_manually_message": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "update_subscription_title": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE_COLLECTION": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "link_password_lock": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "SHARED_USING": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "NO_DUPLICATES_FOUND": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "free_plan_description": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "AUTHENTICATE": "", + "UPLOADED_TO_SINGLE_COLLECTION": "", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "", + "NEVERMIND": "", + "UPDATE_AVAILABLE": "", + "UPDATE_INSTALLABLE_MESSAGE": "", + "INSTALL_NOW": "", + "INSTALL_ON_NEXT_LAUNCH": "", + "UPDATE_AVAILABLE_MESSAGE": "", + "DOWNLOAD_AND_INSTALL": "", + "IGNORE_THIS_VERSION": "", + "TODAY": "", + "YESTERDAY": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "CONTINUOUS_EXPORT": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "delete_account_reason_label": "", + "delete_account_reason_placeholder": "", + "delete_reason": { + "missing_feature": "", + "behaviour": "", + "found_another_service": "", + "not_listed": "" + }, + "delete_account_feedback_label": "", + "delete_account_feedback_placeholder": "", + "delete_account_confirm_checkbox_label": "", + "delete_account_confirm": "", + "delete_account_confirm_message": "", + "feedback_required": "", + "feedback_required_found_another_service": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "editor": { + "crop": "" + }, + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_DESC": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_DESC": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "passkeys": "", + "passkey_fetch_failed": "", + "manage_passkey": "", + "delete_passkey": "", + "delete_passkey_confirmation": "", + "rename_passkey": "", + "add_passkey": "", + "enter_passkey_name": "", + "passkeys_description": "", + "CREATED_AT": "", + "passkey_add_failed": "", + "passkey_login_failed": "", + "passkey_login_invalid_url": "", + "passkey_login_already_claimed_session": "", + "passkey_login_generic_error": "", + "passkey_login_credential_hint": "", + "passkeys_not_supported": "", + "try_again": "", + "check_status": "", + "passkey_login_instructions": "", + "passkey_login": "", + "passkey": "", + "passkey_verify_description": "", + "waiting_for_verification": "", + "verification_still_pending": "", + "passkey_verified": "", + "redirecting_back_to_app": "", + "redirect_close_instructions": "", + "redirect_again": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" +} diff --git a/web/packages/next/locales/tr-TR/translation.json b/web/packages/next/locales/tr-TR/translation.json index 0e2bc528c4..a644dd4a17 100644 --- a/web/packages/next/locales/tr-TR/translation.json +++ b/web/packages/next/locales/tr-TR/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "", "redirect_again": "", "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/locales/zh-CN/translation.json b/web/packages/next/locales/zh-CN/translation.json index db6e5ca1fa..29f980da0a 100644 --- a/web/packages/next/locales/zh-CN/translation.json +++ b/web/packages/next/locales/zh-CN/translation.json @@ -639,5 +639,9 @@ "redirect_close_instructions": "在应用程序打开后您可以关闭此窗口。", "redirect_again": "再次重定向", "autogenerated_first_album_name": "我的第一个相册", - "autogenerated_default_album_name": "新建相册" + "autogenerated_default_album_name": "新建相册", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "" } diff --git a/web/packages/next/origins.ts b/web/packages/next/origins.ts new file mode 100644 index 0000000000..66e6a9aefa --- /dev/null +++ b/web/packages/next/origins.ts @@ -0,0 +1,87 @@ +import { nullToUndefined } from "@/utils/transform"; + +/** + * Return the origin (scheme, host, port triple) that should be used for making + * API requests to museum. + * + * This defaults "https://api.ente.io", Ente's production API servers. but can + * be overridden when self hosting or developing (see {@link customAPIOrigin}). + */ +export const apiOrigin = () => customAPIOrigin() ?? "https://api.ente.io"; + +/** + * Return the overridden API origin, if one is defined by either (in priority + * order): + * + * - Setting the custom server on the landing page (See: [Note: Configuring + * custom server]); or by + * + * - Setting the `NEXT_PUBLIC_ENTE_ENDPOINT` environment variable. + * + * Otherwise return undefined. + */ +export const customAPIOrigin = () => + nullToUndefined(localStorage.getItem("apiOrigin")) ?? + process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? + undefined; + +/** + * A convenience wrapper over {@link customAPIOrigin} that returns the only the + * host part of the custom origin (if any). + * + * This is useful in places where we indicate the custom origin in the UI. + */ +export const customAPIHost = () => { + const origin = customAPIOrigin(); + return origin ? new URL(origin).host : undefined; +}; + +/** + * Return the origin that should be used for uploading files. + * + * This defaults to `https://uploader.ente.io`, serviced by a Cloudflare worker + * (see infra/workers/uploader). But if a {@link customAPIOrigin} is set then + * this value is set to the {@link customAPIOrigin} itself, effectively + * bypassing the Cloudflare worker for non-Ente deployments. + */ +export const uploaderOrigin = () => + customAPIOrigin() ?? "https://uploader.ente.io"; + +/** + * Return the origin that serves the accounts app. + * + * Defaults to our production instance, "https://accounts.ente.io", but can be + * overridden by setting the `NEXT_PUBLIC_ENTE_ACCOUNTS_URL` environment + * variable. + */ +export const accountsAppOrigin = () => + process.env.NEXT_PUBLIC_ENTE_ACCOUNTS_URL ?? `https://accounts.ente.io`; + +/** + * Return the origin that serves public albums. + * + * Defaults to our production instance, "https://albums.ente.io", but can be + * overridden by setting the `NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT` environment + * variable. + */ +export const albumsAppOrigin = () => + process.env.NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT ?? "https://albums.ente.io"; + +/** + * Return the origin that serves the family dashboard which can be used to + * create or manage family plans.. + * + * Defaults to our production instance, "https://family.ente.io", but can be + * overridden by setting the `NEXT_PUBLIC_ENTE_FAMILY_URL` environment variable. + */ +export const familyAppOrigin = () => + process.env.NEXT_PUBLIC_ENTE_FAMILY_URL ?? "https://family.ente.io"; + +/** + * Return the origin that serves the payments app. + * + * Defaults to our production instance, "https://payments.ente.io", but can be + * overridden by setting the `NEXT_PUBLIC_ENTE_PAYMENTS_URL` environment variable. + */ +export const paymentsAppOrigin = () => + process.env.NEXT_PUBLIC_ENTE_PAYMENTS_URL ?? "https://payments.ente.io"; diff --git a/web/packages/shared/components/LoginComponents.tsx b/web/packages/shared/components/LoginComponents.tsx index afe12a690d..8201ccacc9 100644 --- a/web/packages/shared/components/LoginComponents.tsx +++ b/web/packages/shared/components/LoginComponents.tsx @@ -1,5 +1,5 @@ -import { isDevBuild } from "@/next/env"; import log from "@/next/log"; +import { customAPIHost } from "@/next/origins"; import type { BaseAppContextT } from "@/next/types/app"; import { checkPasskeyVerificationStatus, @@ -7,8 +7,7 @@ import { saveCredentialsAndNavigateTo, } from "@ente/accounts/services/passkey"; import EnteButton from "@ente/shared/components/EnteButton"; -import { apiOrigin } from "@ente/shared/network/api"; -import { CircularProgress, Typography, styled } from "@mui/material"; +import { CircularProgress, Stack, Typography, styled } from "@mui/material"; import { t } from "i18next"; import { useRouter } from "next/router"; import React, { useState } from "react"; @@ -46,22 +45,26 @@ const Header_ = styled("div")` gap: 8px; `; -export const ConnectionDetails: React.FC = () => { - const host = new URL(apiOrigin()).host; +export const LoginFlowFormFooter: React.FC = ({ + children, +}) => { + const host = customAPIHost(); return ( - - - {host} - - + + + {children} + + {host && ( + + {host} + + )} + + ); }; -const ConnectionDetails_ = styled("div")` - margin-block-start: 1rem; -`; - interface VerifyingPasskeyProps { /** ID of the current passkey verification session. */ passkeySessionID: string; @@ -161,16 +164,16 @@ export const VerifyingPasskey: React.FC = ({ - - - {t("RECOVER_ACCOUNT")} - - - {t("CHANGE_EMAIL")} - - - - {isDevBuild && } + + + + {t("RECOVER_ACCOUNT")} + + + {t("CHANGE_EMAIL")} + + + ); diff --git a/web/packages/shared/network/api.ts b/web/packages/shared/network/api.ts deleted file mode 100644 index f708e29e04..0000000000 --- a/web/packages/shared/network/api.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Return the origin (scheme, host, port triple) that should be used for making - * API requests to museum. - * - * This defaults to "https://api.ente.io", Ente's own servers, but can be - * overridden when self hosting or developing by setting the - * `NEXT_PUBLIC_ENTE_ENDPOINT` environment variable. - */ -export const apiOrigin = () => customAPIOrigin() ?? "https://api.ente.io"; - -/** - * Return the overridden API origin, if one is defined by setting the - * `NEXT_PUBLIC_ENTE_ENDPOINT` environment variable. - * - * Otherwise return undefined. - */ -export const customAPIOrigin = () => - process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? undefined; - -/** Deprecated, use {@link apiOrigin} instead. */ -export const getEndpoint = apiOrigin; - -export const getUploadEndpoint = () => { - const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT; - if (endpoint) { - return endpoint; - } - return `https://uploader.ente.io`; -}; - -/** - * Return the URL of the Ente Accounts app. - * - * Defaults to our production instance, "https://accounts.ente.io", but can be - * overridden by setting the `NEXT_PUBLIC_ENTE_ACCOUNTS_URL` environment - * variable. - */ -export const accountsAppURL = () => - process.env.NEXT_PUBLIC_ENTE_ACCOUNTS_URL ?? `https://accounts.ente.io`; - -export const getAlbumsURL = () => { - const albumsURL = process.env.NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT; - if (albumsURL) { - return albumsURL; - } - return `https://albums.ente.io`; -}; - -/** - * Return the URL for the family dashboard which can be used to create or manage - * family plans. - */ -export const getFamilyPortalURL = () => { - const familyURL = process.env.NEXT_PUBLIC_ENTE_FAMILY_URL; - if (familyURL) { - return familyURL; - } - return `https://family.ente.io`; -}; - -/** - * Return the URL for the host that handles payment related functionality. - */ -export const getPaymentsURL = () => { - const paymentsURL = process.env.NEXT_PUBLIC_ENTE_PAYMENTS_URL; - if (paymentsURL) { - return paymentsURL; - } - return `https://payments.ente.io`; -}; diff --git a/web/packages/shared/network/cast.ts b/web/packages/shared/network/cast.ts index a18767baa2..7b2af75907 100644 --- a/web/packages/shared/network/cast.ts +++ b/web/packages/shared/network/cast.ts @@ -1,8 +1,8 @@ import log from "@/next/log"; +import { apiOrigin } from "@/next/origins"; import { ApiError } from "../error"; import { getToken } from "../storage/localStorage/helpers"; import HTTPService from "./HTTPService"; -import { getEndpoint } from "./api"; class CastGateway { constructor() {} @@ -11,7 +11,7 @@ class CastGateway { let resp; try { resp = await HTTPService.get( - `${getEndpoint()}/cast/cast-data/${code}`, + `${apiOrigin()}/cast/cast-data/${code}`, ); } catch (e) { log.error("failed to getCastData", e); @@ -24,7 +24,7 @@ class CastGateway { try { const token = getToken(); await HTTPService.delete( - getEndpoint() + "/cast/revoke-all-tokens/", + apiOrigin() + "/cast/revoke-all-tokens/", undefined, undefined, { @@ -42,7 +42,7 @@ class CastGateway { try { const token = getToken(); resp = await HTTPService.get( - `${getEndpoint()}/cast/device-info/${code}`, + `${apiOrigin()}/cast/device-info/${code}`, undefined, { "X-Auth-Token": token, @@ -60,7 +60,7 @@ class CastGateway { public async registerDevice(publicKey: string): Promise { const resp = await HTTPService.post( - getEndpoint() + "/cast/device-info/", + apiOrigin() + "/cast/device-info/", { publicKey: publicKey, }, @@ -76,7 +76,7 @@ class CastGateway { ) { const token = getToken(); await HTTPService.post( - getEndpoint() + "/cast/cast-data/", + apiOrigin() + "/cast/cast-data/", { deviceCode: `${code}`, encPayload: castPayload, diff --git a/web/packages/shared/storage/localStorage/index.ts b/web/packages/shared/storage/localStorage/index.ts index beefbf37fe..3df5caf973 100644 --- a/web/packages/shared/storage/localStorage/index.ts +++ b/web/packages/shared/storage/localStorage/index.ts @@ -18,7 +18,6 @@ export enum LS_KEYS { COLLECTION_SORT_BY = "collectionSortBy", THEME = "theme", WAIT_TIME = "waitTime", - API_ENDPOINT = "apiEndpoint", // Moved to the new wrapper @/next/local-storage // LOCALE = 'locale', MAP_ENABLED = "mapEnabled", diff --git a/web/yarn.lock b/web/yarn.lock index a87928bacf..be1c37c63d 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2611,10 +2611,10 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -formik@^2.1.5: - version "2.4.5" - resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.5.tgz#f899b5b7a6f103a8fabb679823e8fafc7e0ee1b4" - integrity sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ== +formik@^2.4: + version "2.4.6" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.6.tgz#4da75ca80f1a827ab35b08fd98d5a76e928c9686" + integrity sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g== dependencies: "@types/hoist-non-react-statics" "^3.3.1" deepmerge "^2.1.1" @@ -4375,7 +4375,16 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4495,7 +4504,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -4904,7 +4920,16 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==