Merge branch 'main' into main

This commit is contained in:
Felix Häberle
2024-05-31 14:25:10 +02:00
committed by GitHub
26 changed files with 950 additions and 449 deletions

View File

@@ -15,7 +15,7 @@ on:
env:
IS_GITHUB_ACTIONS: 1
CHANNEL: "${{ inputs.channel }}"
FLUTTER_VERSION: '3.19.x'
FLUTTER_VERSION: '3.22.x'
NDK_VERSION: r26b
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
TAG_NAME: "${{ inputs.tag-name }}"

10
.vscode/launch.json vendored
View File

@@ -1,6 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "go Package",
"type": "go",
"request": "launch",
"mode": "auto",
"cwd": "./libcore",
"program": "./libcore/cli/main.go",
"args": ["build","-c","a.txt","-d","b.txt","--full-config"] ,
"buildFlags": "-tags with_clash_api,with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server"
},
{
"name": "Hiddify Dev",
"request": "launch",

21
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,21 @@
{
"dart.lineLength": 250,
"[dart]": {
"editor.defaultFormatter": "Dart-Code.dart-code",
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.tabSize": 2,
"editor.rulers": [
250
],
"editor.detectIndentation": false,
"editor.selectionHighlight": false,
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets",
"editor.wordBasedSuggestions": "off"
},
"html.format.wrapLineLength": 250,
}

View File

@@ -0,0 +1,413 @@
{
"general": {
"appTitle": "Hiddify",
"reset": "إعادة تعيين",
"toggle": {
"enabled": "مُفعّل",
"disabled": "غير مُفعّل"
},
"state": {
"disable": "تعطيل"
},
"sort": "فرز",
"sortBy": "فرز حسب",
"addToClipboard": "إضافة إلى الحافظة",
"notSet": "غير مُحدد",
"agree": "موافقة",
"decline": "رفض",
"unknown": "غير معروف",
"hidden": "مخفي",
"timeout": "انتهاء الوقت",
"clipboardExportSuccessMsg": "تمت إضافة البيانات إلى الحافظة",
"showMore": "عرض المزيد",
"showLess": "عرض أقل",
"openAppSettings": "فتح إعدادات التطبيق",
"grantPermission": "منح الإذن"
},
"intro": {
"termsAndPolicyCaution(rich)": "بمواصلة استخدامك، فإنك توافق على ${tap(@:about.termsAndConditions)}",
"start": "ابدأ"
},
"home": {
"pageTitle": "الصفحة الرئيسية",
"emptyProfilesMsg": "ابدأ بإضافة ملف تعريف اشتراك",
"noActiveProfileMsg": "اختر ملف تعريف"
},
"stats": {
"traffic": "حركة المرور",
"trafficLive": "حركة المرور الحية",
"trafficTotal": "إجمالي حركة المرور",
"uplink": "الصعود",
"downlink": "الهبوط",
"connection": "الاتصال",
"speed": "السرعة",
"totalTransferred": "إجمالي البيانات المنقولة"
},
"profile": {
"overviewPageTitle": "الملفات الشخصية",
"detailsPageTitle": "ملف التعريف",
"activeProfileNameSemanticLabel": "اسم ملف التعريف النشط: \"${name}\".",
"activeProfileBtnSemanticLabel": "عرض جميع ملفات التعريف",
"nonActiveProfileBtnSemanticLabel": "اختر \"${name}\" كملف تعريف نشط",
"subscription": {
"traffic": "حركة المرور",
"updatedTimeAgo": "تم التحديث قبل ${timeago}",
"remainingDuration": "تبقى ${duration} يومًا",
"remainingTrafficSemanticLabel": "استُهلك ${consumed} من أصل ${total} حركة مرور",
"expired": "منتهي الصلاحية",
"noTraffic": "غير متاح",
"upload": "التحميل",
"download": "التنزيل",
"total": "إجمالي حركة المرور",
"expireDate": "تاريخ انتهاء الصلاحية"
},
"sortBy": {
"lastUpdate": "تم التحديث مؤخرًا",
"name": "أبجديًا"
},
"add": {
"buttonText": "ملف تعريف جديد",
"shortBtnTxt": "ملف تعريف جديد",
"fromClipboard": "إضافة من الحافظة",
"scanQr": "مسح رمز الاستجابة السريعة",
"qrScanner": {
"permissionDeniedError": "تم رفض الإذن",
"unexpectedError": "حدث خطأ ما",
"torchSemanticLabel": "ضوء فلاش",
"facingSemanticLabel": "اتجاه الكاميرا",
"permissionRequest": "الإذن للكاميرا لمسح رمز الاستجابة السريعة"
},
"manually": "إدخال يدوي",
"addingProfileMsg": "إضافة ملف التعريف",
"failureMsg": "فشل في إضافة ملف التعريف"
},
"update": {
"buttonTxt": "تحديث",
"tooltip": "تحديث ملف التعريف",
"updateSubscriptions": "تحديث الاشتراكات",
"failureMsg": "فشل في تحديث ملف التعريف",
"successMsg": "تم تحديث ملف التعريف بنجاح",
"namedFailureMsg": "فشل في تحديث \"${name}\"",
"namedSuccessMsg": "تم تحديث \"${name}\" بنجاح"
},
"share": {
"buttonText": "مشاركة",
"exportToClipboardSuccess": "تم تصدير البيانات إلى الحافظة",
"exportSubLinkToClipboard": "تصدير رابط الاشتراك إلى الحافظة",
"subLinkQrCode": "رمز الاستجابة السريعة لرابط الاشتراك",
"exportConfigToClipboard": "تصدير التكوين إلى الحافظة",
"exportConfigToClipboardSuccess": "تم نسخ التكوين إلى الحافظة"
},
"edit": {
"buttonTxt": "تحرير",
"selectActiveTxt": "اختر ملف التعريف النشط"
},
"delete": {
"buttonTxt": "حذف",
"confirmationMsg": "حذف ملف التعريف نهائياً؟",
"successMsg": "تم حذف ملف التعريف بنجاح"
},
"save": {
"buttonText": "حفظ",
"successMsg": "تم حفظ ملف التعريف بنجاح",
"failureMsg": "فشل في حفظ ملف التعريف"
},
"detailsForm": {
"nameLabel": "الاسم",
"nameHint": "اسم ملف التعريف",
"urlLabel": "العنوان",
"urlHint": "عنوان URL للتكوين الكامل",
"emptyNameMsg": "الاسم مطلوب",
"invalidUrlMsg": "عنوان URL غير صالح",
"lastUpdate": "آخر تحديث",
"updateInterval": "التحديث التلقائي",
"updateIntervalDialogTitle": "مُدة التحديث التلقائي (بالساعات)"
}
},
"proxies": {
"pageTitle": "الخوادم الوكيلية",
"emptyProxiesMsg": "لا توجد خوادم وكيلية متاحة",
"delayTestTooltip": "اختبار التأخير",
"sortTooltip": "فرز الخوادم الوكيلية",
"checkIp": "تحقق من عنوان IP",
"unknownIp": "عنوان IP غير معروف",
"sortOptions": {
"unsorted": "افتراضي",
"name": "أبجديًا",
"delay": "حسب التأخير"
},
"activeProxySemanticLabel": "الخادم الوكيل النشط",
"delaySemantics": {
"result": "التأخير: ${delay} مللي ثانية",
"timeout": "انتهاء الوقت في اختبار التأخير",
"testing": "التأخير: قيد الاختبار..."
},
"ipInfoSemantics": {
"address": "عنوان IP",
"country": "الدولة"
}
},
"logs": {
"pageTitle": "السجلات",
"filterHint": "تصفية",
"allLevelsFilter": "الكل",
"shareCoreLogs": "مشاركة سجلات النواة",
"shareAppLogs": "مشاركة سجلات التطبيق",
"pauseTooltip": "إيقاف مؤقت",
"resumeTooltip": "استئناف",
"clearTooltip": "مسح"
},
"settings": {
"pageTitle": "الإعدادات",
"requiresRestartMsg": "لتطبيق هذه التغييرات، أعد تشغيل التطبيق",
"experimental": "تجريبي",
"experimentalMsg": "الميزات ذات العلامة التجريبية لا تزال قيد التطوير وقد تسبب مشاكل.",
"exportOptions": "تصدير الخيارات إلى الحافظة",
"exportAllOptions": "تصدير جميع الخيارات إلى الحافظة (تصحيح الأخطاء)",
"importOptions": "استيراد الخيارات من الحافظة",
"importOptionsMsg": "سيؤدي هذا إلى إعادة كتابة جميع خيارات التكوين بالقيم المحددة. هل أنت متأكد؟",
"general": {
"sectionTitle": "عام",
"locale": "اللغة",
"region": "المنطقة",
"regionMsg": "يساعد على تعيين الخيارات الافتراضية لتجاوز العناوين المحلية",
"regions": {
"ir": "إيران (ir)",
"cn": "الصين (cn)",
"ru": "روسيا (ru)",
"af": "أفغانستان (af)",
"other": "أخرى"
},
"themeMode": "وضع السمة",
"themeModes": {
"system": "اتباع سمة النظام",
"dark": "الوضع الداكن",
"light": "الوضع الفاتح",
"black": "الوضع الأسود"
},
"enableAnalytics": "تمكين التحليلات",
"enableAnalyticsMsg": "منح الإذن بجمع التحليلات وإرسال تقارير الأعطال لتحسين التطبيق",
"autoStart": "بدء التشغيل عند تسجيل الدخول",
"silentStart": "بدء التشغيل مُصغر",
"openWorkingDir": "فتح دليل العمل",
"ignoreBatteryOptimizations": "تعطيل تحسينات البطارية",
"ignoreBatteryOptimizationsMsg": "إزالة القيود للحصول على أفضل أداء VPN",
"dynamicNotification": "عرض السرعة في الإشعار",
"hapticFeedback": "ردود فعل اللمس",
"autoIpCheck": "التحقق من عنوان IP للاتصال تلقائيًا"
},
"advanced": {
"sectionTitle": "متقدم",
"debugMode": "وضع تصحيح الأخطاء",
"debugModeMsg": "أعد تشغيل التطبيق لتطبيق هذا التغيير",
"memoryLimit": "حد الذاكرة",
"memoryLimitMsg": "قم بتمكين هذه الميزة إذا كنت تواجه أخطاء «عدم كفاية الذاكرة» أو تعطل التطبيق بشكل متكرر",
"resetTunnel": "إعادة تعيين ملف تعريف VPN"
},
"network": {
"perAppProxyPageTitle": "الوكيل لكل تطبيق",
"perAppProxyModes": {
"off": "الكل",
"offMsg": "وكيل جميع التطبيقات",
"include": "وكيل",
"includeMsg": "وكيل التطبيقات المحددة فقط",
"exclude": "تجاوز",
"excludeMsg": "لا تستخدم الوكيل للتطبيقات المحددة"
},
"showSystemApps": "عرض تطبيقات النظام",
"hideSystemApps": "إخفاء تطبيقات النظام",
"clearSelection": "مسح الاختيار"
},
"geoAssets": {
"pageTitle": "أصول التوجيه",
"geoip": "GeoIP",
"geosite": "GeoSite",
"version": "الإصدار ${version}",
"fileMissing": "الملف مفقود",
"update": "تحديث",
"download": "تنزيل",
"failureMsg": "فشل في تحديث الأصل",
"successMsg": "تم تحديث الأصل بنجاح",
"addRecommended": "إضافة الأصول الموصى بها",
"missingGeoAssetsMsg": "ملفات أصول التوجيه المحددة مفقودة. قم بتنزيلها أو اختر ملفات موجودة"
}
},
"about": {
"pageTitle": "حول\nتعريب:م. ابراهيم قاسم",
"version": "الإصدار",
"sourceCode": "رمز المصدر",
"telegramChannel": "قناة Telegram",
"checkForUpdate": "التحقق من وجود تحديث",
"privacyPolicy": "سياسة الخصوصية",
"termsAndConditions": "الشروط والأحكام"
},
"appUpdate": {
"notAvailableMsg": "أنت تستخدم أحدث إصدار بالفعل",
"dialogTitle": "تحديث متاح",
"updateMsg": "إصدار جديد من @:general.appTitle متاح. هل تريد التحديث الآن؟",
"currentVersionLbl": "الإصدار الحالي",
"newVersionLbl": "الإصدار الجديد",
"updateNowBtnTxt": "تحديث الآن",
"laterBtnTxt": "لاحقًا",
"ignoreBtnTxt": "تجاهل"
},
"tray": {
"dashboard": "لوحة التحكم",
"quit": "إنهاء",
"open": "فتح",
"status": {
"connect": "الاتصال",
"connecting": "جار الاتصال",
"disconnect": "فصم الاتصال",
"disconnecting": "جار فصم الاتصال"
}
},
"failure": {
"unexpected": "خطأ غير متوقع",
"clash": {
"unexpected": "خطأ غير متوقع",
"core": "خطأ في Clash ${reason}"
},
"singbox": {
"unexpected": "خطأ في الخدمة غير متوقع",
"serviceNotRunning": "الخدمة غير قيد التشغيل",
"missingPrivilege": "غياب الامتياز",
"missingPrivilegeMsg": "وضع VPN يتطلب امتيازات المسؤول. أعد تشغيل التطبيق كمسؤول أو غيّر وضع الخدمة.",
"missingGeoAssets": "أصول Geo مفقودة",
"missingGeoAssetsMsg": "أصول Geo مفقودة. فكر في تغيير الأصل النشط أو تنزيل الأصل المحدد في الإعدادات.",
"invalidConfigOptions": "خيارات التكوين غير صحيحة",
"invalidConfig": "تكوين غير صالح",
"create": "خطأ في إنشاء الخدمة",
"start": "خطأ في بدء تشغيل الخدمة"
},
"connectivity": {
"unexpected": "فشل غير متوقع",
"missingVpnPermission": "غياب إذن VPN",
"missingNotificationPermission": "غياب إذن الإشعارات",
"core": "خطأ في النواة"
},
"profiles": {
"unexpected": "خطأ غير متوقع",
"notFound": "لم يتم العثور على ملف التعريف",
"invalidConfig": "تكوينات غير صحيحة",
"invalidUrl": "عنوان URL غير صالح"
},
"connection": {
"unexpected": "خطأ في الاتصال غير متوقع",
"timeout": "انتهاء الوقت في الاتصال",
"badResponse": "استجابة سيئة",
"connectionError": "خطأ في الاتصال",
"badCertificate": "شهادة غير صالحة"
},
"geoAssets": {
"unexpected": "خطأ غير متوقع",
"notUpdate": "لا يوجد تحديث متاح",
"activeNotFound": "لم يتم العثور على أصل Geo النشط"
}
},
"play": {
"title": "Hiddify (معاينة)",
"short_description": "Auto, SSH, VLESS, VMess, Trojan, Reality, Sing-Box, Clash, XRay, Shadowsocks",
"full_description": "الهدف الرئيسي لـ Hiddify هو توفير عميل نفق آمن وسهل الاستخدام وكفاءة. يمكّنك من توجيه جميع حركة المرور أو حركة المرور من التطبيق المحدد إلى خادم بعيد من اختيارك، باستخدام إذن VPN-Service. \n\nملاحظة: لا نوفر أي خادم، ويتعين على المستخدمين ضمان بقاء أنشطتهم عبر الإنترنت خاصة باستخدام خادمهم المخصص أو الخوادم الموثوقة. \n \nندعم الخوادم مع:\n- رابط اشتراك V2Ray/XRay عادي \n- رابط اشتراك Clash \n- رابط اشتراك Sing-Box \n\nما هي ميزاتنا الفريدة؟\n - سهل الاستخدام \n - مُحسّن وسريع \n - اختيار أدنى Ping تلقائيًا \n - عرض معلومات استخدام المستخدم \n - استيراد sublink بسهولة بنقرة واحدة باستخدام deeplinking \n - مجاني وخالي من الإعلانات \n - تبديل sublinks بسهولة \n - المزيد والمزيد \n\nالدعم:\n- جميع البروتوكولات التي تدعمها Sing-Box \n- VLESS + XTLS Reality, Vision \n- VMess \n- Trojan \n- ShoadowSocks \n- Reality \n- WireGuard \n- V2Ray \n- Hysteria2 \n- TUICv5 \n- SSH \n- ShadowTLS \n\n\nرمز المصدر موجود في https://github.com/hiddify/Hiddify-Next \nتعتمد نواة التطبيق على Sing-Box مفتوحة المصدر.\n\nوصف الإذن:\n- VPN Service: نظرًا لأن هدف هذا التطبيق هو توفير عميل نفق آمن وسهل الاستخدام وكفاءة، نحتاج إلى هذا الإذن لنتمكن من توجيه حركة المرور عبر النفق إلى الخادم البعيد. \n- QUERY ALL PACKAGES: يستخدم هذا الإذن للسماح للمستخدمين بتضمين أو استبعاد تطبيقات محددة للأنفاق. \n- RECEIVE BOOT COMPLETED: يمكن تمكين أو تعطيل هذا الإذن من إعدادات التطبيق لتنشيط هذا التطبيق عند تشغيل الجهاز. \n- POST NOTIFICATIONS: هذا الإذن ضروري لأننا نستخدم خدمة المقدمة لضمان تشغيل خدمة VPN بشكل مستمر. \n- هذا التطبيق خالي من الإعلانات. يتم جمع التحليلات وبيانات الأعطال فقط بموافقة صريحة من المستخدم في أول استخدام للتطبيق."
},
"connection": {
"tapToConnect": "انقر للاتصال",
"connecting": "جار الاتصال",
"disconnecting": "جار فصم الاتصال",
"connected": "متصل",
"reconnect": "أعد الاتصال",
"connectAnyWay": "اتصل",
"experimentalNotice": "ميزات تجريبية قيد الاستخدام",
"experimentalNoticeMsg": "لقد قمت بتمكين بعض الميزات التجريبية التي قد تؤثر على جودة الاتصال وتسبب أخطاء غير متوقعة. يمكنك دائمًا تغيير هذه الخيارات أو إعادة تعيينها من صفحة خيارات التكوين.",
"disableExperimentalNotice": "لا تعرض مرة أخرى",
"reconnectMsg": "أعد الاتصال ليتم تطبيق التغييرات"
},
"config": {
"resetBtn": "إعادة تعيين الخيارات",
"serviceMode": "وضع الخدمة",
"quickSettings": "الإعدادات السريعة",
"setupWarp": "إعداد WARP",
"allOptions": "جميع خيارات التكوين",
"serviceModes": {
"proxy": "خدمة الوكيل فقط",
"systemProxy": "تعيين وكيل النظام",
"tun": "VPN",
"tunService": "خدمة VPN"
},
"shortServiceModes": {
"proxy": "وكيل",
"systemProxy": "وكيل النظام",
"tun": "VPN",
"tunService": "خدمة VPN"
},
"section": {
"route": "خيارات التوجيه",
"dns": "خيارات DNS",
"inbound": "خيارات الوارد",
"mux": "MultiPlexer",
"outbound": "خيارات الخارج",
"tlsTricks": "حيل TLS",
"warp": "خيارات WARP",
"misc": "خيارات متنوعة"
},
"warpConsent": {
"title": "موافقة Cloudflare WARP",
"description(rich)": "Cloudflare WARP هو مزود VPN مجاني لـ WireGuard. بتمكين هذا الخيار، فإنك توافق على ${tos(Terms of Service)} و ${privacy(Privacy Policy)} لـ Cloudflare WARP."
},
"generateWarpConfig": "إنشاء تكوين WARP",
"missingWarpConfig": "تكوين WARP مفقود",
"warpConfigGenerated": "تم إنشاء تكوين WARP",
"pageTitle": "خيارات التكوين",
"logLevel": "مستوى السجل",
"resolveDestination": "حل الوجهة",
"ipv6Mode": "توجيه IPv6",
"ipv6Modes": {
"disable": "تعطيل",
"enable": "تمكين",
"prefer": "مُفضل",
"only": "حصري"
},
"remoteDnsAddress": "DNS البعيد",
"remoteDnsDomainStrategy": "استراتيجية مجال DNS البعيد",
"directDnsAddress": "DNS المباشر",
"directDnsDomainStrategy": "استراتيجية مجال DNS المباشر",
"mixedPort": "منفذ مُختلط",
"tproxyPort": "منفذ الوكيل الشفاف",
"localDnsPort": "منفذ DNS المحلي",
"allowConnectionFromLan": "مشاركة VPN على الشبكة المحلية",
"tunImplementation": "تنفيذ TUN",
"mtu": "MTU",
"connectionTestUrl": "عنوان URL لاختبار الاتصال",
"urlTestInterval": "مُدة اختبار عنوان URL",
"enableClashApi": "تمكين Clash API",
"clashApiPort": "منفذ Clash API",
"enableTun": "تمكين TUN",
"setSystemProxy": "تعيين وكيل النظام",
"enableDnsRouting": "تمكين توجيه DNS",
"enableFakeDns": "تمكين DNS المزيف",
"bypassLan": "تجاوز LAN",
"strictRoute": "توجيه صارم",
"enableTlsFragment": "تمكين تجزئة TLS",
"tlsFragmentSize": "حجم تجزئة TLS",
"tlsFragmentSleep": "وقت تعليق تجزئة TLS",
"enableTlsMixedSniCase": "تمكين خلط حالات SNI في TLS",
"enableTlsPadding": "تمكين حشو TLS",
"tlsPaddingSize": "حشو TLS",
"enableMux": "تمكين Mux",
"muxProtocol": "بروتوكول Mux",
"muxMaxStreams": "أقصى عدد من التدفقات المتزامنة",
"enableWarp": "تمكين WARP",
"warpDetourMode": "وضع التفاف",
"warpDetourModes": {
"proxyOverWarp": "لف الخوادم الوكيلية عبر WARP",
"warpOverProxy": "لف WARP عبر الخوادم الوكيلية",
"inbound": "لف WARP عبر الخوادم الوكيلية",
"outbound": "لف الخوادم الوكيلية عبر WARP"
},
"warpLicenseKey": "مفتاح الترخيص",
"warpCleanIp": "عنوان IP نظيف",
"warpPort": "المنفذ",
"warpNoise": "عدد الضوضاء",
"warpNoiseDelay": "تأخير الضوضاء"
}
}

View File

@@ -78,8 +78,10 @@
"permissionRequest": "Permission to camera to scan QR Code"
},
"manually": "Manual Entry",
"addWarp": "Add Warp",
"addingProfileMsg": "Adding Profile",
"failureMsg": "Failed to Add Profile"
},
"update": {
"buttonTxt": "Update",
@@ -162,8 +164,8 @@
"requiresRestartMsg": "For this to take effect restart the app",
"experimental": "Experimental",
"experimentalMsg": "Features with Experimental flag are still in development and might cause issues.",
"exportOptions": "Export Options to Clipboard",
"exportAllOptions": "Export Options to Clipboard (Debug)",
"exportOptions": "Copy Anonymous Options to Clipboard",
"exportAllOptions": "Copy All Options to Clipboard",
"importOptions": "Import Options From Clipboard",
"importOptionsMsg": "This will rewrite all config options with provided values. Are you sure?",
"general": {

View File

@@ -162,8 +162,8 @@
"requiresRestartMsg": "برای اعمال این تنظیم، برنامه را دوباره راه‌اندازی کنید",
"experimental": "آزمایشی",
"experimentalMsg": "تنظیماتی که عنوان آزمایشی دارند، هم‌چنان در دست توسعه هستند و فعال‌سازی آن‌ها می‌تواند باعث بروز مشکلاتی شود. ",
"exportOptions": "صادر کردن تنظیمات به کلیپ‌بورد",
"exportAllOptions": "صادر کردن تنظیمات به کلیپ‌بورد (اشکال‌زدایی)",
"exportOptions": "کپی تنظیمات ساده به کلیپ‌بورد",
"exportAllOptions": "کپی همه تنظیمات به کلیپ‌بورد",
"importOptions": "وارد کردن تنظیمات از کلیپ‌بورد",
"importOptionsMsg": "این اقدام همه‌ی تنظیمات پیکربندی را با مقادیر اولیه بازنویسی می‌کند. آیا مطمئن هستید؟",
"general": {

View File

@@ -16,13 +16,13 @@
"agree": "Соглашаться",
"decline": "Отклонить",
"unknown": "Неизвестный",
"hidden": "Скрытый",
"timeout": "Таймаут",
"clipboardExportSuccessMsg": "Добавлено в буфер обмена",
"showMore": "Раскрыть",
"showLess": "Свернуть",
"openAppSettings": "Открыть настройки приложения",
"grantPermission": "Дать разрешение"
"clipboardExportSuccessMsg": "Скопировано",
"showMore": "Развернуть ",
"showLess": "Свернуть ",
"openAppSettings": "Открыть настройки",
"grantPermission": "Дать права доступа",
"hidden": "Скрытый"
},
"intro": {
"termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}",
@@ -39,9 +39,10 @@
"trafficTotal": "Трафик",
"uplink": "Скорость отправки",
"downlink": "Скорость загрузки",
"connection": "Соединение",
"connection": "Соединение ",
"speed": "Скорость",
"totalTransferred": "В общем передано"
"totalTransferred": "Всего передано"
},
"profile": {
"overviewPageTitle": "Профили",
@@ -56,10 +57,10 @@
"remainingTrafficSemanticLabel": "Использовано ${consumed} трафика из ${total}",
"expired": "Истекло",
"noTraffic": "Нет доступного трафика",
"upload": "Загрузка",
"download": "Скачивание",
"total": "Общий трафик",
"expireDate": "Истечение срока действия"
"upload": "Отправлено ",
"download": "Скачано",
"total": "Всего трафика",
"expireDate": "Дата окончания подписки "
},
"sortBy": {
"lastUpdate": "по последнему обновлению",
@@ -75,7 +76,7 @@
"unexpectedError": "Неизвестная ошибка",
"torchSemanticLabel": "Вспышка",
"facingSemanticLabel": "Фронтальная камера",
"permissionRequest": "Разрешение камере сканировать QR-код"
"permissionRequest": "Права на использование камеры для считывания QR"
},
"manually": "Ввести вручную",
"addingProfileMsg": "Добавление профиля",
@@ -138,8 +139,8 @@
},
"activeProxySemanticLabel": "Активный прокси",
"delaySemantics": {
"result": "Задержка: ${delay}ms",
"timeout": "Тест задержки по таймауту",
"result": "Задержка: ${delay}мс",
"timeout": "Тайм-аут при тестировании задержки",
"testing": "Задержка: Тестирование..."
},
"ipInfoSemantics": {
@@ -162,8 +163,7 @@
"requiresRestartMsg": "Чтобы применить изменения, перезапустите приложение.",
"experimental": "Экспериментальный",
"experimentalMsg": "Функции с флагом «Экспериментально» все еще находятся в разработке и могут вызвать проблемы.",
"exportOptions": "Экспорт параметров в буфер обмена",
"exportAllOptions": "Экспорт параметров в буфер обмена (отладка)",
"importOptions": "Импорт параметров из буфера обмена",
"importOptionsMsg": "Это перезапишет все параметры конфига предоставленными значениями. Вы уверены?",
"general": {
@@ -193,7 +193,7 @@
"ignoreBatteryOptimizations": "Отключить оптимизацию батареи",
"ignoreBatteryOptimizationsMsg": "Отключение ограничений для оптимальной производительности VPN.",
"dynamicNotification": "Отображение скорости в уведомлении",
"autoIpCheck": "Автоматически проверять IP соединение"
"autoIpCheck": "Автоматически проверять IP-адрес соединения"
},
"advanced": {
"sectionTitle": "Расширенные",
@@ -314,8 +314,8 @@
"connecting": "Подключение",
"disconnecting": "Отключение",
"connected": "Подключено",
"reconnect": "Переподключение",
"connectAnyWay": "Подключение",
"reconnect": "Восстановить соединение",
"connectAnyWay": "Подключиться",
"experimentalNotice": "Экспериментальные функции в использовании",
"experimentalNoticeMsg": "Вы включили некоторые экспериментальные функции, которые могут повлиять на качество соединения и вызвать непредвиденные ошибки. Вы всегда можете изменить или сбросить эти параметры на странице параметров конфигурации.",
"disableExperimentalNotice": "Больше не показывать",
@@ -325,8 +325,8 @@
"resetBtn": "Сбросить параметры",
"serviceMode": "Режим работы",
"quickSettings": "Быстрые настройки",
"allOptions": "Все параметры ",
"setupWarp": "Настроить WARP",
"allOptions": "Все параметры конфига",
"serviceModes": {
"proxy": "Прокси",
"systemProxy": "Системный прокси",

View File

@@ -162,8 +162,7 @@
"requiresRestartMsg": "要使其生效,请重新启动应用程序",
"experimental": "实验性选项",
"experimentalMsg": "带有实验标志的功能仍在开发中,可能会出现问题。",
"exportOptions": "导出选项到剪切板",
"exportAllOptions": "导出选项到剪切板(用于调试)",
"importOptions": "从剪切板导入选项",
"importOptionsMsg": "这将使用提供的值重写所有配置选项。您确定吗?",
"general": {

View File

@@ -12,39 +12,54 @@
"sort": "排序",
"sortBy": "排序方式",
"addToClipboard": "新增至剪貼簿",
"timeout": "暫停",
"showMore": "展示更多",
"notSet": "未設定",
"agree": "同意",
"decline": "拒絕",
"unknown": "不明",
"hidden": "隱藏",
"timeout": "超時",
"clipboardExportSuccessMsg": "已匯出至剪貼簿",
"showMore": "顯示更多",
"showLess": "顯示較少",
"openAppSettings": "開啟應用程式設定",
"grantPermission": "授予權限"
},
"intro": {
"termsAndPolicyCaution(rich)": "繼續即表示您同意協議 ${tap(@:about.termsAndConditions)}",
"termsAndPolicyCaution(rich)": "繼續即表示您同意合約 ${tap(@:about.termsAndConditions)}",
"start": "開始"
},
"home": {
"pageTitle": "頁",
"pageTitle": "頁",
"emptyProfilesMsg": "首先新增訂閱設定檔",
"noActiveProfileMsg": "選擇設定檔"
},
"stats": {
"traffic": "流量",
"trafficLive": "即時流量",
"trafficTotal": "總流量",
"uplink": "上行",
"downlink": "下行"
"downlink": "下行",
"connection": "連線",
"speed": "速度",
"totalTransferred": "總傳輸量"
},
"profile": {
"overviewPageTitle": "設定檔",
"detailsPageTitle": "設定檔",
"activeProfileNameSemanticLabel": "活動設定檔名稱:“${name}”。",
"activeProfileBtnSemanticLabel": "查看所有設定檔",
"activeProfileBtnSemanticLabel": "查看所有設定檔",
"nonActiveProfileBtnSemanticLabel": "選擇“${name}”作為活動設定檔。",
"subscription": {
"traffic": "流量",
"updatedTimeAgo": "更新${timeago}",
"updatedTimeAgo": "更新${timeago}",
"remainingDuration": "剩餘 ${duration} 天",
"remainingTrafficSemanticLabel": "已使用 ${consumed} 流量,總共 ${total} 流量。",
"expired": "已到期",
"noTraffic": "超過配額"
"noTraffic": "超過配額",
"upload": "上傳",
"download": "下載",
"total": "總流量",
"expireDate": "到期時間"
},
"sortBy": {
"lastUpdate": "最近更新",
@@ -53,13 +68,14 @@
"add": {
"buttonText": "新的設定檔",
"shortBtnTxt": "新的設定檔",
"fromClipboard": "從剪貼簿添加",
"fromClipboard": "從剪貼簿新增",
"scanQr": "掃描 QR code",
"qrScanner": {
"permissionDeniedError": "沒有權限",
"unexpectedError": "出了點問題",
"torchSemanticLabel": "手電筒",
"facingSemanticLabel": "相機朝向"
"facingSemanticLabel": "相機朝向",
"permissionRequest": "授予相機權限已允許掃描 QR code"
},
"manually": "手動輸入",
"addingProfileMsg": "新增設定檔",
@@ -84,11 +100,11 @@
},
"edit": {
"buttonTxt": "編輯",
"selectActiveTxt": "選擇並激活設定檔"
"selectActiveTxt": "選擇活設定檔"
},
"delete": {
"buttonTxt": "刪除",
"confirmationMsg": "永久刪除設定檔嗎?",
"confirmationMsg": "永久刪除設定檔嗎?",
"successMsg": "設定檔刪除成功"
},
"save": {
@@ -113,18 +129,30 @@
"emptyProxiesMsg": "沒有可用的代理",
"delayTestTooltip": "測試延遲",
"sortTooltip": "對代理進行排序",
"checkIp": "檢測 IP 地址",
"unknownIp": "不明的 IP",
"sortOptions": {
"unsorted": "預設",
"name": "按字母排序",
"delay": "按延遲排序"
},
"activeProxySemanticLabel": "生效中的代理",
"delaySemantics": {
"result": "延遲: ${delay}ms",
"timeout": "延遲測試超時",
"testing": "正在測試延遲"
},
"ipInfoSemantics": {
"address": "IP 地址",
"country": "國家"
}
},
"logs": {
"pageTitle": "日誌",
"pageTitle": "記錄檔",
"filterHint": "篩選",
"allLevelsFilter": "全部",
"shareCoreLogs": "分享核心日誌",
"shareAppLogs": "享應用程式日誌",
"shareCoreLogs": "分享核心記錄檔",
"shareAppLogs": "享應用程式記錄檔",
"pauseTooltip": "暫停",
"resumeTooltip": "恢復",
"clearTooltip": "清除"
@@ -134,17 +162,21 @@
"requiresRestartMsg": "若要使其生效,請重新啟動應用程式",
"experimental": "實驗性的",
"experimentalMsg": "帶有實驗標誌的功能仍在開發中,可能會導致問題。",
"exportOptions": "匯出選項至剪貼簿",
"exportAllOptions": "匯出選項至剪貼不(用於偵錯) ",
"importOptions": "從剪貼簿匯入選項",
"importOptionsMsg": "浙江使用提供的值複寫所有配置選項。您確定嗎?",
"general": {
"sectionTitle": "一般",
"sectionTitle": "一般",
"locale": "語言",
"region": "地區",
"regionMsg": "幫助設定預設選項以繞過國內地址",
"regions": {
"ir": "伊朗 (ir)",
"cn": "中國大陸 (cn)",
"cn": "中國 (cn)",
"ru": "俄羅斯 (ru)",
"af": "阿富汗 (af)",
"other": "其他地區"
"other": "其他"
},
"themeMode": "主題模式",
"themeModes": {
@@ -154,18 +186,20 @@
"black": "黑色"
},
"enableAnalytics": "啟用分析",
"enableAnalyticsMsg": "授予收集分析並送崩潰報告以改進應用程式的權限",
"enableAnalyticsMsg": "授予收集分析並送崩潰報告以改進應用程式的權限",
"autoStart": "隨系統啟動",
"silentStart": "啟動最小化",
"openWorkingDir": "開啟工作目錄",
"ignoreBatteryOptimizations": "停用電池化",
"ignoreBatteryOptimizations": "停用電池最佳化",
"ignoreBatteryOptimizationsMsg": "消除限制以獲得最佳 VPN 效能",
"dynamicNotification": "在通知中顯示速度"
"dynamicNotification": "在通知中顯示速度",
"hapticFeedback": "觸覺回饋",
"autoIpCheck": "自動檢查連線的 IP"
},
"advanced": {
"sectionTitle": "高級設定",
"sectionTitle": "進階設定",
"debugMode": "偵錯模式",
"debugModeMsg": "重新啟動應用程式以用此更",
"debugModeMsg": "重新啟動應用程式以用此更",
"memoryLimit": "記憶體限制",
"memoryLimitMsg": "如果您遇到記憶體不足錯誤或頻繁應用程式崩潰,請啟用",
"resetTunnel": "重置 VPN 設定檔"
@@ -189,7 +223,7 @@
"geoip": "GeoIP",
"geosite": "Geosite",
"version": "版本${version}",
"fileMissing": "文件遺失",
"fileMissing": "檔案遺失",
"update": "更新",
"download": "下載",
"failureMsg": "更新資源文件失敗",
@@ -201,7 +235,7 @@
"about": {
"pageTitle": "關於",
"version": "版本",
"sourceCode": "原始碼",
"sourceCode": "原始碼",
"telegramChannel": "Telegram 頻道",
"checkForUpdate": "檢查更新",
"privacyPolicy": "隱私政策",
@@ -220,12 +254,12 @@
"tray": {
"dashboard": "儀表板",
"quit": "退出",
"open": "開",
"open": "開",
"status": {
"connect": "連",
"connecting": "連中",
"disconnect": "斷開連接",
"disconnecting": "斷連中"
"connect": "連",
"connecting": "連中",
"disconnect": "中斷連線",
"disconnecting": "斷連中"
}
},
"failure": {
@@ -236,8 +270,8 @@
},
"singbox": {
"unexpected": "意外服務錯誤",
"serviceNotRunning": "服務未行",
"missingPrivilege": "缺權限",
"serviceNotRunning": "服務未行",
"missingPrivilege": "缺權限",
"missingPrivilegeMsg": "VPN 模式需要管理員權限。以管理員身分重新啟動應用程式或變更服務模式。",
"missingGeoAssets": "缺少 GEO 資源文件",
"missingGeoAssetsMsg": "GEO 資源文件缺失。請考慮更改活動資源文件或在設定中下載選定的資源文件。",
@@ -277,42 +311,54 @@
"full_description": "Hiddify 的主要目標是提供安全、使用者友好且高效率的隧道用戶端。它使您能夠利用 VPN 服務權限將所有流量或選定的應用程式流量路由到您選擇的遠端伺服器。\n\n註我們不提供任何伺服器使用者需要使用自己的自託管伺服器或受信任的伺服器來確保其線上活動的隱私。\n\n我們透過以下方式支援伺服器\n - 普通 V2ray/Xray 訂閱連結\n - Clash 訂閱連結\n - Sing-Box 訂閱連結\n\n 我們的獨特功能是什麼?\n - 使用者友善\n - 最佳化且快速\n - 自動選擇最低延遲\n - 顯示使用者使用資訊\n - 使用一鍵連結輕鬆導入\n - 免費且無廣告\n - 輕鬆切換線路\n - 等等\n 支援:\n - Sing-Box 支援的所有協定 \n - VLESS + XTLS Reality、Vision 協定 \n - VMESS\n - Trojan\n - ShoadowSocks\n - Reality\n - WireGuard\n - V2ray\n - Hystria2\n - TUIC \n - SSH\n - ShadowTLS\n\n\n 原始碼位於 https://github.com/hiddify/Hiddify-Next\n 應用程式核心基於開源的 Sing-Box。\n\n權限說明\n\n - VPN 服務:由於此應用程式的目標是提供安全性、使用者友好且高效的隧道用戶端,因此我們需要此權限才能透過隧道將流量路由到遠端伺服器。\n - 獲取應用程式列表:此權限用於允許使用者包含或排除隧道的特定應用程式。\n - 接收啟動廣播:可以從應用程式設定中啟用或停用此權限,以在裝置啟動時啟動此應用程式。\n - 發送通知:此權限至關重要,因為我們使用前台服務來確保 VPN 服務的持續運作。\n - 該應用程式沒有廣告。分析和崩潰數據僅在用戶首次使用應用程式時明確同意的情況下才會出現。"
},
"connection": {
"tapToConnect": "點擊以連",
"connecting": "連中",
"disconnecting": "斷連中",
"connected": "已連",
"tapToConnect": "點擊以連",
"connecting": "連中",
"disconnecting": "斷連中",
"connected": "已連",
"reconnect": "重新連線",
"connectAnyWay": "連線",
"experimentalNotice": "使用中的實驗性功能",
"experimentalNoticeMsg": "您啟用了一些實驗性功能,這些功能可能會影響連線品質並導致某些意外錯誤。您始終可以從「配置選項」頁面變更或重設這些選項。",
"disableExperimentalNotice": "不再提示"
"disableExperimentalNotice": "不再提示",
"reconnectMsg": "重新連線以使變更生效"
},
"config": {
"resetBtn": "重置選項",
"serviceMode": "服務模式",
"quickSettings": "快速設定",
"setupWarp": "配置 WARP",
"allOptions": "全部配置選項",
"serviceModes": {
"proxy": "僅代理",
"systemProxy": "系統代理",
"tun": "VPN",
"tunService": "VPN 服務"
},
"shortServiceModes": {
"proxy": "僅代理",
"systemProxy": "系統代理",
"tun": "VPN",
"tunService": "VPN 服務"
},
"section": {
"route": "路由選項",
"dns": "DNS 選項",
"inbound": "入站選項",
"mux": "復用器",
"outbound": "出站選項",
"tlsTricks": "TLS Tricks",
"tlsTricks": "TLS 特性",
"warp": "WARP 選項",
"misc": "其他選項"
},
"warpConsent": {
"title": "Cloudflare WARP 許可條款"
"title": "Cloudflare WARP 授權條款",
"description(rich)": "Cloudflare WARP 是一個免費的 WireGuard VPN 提供商。啟用此選項即表示您同意 Cloudflare WARP 的 ${tos(服務條款)} 和 ${privacy(隱私權政策)}"
},
"generateWarpConfig": "生成 WARP 配置檔案",
"missingWarpConfig": "WARP 配置檔案缺失",
"warpConfigGenerated": "WARP 配置檔案已生成",
"pageTitle": "配置選項",
"logLevel": "日誌等級",
"logLevel": "紀錄等級",
"resolveDestination": "解析目標地址",
"ipv6Mode": "IPv6 路由",
"ipv6Modes": {
@@ -328,11 +374,11 @@
"mixedPort": "混合連接埠",
"tproxyPort": "透明代理埠",
"localDnsPort": "本地 DNS 連接埠",
"allowConnectionFromLan": "允許區域網連線",
"allowConnectionFromLan": "允許區域網連線",
"tunImplementation": "TUN 實現",
"mtu": "MTU",
"connectionTestUrl": "連測試網址",
"urlTestInterval": "URL 測試間隔",
"connectionTestUrl": "連測試網址",
"urlTestInterval": "網址測試間隔",
"enableClashApi": "啟用 Clash API",
"clashApiPort": "Clash API 連接埠",
"enableTun": "啟用 TUN",
@@ -358,7 +404,7 @@
"inbound": "透過代理繞過 WARP",
"outbound": "透過 WARP 繞過代理"
},
"warpLicenseKey": "許可證金鑰",
"warpLicenseKey": "授權金鑰",
"warpCleanIp": "清理 IP",
"warpPort": "埠",
"warpNoise": "噪音計數",

View File

@@ -23,8 +23,7 @@ class AnalyticsController extends _$AnalyticsController with AppLogger {
return _preferences.getBool(enableAnalyticsPrefKey) ?? true;
}
SharedPreferences get _preferences =>
ref.read(sharedPreferencesProvider).requireValue;
SharedPreferences get _preferences => ref.read(sharedPreferencesProvider).requireValue;
Future<void> enableAnalytics() async {
if (state case AsyncData(value: final enabled)) {

View File

@@ -6,6 +6,7 @@ extension AppLocaleX on AppLocale {
this == AppLocale.fa ? FontFamily.shabnam : FontFamily.emoji;
String get localeName => switch (flutterLocale.toString()) {
"ar" => "العربية",
"en" => "English",
"fa" => "فارسی",
"ru" => "Русский",

View File

@@ -51,8 +51,7 @@ abstract class ConfigOptions {
validator: (value) => value.isNotBlank,
);
static final remoteDnsDomainStrategy =
PreferencesNotifier.create<DomainStrategy, String>(
static final remoteDnsDomainStrategy = PreferencesNotifier.create<DomainStrategy, String>(
"remote-dns-domain-strategy",
DomainStrategy.auto,
mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value),
@@ -65,8 +64,7 @@ abstract class ConfigOptions {
validator: (value) => value.isNotBlank,
);
static final directDnsDomainStrategy =
PreferencesNotifier.create<DomainStrategy, String>(
static final directDnsDomainStrategy = PreferencesNotifier.create<DomainStrategy, String>(
"direct-dns-domain-strategy",
DomainStrategy.auto,
mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value),
@@ -91,8 +89,7 @@ abstract class ConfigOptions {
validator: (value) => isPort(value.toString()),
);
static final tunImplementation =
PreferencesNotifier.create<TunImplementation, String>(
static final tunImplementation = PreferencesNotifier.create<TunImplementation, String>(
"tun-implementation",
TunImplementation.mixed,
mapFrom: TunImplementation.values.byName,
@@ -101,8 +98,7 @@ abstract class ConfigOptions {
static final mtu = PreferencesNotifier.create<int, int>("mtu", 9000);
static final strictRoute =
PreferencesNotifier.create<bool, bool>("strict-route", true);
static final strictRoute = PreferencesNotifier.create<bool, bool>("strict-route", true);
static final connectionTestUrl = PreferencesNotifier.create<String, String>(
"connection-test-url",
@@ -128,8 +124,7 @@ abstract class ConfigOptions {
validator: (value) => isPort(value.toString()),
);
static final bypassLan =
PreferencesNotifier.create<bool, bool>("bypass-lan", false);
static final bypassLan = PreferencesNotifier.create<bool, bool>("bypass-lan", false);
static final allowConnectionFromLan = PreferencesNotifier.create<bool, bool>(
"allow-connection-from-lan",
@@ -156,16 +151,14 @@ abstract class ConfigOptions {
false,
);
static final tlsFragmentSize =
PreferencesNotifier.create<OptionalRange, String>(
static final tlsFragmentSize = PreferencesNotifier.create<OptionalRange, String>(
"tls-fragment-size",
const OptionalRange(min: 1, max: 500),
mapFrom: OptionalRange.parse,
mapTo: const OptionalRangeJsonConverter().toJson,
);
static final tlsFragmentSleep =
PreferencesNotifier.create<OptionalRange, String>(
static final tlsFragmentSleep = PreferencesNotifier.create<OptionalRange, String>(
"tls-fragment-sleep",
const OptionalRange(min: 0, max: 500),
mapFrom: OptionalRange.parse,
@@ -182,8 +175,7 @@ abstract class ConfigOptions {
false,
);
static final tlsPaddingSize =
PreferencesNotifier.create<OptionalRange, String>(
static final tlsPaddingSize = PreferencesNotifier.create<OptionalRange, String>(
"tls-padding-size",
const OptionalRange(min: 1, max: 1500),
mapFrom: OptionalRange.parse,
@@ -218,8 +210,7 @@ abstract class ConfigOptions {
false,
);
static final warpDetourMode =
PreferencesNotifier.create<WarpDetourMode, String>(
static final warpDetourMode = PreferencesNotifier.create<WarpDetourMode, String>(
"warp-detour-mode",
WarpDetourMode.proxyOverWarp,
mapFrom: WarpDetourMode.values.byName,
@@ -230,16 +221,28 @@ abstract class ConfigOptions {
"warp-license-key",
"",
);
static final warp2LicenseKey = PreferencesNotifier.create<String, String>(
"warp2s-license-key",
"",
);
static final warpAccountId = PreferencesNotifier.create<String, String>(
"warp-account-id",
"",
);
static final warp2AccountId = PreferencesNotifier.create<String, String>(
"warp2-account-id",
"",
);
static final warpAccessToken = PreferencesNotifier.create<String, String>(
"warp-access-token",
"",
);
static final warp2AccessToken = PreferencesNotifier.create<String, String>(
"warp2-access-token",
"",
);
static final warpCleanIp = PreferencesNotifier.create<String, String>(
"warp-clean-ip",
@@ -259,8 +262,7 @@ abstract class ConfigOptions {
mapTo: const OptionalRangeJsonConverter().toJson,
);
static final warpNoiseDelay =
PreferencesNotifier.create<OptionalRange, String>(
static final warpNoiseDelay = PreferencesNotifier.create<OptionalRange, String>(
"warp-noise-delay",
const OptionalRange(min: 20, max: 200),
mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true),
@@ -271,6 +273,10 @@ abstract class ConfigOptions {
"warp-wireguard-config",
"",
);
static final warp2WireguardConfig = PreferencesNotifier.create<String, String>(
"warp2-wireguard-config",
"",
);
static final hasExperimentalFeatures = Provider.autoDispose<bool>(
(ref) {
@@ -278,13 +284,7 @@ abstract class ConfigOptions {
if (PlatformUtils.isDesktop && mode == ServiceMode.tun) {
return true;
}
if (ref.watch(enableTlsFragment) ||
ref.watch(enableTlsMixedSniCase) ||
ref.watch(enableTlsPadding) ||
ref.watch(enableMux) ||
ref.watch(enableWarp) ||
ref.watch(bypassLan) ||
ref.watch(allowConnectionFromLan)) {
if (ref.watch(enableTlsFragment) || ref.watch(enableTlsMixedSniCase) || ref.watch(enableTlsPadding) || ref.watch(enableMux) || ref.watch(enableWarp) || ref.watch(bypassLan) || ref.watch(allowConnectionFromLan)) {
return true;
}
@@ -298,10 +298,13 @@ abstract class ConfigOptions {
"warp.access-token",
"warp.account-id",
"warp.wireguard-config",
"warp2.license-key",
"warp2.access-token",
"warp2.account-id",
"warp2.wireguard-config",
};
static final Map<String, StateNotifierProvider<PreferencesNotifier, dynamic>>
preferences = {
static final Map<String, StateNotifierProvider<PreferencesNotifier, dynamic>> preferences = {
"service-mode": serviceMode,
"log-level": logLevel,
"resolve-destination": resolveDestination,
@@ -348,6 +351,10 @@ abstract class ConfigOptions {
"warp.noise": warpNoise,
"warp.noise-delay": warpNoiseDelay,
"warp.wireguard-config": warpWireguardConfig,
"warp2.license-key": warp2LicenseKey,
"warp2.account-id": warp2AccountId,
"warp2.access-token": warp2AccessToken,
"warp2.wireguard-config": warp2WireguardConfig,
};
static final singboxConfigOptions = FutureProvider<SingboxConfigOption>(
@@ -386,8 +393,7 @@ abstract class ConfigOptions {
};
final geoAssetsRepo = await ref.watch(geoAssetRepositoryProvider.future);
final geoAssets =
await geoAssetsRepo.getActivePair().getOrElse((l) => throw l).run();
final geoAssets = await geoAssetsRepo.getActivePair().getOrElse((l) => throw l).run();
final mode = ref.watch(serviceMode);
return SingboxConfigOption(
@@ -443,6 +449,18 @@ abstract class ConfigOptions {
noise: ref.watch(warpNoise),
noiseDelay: ref.watch(warpNoiseDelay),
),
warp2: SingboxWarpOption(
enable: ref.watch(enableWarp),
mode: ref.watch(warpDetourMode),
wireguardConfig: ref.watch(warp2WireguardConfig),
licenseKey: ref.watch(warp2LicenseKey),
accountId: ref.watch(warp2AccountId),
accessToken: ref.watch(warp2AccessToken),
cleanIp: ref.watch(warpCleanIp),
cleanPort: ref.watch(warpPort),
noise: ref.watch(warpNoise),
noiseDelay: ref.watch(warpNoiseDelay),
),
geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath(
geoAssets.geoip.providerName,
geoAssets.geoip.fileName,
@@ -470,8 +488,7 @@ class ConfigOptionRepository with ExceptionHandler, InfraLogger {
final GeoAssetRepository geoAssetRepository;
final GeoAssetPathResolver geoAssetPathResolver;
TaskEither<ConfigOptionFailure, SingboxConfigOption>
getFullSingboxConfigOption() {
TaskEither<ConfigOptionFailure, SingboxConfigOption> getFullSingboxConfigOption() {
return exceptionHandler(
() async {
return right(await getConfigOptions());

View File

@@ -26,20 +26,14 @@ class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger {
return WarpOptions(
consentGiven: consent,
configGeneration: hasWarpConfig
? const AsyncValue.data("")
: AsyncError(const MissingWarpConfigFailure(), StackTrace.current),
configGeneration: hasWarpConfig ? const AsyncValue.data("") : AsyncError(const MissingWarpConfigFailure(), StackTrace.current),
);
}
SharedPreferences get _prefs =>
ref.read(sharedPreferencesProvider).requireValue;
SharedPreferences get _prefs => ref.read(sharedPreferencesProvider).requireValue;
Future<void> agree() async {
await ref
.read(sharedPreferencesProvider)
.requireValue
.setBool(warpConsentGiven, true);
await ref.read(sharedPreferencesProvider).requireValue.setBool(warpConsentGiven, true);
state = state.copyWith(consentGiven: true);
await generateWarpConfig();
}
@@ -59,15 +53,33 @@ class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger {
.getOrElse((l) => throw l)
.run();
await ref
.read(ConfigOptions.warpAccountId.notifier)
.update(warp.accountId);
await ref
.read(ConfigOptions.warpAccessToken.notifier)
.update(warp.accessToken);
await ref
.read(ConfigOptions.warpWireguardConfig.notifier)
.update(warp.wireguardConfig);
await ref.read(ConfigOptions.warpAccountId.notifier).update(warp.accountId);
await ref.read(ConfigOptions.warpAccessToken.notifier).update(warp.accessToken);
await ref.read(ConfigOptions.warpWireguardConfig.notifier).update(warp.wireguardConfig);
return warp.log;
});
state = state.copyWith(configGeneration: result);
}
Future<void> generateWarp2Config() async {
if (state.configGeneration.isLoading) return;
state = state.copyWith(configGeneration: const AsyncLoading());
final result = await AsyncValue.guard(() async {
final warp = await ref
.read(singboxServiceProvider)
.generateWarpConfig(
licenseKey: ref.read(ConfigOptions.warpLicenseKey),
previousAccountId: ref.read(ConfigOptions.warp2AccountId),
previousAccessToken: ref.read(ConfigOptions.warp2AccessToken),
)
.getOrElse((l) => throw l)
.run();
await ref.read(ConfigOptions.warp2AccountId.notifier).update(warp.accountId);
await ref.read(ConfigOptions.warp2AccessToken.notifier).update(warp.accessToken);
await ref.read(ConfigOptions.warp2WireguardConfig.notifier).update(warp.wireguardConfig);
return warp.log;
});

View File

@@ -23,17 +23,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:humanizer/humanizer.dart';
enum ConfigOptionSection {
warp;
warp,
fragment;
static final _warpKey = GlobalKey(debugLabel: "warp-section-key");
static final _fragmentKey = GlobalKey(debugLabel: "fragment-section-key");
GlobalKey get key => switch (this) { _ => _warpKey };
GlobalKey get key => switch (this) {
ConfigOptionSection.warp => _warpKey,
ConfigOptionSection.fragment => _fragmentKey,
};
}
class ConfigOptionsPage extends HookConsumerWidget {
ConfigOptionsPage({super.key, String? section})
: section =
section != null ? ConfigOptionSection.values.byName(section) : null;
ConfigOptionsPage({super.key, String? section}) : section = section != null ? ConfigOptionSection.values.byName(section) : null;
final ConfigOptionSection? section;
@@ -47,14 +50,10 @@ class ConfigOptionsPage extends HookConsumerWidget {
if (section != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final box =
section!.key.currentContext?.findRenderObject() as RenderBox?;
final box = section!.key.currentContext?.findRenderObject() as RenderBox?;
final offset = box?.localToGlobal(Offset.zero);
if (offset == null) return;
final height = scrollController.offset +
offset.dy -
MediaQueryData.fromView(View.of(context)).padding.top -
kToolbarHeight;
final height = scrollController.offset + offset.dy - MediaQueryData.fromView(View.of(context)).padding.top - kToolbarHeight;
scrollController.animateTo(
height,
duration: const Duration(milliseconds: 500),
@@ -83,36 +82,26 @@ class ConfigOptionsPage extends HookConsumerWidget {
itemBuilder: (context) {
return [
PopupMenuItem(
onTap: () async => ref
.read(configOptionNotifierProvider.notifier)
.exportJsonToClipboard()
.then((success) {
onTap: () async => ref.read(configOptionNotifierProvider.notifier).exportJsonToClipboard().then((success) {
if (success) {
ref
.read(inAppNotificationControllerProvider)
.showSuccessToast(
ref.read(inAppNotificationControllerProvider).showSuccessToast(
t.general.clipboardExportSuccessMsg,
);
}
}),
child: Text(t.settings.exportOptions),
),
if (ref.watch(debugModeNotifierProvider))
PopupMenuItem(
onTap: () async => ref
.read(configOptionNotifierProvider.notifier)
.exportJsonToClipboard(excludePrivate: false)
.then((success) {
if (success) {
ref
.read(inAppNotificationControllerProvider)
.showSuccessToast(
t.general.clipboardExportSuccessMsg,
);
}
}),
child: Text(t.settings.exportAllOptions),
),
// if (ref.watch(debugModeNotifierProvider))
PopupMenuItem(
onTap: () async => ref.read(configOptionNotifierProvider.notifier).exportJsonToClipboard(excludePrivate: false).then((success) {
if (success) {
ref.read(inAppNotificationControllerProvider).showSuccessToast(
t.general.clipboardExportSuccessMsg,
);
}
}),
child: Text(t.settings.exportAllOptions),
),
PopupMenuItem(
onTap: () async {
final shouldImport = await showConfirmationDialog(
@@ -121,9 +110,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
message: t.settings.importOptionsMsg,
);
if (shouldImport) {
await ref
.read(configOptionNotifierProvider.notifier)
.importFromClipboard();
await ref.read(configOptionNotifierProvider.notifier).importFromClipboard();
}
},
child: Text(t.settings.importOptions),
@@ -131,9 +118,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
PopupMenuItem(
child: Text(t.config.resetBtn),
onTap: () async {
await ref
.read(configOptionNotifierProvider.notifier)
.resetOption();
await ref.read(configOptionNotifierProvider.notifier).resetOption();
},
),
];
@@ -158,15 +143,12 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile(
title: Text(experimental(t.config.bypassLan)),
value: ref.watch(ConfigOptions.bypassLan),
onChanged:
ref.watch(ConfigOptions.bypassLan.notifier).update,
onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update,
),
SwitchListTile(
title: Text(t.config.resolveDestination),
value: ref.watch(ConfigOptions.resolveDestination),
onChanged: ref
.watch(ConfigOptions.resolveDestination.notifier)
.update,
onChanged: ref.watch(ConfigOptions.resolveDestination.notifier).update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.ipv6Mode),
@@ -179,28 +161,24 @@ class ConfigOptionsPage extends HookConsumerWidget {
SettingsSection(t.config.section.dns),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.remoteDnsAddress),
preferences:
ref.watch(ConfigOptions.remoteDnsAddress.notifier),
preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier),
title: t.config.remoteDnsAddress,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy),
preferences: ref
.watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
preferences: ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
choices: DomainStrategy.values,
title: t.config.remoteDnsDomainStrategy,
presentChoice: (value) => value.displayName,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.directDnsAddress),
preferences:
ref.watch(ConfigOptions.directDnsAddress.notifier),
preferences: ref.watch(ConfigOptions.directDnsAddress.notifier),
title: t.config.directDnsAddress,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.directDnsDomainStrategy),
preferences: ref
.watch(ConfigOptions.directDnsDomainStrategy.notifier),
preferences: ref.watch(ConfigOptions.directDnsDomainStrategy.notifier),
choices: DomainStrategy.values,
title: t.config.directDnsDomainStrategy,
presentChoice: (value) => value.displayName,
@@ -208,9 +186,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile(
title: Text(t.config.enableDnsRouting),
value: ref.watch(ConfigOptions.enableDnsRouting),
onChanged: ref
.watch(ConfigOptions.enableDnsRouting.notifier)
.update,
onChanged: ref.watch(ConfigOptions.enableDnsRouting.notifier).update,
),
// const SettingsDivider(),
// SettingsSection(experimental(t.config.section.mux)),
@@ -247,13 +223,11 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile(
title: Text(t.config.strictRoute),
value: ref.watch(ConfigOptions.strictRoute),
onChanged:
ref.watch(ConfigOptions.strictRoute.notifier).update,
onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.tunImplementation),
preferences:
ref.watch(ConfigOptions.tunImplementation.notifier),
preferences: ref.watch(ConfigOptions.tunImplementation.notifier),
choices: TunImplementation.values,
title: t.config.tunImplementation,
presentChoice: (value) => value.name,
@@ -287,25 +261,21 @@ class ConfigOptionsPage extends HookConsumerWidget {
experimental(t.config.allowConnectionFromLan),
),
value: ref.watch(ConfigOptions.allowConnectionFromLan),
onChanged: ref
.read(ConfigOptions.allowConnectionFromLan.notifier)
.update,
onChanged: ref.read(ConfigOptions.allowConnectionFromLan.notifier).update,
),
const SettingsDivider(),
SettingsSection(
experimental(t.config.section.tlsTricks),
key: ConfigOptionSection._fragmentKey,
),
SwitchListTile(
title: Text(t.config.enableTlsFragment),
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged: ref
.watch(ConfigOptions.enableTlsFragment.notifier)
.update,
onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSize),
preferences:
ref.watch(ConfigOptions.tlsFragmentSize.notifier),
preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier),
title: t.config.tlsFragmentSize,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t),
@@ -313,8 +283,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSleep),
preferences:
ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
title: t.config.tlsFragmentSleep,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t),
@@ -323,21 +292,16 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile(
title: Text(t.config.enableTlsMixedSniCase),
value: ref.watch(ConfigOptions.enableTlsMixedSniCase),
onChanged: ref
.watch(ConfigOptions.enableTlsMixedSniCase.notifier)
.update,
onChanged: ref.watch(ConfigOptions.enableTlsMixedSniCase.notifier).update,
),
SwitchListTile(
title: Text(t.config.enableTlsPadding),
value: ref.watch(ConfigOptions.enableTlsPadding),
onChanged: ref
.watch(ConfigOptions.enableTlsPadding.notifier)
.update,
onChanged: ref.watch(ConfigOptions.enableTlsPadding.notifier).update,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsPaddingSize),
preferences:
ref.watch(ConfigOptions.tlsPaddingSize.notifier),
preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier),
title: t.config.tlsPaddingSize,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.format(),
@@ -350,38 +314,26 @@ class ConfigOptionsPage extends HookConsumerWidget {
SettingsSection(t.config.section.misc),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.connectionTestUrl),
preferences:
ref.watch(ConfigOptions.connectionTestUrl.notifier),
preferences: ref.watch(ConfigOptions.connectionTestUrl.notifier),
title: t.config.connectionTestUrl,
),
ListTile(
title: Text(t.config.urlTestInterval),
subtitle: Text(
ref
.watch(ConfigOptions.urlTestInterval)
.toApproximateTime(isRelativeToNow: false),
ref.watch(ConfigOptions.urlTestInterval).toApproximateTime(isRelativeToNow: false),
),
onTap: () async {
final urlTestInterval = await SettingsSliderDialog(
title: t.config.urlTestInterval,
initialValue: ref
.watch(ConfigOptions.urlTestInterval)
.inMinutes
.coerceIn(0, 60)
.toDouble(),
onReset: ref
.read(ConfigOptions.urlTestInterval.notifier)
.reset,
initialValue: ref.watch(ConfigOptions.urlTestInterval).inMinutes.coerceIn(0, 60).toDouble(),
onReset: ref.read(ConfigOptions.urlTestInterval.notifier).reset,
min: 1,
max: 60,
divisions: 60,
labelGen: (value) => Duration(minutes: value.toInt())
.toApproximateTime(isRelativeToNow: false),
labelGen: (value) => Duration(minutes: value.toInt()).toApproximateTime(isRelativeToNow: false),
).show(context);
if (urlTestInterval == null) return;
await ref
.read(ConfigOptions.urlTestInterval.notifier)
.update(Duration(minutes: urlTestInterval.toInt()));
await ref.read(ConfigOptions.urlTestInterval.notifier).update(Duration(minutes: urlTestInterval.toInt()));
},
),
ValuePreferenceWidget(

View File

@@ -63,17 +63,15 @@ class WarpOptionsTiles extends HookConsumerWidget {
AsyncLoading() => const LinearProgressIndicator(),
AsyncError() => Text(
t.config.missingWarpConfig,
style:
TextStyle(color: Theme.of(context).colorScheme.error),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
_ => null,
}
: null,
enabled: canChangeOptions,
onTap: () async {
await ref
.read(warpOptionNotifierProvider.notifier)
.generateWarpConfig();
await ref.read(warpOptionNotifierProvider.notifier).generateWarpConfig();
await ref.read(warpOptionNotifierProvider.notifier).generateWarp2Config();
},
),
ChoicePreferenceWidget(
@@ -111,8 +109,7 @@ class WarpOptionsTiles extends HookConsumerWidget {
preferences: ref.watch(ConfigOptions.warpNoise.notifier),
enabled: canChangeOptions,
title: t.config.warpNoise,
inputToValue: (input) =>
OptionalRange.tryParse(input, allowEmpty: true),
inputToValue: (input) => OptionalRange.tryParse(input, allowEmpty: true),
presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(),
),
@@ -121,8 +118,7 @@ class WarpOptionsTiles extends HookConsumerWidget {
preferences: ref.watch(ConfigOptions.warpNoiseDelay.notifier),
enabled: canChangeOptions,
title: t.config.warpNoiseDelay,
inputToValue: (input) =>
OptionalRange.tryParse(input, allowEmpty: true),
inputToValue: (input) => OptionalRange.tryParse(input, allowEmpty: true),
presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(),
),

View File

@@ -16,8 +16,7 @@ class QuickSettingsModal extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final warpPrefaceCompleted =
ref.watch(warpOptionNotifierProvider).consentGiven;
final warpPrefaceCompleted = ref.watch(warpOptionNotifierProvider).consentGiven;
return SingleChildScrollView(
child: Column(
@@ -33,37 +32,41 @@ class QuickSettingsModal extends HookConsumerWidget {
e.presentShort(t),
overflow: TextOverflow.ellipsis,
),
tooltip:
e.isExperimental ? t.settings.experimental : null,
tooltip: e.isExperimental ? t.settings.experimental : null,
),
)
.toList(),
selected: {ref.watch(ConfigOptions.serviceMode)},
onSelectionChanged: (newSet) => ref
.read(ConfigOptions.serviceMode.notifier)
.update(newSet.first),
onSelectionChanged: (newSet) => ref.read(ConfigOptions.serviceMode.notifier).update(newSet.first),
),
),
const Gap(8),
if (warpPrefaceCompleted)
SwitchListTile(
value: ref.watch(ConfigOptions.enableWarp),
onChanged: ref.watch(ConfigOptions.enableWarp.notifier).update,
title: Text(t.config.enableWarp),
GestureDetector(
onLongPress: () {
ConfigOptionsRoute(section: ConfigOptionSection.warp.name).go(context);
},
child: SwitchListTile(
value: ref.watch(ConfigOptions.enableWarp),
onChanged: ref.watch(ConfigOptions.enableWarp.notifier).update,
title: Text(t.config.enableWarp),
),
)
else
ListTile(
title: Text(t.config.setupWarp),
trailing: const Icon(FluentIcons.chevron_right_24_regular),
onTap: () =>
ConfigOptionsRoute(section: ConfigOptionSection.warp.name)
.go(context),
onTap: () => ConfigOptionsRoute(section: ConfigOptionSection.warp.name).go(context),
),
GestureDetector(
onLongPress: () {
ConfigOptionsRoute(section: ConfigOptionSection.fragment.name).go(context);
},
child: SwitchListTile(
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
title: Text(t.config.enableTlsFragment),
),
SwitchListTile(
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged:
ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
title: Text(t.config.enableTlsFragment),
),
// SwitchListTile(
// value: ref.watch(ConfigOptions.enableMux),

View File

@@ -5,11 +5,16 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/features/common/qr_code_scanner_screen.dart';
import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart';
import 'package:hiddify/features/config_option/overview/config_options_page.dart';
import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart';
import 'package:hiddify/features/profile/notifier/profile_notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AddProfileModal extends HookConsumerWidget {
const AddProfileModal({
@@ -17,7 +22,7 @@ class AddProfileModal extends HookConsumerWidget {
this.url,
this.scrollController,
});
static const warpConsentGiven = "warp_consent_given";
final String? url;
final ScrollController? scrollController;
@@ -58,8 +63,7 @@ class AddProfileModal extends HookConsumerWidget {
child: LayoutBuilder(
builder: (context, constraints) {
// temporary solution, aspect ratio widget relies on height and in a row there no height!
final buttonWidth =
constraints.maxWidth / 2 - (buttonsPadding + (buttonsGap / 2));
final buttonWidth = constraints.maxWidth / 2 - (buttonsPadding + (buttonsGap / 2));
return AnimatedCrossFade(
firstChild: SizedBox(
@@ -93,8 +97,7 @@ class AddProfileModal extends HookConsumerWidget {
secondChild: Column(
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: buttonsPadding),
padding: const EdgeInsets.symmetric(horizontal: buttonsPadding),
child: Row(
children: [
_Button(
@@ -103,13 +106,9 @@ class AddProfileModal extends HookConsumerWidget {
icon: FluentIcons.clipboard_paste_24_regular,
size: buttonWidth,
onTap: () async {
final captureResult =
await Clipboard.getData(Clipboard.kTextPlain)
.then((value) => value?.text ?? '');
final captureResult = await Clipboard.getData(Clipboard.kTextPlain).then((value) => value?.text ?? '');
if (addProfileState.isLoading) return;
ref
.read(addProfileProvider.notifier)
.add(captureResult);
ref.read(addProfileProvider.notifier).add(captureResult);
},
),
const Gap(buttonsGap),
@@ -120,8 +119,7 @@ class AddProfileModal extends HookConsumerWidget {
icon: FluentIcons.qr_code_24_regular,
size: buttonWidth,
onTap: () async {
final cr =
await QRCodeScannerScreen().open(context);
final cr = await QRCodeScannerScreen().open(context);
if (cr == null) return;
if (addProfileState.isLoading) return;
@@ -142,56 +140,118 @@ class AddProfileModal extends HookConsumerWidget {
],
),
),
if (!PlatformUtils.isDesktop)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: buttonsPadding,
vertical: 16,
),
child: Semantics(
button: true,
child: SizedBox(
height: 36,
child: Material(
key: const ValueKey("add_manually_button"),
elevation: 8,
color: theme.colorScheme.surface,
surfaceTintColor: theme.colorScheme.surfaceTint,
shadowColor: Colors.transparent,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () async {
context.pop();
await const NewProfileRoute().push(context);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
FluentIcons.add_24_regular,
color: theme.colorScheme.primary,
),
const Gap(8),
Text(
t.profile.add.manually,
style: theme.textTheme.labelLarge?.copyWith(
Padding(
padding: const EdgeInsets.symmetric(
horizontal: buttonsPadding,
vertical: 16,
),
child: Column(
children: [
Semantics(
button: true,
child: SizedBox(
height: 36,
child: Material(
key: const ValueKey("add_warp_button"),
elevation: 8,
color: theme.colorScheme.surface,
surfaceTintColor: theme.colorScheme.surfaceTint,
shadowColor: Colors.transparent,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () async {
Future.microtask(() async {
context.pop();
final _prefs = ref.read(sharedPreferencesProvider).requireValue;
final consent = _prefs.getBool(warpConsentGiven) ?? false;
if (!consent) {
final agreed = await showDialog<bool>(
context: context,
builder: (context) => const WarpLicenseAgreementModal(),
);
if (agreed ?? false) {
await ref.read(warpOptionNotifierProvider.notifier).agree();
}
}
final accountId = _prefs.getString("warp2-account-id");
final accessToken = _prefs.getString("warp2-access-token");
final hasWarp2Config = accountId != null && accessToken != null;
if (!hasWarp2Config) {
await ref.read(warpOptionNotifierProvider.notifier).generateWarp2Config();
}
await ref.read(addProfileProvider.notifier).add("#profile-title: Hiddify WARP\nwarp://p2@auto#Remote&&detour=warp://p1@auto#Local"); //
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
FluentIcons.add_24_regular,
color: theme.colorScheme.primary,
),
),
],
const SizedBox(width: 8),
Text(
t.profile.add.addWarp,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.primary,
),
),
],
),
),
),
),
),
),
if (!PlatformUtils.isDesktop) const SizedBox(height: 16), // Spacing between the buttons
if (!PlatformUtils.isDesktop)
Semantics(
button: true,
child: SizedBox(
height: 36,
child: Material(
key: const ValueKey("add_manually_button"),
elevation: 8,
color: theme.colorScheme.surface,
surfaceTintColor: theme.colorScheme.surfaceTint,
shadowColor: Colors.transparent,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () async {
context.pop();
await const NewProfileRoute().push(context);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
FluentIcons.add_24_regular,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Text(
t.profile.add.manually,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.primary,
),
),
],
),
),
),
),
),
],
),
),
const Gap(24),
],
),
crossFadeState: addProfileState.isLoading
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
crossFadeState: addProfileState.isLoading ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: const Duration(milliseconds: 250),
);
},

View File

@@ -47,8 +47,7 @@ class AddProfile extends _$AddProfile with AppLogger {
return const AsyncData(null);
}
ProfileRepository get _profilesRepo =>
ref.read(profileRepositoryProvider).requireValue;
ProfileRepository get _profilesRepo => ref.read(profileRepositoryProvider).requireValue;
CancelToken? _cancelToken;
Future<void> add(String rawInput) async {
@@ -57,8 +56,7 @@ class AddProfile extends _$AddProfile with AppLogger {
state = await AsyncValue.guard(
() async {
final activeProfile = await ref.read(activeProfileProvider.future);
final markAsActive =
activeProfile == null || ref.read(Preferences.markNewProfileActive);
final markAsActive = activeProfile == null || ref.read(Preferences.markNewProfileActive);
final TaskEither<ProfileFailure, Unit> task;
if (LinkParser.parse(rawInput) case (final link)?) {
loggy.debug("adding profile, url: [${link.url}]");
@@ -70,7 +68,11 @@ class AddProfile extends _$AddProfile with AppLogger {
} else if (LinkParser.protocol(rawInput) case (final parsed)?) {
loggy.debug("adding profile, content");
var name = parsed.name;
var oldItem = await _profilesRepo.getByName(name);
if (name == "Hiddify WARP" && oldItem != null) {
_profilesRepo.setAsActive(oldItem.id).run();
return unit;
}
while (await _profilesRepo.getByName(name) != null) {
name += '${randomInt(0, 9).run()}';
}
@@ -122,8 +124,7 @@ class UpdateProfile extends _$UpdateProfile with AppLogger {
return const AsyncData(null);
}
ProfileRepository get _profilesRepo =>
ref.read(profileRepositoryProvider).requireValue;
ProfileRepository get _profilesRepo => ref.read(profileRepositoryProvider).requireValue;
Future<void> updateProfile(RemoteProfileEntity profile) async {
if (state.isLoading) return;
@@ -143,9 +144,7 @@ class UpdateProfile extends _$UpdateProfile with AppLogger {
await ref.read(activeProfileProvider.future).then((active) async {
if (active != null && active.id == profile.id) {
await ref
.read(connectionNotifierProvider.notifier)
.reconnect(profile);
await ref.read(connectionNotifierProvider.notifier).reconnect(profile);
}
});
return unit;

View File

@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:combine/combine.dart';
import 'package:dartx/dartx.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/haptic/haptic_service.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart';
@@ -85,46 +85,41 @@ class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger {
List<ProxyGroupEntity> proxies,
ProxiesSort sortBy,
) async {
return CombineWorker().execute(
() {
final groupWithSelected = {
for (final o in proxies) o.tag: o.selected,
};
final sortedProxies = <ProxyGroupEntity>[];
for (final group in proxies) {
final sortedItems = switch (sortBy) {
ProxiesSort.name => group.items.sortedWith((a, b) {
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
return a.tag.compareTo(b.tag);
}),
ProxiesSort.delay => group.items.sortedWith((a, b) {
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
final groupWithSelected = {
for (final o in proxies) o.tag: o.selected,
};
final sortedProxies = <ProxyGroupEntity>[];
for (final group in proxies) {
final sortedItems = switch (sortBy) {
ProxiesSort.name => group.items.sortedWith((a, b) {
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
return a.tag.compareTo(b.tag);
}),
ProxiesSort.delay => group.items.sortedWith((a, b) {
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
final ai = a.urlTestDelay;
final bi = b.urlTestDelay;
if (ai == 0 && bi == 0) return -1;
if (ai == 0 && bi > 0) return 1;
if (ai > 0 && bi == 0) return -1;
return ai.compareTo(bi);
}),
ProxiesSort.unsorted => group.items,
};
final items = <ProxyItemEntity>[];
for (final item in sortedItems) {
if (groupWithSelected.keys.contains(item.tag)) {
items
.add(item.copyWith(selectedTag: groupWithSelected[item.tag]));
} else {
items.add(item);
}
}
sortedProxies.add(group.copyWith(items: items));
final ai = a.urlTestDelay;
final bi = b.urlTestDelay;
if (ai == 0 && bi == 0) return -1;
if (ai == 0 && bi > 0) return 1;
if (ai > 0 && bi == 0) return -1;
return ai.compareTo(bi);
}),
ProxiesSort.unsorted => group.items,
};
final items = <ProxyItemEntity>[];
for (final item in sortedItems) {
if (groupWithSelected.keys.contains(item.tag)) {
items.add(item.copyWith(selectedTag: groupWithSelected[item.tag]));
} else {
items.add(item);
}
return sortedProxies;
},
);
}
sortedProxies.add(group.copyWith(items: items));
}
return sortedProxies;
}
Future<void> changeProxy(String groupTag, String outboundTag) async {

View File

@@ -48,6 +48,7 @@ class SingboxConfigOption with _$SingboxConfigOption {
required SingboxMuxOption mux,
required SingboxTlsTricks tlsTricks,
required SingboxWarpOption warp,
required SingboxWarpOption warp2,
}) = _SingboxConfigOption;
String format() {
@@ -55,8 +56,7 @@ class SingboxConfigOption with _$SingboxConfigOption {
return encoder.convert(toJson());
}
factory SingboxConfigOption.fromJson(Map<String, dynamic> json) =>
_$SingboxConfigOptionFromJson(json);
factory SingboxConfigOption.fromJson(Map<String, dynamic> json) => _$SingboxConfigOptionFromJson(json);
}
@freezed
@@ -75,8 +75,7 @@ class SingboxWarpOption with _$SingboxWarpOption {
@OptionalRangeJsonConverter() required OptionalRange noiseDelay,
}) = _SingboxWarpOption;
factory SingboxWarpOption.fromJson(Map<String, dynamic> json) =>
_$SingboxWarpOptionFromJson(json);
factory SingboxWarpOption.fromJson(Map<String, dynamic> json) => _$SingboxWarpOptionFromJson(json);
}
@freezed
@@ -89,8 +88,7 @@ class SingboxMuxOption with _$SingboxMuxOption {
required MuxProtocol protocol,
}) = _SingboxMuxOption;
factory SingboxMuxOption.fromJson(Map<String, dynamic> json) =>
_$SingboxMuxOptionFromJson(json);
factory SingboxMuxOption.fromJson(Map<String, dynamic> json) => _$SingboxMuxOptionFromJson(json);
}
@freezed
@@ -105,6 +103,5 @@ class SingboxTlsTricks with _$SingboxTlsTricks {
@OptionalRangeJsonConverter() required OptionalRange paddingSize,
}) = _SingboxTlsTricks;
factory SingboxTlsTricks.fromJson(Map<String, dynamic> json) =>
_$SingboxTlsTricksFromJson(json);
factory SingboxTlsTricks.fromJson(Map<String, dynamic> json) => _$SingboxTlsTricksFromJson(json);
}

View File

@@ -4,7 +4,6 @@ import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:combine/combine.dart';
import 'package:ffi/ffi.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/model/directories.dart';
@@ -52,10 +51,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
Future<void> init() async {
loggy.debug("initializing");
_statusReceiver = ReceivePort('service status receiver');
final source = _statusReceiver
.asBroadcastStream()
.map((event) => jsonDecode(event as String))
.map(SingboxStatus.fromEvent);
final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent);
_status = ValueConnectableStream.seeded(
source,
const SingboxStopped(),
@@ -69,8 +65,8 @@ class FFISingboxService with InfraLogger implements SingboxService {
) {
final port = _statusReceiver.sendPort.nativePort;
return TaskEither(
() => CombineWorker().execute(
() {
() => Future.microtask(
() async {
_box.setupOnce(NativeApi.initializeApiDLData);
final err = _box
.setup(
@@ -98,8 +94,8 @@ class FFISingboxService with InfraLogger implements SingboxService {
bool debug,
) {
return TaskEither(
() => CombineWorker().execute(
() {
() => Future.microtask(
() async {
final err = _box
.parse(
path.toNativeUtf8().cast(),
@@ -120,13 +116,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
return TaskEither(
() => CombineWorker().execute(
() {
() => Future.microtask(
() async {
final json = jsonEncode(options.toJson());
final err = _box
.changeConfigOptions(json.toNativeUtf8().cast())
.cast<Utf8>()
.toDartString();
final err = _box.changeConfigOptions(json.toNativeUtf8().cast()).cast<Utf8>().toDartString();
if (err.isNotEmpty) {
return left(err);
}
@@ -141,8 +134,8 @@ class FFISingboxService with InfraLogger implements SingboxService {
String path,
) {
return TaskEither(
() => CombineWorker().execute(
() {
() => Future.microtask(
() async {
final response = _box
.generateConfig(
path.toNativeUtf8().cast(),
@@ -166,8 +159,8 @@ class FFISingboxService with InfraLogger implements SingboxService {
) {
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
return TaskEither(
() => CombineWorker().execute(
() {
() => Future.microtask(
() async {
final err = _box
.start(
configPath.toNativeUtf8().cast(),
@@ -187,8 +180,8 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override
TaskEither<String, Unit> stop() {
return TaskEither(
() => CombineWorker().execute(
() {
() => Future.microtask(
() async {
final err = _box.stop().cast<Utf8>().toDartString();
if (err.isNotEmpty) {
return left(err);
@@ -207,8 +200,8 @@ class FFISingboxService with InfraLogger implements SingboxService {
) {
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
return TaskEither(
() => CombineWorker().execute(
() {
() => Future.microtask(
() async {
final err = _box
.restart(
configPath.toNativeUtf8().cast(),
@@ -265,10 +258,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
},
);
final err = _box
.startCommandClient(1, receiver.sendPort.nativePort)
.cast<Utf8>()
.toDartString();
final err = _box.startCommandClient(1, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
if (err.isNotEmpty) {
loggy.error("error starting status command: $err");
throw err;
@@ -310,10 +300,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
);
try {
final err = _box
.startCommandClient(5, receiver.sendPort.nativePort)
.cast<Utf8>()
.toDartString();
final err = _box.startCommandClient(5, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
if (err.isNotEmpty) {
logger.error("error starting group command: $err");
throw err;
@@ -357,10 +344,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
);
try {
final err = _box
.startCommandClient(13, receiver.sendPort.nativePort)
.cast<Utf8>()
.toDartString();
final err = _box.startCommandClient(13, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
if (err.isNotEmpty) {
logger.error("error starting: $err");
throw err;
@@ -376,8 +360,8 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
return TaskEither(
() => CombineWorker().execute(
() {
() => Future.microtask(
() async {
final err = _box
.selectOutbound(
groupTag.toNativeUtf8().cast(),
@@ -397,12 +381,9 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override
TaskEither<String, Unit> urlTest(String groupTag) {
return TaskEither(
() => CombineWorker().execute(
() {
final err = _box
.urlTest(groupTag.toNativeUtf8().cast())
.cast<Utf8>()
.toDartString();
() => Future.microtask(
() async {
final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString();
if (err.isNotEmpty) {
return left(err);
}
@@ -418,9 +399,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override
Stream<List<String>> watchLogs(String path) async* {
yield await _readLogFile(File(path));
yield* Watcher(path, pollingDelay: const Duration(seconds: 1))
.events
.asyncMap((event) async {
yield* Watcher(path, pollingDelay: const Duration(seconds: 1)).events.asyncMap((event) async {
if (event.type == ChangeType.MODIFY) {
await _readLogFile(File(path));
}
@@ -431,17 +410,18 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override
TaskEither<String, Unit> clearLogs() {
return TaskEither(
() async {
_logBuffer.clear();
return right(unit);
},
() => Future.microtask(
() async {
_logBuffer.clear();
return right(unit);
},
),
);
}
Future<List<String>> _readLogFile(File file) async {
if (_logFilePosition == 0 && file.lengthSync() == 0) return [];
final content =
await file.openRead(_logFilePosition).transform(utf8.decoder).join();
final content = await file.openRead(_logFilePosition).transform(utf8.decoder).join();
_logFilePosition = file.lengthSync();
final lines = const LineSplitter().convert(content);
if (lines.length > 300) {
@@ -464,8 +444,8 @@ class FFISingboxService with InfraLogger implements SingboxService {
}) {
loggy.debug("generating warp config");
return TaskEither(
() => CombineWorker().execute(
() {
() => Future.microtask(
() async {
final response = _box
.generateWarpConfig(
licenseKey.toNativeUtf8().cast(),

Submodule libcore updated: b265886306...bc48ec07a8

View File

@@ -2,6 +2,7 @@
"$schema": "https://inlang.com/schema/project-settings",
"sourceLanguageTag": "en",
"languageTags": [
"ar",
"en",
"ckb-KUR",
"es",

View File

@@ -218,14 +218,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
combine:
dependency: "direct main"
description:
name: combine
sha256: "8b52083c822a614a448fdd307e78c05266080e9747604b61fca5ddfe736a6c1e"
url: "https://pub.dev"
source: hosted
version: "0.5.7-0.1.pre"
convert:
dependency: transitive
description:
@@ -801,11 +793,12 @@ packages:
humanizer:
dependency: "direct main"
description:
name: humanizer
sha256: "08728a4b6d62accd7d09e668bd54e81e6e09a82c8cfda30553224b3eb868d4f2"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path: "."
ref: up-version
resolved-ref: "8ae61d68357fae197be7ee71d67ccb9498b9d5c7"
url: "https://github.com/alex-relov/humanizer"
source: git
version: "2.3.0"
iconsax_flutter:
dependency: transitive
description:
@@ -850,10 +843,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.19.0"
io:
dependency: transitive
description:
@@ -922,26 +915,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.1"
lint:
dependency: "direct dev"
description:
@@ -1002,10 +995,10 @@ packages:
dependency: "direct main"
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.12.0"
mime:
dependency: transitive
description:
@@ -1703,26 +1696,26 @@ packages:
dependency: transitive
description:
name: test
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
url: "https://pub.dev"
source: hosted
version: "1.24.9"
version: "1.25.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
version: "0.7.0"
test_core:
dependency: transitive
description:
name: test_core
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
url: "https://pub.dev"
source: hosted
version: "0.5.9"
version: "0.6.0"
time:
dependency: transitive
description:
@@ -1935,10 +1928,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
version: "14.2.1"
watcher:
dependency: "direct main"
description:

View File

@@ -12,7 +12,7 @@ dependencies:
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.6
intl: ^0.18.1
intl: ^0.19.0
slang: ^3.30.1
slang_flutter: ^3.30.0
fpdart: ^1.1.0
@@ -40,7 +40,7 @@ dependencies:
launch_at_startup: ^0.2.2
sentry_flutter: ^7.16.1
sentry_dart_plugin: ^1.7.1
combine: ^0.5.7-0.1.pre
path: ^1.8.3
loggy: ^2.0.3
flutter_loggy: ^2.0.2
@@ -58,7 +58,11 @@ dependencies:
percent_indicator: ^4.2.3
sliver_tools: ^0.2.12
flutter_adaptive_scaffold: ^0.1.8
humanizer: ^2.2.0
# humanizer: ^2.2.0
humanizer:
git:
url: https://github.com/alex-relov/humanizer
ref: up-version
upgrader: ^9.0.0
toastification: ^1.2.1
version: ^3.0.2

View File

@@ -5,9 +5,10 @@ display_name: Hiddify
executable_name: Hiddify.exe
output_base_file_name: Hiddify.exe
create_desktop_icon: true
install_dir_name: "{autopf64}\\hiddify"
install_dir_name: "{autopf64}\\Hiddify"
setup_icon_file: ..\..\windows\runner\resources\app_icon.ico
locales:
- ar
- en
- fa
- ru