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 ( + + + + X + + Update Subscription + + + Expiry Time: + setExpiryTime(date)} + dateFormat="dd/MM/yyyy" + showYearDropdown + scrollableYearDropdown + yearDropdownItemNumber={15} + /> + + + Choose Your Plan: + setProductId(e.target.value)} + > + 50GB/Month + 50GB/Year + 200GB/Month + 200GB/Year + 500GB/Month + 500GB/Year + 2000GB/Month + 2000GB/Year + + + + + Payment Provider: + + setPaymentProvider(e.target.value)} + > + BitPay + PayPal + + + + Transaction ID: + setTransactionId(e.target.value)} + /> + + + Update + + + {(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 @@
? _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 ( + + + + X + + Update Subscription + + + Expiry Time: + setExpiryTime(date)} + dateFormat="dd/MM/yyyy" + showYearDropdown + scrollableYearDropdown + yearDropdownItemNumber={15} + /> + + + Choose Your Plan: + setProductId(e.target.value)} + > + 50GB/Month + 50GB/Year + 200GB/Month + 200GB/Year + 500GB/Month + 500GB/Year + 2000GB/Month + 2000GB/Year + + + + + Payment Provider: + + setPaymentProvider(e.target.value)} + > + BitPay + PayPal + + + + Transaction ID: + setTransactionId(e.target.value)} + /> + + + Update + + + {(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 @@
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