diff --git a/.github/workflows/mobile-daily-internal.yml b/.github/workflows/mobile-daily-internal.yml new file mode 100644 index 0000000000..0ca74c1bb0 --- /dev/null +++ b/.github/workflows/mobile-daily-internal.yml @@ -0,0 +1,133 @@ +name: "Internal release (photos)" + +on: + schedule: + # Runs daily at 12:30 UTC (6:00 PM IST) + - cron: "30 12 * * *" + workflow_dispatch: # Allow manual trigger + +env: + FLUTTER_VERSION: "3.24.3" + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: mobile/apps/photos + + steps: + - name: Checkout code and submodules + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + + - name: Install Flutter ${{ env.FLUTTER_VERSION }} + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Increment version code for build + run: | + CURRENT_VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //') + VERSION_NAME=$(echo $CURRENT_VERSION | cut -d'+' -f1) + CURRENT_BUILD=$(echo $CURRENT_VERSION | cut -d'+' -f2) + NEW_BUILD=$((CURRENT_BUILD + ${{ github.run_number }})) + NEW_VERSION="${VERSION_NAME}+${NEW_BUILD}" + + sed -i "s/^version: .*/version: $NEW_VERSION/" pubspec.yaml + echo "Building with version ${NEW_VERSION}" + + # Store version for later use + echo "NEW_VERSION=${NEW_VERSION}" >> $GITHUB_ENV + + - name: Prepare and validate changelog for Play Store + run: | + mkdir -p whatsnew + CHANGELOG_FILE="scripts/changes.txt" + OUTPUT_FILE="whatsnew/en-US.txt" + + # Use provided changelog or fallback + if [ -f "$CHANGELOG_FILE" ]; then + head -c 500 "$CHANGELOG_FILE" > "$OUTPUT_FILE" + else + echo "Bug fixes and improvements" > "$OUTPUT_FILE" + fi + + # Validate: file exists + if [ ! -s "$OUTPUT_FILE" ]; then + echo "❌ Changelog is empty." + exit 1 + fi + + # Validate: <= 500 chars + LENGTH=$(wc -m < "$OUTPUT_FILE") + if [ "$LENGTH" -gt 500 ]; then + echo "❌ Changelog exceeds 500 characters ($LENGTH)." + exit 1 + fi + + # Validate: no markdown or HTML + if grep -Eq '[\*\_\<\>\[\]\(\)]' "$OUTPUT_FILE"; then + echo "❌ Changelog contains markdown/HTML formatting." + exit 1 + fi + + echo "✅ Changelog valid:" + cat "$OUTPUT_FILE" + + # Store changelog for later use - Simple and reliable method + CHANGELOG=$(cat "$OUTPUT_FILE" | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g') + echo "CHANGELOG=${CHANGELOG}" >> $GITHUB_ENV + + - name: Setup keys + uses: timheuer/base64-to-file@v1 + with: + fileName: "keystore/ente_photos_key.jks" + encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }} + + - name: Build PlayStore AAB + run: | + flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore + env: + SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks" + SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_PHOTOS }} + SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }} + + - name: Upload AAB to PlayStore + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} + packageName: io.ente.photos + releaseFiles: mobile/apps/photos/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab + track: internal + whatsNewDirectory: mobile/apps/photos/whatsnew + mappingFile: mobile/apps/photos/build/app/outputs/mapping/playstoreRelease/mapping.txt + + - name: Notify Discord + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} + nodetail: true + title: "🏆 Daily release Photos v${{ env.NEW_VERSION }} (Branch: ${{ github.ref_name }})" + description: | + **Version:** ${{ env.NEW_VERSION }} + **Flutter:** ${{ env.FLUTTER_VERSION }} + **Commit:** [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) + **Download:** [Play Store](https://play.google.com/store/apps/details?id=io.ente.photos) + + **Changes:** + ${{ env.CHANGELOG }} + color: 0x00ff00 \ No newline at end of file diff --git a/.github/workflows/mobile-internal-release-rust.yml b/.github/workflows/mobile-internal-release-rust.yml index 31b9bf1b31..e5c85f1e37 100644 --- a/.github/workflows/mobile-internal-release-rust.yml +++ b/.github/workflows/mobile-internal-release-rust.yml @@ -1,4 +1,4 @@ -name: "Internal release (photos)" +name: "Internal release (photos with rust)" on: workflow_dispatch: # Allow manually running the action diff --git a/.github/workflows/mobile-internal-release.yml b/.github/workflows/mobile-internal-release.yml index c7af19895b..8ce8febf9f 100644 --- a/.github/workflows/mobile-internal-release.yml +++ b/.github/workflows/mobile-internal-release.yml @@ -1,4 +1,4 @@ -name: "Internal release (photos)" +name: "Old Internal release (photos)" on: workflow_dispatch: # Allow manually running the action diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index 6c2691ca35..ee6e1d6e7b 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -54,6 +54,18 @@ jobs: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} command: pages deploy --project-name=ente --commit-dirty=true --branch=deploy/photos web/apps/photos/out + - name: Build custom-albums + run: yarn build:photos + env: + NEXT_PUBLIC_ENTE_ONLY_SERVE_ALBUMS_APP: 1 + + - name: Publish custom-albums + uses: cloudflare/wrangler-action@v3 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + command: pages deploy --project-name=ente --commit-dirty=true --branch=deploy/custom-albums web/apps/photos/out + - name: Build accounts run: yarn build:accounts diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index 71f1fd88bd..a764df70a1 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -17,8 +17,7 @@ "uidai", "UIDAI", "Unique Identification Authority of India" - ], - "hex": "FBB401" + ] }, { "title": "Accredible", @@ -57,8 +56,7 @@ }, { "title": "Amtrak", - "slug": "amtrak", - "hex": "003A5D" + "slug": "amtrak" }, { "title": "Animal Crossing", @@ -321,8 +319,7 @@ }, { "title": "Caltrain", - "slug": "caltrain", - "hex": "E31837" + "slug": "caltrain" }, { "title": "Canva" @@ -387,8 +384,7 @@ "ClipperCard", "clipper-card", "Clipper Card" - ], - "hex": "006298" + ] }, { "title": "CloudAMQP" @@ -433,8 +429,7 @@ }, { "title": "Coolify", - "slug": "coolify", - "hex": "8C52FF" + "slug": "coolify" }, { "title": "Crowdpear" @@ -547,8 +542,7 @@ "altNames": [ "Domino's", "Domino's Pizza" - ], - "hex": "0B648F" + ] }, { "title": "Doppler" @@ -571,8 +565,7 @@ "Dunkin'", "Dunkin", "Dunkin Donuts" - ], - "hex": "C63663" + ] }, { "title": "eBay" @@ -638,8 +631,7 @@ }, { "title": "Experian", - "slug": "experian", - "hex": "AF1685" + "slug": "experian" }, { "title": "Fanatical", @@ -763,8 +755,7 @@ "altNames": [ "green man gaming", "gmg" - ], - "hex": "00E205" + ] }, { "title": "Guideline" @@ -891,8 +882,7 @@ }, { "title": "Kayak", - "slug": "kayak", - "hex": "FF6900" + "slug": "kayak" }, { "title": "Keygen", @@ -1193,8 +1183,7 @@ "slug": "njtransit", "altNames": [ "NJ Transit" - ], - "hex": "1A2B57" + ] }, { "title": "nordvpn", @@ -1299,8 +1288,7 @@ "slug": "pcpartpicker", "altNames": [ "PC Part Picker" - ], - "hex": "EDA920" + ] }, { "title": "Peerberry" @@ -1510,8 +1498,7 @@ "altNames": [ "onlinesbi", "State Bank of India" - ], - "hex": "12A8E0" + ] }, { "title": "SEI", @@ -1612,8 +1599,7 @@ }, { "title": "Supercell", - "slug": "supercell", - "hex": "000000" + "slug": "supercell" }, { "title": "Surfshark" @@ -1696,8 +1682,7 @@ "altNames": [ "StoryGraph", "TheStoryGraph" - ], - "hex": "15919B" + ] }, { "title": "tianyiyun", @@ -1872,8 +1857,7 @@ "Washington Metro", "DC Metro", "Washington Metropolitan Area Transit Authority" - ], - "hex": "2D2D2D" + ] }, { "title": "Wolvesville" @@ -1993,6 +1977,10 @@ { "title": "ISC2", "slug": "isc2" + }, + { + "title": "Allegro", + "slug": "allegro" } ] -} \ No newline at end of file +} diff --git a/mobile/apps/auth/assets/custom-icons/icons/allegro.svg b/mobile/apps/auth/assets/custom-icons/icons/allegro.svg new file mode 100644 index 0000000000..151c5408ec --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/allegro.svg @@ -0,0 +1,8 @@ + + logo (15)-svg copy-svg + + + \ No newline at end of file diff --git a/mobile/apps/auth/lib/l10n/arb/app_pl.arb b/mobile/apps/auth/lib/l10n/arb/app_pl.arb index a4177c8133..f17467db12 100644 --- a/mobile/apps/auth/lib/l10n/arb/app_pl.arb +++ b/mobile/apps/auth/lib/l10n/arb/app_pl.arb @@ -45,7 +45,7 @@ "timeBasedKeyType": "Oparte na czasie (TOTP)", "counterBasedKeyType": "Oparte na liczniku (HOTP)", "saveAction": "Zapisz", - "nextTotpTitle": "dalej", + "nextTotpTitle": "następny", "deleteCodeTitle": "Usunąć kod?", "deleteCodeMessage": "Czy na pewno chcesz usunąć ten kod? Ta akcja jest nieodwracalna.", "trashCode": "Przenieść kod do kosza?", diff --git a/mobile/apps/auth/lib/l10n/arb/app_sv.arb b/mobile/apps/auth/lib/l10n/arb/app_sv.arb index 4092736661..890e97ce0b 100644 --- a/mobile/apps/auth/lib/l10n/arb/app_sv.arb +++ b/mobile/apps/auth/lib/l10n/arb/app_sv.arb @@ -19,7 +19,7 @@ "pleaseVerifyDetails": "Kontrollera dina detaljer och försök igen", "codeIssuerHint": "Utfärdare", "codeSecretKeyHint": "Secret Key", - "secret": "Säkerhets nyckel", + "secret": "Säkerhetsnyckel", "all": "Alla", "notes": "Anteckningar", "notesLengthLimit": "Anteckningar kan vara högst {count} tecken långa", diff --git a/mobile/apps/auth/lib/l10n/arb/app_ti.arb b/mobile/apps/auth/lib/l10n/arb/app_ti.arb index 377dc51fff..680067d211 100644 --- a/mobile/apps/auth/lib/l10n/arb/app_ti.arb +++ b/mobile/apps/auth/lib/l10n/arb/app_ti.arb @@ -10,7 +10,7 @@ "onBoardingGetStarted": "ጀምር", "setupFirstAccount": "ናይ መጀመርታ ሕሳብካ ኣዳል", "importScanQrCode": "QR ኮድ ስካን ግበር", - "qrCode": "ኪዊኣር ስርዓት", + "qrCode": "ኪዊኣር ኮድ", "importEnterSetupKey": "ምድላው መፍትሕ ኣእቱ", "importAccountPageTitle": "ዝርዝር ሕሳብ ኣእትዉ", "secretCanNotBeEmpty": "ምስጢር ባዶ ኪኸውን ኣይክእልን እዩ", @@ -19,6 +19,8 @@ "pleaseVerifyDetails": "በጃኹም ዝርዝር-ሓበሬታ ኣረጋግጹ እሞ እንደገና ፈትኑ", "codeIssuerHint": "ኣዋጂ", "codeSecretKeyHint": "ምስጢራዊ መፍትሕ", + "all": "ኩሉ", + "notes": "መዘኻኸሪታት", "codeAccountHint": "ሕሳብ (you@domain.com)", "codeTagHint": "ልጣፍ", "accountKeyType": "ዓይነት ቁልፊ", @@ -30,12 +32,12 @@ "loggingOut": "ወጸ...", "timeBasedKeyType": "ግዜ እተመስረተ (TOTP)", "counterBasedKeyType": "ቆጻሪ እተመስረተ (TOTP)", - "saveAction": "", + "saveAction": "ዓቅብ", "nextTotpTitle": "ቀጽሊ", - "deleteCodeTitle": "ኮድ ምድምሳስ፧", - "deleteCodeMessage": "ነዚ ኮድ ክትድምስሶ ከም እትደሊ ርግጸኛ ዲኻ፧ እዚ ተግባር ንድሕሪት ዘይምለስ እዩ።", + "deleteCodeTitle": "ኮድ ይደምሰሰ፧", + "deleteCodeMessage": "ነዚ ኮድ ክትድምስሶ ከም እትደሊ ርግጸኛ ዲኻ፧ እዚ ተግባር ንድሕሪት ዘይምለስ ኣይኮነን።", "viewLogsAction": "ምዝገባታት ርአ", - "sendLogsDescription": "", + "sendLogsDescription": "This will send across logs to help us debug your issue. While we take precautions to ensure that sensitive information is not logged, we encourage you to view these logs before sharing them.", "preparingLogsTitle": "ምዝገባ ድላው...", "emailLogsTitle": "መዝገብ ኢ-መይል", "emailLogsMessage": "በጃኹም ነቲ መዝገብ ናብ {email} ስደዱሉ", @@ -67,7 +69,7 @@ "pleaseWait": "በጃኻ ተጸበ...", "generatingEncryptionKeysTitle": "ናይ ምስጢራዊ ቁልፊ ዪፍጠር...", "recreatePassword": "ቃለ-ምስጢር እንደገና ፍጠር", - "recreatePasswordMessage": "እዛ ሕጂ ዘላ ኤለክትሮኒካዊት መሳርሒት ነቲ passwordካ ንምርግጋጽ እኹል ሓይሊ ስለ ዘይብላ ምስ ኵሉ መሳርሒታት ብዚሰማማዕ መገዲ ሓንሳእ እንደገና ኸነሐድሶ ኣሎና ።\n\nበጃኻ በቲ ምሕዋይ-መፍትሕ ኣቲኻ ቃለ-ምስጢር ኣሐድሶ (እንተ ደሊኻ ነታ ቃለ-ምስጢር እንደገና ኽትጥቀመላ ትኽእል ኢኻ)።", + "recreatePasswordMessage": "እዛ ሕጂ ዘላ ኤለክትሮኒካዊት መሳርሒት ነቲ passwordካ ንምርግጋጽ እኹል ሓይሊ ስለ ዘይብላ ምስ ኵሉ መሳርሒታት ብዚሰማማዕ መገዲ ሓንሳእ እንደገና ኸነሐድሶ ኣሎና። \n\nበጃኻ በቲ ምሕዋይ-መፍትሕ ኣቲኻ ቃለ-ምስጢር ኣሐድሶ (እንተ ደሊኻ ነታ ቃለ-ምስጢር እንደገና ኽትጥቀመላ ትኽእል ኢኻ)።", "useRecoveryKey": "ምሕዋይ መፍትሕ ተጠቐም", "incorrectPasswordTitle": "ግጉይ ቃለ-ምስጢር", "welcomeBack": "እንኳዕ ብደሓን ተመለስካ!", diff --git a/mobile/apps/auth/lib/utils/toast_util.dart b/mobile/apps/auth/lib/utils/toast_util.dart index ff26b4cedc..2b5f319856 100644 --- a/mobile/apps/auth/lib/utils/toast_util.dart +++ b/mobile/apps/auth/lib/utils/toast_util.dart @@ -1,4 +1,5 @@ import 'package:ente_auth/ente_theme_data.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -9,6 +10,58 @@ void showToast( toastLength = Toast.LENGTH_LONG, iOSDismissOnTap = true, }) async { + // If on mobile render toast above the keyboard using FToast. + final bool isMobile = PlatformUtil.isMobile(); + + if (isMobile) { + final baseToast = Container( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.0), + color: Theme.of(context).colorScheme.toastBackgroundColor, + ), + child: Text( + message, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.toastTextColor, + fontSize: 16.0, + ), + ), + ); + + final fToast = FToast()..init(context); + + Widget toastChild = baseToast; + if (iOSDismissOnTap == true) { + toastChild = GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + fToast.removeCustomToast(); + fToast.removeQueuedCustomToasts(); + }, + child: baseToast, + ); + } + + fToast.showToast( + child: toastChild, + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + positionedToastBuilder: (context, child) { + final double currentInset = MediaQuery.of(context).viewInsets.bottom; + return Positioned( + left: 16, + right: 16, + bottom: currentInset + 16, + child: child, + ); + }, + ); + return; + } + + // Default path (desktop) try { await Fluttertoast.cancel(); await Fluttertoast.showToast( @@ -21,7 +74,8 @@ void showToast( fontSize: 16.0, ); } on MissingPluginException catch (_) { - Widget toast = Container( + final fToast = FToast()..init(context); + final Widget baseToast = Container( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(25.0), @@ -36,11 +90,8 @@ void showToast( ), ); - final fToast = FToast(); - fToast.init(context); - fToast.showToast( - child: toast, + child: baseToast, gravity: ToastGravity.BOTTOM, toastDuration: const Duration(seconds: 2), ); diff --git a/mobile/apps/photos/lib/ente_theme_data.dart b/mobile/apps/photos/lib/ente_theme_data.dart index 39fc35e4cf..5b6071cad4 100644 --- a/mobile/apps/photos/lib/ente_theme_data.dart +++ b/mobile/apps/photos/lib/ente_theme_data.dart @@ -228,17 +228,17 @@ extension CustomColorScheme on ColorScheme { Color get videoPlayerPrimaryColor => brightness == Brightness.light ? const Color.fromRGBO(0, 179, 60, 1) : const Color.fromRGBO(1, 222, 77, 1); - - Color get videoPlayerBackgroundColor => brightness == Brightness.light - ? const Color(0xFFF5F5F5) - : const Color(0xFF252525); - + Color get videoPlayerBorderColor => brightness == Brightness.light ? const Color(0xFF424242) : const Color(0xFFFFFFFF); Color get imageEditorPrimaryColor => const Color.fromRGBO(8, 194, 37, 1); + Color get editorBackgroundColor => brightness == Brightness.light + ? const Color(0xFFF5F5F5) + : const Color(0xFF252525); + Color get defaultBackgroundColor => brightness == Brightness.light ? backgroundBaseLight : backgroundBaseDark; diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/circular_icon_button.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/circular_icon_button.dart index 7a025fe81f..1ecc52a7b7 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/circular_icon_button.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/circular_icon_button.dart @@ -3,7 +3,7 @@ import "package:flutter_svg/svg.dart"; import "package:photos/ente_theme_data.dart"; import "package:photos/theme/ente_theme.dart"; -class CircularIconButton extends StatelessWidget { +class CircularIconButton extends StatelessWidget { final String label; final VoidCallback onTap; final String? svgPath; @@ -11,7 +11,7 @@ class CircularIconButton extends StatelessWidget { final bool isSelected; const CircularIconButton({ - super.key, + super.key, required this.label, required this.onTap, this.svgPath, @@ -40,12 +40,12 @@ class CircularIconButton extends StatelessWidget { .colorScheme .imageEditorPrimaryColor .withOpacity(0.24) - : colorScheme.backgroundElevated2, + : Theme.of(context).colorScheme.editorBackgroundColor, shape: BoxShape.circle, border: Border.all( color: isSelected ? Theme.of(context).colorScheme.imageEditorPrimaryColor - : colorScheme.backgroundElevated2, + : Theme.of(context).colorScheme.editorBackgroundColor, width: 2, ), ), diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_color_picker.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_color_picker.dart index b87175d757..7aaa5f8880 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_color_picker.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_color_picker.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:photos/theme/ente_theme.dart"; +import "package:photos/ente_theme_data.dart"; class ImageEditorColorPicker extends StatefulWidget { final double value; @@ -23,7 +23,6 @@ class ColorSliderState extends State { @override Widget build(BuildContext context) { - final colorScheme = getEnteColorScheme(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: SizedBox( @@ -55,7 +54,7 @@ class ColorSliderState extends State { end: Alignment.centerRight, ), border: Border.all( - color: colorScheme.backgroundElevated2, + color: Theme.of(context).colorScheme.editorBackgroundColor, width: 6, ), ), diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart index 10333cd3ce..31251e27ef 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import "package:flutter_svg/svg.dart"; +import "package:photos/ente_theme_data.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart"; @@ -191,7 +192,7 @@ class CropAspectChip extends StatelessWidget { decoration: BoxDecoration( color: isSelected ? colorScheme.fillBasePressed - : colorScheme.backgroundElevated2, + : Theme.of(context).colorScheme.editorBackgroundColor, borderRadius: BorderRadius.circular(25), ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart index e091759d69..726083d999 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart @@ -1,10 +1,10 @@ import "dart:async"; import "dart:io"; import "dart:math"; -import "dart:typed_data"; import 'dart:ui' as ui show Image; import 'package:flutter/material.dart'; +import "package:flutter/services.dart"; import "package:flutter_image_compress/flutter_image_compress.dart"; import "package:flutter_svg/svg.dart"; import "package:logging/logging.dart"; @@ -176,17 +176,18 @@ class _ImageEditorPageState extends State { final isLightMode = Theme.of(context).brightness == Brightness.light; final colorScheme = getEnteColorScheme(context); final textTheme = getEnteTextTheme(context); - return Scaffold( - resizeToAvoidBottomInset: false, - backgroundColor: colorScheme.backgroundBase, - body: PopScope( - canPop: false, - onPopInvoked: (didPop) { - if (didPop) return; - editorKey.currentState?.disablePopScope = true; - _showExitConfirmationDialog(context); - }, - child: ProImageEditor.file( + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) return; + editorKey.currentState?.disablePopScope = true; + _showExitConfirmationDialog(context); + }, + child: Scaffold( + extendBodyBehindAppBar: true, + resizeToAvoidBottomInset: false, + backgroundColor: colorScheme.backgroundBase, + body: ProImageEditor.file( key: editorKey, widget.file, callbacks: ProImageEditorCallbacks( @@ -205,6 +206,14 @@ class _ImageEditorPageState extends State { ), configs: ProImageEditorConfigs( imageEditorTheme: ImageEditorTheme( + uiOverlayStyle: SystemUiOverlayStyle( + systemNavigationBarContrastEnforced: true, + systemNavigationBarColor: Colors.transparent, + statusBarBrightness: + isLightMode ? Brightness.dark : Brightness.light, + statusBarIconBrightness: + isLightMode ? Brightness.dark : Brightness.light, + ), appBarBackgroundColor: colorScheme.backgroundBase, background: colorScheme.backgroundBase, bottomBarBackgroundColor: colorScheme.backgroundBase, @@ -212,6 +221,7 @@ class _ImageEditorPageState extends State { background: colorScheme.backgroundBase, ), paintingEditor: PaintingEditorTheme( + initialColor: const Color(0xFF00FFFF), background: colorScheme.backgroundBase, ), textEditor: const TextEditorTheme( @@ -227,6 +237,12 @@ class _ImageEditorPageState extends State { background: colorScheme.backgroundBase, ), emojiEditor: EmojiEditorTheme( + bottomActionBarConfig: BottomActionBarConfig( + showSearchViewButton: true, + buttonColor: colorScheme.backgroundBase, + buttonIconColor: colorScheme.tabIcon, + backgroundColor: colorScheme.backgroundBase, + ), backgroundColor: colorScheme.backgroundBase, ), ), diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart index d4327fb136..14a21e00f1 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import "package:flutter_svg/svg.dart"; +import "package:photos/ente_theme_data.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart"; @@ -174,7 +175,7 @@ class _FontPickerWidget extends StatelessWidget { decoration: BoxDecoration( color: isSelected ? colorScheme.fillBasePressed - : colorScheme.backgroundElevated2, + : Theme.of(context).colorScheme.editorBackgroundColor, borderRadius: BorderRadius.circular(25), ), child: Center( @@ -209,7 +210,7 @@ class _BackgroundPickerWidget extends StatelessWidget { 'text': 'Aa', 'selectedBackgroundColor': isLightMode ? colorScheme.fillFaint : Colors.white, - 'backgroundColor': colorScheme.backgroundElevated2, + 'backgroundColor': Theme.of(context).colorScheme.editorBackgroundColor, 'border': null, 'textColor': Colors.white, 'selectedInnerBackgroundColor': Colors.black, @@ -219,7 +220,7 @@ class _BackgroundPickerWidget extends StatelessWidget { 'text': 'Aa', 'selectedBackgroundColor': isLightMode ? colorScheme.fillFaint : Colors.white, - 'backgroundColor': colorScheme.backgroundElevated2, + 'backgroundColor': Theme.of(context).colorScheme.editorBackgroundColor, 'border': null, 'textColor': Colors.black, 'selectedInnerBackgroundColor': Colors.transparent, @@ -229,7 +230,7 @@ class _BackgroundPickerWidget extends StatelessWidget { 'text': 'Aa', 'selectedBackgroundColor': isLightMode ? colorScheme.fillFaint : Colors.white, - 'backgroundColor': colorScheme.backgroundElevated2, + 'backgroundColor': Theme.of(context).colorScheme.editorBackgroundColor, 'border': null, 'textColor': Colors.black, 'selectedInnerBackgroundColor': Colors.black.withOpacity(0.11), @@ -241,7 +242,7 @@ class _BackgroundPickerWidget extends StatelessWidget { 'text': 'Aa', 'selectedBackgroundColor': isLightMode ? colorScheme.fillFaint : Colors.black, - 'backgroundColor': colorScheme.backgroundElevated2, + 'backgroundColor': Theme.of(context).colorScheme.editorBackgroundColor, 'border': isLightMode ? null : Border.all(color: Colors.white, width: 2), 'textColor': Colors.black, @@ -354,7 +355,7 @@ class _AlignPickerWidget extends StatelessWidget { decoration: BoxDecoration( color: isSelected ? colorScheme.fillBasePressed - : colorScheme.backgroundElevated2, + : Theme.of(context).colorScheme.editorBackgroundColor, borderRadius: BorderRadius.circular(25), border: isSelected ? Border.all(color: Colors.black, width: 2) diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_tune_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_tune_bar.dart index a7b5c69da4..7dcf73d4ed 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_tune_bar.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_tune_bar.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import "package:flutter/services.dart"; import "package:flutter_svg/svg.dart"; import "package:photos/ente_theme_data.dart"; import "package:photos/theme/ente_theme.dart"; @@ -36,6 +37,24 @@ class _ImageEditorTuneBarState extends State with ImageEditorConvertedConfigs, SimpleConfigsAccessState { TuneEditorState get tuneEditor => widget.editor; + final Map _lastValues = {}; + + void _handleTuneItemTap(int index) { + if (tuneEditor.selectedIndex == index) { + final currentValue = tuneEditor.tuneAdjustmentMatrix[index].value; + if (currentValue != 0) { + _lastValues[index] = currentValue; + tuneEditor.onChanged(0); + } else if (_lastValues.containsKey(index)) { + tuneEditor.onChanged(_lastValues[index]!); + } + } else { + tuneEditor.setState(() { + tuneEditor.selectedIndex = index; + }); + } + } + @override Widget build(BuildContext context) { return LayoutBuilder( @@ -77,11 +96,7 @@ class _ImageEditorTuneBarState extends State value: tuneEditor.tuneAdjustmentMatrix[index].value, max: item.max, min: item.min, - onTap: () { - tuneEditor.setState(() { - tuneEditor.selectedIndex = index; - }); - }, + onTap: () => _handleTuneItemTap(index), ); }), ), @@ -224,7 +239,10 @@ class _CircularProgressWithValueState extends State @override void didUpdateWidget(CircularProgressWithValue oldWidget) { super.didUpdateWidget(oldWidget); - + if ((oldWidget.value < 0 && widget.value >= 0) || + (oldWidget.value > 0 && widget.value <= 0)) { + HapticFeedback.vibrate(); + } if (oldWidget.value != widget.value) { _previousValue = oldWidget.value; _progressAnimation = Tween( @@ -303,11 +321,11 @@ class _CircularProgressWithValueState extends State shape: BoxShape.circle, color: showValue || widget.isSelected ? progressColor.withOpacity(0.2) - : colorTheme.backgroundElevated2, + : Theme.of(context).colorScheme.editorBackgroundColor, border: Border.all( color: widget.isSelected ? progressColor.withOpacity(0.4) - : colorTheme.backgroundElevated2, + : Theme.of(context).colorScheme.editorBackgroundColor, width: 2, ), ), @@ -395,7 +413,7 @@ class _TuneAdjustWidget extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(25), - color: colorScheme.backgroundElevated2, + color: Theme.of(context).colorScheme.editorBackgroundColor, ), ), ), @@ -410,7 +428,8 @@ class _TuneAdjustWidget extends StatelessWidget { overlayShape: const RoundSliderOverlayShape(overlayRadius: 0), activeTrackColor: Theme.of(context).colorScheme.imageEditorPrimaryColor, - inactiveTrackColor: colorScheme.backgroundElevated2, + inactiveTrackColor: + Theme.of(context).colorScheme.editorBackgroundColor, trackShape: const _CenterBasedTrackShape(), trackHeight: 24, ), diff --git a/mobile/apps/photos/lib/ui/tools/editor/video_editor/video_editor_bottom_action.dart b/mobile/apps/photos/lib/ui/tools/editor/video_editor/video_editor_bottom_action.dart index ed21e96dbf..d6c0f8aef6 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/video_editor/video_editor_bottom_action.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/video_editor/video_editor_bottom_action.dart @@ -31,7 +31,7 @@ class VideoEditorBottomAction extends StatelessWidget { height: 48, width: 48, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.videoPlayerBackgroundColor, + color: Theme.of(context).colorScheme.editorBackgroundColor, shape: BoxShape.circle, border: Border.all( color: isSelected diff --git a/mobile/apps/photos/lib/ui/tools/editor/video_editor/video_editor_player_control.dart b/mobile/apps/photos/lib/ui/tools/editor/video_editor/video_editor_player_control.dart index 719b62dd60..b05fc13789 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/video_editor/video_editor_player_control.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/video_editor/video_editor_player_control.dart @@ -43,7 +43,7 @@ class VideoEditorPlayerControl extends StatelessWidget { vertical: 4, ), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.videoPlayerBackgroundColor, + color: Theme.of(context).colorScheme.editorBackgroundColor, borderRadius: BorderRadius.circular(56), ), child: Row( diff --git a/mobile/apps/photos/lib/ui/tools/editor/video_editor_page.dart b/mobile/apps/photos/lib/ui/tools/editor/video_editor_page.dart index a7bd22dc00..4b37aad2f8 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/video_editor_page.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/video_editor_page.dart @@ -74,7 +74,7 @@ class _VideoEditorPageState extends State { trimStyle: TrimSliderStyle( onTrimmedColor: const ColorScheme.dark().videoPlayerPrimaryColor, onTrimmingColor: const ColorScheme.dark().videoPlayerPrimaryColor, - background: Theme.of(context).colorScheme.videoPlayerBackgroundColor, + background: Theme.of(context).colorScheme.editorBackgroundColor, positionLineColor: Theme.of(context).colorScheme.videoPlayerBorderColor, lineColor: Theme.of(context) diff --git a/mobile/apps/photos/scripts/changes.txt b/mobile/apps/photos/scripts/changes.txt new file mode 100644 index 0000000000..6abb45295c --- /dev/null +++ b/mobile/apps/photos/scripts/changes.txt @@ -0,0 +1,2 @@ +- Aman: Fixed bottom nav bar color in light theme, resolved paint editor's initial color, and added tap-to-reset with haptics for tune adjustments (brightness/exposure) +- Gracefully handle heic rendering on Android \ No newline at end of file diff --git a/web/apps/photos/src/components/Collections/CollectionShare.tsx b/web/apps/photos/src/components/Collections/CollectionShare.tsx index e22740e1eb..b23778551f 100644 --- a/web/apps/photos/src/components/Collections/CollectionShare.tsx +++ b/web/apps/photos/src/components/Collections/CollectionShare.tsx @@ -58,6 +58,7 @@ import type { import { type CollectionUser } from "ente-media/collection"; import type { RemotePullOpts } from "ente-new/photos/components/gallery"; import { PublicLinkCreated } from "ente-new/photos/components/share/PublicLinkCreated"; +import { useSettingsSnapshot } from "ente-new/photos/components/utils/use-snapshot"; import { avatarTextColor } from "ente-new/photos/services/avatar"; import { createPublicURL, @@ -1105,6 +1106,8 @@ const PublicShare: React.FC = ({ setBlockingLoad, onRemotePull, }) => { + const { customDomain } = useSettingsSnapshot(); + const { show: showPublicLinkCreated, props: publicLinkCreatedVisibilityProps, @@ -1126,11 +1129,15 @@ const PublicShare: React.FC = ({ void appendCollectionKeyToShareURL( publicURL.url, collection.key, - ).then((url) => setResolvedURL(url)); + ).then((url) => + setResolvedURL( + substituteCustomDomainIfNeeded(url, customDomain), + ), + ); } else { setResolvedURL(undefined); } - }, [collection.key, publicURL]); + }, [collection.key, publicURL, customDomain]); const handleCopyLink = () => { if (resolvedURL) void navigator.clipboard.writeText(resolvedURL); @@ -1164,6 +1171,16 @@ const PublicShare: React.FC = ({ ); }; +const substituteCustomDomainIfNeeded = ( + url: string, + customDomain: string | undefined, +) => { + if (!customDomain) return url; + const u = new URL(url); + u.host = customDomain; + return u.href; +}; + type EnablePublicShareOptionsProps = { setPublicURL: (value: PublicURL) => void; onLinkCreated: () => void; diff --git a/web/apps/photos/src/components/Sidebar.tsx b/web/apps/photos/src/components/Sidebar.tsx index 8c9769d0c5..27f6aca66c 100644 --- a/web/apps/photos/src/components/Sidebar.tsx +++ b/web/apps/photos/src/components/Sidebar.tsx @@ -17,6 +17,7 @@ import { Skeleton, Stack, styled, + TextField, Tooltip, useColorScheme, } from "@mui/material"; @@ -38,6 +39,7 @@ import { import { SpacedRow } from "ente-base/components/containers"; import { DialogCloseIconButton } from "ente-base/components/mui/DialogCloseIconButton"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; +import { LoadingButton } from "ente-base/components/mui/LoadingButton"; import { SidebarDrawer, TitledNestedSidebarDrawer, @@ -49,8 +51,10 @@ import { type ModalVisibilityProps, } from "ente-base/components/utils/modal"; import { useBaseContext } from "ente-base/context"; +import { isHTTPErrorWithStatus } from "ente-base/http"; import { getLocaleInUse, + pt, setLocaleInUse, supportedLocales, ut, @@ -88,6 +92,7 @@ import { isDevBuildAndUser, pullSettings, updateCFProxyDisabledPreference, + updateCustomDomain, updateMapEnabled, } from "ente-new/photos/services/settings"; import { @@ -110,6 +115,7 @@ import { import { usePhotosAppContext } from "ente-new/photos/types/context"; import { initiateEmail, openURL } from "ente-new/photos/utils/web"; import { wait } from "ente-utils/promise"; +import { useFormik } from "formik"; import { t } from "i18next"; import { useRouter } from "next/router"; import React, { @@ -776,6 +782,8 @@ const Preferences: React.FC = ({ onClose, onRootClose, }) => { + const { show: showDomainSettings, props: domainSettingsVisibilityProps } = + useModalVisibility(); const { show: showMapSettings, props: mapSettingsVisibilityProps } = useModalVisibility(); const { @@ -816,6 +824,50 @@ const Preferences: React.FC = ({ /> )} + { + /* TODO: CD */ process.env.NEXT_PUBLIC_ENTE_WIP_CD && ( + + + + + + + } + onClick={showDomainSettings} + /> + ) + } } label={t("map")} @@ -836,6 +888,10 @@ const Preferences: React.FC = ({ )} + { ); }; +const DomainSettings: React.FC = ({ + open, + onClose, + onRootClose, +}) => { + const handleRootClose = () => { + onClose(); + onRootClose(); + }; + + return ( + + + + ); +}; + +// Separate component to reset state on back. +const DomainSettingsContents: React.FC = () => { + const { customDomain, customDomainCNAME } = useSettingsSnapshot(); + + const formik = useFormik({ + initialValues: { domain: customDomain ?? "" }, + onSubmit: async (values, { setFieldError }) => { + const domain = values.domain; + const setValueFieldError = (message: string) => + setFieldError("domain", message); + + try { + await updateCustomDomain(domain); + } catch (e) { + log.error(`Failed to submit input ${domain}`, e); + if (isHTTPErrorWithStatus(e, 400)) { + setValueFieldError(pt("Invalid domain")); + } else if (isHTTPErrorWithStatus(e, 409)) { + setValueFieldError(pt("Domain already linked by a user")); + } else { + setValueFieldError(t("generic_error")); + } + } + }, + }); + + // TODO: CD: help + + return ( + + +
+ + + {customDomain ? pt("Update") : pt("Save")} + + +
+ + + + On your DNS provider, add a CNAME from your domain to{" "} + + {customDomainCNAME} + + + + + + + Within 1 hour, your public albums will be accessible via + your domain! + + + For more information, see + + {" help "} + + + +
+ ); +}; + +interface DomainSectionProps { + title: string; + ordinal: string; + isEmoji?: boolean; +} + +const DomainItem: React.FC> = ({ + title, + ordinal, + isEmoji, + children, +}) => ( + + + {title} + + {ordinal} + + + {children} + +); + const MapSettings: React.FC = ({ open, onClose, diff --git a/web/apps/photos/src/pages/index.tsx b/web/apps/photos/src/pages/index.tsx index d276aad67b..1796318bca 100644 --- a/web/apps/photos/src/pages/index.tsx +++ b/web/apps/photos/src/pages/index.tsx @@ -7,7 +7,11 @@ import { EnteLogo } from "ente-base/components/EnteLogo"; import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import { useBaseContext } from "ente-base/context"; -import { albumsAppOrigin, customAPIHost } from "ente-base/origins"; +import { + albumsAppOrigin, + customAPIHost, + shouldOnlyServeAlbumsApp, +} from "ente-base/origins"; import { masterKeyFromSession, updateSessionFromElectronSafeStorageIfNeeded, @@ -41,7 +45,8 @@ const Page: React.FC = () => { const albumsURL = new URL(albumsAppOrigin()); currentURL.pathname = router.pathname; if ( - currentURL.host == albumsURL.host && + (shouldOnlyServeAlbumsApp || + currentURL.host == albumsURL.host) && currentURL.pathname != "/shared-albums" ) { const end = currentURL.hash.lastIndexOf("&"); diff --git a/web/packages/base/locales/pl-PL/translation.json b/web/packages/base/locales/pl-PL/translation.json index ecbbff918f..064add62dc 100644 --- a/web/packages/base/locales/pl-PL/translation.json +++ b/web/packages/base/locales/pl-PL/translation.json @@ -62,8 +62,8 @@ "processed_counts": "{{count, number}} / {{total, number}}", "upload_reading_metadata_files": "Czytanie plików metadanych", "upload_cancelling": "Anulowanie pozostałych przesłań", - "upload_done": "", - "upload_skipped": "", + "upload_done": "Przesłano {{count, number}}", + "upload_skipped": "Pominięto {{count, number}}", "initial_load_delay_warning": "Pierwsze ładowanie może zająć trochę czasu", "no_account": "Nie mam konta", "existing_account": "Posiadam już konto", @@ -84,12 +84,12 @@ "tap_outside_image": "Dotknij na zewnątrz obrazu", "shortcuts": "Skróty", "show_shortcuts": "Pokaż skróty", - "zoom_preset": "", - "toggle_controls": "", + "zoom_preset": "Ustawienie powiększenia", + "toggle_controls": "Przełącz kontrolki", "toggle_live": "", - "toggle_audio": "", - "toggle_favorite": "", - "toggle_archive": "", + "toggle_audio": "Przełącz dźwięk", + "toggle_favorite": "Przełącz ulubione", + "toggle_archive": "Przełącz archiwizację", "view_info": "Zobacz informacje", "copy_as_png": "Kopiuj jako PNG", "toggle_fullscreen": "Przełącz tryb pełnoekranowy", diff --git a/web/packages/base/locales/vi-VN/translation.json b/web/packages/base/locales/vi-VN/translation.json index f3dec27f76..fdd39566fb 100644 --- a/web/packages/base/locales/vi-VN/translation.json +++ b/web/packages/base/locales/vi-VN/translation.json @@ -515,7 +515,7 @@ "enter_name": "Nhập tên", "uploader_name_hint": "Thêm tên để bạn bè biết ai là người chụp những tấm ảnh tuyệt vời này!", "name_placeholder": "Tên...", - "more_details": "Thêm chi tiết", + "more_details": "Thông tin thêm", "ml_search": "Học máy", "ml_search_description": "Ente hỗ trợ học máy trên-thiết-bị nhằm nhận diện khuôn mặt, tìm kiếm vi diệu và các tính năng tìm kiếm nâng cao khác", "ml_search_footnote": "Tìm kiếm vi diệu cho phép tìm ảnh theo nội dung của chúng, ví dụ: 'xe hơi', 'xe hơi đỏ', 'Ferrari'", @@ -560,13 +560,13 @@ "delete_account_reason_placeholder": "Chọn một lý do", "delete_reason": { "missing_feature": "Thiếu một tính năng quan trọng mà tôi cần", - "behaviour": "Ứng dụng hoặc một tính năng nhất định không hoạt động như tôi muốn", + "behaviour": "Ứng dụng hoặc một tính năng không hoạt động như tôi muốn", "found_another_service": "Tôi tìm thấy một dịch vụ khác mà tôi thích hơn", "not_listed": "Lý do không có trong danh sách" }, "delete_account_feedback_label": "Chúng tôi rất tiếc khi thấy bạn ra đi. Vui lòng giải thích lý do bạn rời đi để giúp chúng tôi cải thiện.", "delete_account_feedback_placeholder": "Phản hồi", - "delete_account_confirm_checkbox_label": "Có, tôi muốn xóa vĩnh viễn tài khoản này và tất cả dữ liệu của nó", + "delete_account_confirm_checkbox_label": "Có, tôi muốn xóa vĩnh viễn tài khoản này và tất cả dữ liệu", "delete_account_confirm": "Xác nhận xóa tài khoản", "delete_account_confirm_message": "

Tài khoản này được liên kết với các ứng dụng Ente khác, nếu bạn có dùng.

Dữ liệu bạn đã tải lên, trên tất cả ứng dụng Ente, sẽ được lên lịch để xóa, và tài khoản của bạn sẽ bị xóa vĩnh viễn.

", "feedback_required": "Mong bạn giúp chúng tôi thông tin này", diff --git a/web/packages/base/origins.ts b/web/packages/base/origins.ts index 8e1ef1ddac..e6c8b89ebd 100644 --- a/web/packages/base/origins.ts +++ b/web/packages/base/origins.ts @@ -97,3 +97,9 @@ export const isCustomAlbumsAppOrigin = */ export const albumsAppOrigin = () => process.env.NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT ?? "https://albums.ente.io"; + +/** + * Return true if this build is meant to only serve public albums. + */ +export const shouldOnlyServeAlbumsApp = + !!process.env.NEXT_PUBLIC_ENTE_ONLY_SERVE_ALBUMS_APP; diff --git a/web/packages/new/photos/services/settings.ts b/web/packages/new/photos/services/settings.ts index aedda42384..e09844ce7b 100644 --- a/web/packages/new/photos/services/settings.ts +++ b/web/packages/new/photos/services/settings.ts @@ -8,7 +8,11 @@ import log from "ente-base/log"; import { updateShouldDisableCFUploadProxy } from "ente-gallery/services/upload"; import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod/v4"; -import { fetchFeatureFlags, updateRemoteFlag } from "./remote-store"; +import { + fetchFeatureFlags, + updateRemoteFlag, + updateRemoteValue, +} from "./remote-store"; /** * In-memory flags that tracks various settings. @@ -66,6 +70,24 @@ export interface Settings { * Default: "https://cast.ente.io" */ castURL: string; + + /** + * Set to the domain (host, e.g. "photos.example.org") that the user wishes + * to use for sharing their public albums. + * + * An empty string is treated as `undefined`. + */ + customDomain?: string; + + /** + * The URL we should ask the user to CNAME their {@link customDomain} to + * for wiring up their domain to the public albums app. + * + * See also `apps.custom-domain.cname` in `server/local.yaml`. + * + * Default: "my.ente.io" + */ + customDomainCNAME: string; } const createDefaultSettings = (): Settings => ({ @@ -73,6 +95,7 @@ const createDefaultSettings = (): Settings => ({ mapEnabled: false, cfUploadProxyDisabled: false, castURL: "https://cast.ente.io", + customDomainCNAME: "my.ente.io", }); /** @@ -147,6 +170,8 @@ const FeatureFlags = z.object({ betaUser: z.boolean().nullish().transform(nullToUndefined), mapEnabled: z.boolean().nullish().transform(nullToUndefined), castUrl: z.string().nullish().transform(nullToUndefined), + customDomain: z.string().nullish().transform(nullToUndefined), + customDomainCNAME: z.string().nullish().transform(nullToUndefined), }); type FeatureFlags = z.infer; @@ -158,6 +183,9 @@ const syncSettingsSnapshotWithLocalStorage = () => { settings.mapEnabled = flags?.mapEnabled || false; settings.cfUploadProxyDisabled = savedCFProxyDisabled(); if (flags?.castUrl) settings.castURL = flags.castUrl; + if (flags?.customDomain) settings.customDomain = flags.customDomain; + if (flags?.customDomainCNAME) + settings.customDomainCNAME = flags.customDomainCNAME; setSettingsSnapshot(settings); }; @@ -198,6 +226,17 @@ export const isDevBuildAndUser = () => isDevBuild && isDevUserViaEmail(); const isDevUserViaEmail = () => !!savedPartialLocalUser()?.email?.endsWith("@ente.io"); +/** + * Persist the user's custom domain preference both locally and on remote. + * + * Setting the value to a blank string is equivalent to deleting the custom + * domain value altogether. + */ +export const updateCustomDomain = async (customDomain: string) => { + await updateRemoteValue("customDomain", customDomain); + return pullSettings(); +}; + /** * Persist the user's map enabled preference both locally and on remote. */