Merge remote-tracking branch 'origin/flutter-upgrade' into isolated-ffmpeg
This commit is contained in:
133
.github/workflows/mobile-daily-internal.yml
vendored
Normal file
133
.github/workflows/mobile-daily-internal.yml
vendored
Normal 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
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Internal release (photos)"
|
||||
name: "Internal release (photos with rust)"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Internal release (photos)"
|
||||
name: "Old Internal release (photos)"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
12
.github/workflows/web-deploy.yml
vendored
12
.github/workflows/web-deploy.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
8
mobile/apps/auth/assets/custom-icons/icons/allegro.svg
Normal file
8
mobile/apps/auth/assets/custom-icons/icons/allegro.svg
Normal 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 |
@@ -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?",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "እንኳዕ ብደሓን ተመለስካ!",
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
2
mobile/apps/photos/scripts/changes.txt
Normal file
2
mobile/apps/photos/scripts/changes.txt
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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("&");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user