Merge remote-tracking branch 'origin/main' into flutter-upgrade

This commit is contained in:
Prateek Sunal
2025-08-12 13:13:09 +05:30
28 changed files with 608 additions and 107 deletions

View File

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

View File

@@ -1,4 +1,4 @@
name: "Internal release (photos)"
name: "Internal release (photos with rust)"
on:
workflow_dispatch: # Allow manually running the action

View File

@@ -1,4 +1,4 @@
name: "Internal release (photos)"
name: "Old Internal release (photos)"
on:
workflow_dispatch: # Allow manually running the action

View File

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

View File

@@ -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"
}
]
}
}

View File

@@ -0,0 +1,8 @@
<svg version="1.2" baseProfile="tiny-ps" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1386 1594" width="1386" height="1594">
<title>logo (15)-svg copy-svg</title>
<style>
tspan { white-space:pre }
.shp0 { fill: #ff5a00 }
</style>
<path id="Layer" fill-rule="evenodd" class="shp0" d="M1385.14 634.99L1385.14 1547.13C1385.14 1553.03 1384.01 1558.88 1381.76 1564.35C1379.45 1569.82 1376.13 1574.75 1372 1578.93C1367.82 1583.11 1362.83 1586.44 1357.36 1588.69C1351.89 1590.94 1346.04 1592.12 1340.14 1592.12L570.51 1592.12C534.63 1594.16 498.65 1593.2 462.98 1589.23C427.26 1585.26 391.98 1578.29 357.44 1568.42C322.9 1558.55 289.28 1545.79 256.88 1530.35C224.49 1514.85 193.44 1496.67 164.05 1475.97C137.02 1453.77 112.73 1428.52 91.6 1400.63C70.47 1372.8 52.66 1342.56 38.61 1310.54C24.56 1278.53 14.32 1244.96 8.1 1210.59C1.82 1176.16 -0.32 1141.14 1.61 1106.29L1.61 1101.41C1.61 641.05 502.94 580.24 666.61 580.24L1046.9 580.24L1046.9 546.83C1047.7 524.84 1045.5 502.8 1040.36 481.41C1035.21 460.01 1027.22 439.42 1016.54 420.17C1005.87 400.92 992.63 383.17 977.23 367.51C961.79 351.8 944.25 338.28 925.21 327.29C785.88 256.77 512.11 282.89 283.91 423.38C278.5 428.1 271.85 431.32 264.77 432.66C257.69 434 250.34 433.47 243.53 431.05C236.72 428.64 230.66 424.46 225.99 418.93C221.38 413.41 218.27 406.76 216.98 399.63L202.99 171.03C202.99 168.08 203.31 165.18 204.06 162.34C204.76 159.5 205.88 156.77 207.33 154.19C208.73 151.67 210.5 149.31 212.53 147.22C214.62 145.13 216.93 143.31 219.45 141.8C259.67 118.58 301.4 97.99 344.3 80.24C387.26 62.49 431.29 47.59 476.17 35.63C521.12 23.67 566.75 14.71 612.82 8.76C658.89 2.86 705.33 -0.03 751.78 0.13C937.98 0.13 1138.77 54.24 1238.52 163.09C1338.32 271.95 1384.55 429.44 1384.55 636.81L1385.14 634.99ZM1046.9 858.76L687.31 858.76C410.48 858.76 355.13 1022.96 355.13 1105.05C354.87 1119.64 356.37 1134.17 359.64 1148.38C362.91 1162.59 367.9 1176.32 374.49 1189.3C381.09 1202.33 389.24 1214.45 398.84 1225.44C408.39 1236.49 419.22 1246.25 431.18 1254.61C442.71 1263.35 454.94 1271.18 467.7 1277.99C480.47 1284.8 493.71 1290.65 507.39 1295.47C521.01 1300.25 535.06 1304 549.27 1306.63C563.48 1309.26 577.91 1310.76 592.39 1311.19L1046.9 1311.19L1046.9 858.76Z" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -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?",

View File

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

View File

@@ -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": "እንኳዕ ብደሓን ተመለስካ!",

View File

@@ -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),
);

View File

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

View File

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

View File

@@ -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<ImageEditorColorPicker> {
@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<ImageEditorColorPicker> {
end: Alignment.centerRight,
),
border: Border.all(
color: colorScheme.backgroundElevated2,
color: Theme.of(context).colorScheme.editorBackgroundColor,
width: 6,
),
),

View File

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

View File

@@ -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<ImageEditorPage> {
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<ImageEditorPage> {
),
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<ImageEditorPage> {
background: colorScheme.backgroundBase,
),
paintingEditor: PaintingEditorTheme(
initialColor: const Color(0xFF00FFFF),
background: colorScheme.backgroundBase,
),
textEditor: const TextEditorTheme(
@@ -227,6 +237,12 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
background: colorScheme.backgroundBase,
),
emojiEditor: EmojiEditorTheme(
bottomActionBarConfig: BottomActionBarConfig(
showSearchViewButton: true,
buttonColor: colorScheme.backgroundBase,
buttonIconColor: colorScheme.tabIcon,
backgroundColor: colorScheme.backgroundBase,
),
backgroundColor: colorScheme.backgroundBase,
),
),

View File

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

View File

@@ -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<ImageEditorTuneBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
TuneEditorState get tuneEditor => widget.editor;
final Map<int, double> _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<ImageEditorTuneBar>
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<CircularProgressWithValue>
@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<double>(
@@ -303,11 +321,11 @@ class _CircularProgressWithValueState extends State<CircularProgressWithValue>
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,
),

View File

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

View File

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

View File

@@ -74,7 +74,7 @@ class _VideoEditorPageState extends State<VideoEditorPage> {
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)

View File

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

View File

@@ -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<PublicShareProps> = ({
setBlockingLoad,
onRemotePull,
}) => {
const { customDomain } = useSettingsSnapshot();
const {
show: showPublicLinkCreated,
props: publicLinkCreatedVisibilityProps,
@@ -1126,11 +1129,15 @@ const PublicShare: React.FC<PublicShareProps> = ({
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<PublicShareProps> = ({
);
};
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;

View File

@@ -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<NestedSidebarDrawerVisibilityProps> = ({
onClose,
onRootClose,
}) => {
const { show: showDomainSettings, props: domainSettingsVisibilityProps } =
useModalVisibility();
const { show: showMapSettings, props: mapSettingsVisibilityProps } =
useModalVisibility();
const {
@@ -816,6 +824,50 @@ const Preferences: React.FC<NestedSidebarDrawerVisibilityProps> = ({
/>
</RowButtonGroup>
)}
{
/* TODO: CD */ process.env.NEXT_PUBLIC_ENTE_WIP_CD && (
<RowButton
label={pt("Custom domains")}
endIcon={
<Stack
direction="row"
sx={{
alignSelf: "stretch",
alignItems: "center",
}}
>
<Box
sx={{
width: "8px",
bgcolor: "stroke.faint",
alignSelf: "stretch",
mr: 0.5,
}}
/>
<Box
sx={{
width: "8px",
bgcolor: "stroke.muted",
alignSelf: "stretch",
mr: 0.5,
}}
/>
<Box
sx={{
width: "8px",
bgcolor: "stroke.base",
alignSelf: "stretch",
opacity: 0.3,
mr: 1.5,
}}
/>
<ChevronRightIcon />
</Stack>
}
onClick={showDomainSettings}
/>
)
}
<RowButton
endIcon={<ChevronRightIcon />}
label={t("map")}
@@ -836,6 +888,10 @@ const Preferences: React.FC<NestedSidebarDrawerVisibilityProps> = ({
</RowButtonGroup>
)}
</Stack>
<DomainSettings
{...domainSettingsVisibilityProps}
onRootClose={onRootClose}
/>
<MapSettings
{...mapSettingsVisibilityProps}
onRootClose={onRootClose}
@@ -954,6 +1010,152 @@ const ThemeSelector = () => {
);
};
const DomainSettings: React.FC<NestedSidebarDrawerVisibilityProps> = ({
open,
onClose,
onRootClose,
}) => {
const handleRootClose = () => {
onClose();
onRootClose();
};
return (
<TitledNestedSidebarDrawer
{...{ open, onClose }}
onRootClose={handleRootClose}
// TODO: CD: Translations
title={pt("Custom domains")}
// caption={pt("Your albums, your domain")}
caption="Use your own domain when sharing"
>
<DomainSettingsContents />
</TitledNestedSidebarDrawer>
);
};
// 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 (
<Stack sx={{ px: 2, py: "12px" }}>
<DomainItem title={pt("Link your domain")} ordinal={pt("1")}>
<form onSubmit={formik.handleSubmit}>
<TextField
name="domain"
value={formik.values.domain}
onChange={formik.handleChange}
type={"text"}
fullWidth
autoFocus={true}
margin="dense"
disabled={formik.isSubmitting}
error={!!formik.errors.domain}
helperText={
formik.errors.domain ??
pt("Any domain or subdomain you own")
}
label={t("Domain")}
placeholder={ut("photos.example.org")}
sx={{ mb: 2 }}
/>
<LoadingButton
fullWidth
type="submit"
loading={formik.isSubmitting}
color="accent"
>
{customDomain ? pt("Update") : pt("Save")}
</LoadingButton>
</form>
</DomainItem>
<Divider sx={{ mt: 4, mb: 2, opacity: 0.5 }} />
<DomainItem title={pt("Add DNS entry")} ordinal={pt("2")}>
<Typography sx={{ color: "text.muted" }}>
On your DNS provider, add a CNAME from your domain to{" "}
<Typography
component="span"
sx={{ fontWeight: "bold", color: "text.base" }}
>
{customDomainCNAME}
</Typography>
</Typography>
</DomainItem>
<Divider sx={{ mt: 5, mb: 2, opacity: 0.5 }} />
<DomainItem title={ut("🎉")} ordinal={pt("3")} isEmoji>
<Typography sx={{ color: "text.muted", mt: 2 }}>
Within 1 hour, your public albums will be accessible via
your domain!
</Typography>
<Typography sx={{ color: "text.muted", mt: 3 }}>
For more information, see
<Typography component="span" sx={{ color: "accent.main" }}>
{" help "}
</Typography>
</Typography>
</DomainItem>
</Stack>
);
};
interface DomainSectionProps {
title: string;
ordinal: string;
isEmoji?: boolean;
}
const DomainItem: React.FC<React.PropsWithChildren<DomainSectionProps>> = ({
title,
ordinal,
isEmoji,
children,
}) => (
<Stack>
<Stack
direction="row"
sx={{ alignItems: "center", justifyContent: "space-between" }}
>
<Typography variant={isEmoji ? "h3" : "h6"}>{title}</Typography>
<Typography
variant="h1"
sx={{
minWidth: "28px",
textAlign: "center",
color: "stroke.faint",
}}
>
{ordinal}
</Typography>
</Stack>
{children}
</Stack>
);
const MapSettings: React.FC<NestedSidebarDrawerVisibilityProps> = ({
open,
onClose,

View File

@@ -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("&");

View File

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

View File

@@ -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": "<p>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.</p><p>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.</p>",
"feedback_required": "Mong bạn giúp chúng tôi thông tin này",

View File

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

View File

@@ -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<typeof FeatureFlags>;
@@ -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.
*/