diff --git a/android/app/src/main/res/values-ar/strings.xml b/android/app/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..d266a56 --- /dev/null +++ b/android/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,26 @@ + + Conduit + contain + false + automatic + + قف + + %1$d / %2$d \u00B7 %3$s + + المستخدمين: %1$dمتصل / %2$dمتصلين\nالبيانات: %3$s تم نقلها + + في انتظار اتصال الشبكة + + إشعار بشأن خدمة Conduit + + التحديث مطلوب: يرجى تنزيل أحدث إصدار من التطبيق لمواصلة استخدامه. + + فشلت خدمة Conduit في البدء التشغيل. انقر فوق الإشعار للحصول على مزيد من المعلومات + + فشلت خدمة Conduit في إعادة التشغيل. انقر فوق الإشعار للحصول على مزيد من المعلومات + + بدأت خدمة Conduit… + + true + diff --git a/android/app/src/main/res/values-fa/strings.xml b/android/app/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000..fab8920 --- /dev/null +++ b/android/app/src/main/res/values-fa/strings.xml @@ -0,0 +1,26 @@ + + Conduit + contain + false + automatic + + توقف + + ۱\n۲\n۳ + + اتصال فعال \nدر حال اتصال \nانتقال داده شد + + منتظر اتصال به شبکه + + اعلان‌های Conduit + + آپدیت کنید: برای استفاده از این برنامه، آخرین نسخه را دانلود کنید. + + Conduit راه نیفتاد. برای اطلاعات بیشتر اینجا کلیک کنید. + + Conduit راه نیفتاد. برای اطلاعات بیشتر اینجا کلیک کنید. + + Conduit در حال شروع است... + + true + diff --git a/android/app/src/main/res/values-tr/strings.xml b/android/app/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000..54d327d --- /dev/null +++ b/android/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,26 @@ + + Conduit + contain + false + automatic + + Durdur + + %1$d / %2$d \u00B7 %3$s + + İstemciler: %1$d bağlı / %2$d bağlanıyor\nVeri: %3$s aktarıldı + + Ağ bağlantısı bekleniyor + + Conduit hizmeti bildirimi + + Güncelleme gerekiyor: kullanmaya devam etmek için lütfen uygulamanın son sürümünü indirin. + + Conduit hizmeti başlatılamadı. Daha fazla bilgi için bildirime tıklayın. + + Conduit hizmeti yeniden başlatılamadı. Daha fazla bilgi için bildirime tıklayın. + + Conduit hizmeti başlatılıyor… + + true + diff --git a/src/app/(app)/onboarding.tsx b/src/app/(app)/onboarding.tsx index bab7923..404fda9 100644 --- a/src/app/(app)/onboarding.tsx +++ b/src/app/(app)/onboarding.tsx @@ -32,6 +32,7 @@ import { SkTextStyle, Skia, TextAlign, + TextDirection, useFonts, vec, } from "@shopify/react-native-skia"; @@ -72,7 +73,8 @@ import { fonts, palette, sharedStyles as ss } from "@/src/styles"; export default function OnboardingScreen() { const win = useWindowDimensions(); const insets = useSafeAreaInsets(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const isRTL = i18n.dir() === "rtl" ? true : false; const notificationPermissions = useNotificationsPermissions(); const router = useRouter(); @@ -228,9 +230,12 @@ export default function OnboardingScreen() { if (!fontMgr) { return null; } - const paragraphStyle = { + const paragraphStyle: SkParagraphStyle = { textAlign: TextAlign.Center, }; + if (isRTL) { + paragraphStyle.textDirection = TextDirection.RTL; + } const textStyle: SkTextStyle = { color: Skia.Color(palette.white), fontFamilies: ["Jura"], @@ -252,8 +257,11 @@ export default function OnboardingScreen() { return null; } const paragraphStyle: SkParagraphStyle = { - textAlign: TextAlign.Left, + textAlign: isRTL ? TextAlign.Right : TextAlign.Left, }; + if (isRTL) { + paragraphStyle.textDirection = TextDirection.RTL; + } const textStyle: SkTextStyle = { color: Skia.Color(palette.white), fontFamilies: ["Rajdhani"], @@ -275,9 +283,12 @@ export default function OnboardingScreen() { if (!fontMgr) { return null; } - const paragraphStyle = { + const paragraphStyle: SkParagraphStyle = { textAlign: TextAlign.Center, }; + if (isRTL) { + paragraphStyle.textDirection = TextDirection.RTL; + } const textStyle: SkTextStyle = { color: Skia.Color(palette.blueTint2), diff --git a/src/components/ConduitName.tsx b/src/components/ConduitName.tsx index 72f8988..787e3a1 100644 --- a/src/components/ConduitName.tsx +++ b/src/components/ConduitName.tsx @@ -47,7 +47,8 @@ export function ConduitName() { // The Conduit Name is purely cosmetic, it is optional and only stored locally export function EditableConduitName({ initialName }: { initialName: string }) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const isRTL = i18n.dir() === "rtl" ? true : false; const queryClient = useQueryClient(); @@ -126,6 +127,7 @@ export function EditableConduitName({ initialName }: { initialName: string }) { maxLength={maxLength} autoCorrect={false} autoComplete={"off"} + textAlign={isRTL ? "right" : "left"} /> {showCharsUsed && ( diff --git a/src/components/ConduitOrbToggle.tsx b/src/components/ConduitOrbToggle.tsx index 543a748..a7b0735 100644 --- a/src/components/ConduitOrbToggle.tsx +++ b/src/components/ConduitOrbToggle.tsx @@ -22,22 +22,26 @@ import { Canvas, Circle, ColorMatrix, - ColorShader, Group, Image, Paint, + Paragraph, RadialGradient, Shadow, - Text, + SkParagraphStyle, + SkTextStyle, + Skia, + TextAlign, + TextDirection, interpolateColors, - useFont, + useFonts, useImage, vec, } from "@shopify/react-native-skia"; import * as Haptics from "expo-haptics"; import React from "react"; import { useTranslation } from "react-i18next"; -import { View } from "react-native"; +import { View, useWindowDimensions } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { Easing, @@ -54,7 +58,7 @@ import Animated, { import { z } from "zod"; import { useAnimatedImageValue } from "@/src/animationHooks"; -import { timedLog } from "@/src/common/utils"; +import { drawBigFont, timedLog } from "@/src/common/utils"; import { ConduitConnectionLight } from "@/src/components/canvas/ConduitConnectionLight"; import { INPROXY_MAX_CLIENTS_MAX, @@ -67,6 +71,7 @@ import { useInproxyStatus, } from "@/src/inproxy/hooks"; import { fonts, palette, sharedStyles as ss } from "@/src/styles"; +import { FaderGroup } from "./canvas/FaderGroup"; export function ConduitOrbToggle({ width, @@ -75,7 +80,10 @@ export function ConduitOrbToggle({ width: number; height: number; }) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const isRTL = i18n.dir() === "rtl" ? true : false; + const win = useWindowDimensions(); + const { toggleInproxy } = useInproxyContext(); const { data: inproxyStatus } = useInproxyStatus(); const { data: inproxyCurrentConnectedClients } = @@ -116,15 +124,43 @@ export function ConduitOrbToggle({ // The "Turn On" text also uses interpolation to appear to fade in by going // from transparent to it's final color. const orbText = t("TAP_TO_TURN_ON_I18N.string"); - const orbTextColors = [palette.transparent, palette.midGrey]; - const orbTextColorIndex = useSharedValue(0); - const orbTextColor = useDerivedValue(() => { - return interpolateColors( - orbTextColorIndex.value, - [0, 1], - orbTextColors, - ); - }); + const orbTextOpacity = useSharedValue(0); + + const fontMgr = useFonts({ Jura: [fonts.JuraRegular] }); + const fontSize = drawBigFont(win) ? 20 : 16; + const orbTextParagraph = React.useMemo(() => { + if (!fontMgr) { + return null; + } + let paragraphStyle: SkParagraphStyle = { + textAlign: TextAlign.Center, + }; + if (isRTL) { + paragraphStyle.textDirection = TextDirection.RTL; + } + + const mainTextStyle: SkTextStyle = { + fontFamilies: ["Jura"], + fontSize: fontSize, + fontStyle: { + weight: 300, + }, + letterSpacing: 1, // 5% of 20 + }; + + return Skia.ParagraphBuilder.Make(paragraphStyle, fontMgr) + .pushStyle(mainTextStyle) + .addText(orbText) + .build(); + }, [fontMgr]); + + const orbTextXOffset = orbTextParagraph + ? -orbTextParagraph.getMaxWidth() / 2 + : 0; + const orbTextYOffset = orbTextParagraph + ? -orbTextParagraph.getHeight() / 2 + : 0; + // The orb will pop into existence at the start, animating from radius 0 up const orbRadius = useSharedValue(0); const orbDiameter = useDerivedValue(() => orbRadius.value * 2); @@ -141,6 +177,17 @@ export function ConduitOrbToggle({ }, ]; + // The overlay is rendered in a regular view, while the orb itself is within + // Skia, so we need to accomodate RTL in the regular view. + const orbOverlayTransform = [ + { + translateY: orbCenterY, + }, + { + translateX: isRTL ? (-1 * width) / 2 : width / 2, + }, + ]; + function animateProxyAnnouncing() { timedLog("animateProxyAnnouncing()"); orbColorsIndex.value = withRepeat( @@ -151,7 +198,7 @@ export function ConduitOrbToggle({ -1, true, ); - orbTextColorIndex.value = withTiming(0, { duration: 500 }); + orbTextOpacity.value = withTiming(0, { duration: 500 }); dotsOpacity.value = withTiming(1, { duration: 1000 }); } @@ -166,7 +213,7 @@ export function ConduitOrbToggle({ timedLog("animateTurnOffProxy()"); cancelAnimation(orbColorsIndex); orbColorsIndex.value = withTiming(0, { duration: 500 }); - orbTextColorIndex.value = withTiming(1, { duration: 500 }); + orbTextOpacity.value = withTiming(1, { duration: 500 }); dotsOpacity.value = withTiming(0.2, { duration: 1000 }); } @@ -186,14 +233,10 @@ export function ConduitOrbToggle({ delay, withTiming(0.2, { duration: 1000 }), ); - if (delay > 0) { - // if we're introing with a delay, it means the Inproxy is stopped, - // so we will fade in our button text. - orbTextColorIndex.value = withDelay( - delay, - withTiming(1, { duration: 1000 }), - ); - } + orbTextOpacity.value = withDelay( + delay, + withTiming(1, { duration: 1000 }), + ); } // We have 4 animation states that depend on the state of the Inproxy: @@ -301,11 +344,6 @@ export function ConduitOrbToggle({ ); }, []); - // TODO: switch to the newer Paragraph model - const font = useFont(fonts.JuraRegular, 20); - const orbTextXOffset = font ? -font.measureText(orbText).width / 2 : 0; - const orbTextYOffset = font ? font.measureText(orbText).height / 2 : 0; - // Orb Gesture // Since turning off the proxy will disconnect any connected users, require // a long press to turn off. When the user clicks the orb and a toggle would @@ -393,10 +431,10 @@ export function ConduitOrbToggle({ {/* Intro particle swirl animation */} @@ -480,14 +518,14 @@ export function ConduitOrbToggle({ {/* Turn ON text displayed when Conduit is off */} - - - + + + @@ -519,7 +557,7 @@ export function ConduitOrbToggle({ width: orbDiameter, height: orbDiameter, borderRadius: orbRadius, - transform: orbCenteringTransform, + transform: orbOverlayTransform, }, ]} /> diff --git a/src/components/ConduitSettings.tsx b/src/components/ConduitSettings.tsx index c9c83cd..3637277 100644 --- a/src/components/ConduitSettings.tsx +++ b/src/components/ConduitSettings.tsx @@ -72,16 +72,10 @@ import { InproxyParametersSchema, } from "@/src/inproxy/types"; import { getProxyId } from "@/src/inproxy/utils"; -import { - lineItemRTLStyle, - lineItemStyle, - palette, - sharedStyles as ss, -} from "@/src/styles"; +import { lineItemStyle, palette, sharedStyles as ss } from "@/src/styles"; export function ConduitSettings() { - const { t, i18n } = useTranslation(); - const isRTL = i18n.dir() === "rtl" ? true : false; + const { t } = useTranslation(); const win = useWindowDimensions(); const router = useRouter(); @@ -182,18 +176,12 @@ export function ConduitSettings() { const scrollRef = React.useRef(null); function Settings() { - let lineStyle; - if (isRTL) { - lineStyle = lineItemRTLStyle; - } else { - lineStyle = lineItemStyle; - } return ( @@ -268,13 +256,13 @@ export function ConduitSettings() { )} min={8} max={INPROXY_MAX_MBPS_PER_PEER_MAX} - style={[...lineStyle, ss.alignCenter]} + style={[...lineItemStyle, ss.alignCenter]} onChange={updateInproxyLimitBytesPerSecond} scrollRef={scrollRef} /> )} - + {t("ALIAS_I18N.string")}: @@ -337,7 +319,7 @@ export function ConduitSettings() { { const changed = value.value === originalValue ? " " : "*"; @@ -122,23 +114,11 @@ export function EditableNumberSlider({ }); // Overlay for GestureDetector - const overlaySize = useDerivedValue(() => circleR.value * 5); - const overlayTransform = useDerivedValue(() => { - return [ - { - translateX: circleCx.value - circleR.value * 2.5, - }, - { - translateY: circleCy.value - circleR.value * 2, - }, - ]; - }); const overlayStyle = useAnimatedStyle(() => ({ - position: "absolute", flex: 1, - height: overlaySize.value, - width: overlaySize.value, - transform: overlayTransform.value, + position: "absolute", + width: "100%", + height: "100%", })); const sliderGesture = Gesture.Pan() @@ -165,15 +145,10 @@ export function EditableNumberSlider({ return ( {label} - - + +