Merge branch 'main' of hiddify-github:hiddify/hiddify-next

This commit is contained in:
Hiddify
2023-10-24 11:26:57 +02:00
parent 45d3243d9e
commit aa946deebd
38 changed files with 668 additions and 1686 deletions

20
CHANGELOG.md Normal file
View File

@@ -0,0 +1,20 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Basic region based routing rules
- Russian region
- Logs flow control
### Changed
- Theme preferences
- Logs page
### Fixed
- Localization mistakes in Russian from [solokot](https://github.com/solokot)
- Localization mistakes in Russian from [Elshad Guseynov](https://github.com/lifeindarkside)
- Logs filtering

View File

@@ -2,6 +2,9 @@
# Attribution-NonCommercial-ShareAlike 4.0 International
## Summary:
- The forks of the app are not allowed to be listed on F-Droid or other app stores under the original name or original design.
- Any forks should be published open-source under the same license.
- You need prior consent to publish a fork or use any part of this code in an application published in Apple Store.
- You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

View File

@@ -119,9 +119,8 @@ release: # Create a new tag for release.
echo "version: $${VERSION_STR}+$${BUILD_NUMBER}" && \
sed -i "s/version: .*/version: $${VERSION_STR}\+$${BUILD_NUMBER}/g" pubspec.yaml && \
git tag $${TAG} > /dev/null && \
gitchangelog > changelog.md || { git tag -d $${TAG}; echo "Please run pip install git gitchangelog pystache mustache markdown"; exit 2; } && \
git tag -d $${TAG} > /dev/null && \
git add pubspec.yaml changelog.md && \
git add pubspec.yaml CHANGELOG.md && \
make sync_translate && \
git add assets/translations/* && \
git commit -m "release: version $${TAG}" && \

View File

@@ -13,11 +13,6 @@
</div>
<div align=center>
<img width=90% alt="English Demo" src="https://github.com/hiddify/hiddify-next/assets/125398461/ffe5346d-3404-470f-b5e0-4364e23743d2">
</div>
## What is Hiddify-Next?
@@ -25,14 +20,11 @@
The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information you can read through our [Contribution Guidelines](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) for development.
## Improve Translations
You can easily contribute to this project by using the following links to improve the translations:
- [English](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en)
- [Persian](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=fa)
- [Russian](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=ru)
- [Chinese](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=zh)
<div align=center>
<img width=90% alt="English Demo" src="https://github.com/hiddify/hiddify-next/assets/125398461/ffe5346d-3404-470f-b5e0-4364e23743d2">
</div>
## 🚀 Main features
@@ -80,9 +72,9 @@ You can easily contribute to this project by using the following links to improv
<td>Android</td><td>
<a href="https://play.google.com/store/apps/details?id=app.hiddify.com"><img width=150px src="https://github.com/hiddify/hiddify-next/blob/main/docs/google-play-badge.png"></a><br>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-android-universal.apk"><img src="https://img.shields.io/badge/APK-Universal-044d29.svg?logo=github"></a><br>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-android-arm64.apk"><img src="https://img.shields.io/badge/APK-ArmV8-168039.svg?logo=github"></a><br>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-android-arm7.apk"><img src="https://img.shields.io/badge/APK-ArmV7-45bf55.svg?logo=github"></a><br>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-android-x86_64.apk"><img src="https://img.shields.io/badge/APK-x86_64-96ed89.svg?logo=github"></a>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-android-arm64.apk"><img src="https://img.shields.io/badge/APK-ARMv8-168039.svg?logo=github"></a><br>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-android-arm7.apk"><img src="https://img.shields.io/badge/APK-ARMv7-45bf55.svg?logo=github"></a><br>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-android-x86_64.apk"><img src="https://img.shields.io/badge/APK-x64-96ed89.svg?logo=github"></a>
</td>
</tr>
<tr>
@@ -97,7 +89,7 @@ You can easily contribute to this project by using the following links to improv
</tr>
<tr>
<td>Linux</td>
<td><a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-linux-x64.zip"><img src="https://img.shields.io/badge/AppImage-amd64-f84e29.svg?logo=github"> </a></td>
<td><a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-linux-x64.zip"><img src="https://img.shields.io/badge/AppImage-x64-f84e29.svg?logo=github"> </a></td>
</tr>
</tbody>
</table>
@@ -108,6 +100,15 @@ You can easily contribute to this project by using the following links to improv
## Installation and tutorials
Please find tutorial information on the [wiki page](https://github.com/hiddify/hiddify-next/wiki).
## Improve Translations
You can easily contribute to this project by using the following links to improve the translations:
- [English](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en)
- [Persian](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=fa)
- [Russian](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=ru)
- [Chinese](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=zh)
## Acknowledgements
- [Sing-box](https://github.com/SagerNet/sing-box)

View File

@@ -13,7 +13,7 @@
</div>
## 什么是 Hiddify-Next
## Hiddify-Next 是什么
基于 [Sing-box](https://github.com/SagerNet/sing-box) 的多平台客户端,用作通用代理工具链。 该应用程序提供了广泛的功能,如下所列。 它还支持大量协议。 该应用程序免费使用、无广告且开源。 它提供了一个安全且私密的工具来访问免费互联网。
该应用程序是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 欲了解更多信息,您可以阅读我们的开发贡献指南。
@@ -31,9 +31,9 @@
🔍 基于延迟的自动选择
🟡 广泛的协议ECH、Sing-box、V2ray、Xray、Vless、Vmess、Reality、TUIC、Hysteria、ShadowTLS、SSH、Clash、Clash meta
🟡 广泛的协议支持ECH、Sing-box、V2ray、Xray、Vless、Vmess、Trojan、Trojan with websocket、Reality、TUIC、Hysteria、Hysteria2、ShadowTLS、SSH、Clash、Clash meta
🟡 订阅链接Clash、Clash meta、Sing-box 和 Shadowsocks
🟡 支持多种订阅链接导入Clash、Clash meta、Sing-box 和 Shadowsocks
🔄 自动订阅更新
@@ -45,9 +45,9 @@
🌙 深色和浅色模式
⚙ 与所有代理管理面板兼容
⚙ 与所有代理管理面板的节点兼容
⭐ 适伊朗、中国、俄罗斯等国家配置
⭐ 适用于伊朗、中国、俄罗斯等国家配置
📱 可在 Google Play 上获取
@@ -63,7 +63,7 @@
</thead>
<tbody align=left>
<tr>
<td>安卓</td><td>
<td>Android</td><td>
<a href="https://play.google.com/store/apps/details?id=app.hiddify.com"><img width=150px src="https://github.com/hiddify/hiddify-next/blob/main/docs/google-play-badge.png"></a><br>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-android-universal.apk"><img src="https://img.shields.io/badge/APK-Universal-044d29.svg?logo=github"></a><br>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-android-arm64.apk"><img src="https://img.shields.io/badge/APK-ArmV8-168039.svg?logo=github"></a><br>
@@ -72,13 +72,13 @@
</td>
</tr>
<tr>
<td>视窗</td>
<td>Windows</td>
<td><a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-windows-x64-setup.zip"><img src="https://img.shields.io/badge/Setup-x64-0078d7.svg?logo=github"></a><br>
<a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-windows-x64-portable.zip"><img src="https://img.shields.io/badge/Portable-x64-2d7d9a.svg?logo=github"></a>
</td>
</tr>
<tr>
<td>苹果系统</td>
<td>macOS</td>
<td><a href="https://github.com/hiddify/hiddify-next/releases/latest/download/hiddify-macos-universal.zip"><img src="https://img.shields.io/badge/DMG-Universal-ea005e.svg?logo=github"></a></td>
</tr>
<tr>
@@ -95,16 +95,16 @@
## 致谢
- [Sing-box](https://github.com/SagerNet/sing-box)
- [Sing-box 适用于安卓](https://github.com/SagerNet/sing-box-for-android)
- [Sing-box for Android](https://github.com/SagerNet/sing-box-for-android)
- [Clash](https://github.com/Dreamacro/clash)
- [Clash Meta](https://github.com/MetaCubeX/Clash.Meta)
- [FClash](https://github.com/Fclash/Fclash)
- [其他](./pubspec.yaml)
- [其他](./pubspec.yaml)
## 捐赠与支持
支持我们的最简单方法是点击本页顶部的星号 (⭐)
支持我们的最简单方法是单击此页面顶部的 Star (⭐)
我们的服务需要财政支持。 我们所有的活动都是自愿进行的,财政支持将用于项目的开发。 您可以[此处](https://github.com/hiddify/hiddify-server/wiki/support)查看我们的支持地址
我们的服务需要资金支持。我们所有的活动都是自愿进行的,资金支持将用于项目的开发和维护。您可以[此处](https://github.com/hiddify/hiddify-manager/wiki/support) 查看我们的支持地址
<div align=center>
@@ -119,7 +119,7 @@
</div>
<p align=center>
我们感谢所有参与该项目的人。 这里有一些人,还有 Github 之外的更多人。 这对我们来说意义重大。 ♥
感谢所有参与该项目的人。包括以下列出的人,和更多其他来自 Github 的人。你们对我们的意义非常重大。 ♥ </p>
</p>
<p align=center>
@@ -128,7 +128,7 @@
</a>
</p>
<p align=center>
制作与 <a rel="" target="_blank" href="https://contrib.rocks">Contrib.Rocks</a>
使用 <a rel="" target="_blank" href="https://contrib.rocks">Contrib.Rocks</a> 制作
</p>

View File

@@ -1,5 +1,6 @@
package com.hiddify.hiddify
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
@@ -18,13 +19,15 @@ class LogHandler : FlutterPlugin {
logsChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
MainActivity.instance.serviceLogs.observeForever {
if (it == null) return@observeForever
events?.success(it)
val activity = MainActivity.instance
events?.success(activity.logList)
activity.logCallback = {
events?.success(activity.logList)
}
}
override fun onCancel(arguments: Any?) {
MainActivity.instance.logCallback = null
}
})
}

View File

@@ -39,7 +39,6 @@ class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
var logCallback: ((Boolean) -> Unit)? = null
val serviceStatus = MutableLiveData(Status.Stopped)
val serviceAlerts = MutableLiveData<ServiceEvent?>(null)
val serviceLogs = MutableLiveData<String?>(null)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
@@ -102,37 +101,18 @@ class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
serviceAlerts.postValue(ServiceEvent(Status.Stopped, type, message))
}
private var paused = false
override fun onPause() {
super.onPause()
paused = true
}
override fun onResume() {
super.onResume()
paused = false
logCallback?.invoke(true)
}
override fun onServiceWriteLog(message: String?) {
if (paused) {
if (logList.size > 300) {
logList.removeFirst()
}
if (logList.size > 300) {
logList.removeFirst()
}
logList.addLast(message)
if (!paused) {
logCallback?.invoke(false)
serviceLogs.postValue(message)
}
logCallback?.invoke(false)
}
override fun onServiceResetLogs(messages: MutableList<String>) {
logList.clear()
logList.addAll(messages)
if (!paused) logCallback?.invoke(true)
logCallback?.invoke(true)
}
override fun onDestroy() {

View File

@@ -29,6 +29,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
Restart("restart"),
SelectOutbound("select_outbound"),
UrlTest("url_test"),
ClearLogs("clear_logs"),
}
}
@@ -63,38 +64,44 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
}
Trigger.ChangeConfigOptions.method -> {
result.runCatching {
val args = call.arguments as String
Settings.configOptions = args
success(true)
scope.launch {
result.runCatching {
val args = call.arguments as String
Settings.configOptions = args
success(true)
}
}
}
Trigger.Start.method -> {
result.runCatching {
val args = call.arguments as Map<*, *>
Settings.activeConfigPath = args["path"] as String? ?: ""
val mainActivity = MainActivity.instance
val started = mainActivity.serviceStatus.value == Status.Started
if (started) {
Log.w(TAG, "service is already running")
return success(true)
scope.launch {
result.runCatching {
val args = call.arguments as Map<*, *>
Settings.activeConfigPath = args["path"] as String? ?: ""
val mainActivity = MainActivity.instance
val started = mainActivity.serviceStatus.value == Status.Started
if (started) {
Log.w(TAG, "service is already running")
return@launch success(true)
}
mainActivity.startService()
success(true)
}
mainActivity.startService()
success(true)
}
}
Trigger.Stop.method -> {
result.runCatching {
val mainActivity = MainActivity.instance
val started = mainActivity.serviceStatus.value == Status.Started
if (!started) {
Log.w(TAG, "service is not running")
return success(true)
scope.launch {
result.runCatching {
val mainActivity = MainActivity.instance
val started = mainActivity.serviceStatus.value == Status.Started
if (!started) {
Log.w(TAG, "service is not running")
return@launch success(true)
}
BoxService.stop()
success(true)
}
BoxService.stop()
success(true)
}
}
@@ -151,6 +158,15 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
}
}
Trigger.ClearLogs.method -> {
scope.launch {
result.runCatching {
MainActivity.instance.onServiceResetLogs(mutableListOf())
success(true)
}
}
}
else -> result.notImplemented()
}
}

View File

@@ -46,7 +46,7 @@
"remainingDuration": "${duration} Days Remaining",
"remainingTrafficSemanticLabel": "${consumed} of ${total} traffic consumed.",
"expired": "Expired",
"noTraffic": "No more traffic"
"noTraffic": "Out of Quota"
},
"sortBy": {
"lastUpdate": "Recently updated",
@@ -106,11 +106,13 @@
},
"logs": {
"pageTitle": "Logs",
"clearLogsButtonText": "Clear Logs",
"filterHint": "Filter",
"allLevelsFilter": "All",
"shareCoreLogs": "Share Core Logs",
"shareAppLogs": "Share App logs"
"shareAppLogs": "Share App logs",
"pauseTooltip": "Pause",
"resumeTooltip": "Resume",
"clearTooltip": "Clear"
},
"settings": {
"pageTitle": "Settings",
@@ -123,17 +125,18 @@
"regions": {
"ir": "Iran (ir)",
"cn": "China (cn)",
"ru": "Russia (ru)",
"other": "Other"
},
"themeMode": "Theme Mode",
"themeModes": {
"system": "Follow system theme",
"dark": "Dark mode",
"light": "Light mode"
"light": "Light mode",
"black": "Black mode"
},
"enableAnalytics": "Enable Analytics",
"enableAnalyticsMsg": "Give permission to collect analytics and send crash reports to improve the app",
"trueBlack": "Pure Black",
"autoStart": "Start on Boot",
"silentStart": "Silent Start",
"openWorkingDir": "Open Working Directory",
@@ -257,4 +260,4 @@
"short_description": "Auto, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
"full_description": "The key goal of HiddifyNext is to provide a secure, user-friendly and efficient tunneling client. It enables you to route all traffic or selected app traffic to a remote server of your choose, utilizing VPN-Service permission.\n\nNote: We do not provide any server; users are required to ensure their online activities stay private by using use their own self-hosted server or trusted servers. \n \nWe support servers with:\n- Normal V2ray/Xray Subscription Link\n- Clash Subscription Link\n- Sing-Box Subscription Link\n\nWhat is our unique features?\n - User Friendly\n - Optimized and Fast\n - Automatically select LowestPing \n - Show user usage information\n - Easily import sublink by one click using deeplinking \n - Free and No ADS\n - Easily switch user sublinks\n - more and more\n\nSupport:\n- All Protocols supported by Sing-Box \n- VLESS + xtls reality, vision\n- VMESS\n- Trojan\n- ShoadowSocks\n- Reality\n- V2ray\n- Hystria2\n- TUIC\n- SSH\n- ShadowTLS\n\n\nThe source code exist in https://github.com/hiddify/Hiddify-Next\nThe application core is based on open-source sing-box.\n\nPermission Description:\n- VPN Service: As the goal of this application is to provide a secure, user-friendly and efficient tunneling client, we need this permission to be able to route the traffic via tunnel to the remote server. \n- QUERY ALL PACKAGES: This permission is used to allow users to include or exclude specific applications for tunneling.\n- RECEIVE BOOT COMPLETED: This permission can be enabled or disabled from app settings to activate this application upon device boot.\n- POST NOTIFICATIONS: This permission is essential as we employ a foreground service to ensure the continuous operation of the VPN service.\n- This application is free from advertisements. The analytics and crash data only occurs with the explicit consent of the user in the first use of application."
}
}
}

View File

@@ -106,11 +106,13 @@
},
"logs": {
"pageTitle": "لاگ‌ها",
"clearLogsButtonText": "پاک‌سازی",
"filterHint": "فیلتر",
"allLevelsFilter": "همه",
"shareCoreLogs": "اشتراک‌گذاری لاگ هسته",
"shareAppLogs": "اشتراک‌گذاری لاگ برنامه"
"shareAppLogs": "اشتراک‌گذاری لاگ برنامه",
"pauseTooltip": "مکث",
"resumeTooltip": "از سرگیری",
"clearTooltip": "پاک‌سازی"
},
"settings": {
"pageTitle": "تنظیمات",
@@ -123,17 +125,18 @@
"regions": {
"ir": "ایران (ir)",
"cn": "چین (cn)",
"ru": "روسیه (ru)",
"other": "سایر"
},
"themeMode": "تم مود",
"themeModes": {
"system": "پیروی از تم دستگاه",
"dark": "تم تیره",
"light": "تم روشن"
"light": "تم روشن",
"black": "تم سیاه"
},
"enableAnalytics": "فعال‌سازی آنالیتیکز",
"enableAnalyticsMsg": "ارائه دسترسی آنالیز و گزارش خطا برای بهبود عملکرد برنامه",
"trueBlack": "کاملا سیاه",
"autoStart": "اجرا با روشن شدن سیستم",
"silentStart": "اجرای ساکت",
"openWorkingDir": "باز کردن دایرکتوری کاری",
@@ -257,4 +260,4 @@
"short_description": "Auto, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
"full_description": "هدف اصلی HiddifyNext ارائه یک کلاینت تونل زنی ایمن، کاربرپسند و کارآمد است. این به شما امکان می دهد تا با استفاده از مجوز VPN-Service، تمام ترافیک یا ترافیک برنامه انتخابی را به یک سرور راه دور مورد نظر خود هدایت کنید.\n\nتوجه: ما هیچ سروری ارائه نمی دهیم. کاربران موظفند با استفاده از سرورهای خود میزبان یا سرورهای مورد اعتماد، فعالیت‌های آنلاین خود را خصوصی نگه دارند.\n \nما از سرورهایی با موارد زیر پشتیبانی می کنیم:\n- لینک اشتراک V2ray/Xray معمولی\n- لینک اشتراک کلش\n- لینک اشتراک Sing-Box\n\nویژگی های منحصر به فرد ما چیست؟\n - کاربر پسند\n - بهینه و سریع\n - به طور خودکار LowestPing را انتخاب کنید\n - نمایش اطلاعات استفاده کاربر\n - به راحتی لینک فرعی را با یک کلیک با استفاده از دیپ لینک وارد کنید\n - رایگان و بدون تبلیغات\n - به راحتی پیوندهای فرعی کاربر را تغییر دهید\n - بیشتر و بیشتر\n\nحمایت کردن:\n- تمام پروتکل های پشتیبانی شده توسط Sing-Box\n- VLESS + xtls \n- VMESS\n- تروجان\n- ShoadowSocks\n- ریالیتی\n- V2ray\n- هیستریا 2\n- TUIC\n- SSH\n- ShadowTLS\n\n\nکد منبع در https://github.com/hiddify/Hiddify-Next وجود دارد\nهسته برنامه بر اساس sing-box منبع باز است.\n\nتوضیحات مجوز:\n- سرویس VPN: از آنجا که هدف این برنامه ارائه یک کلاینت تونل زنی ایمن، کاربر پسند و کارآمد است، ما به این مجوز نیاز داریم تا بتوانیم ترافیک را از طریق تونل به سرور راه دور هدایت کنیم.\n- QUERY ALL PACKAGES: این مجوز برای اجازه دادن به کاربران برای گنجاندن یا حذف برنامه های کاربردی خاص برای تونل زدن استفاده می شود.\n- دریافت بوت تکمیل شد: این مجوز را می توان از تنظیمات برنامه فعال یا غیرفعال کرد تا این برنامه پس از بوت شدن دستگاه فعال شود.\n- اعلان های ارسالی: این مجوز ضروری است زیرا ما از یک سرویس پیش زمینه برای اطمینان از عملکرد مداوم سرویس VPN استفاده می کنیم.\n- این برنامه بدون تبلیغات است. تجزیه و تحلیل و داده های اشکال فقط با رضایت صریح کاربر در اولین استفاده از برنامه اتفاق می افتد."
}
}
}

View File

@@ -1,6 +1,6 @@
{
"general": {
"appTitle": "HiddifyNext",
"appTitle": "Hiddify Next",
"reset": "Сброс",
"toggle": {
"enabled": "Включено",
@@ -30,8 +30,8 @@
"stats": {
"traffic": "Скорость",
"trafficTotal": "Трафик",
"uplink": "Входящий канал",
"downlink": "Исходящий канал"
"uplink": "Исходящий канал",
"downlink": "Входящий канал"
}
},
"profile": {
@@ -106,11 +106,13 @@
},
"logs": {
"pageTitle": "Журналы",
"clearLogsButtonText": "Очистить журналы",
"filterHint": "Фильтр",
"allLevelsFilter": "Все",
"shareCoreLogs": "Поделиться журналами ядра",
"shareAppLogs": "Поделиться журналами приложения"
"shareAppLogs": "Поделиться журналами приложения",
"pauseTooltip": "Приостановить",
"resumeTooltip": "Возобновить",
"clearTooltip": "Очистить"
},
"settings": {
"pageTitle": "Настройки",
@@ -123,17 +125,18 @@
"regions": {
"ir": "Иран (ir)",
"cn": "Китай (cn)",
"ru": "Россия (ru)",
"other": "Другой"
},
"themeMode": "Оформление",
"themeModes": {
"system": "Системная тема",
"dark": "Тёмная тема",
"light": "Светлая тема"
"light": "Светлая тема",
"black": "Чёрная тема"
},
"enableAnalytics": "Сбор аналитики",
"enableAnalyticsMsg": "Сбор аналитических данных и отправка отчётов о сбоях для улучшения приложения.",
"trueBlack": "Чистый чёрный цвет",
"autoStart": "Запуск при загрузке",
"silentStart": "Тихий запуск",
"openWorkingDir": "Открыть рабочую папку",
@@ -198,7 +201,7 @@
"pageTitle": "О программе",
"version": "Версия",
"sourceCode": "Исходный код",
"telegramChannel": "Telegram канал",
"telegramChannel": "Telegram-канал",
"checkForUpdate": "Проверка обновления",
"privacyPolicy": "Политика конфиденциальности",
"termsAndConditions": "Условия и положения"
@@ -209,7 +212,7 @@
"updateMsg": "Доступна новая версия @:general.appTitle. Обновить сейчас?",
"currentVersionLbl": "Текущая версия",
"newVersionLbl": "Новая версия",
"updateNowBtnTxt": "Обновить сейчас",
"updateNowBtnTxt": "Обновить",
"laterBtnTxt": "Позже",
"ignoreBtnTxt": "Пропустить"
},
@@ -255,6 +258,6 @@
"play": {
"title": "Hiddify Next (Preview)",
"short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
"full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPNсервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на SingBox\n\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• V2ray\n• Hystria2\n• TUIC\n• SSH\n• ShadowTLS\n\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.\nЯдро приложения основано на открытом исходном коде SingBox.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n- ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения."
"full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPNсервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на SingBox\n\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• V2ray\n• Hystria2\n• TUIC\n• SSH\n• ShadowTLS\n\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.\nЯдро приложения основано на открытом исходном коде SingBox.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения."
}
}
}

View File

@@ -106,11 +106,13 @@
},
"logs": {
"pageTitle": "日志",
"clearLogsButtonText": "清除日志",
"filterHint": "筛选",
"allLevelsFilter": "全部",
"shareCoreLogs": "分享核心日志",
"shareAppLogs": "分享日志"
"shareAppLogs": "分享日志",
"pauseTooltip": "暂停",
"resumeTooltip": "恢复",
"clearTooltip": "清除"
},
"settings": {
"pageTitle": "设置",
@@ -123,17 +125,18 @@
"regions": {
"ir": "伊朗 (ir)",
"cn": "中国 (cn)",
"ru": "俄罗斯 (ru)",
"other": "其他"
},
"themeMode": "主题模式",
"themeModes": {
"system": "遵循系统主题",
"dark": "深色模式",
"light": "灯光模式"
"light": "灯光模式",
"black": "黑色模式"
},
"enableAnalytics": "启用分析",
"enableAnalyticsMsg": "授予收集分析并发送崩溃报告以改进应用程序的权限",
"trueBlack": "纯黑",
"autoStart": "开机启动",
"silentStart": "无声启动",
"openWorkingDir": "打开工作目录",
@@ -257,4 +260,4 @@
"short_description": "自动SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
"full_description": "HiddifyNext 的主要目标是提供安全、用户友好且高效的隧道客户端。它使您能够利用 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\n支持\n- Sing-Box 支持的所有协议\n- VLESS + xtls 现实、愿景\n- VMESS\n- Trojan\n- ShoadowSocks\n- Reality\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\nVPN服务由于此应用程序的目标是提供安全、用户友好和高效的隧道客户端我们需要此权限以能够通过隧道将流量路由到远程服务器。\n查询所有包此权限用于允许用户包括或排除特定应用程序以进行隧道传输。\n接收启动完成此权限可以从应用程序设置中启用或禁用以在设备启动时激活此应用程序。\n发送通知此权限是必需的因为我们使用前台服务来确保VPN服务的持续运行。\n此应用程序没有广告。分析和崩溃数据仅在用户在首次使用应用程序时明确同意的情况下发生。"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ class AppView extends HookConsumerWidget with PresLogger {
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
debugShowCheckedModeBanner: false,
themeMode: theme.mode,
themeMode: theme.mode.flutterThemeMode,
theme: theme.light(),
darkTheme: theme.dark(),
title: Constants.appName,

View File

@@ -19,6 +19,5 @@ TranslationsEn translations(TranslationsRef ref) =>
@Riverpod(keepAlive: true)
AppTheme theme(ThemeRef ref) => AppTheme(
ref.watch(themeModeNotifierProvider),
ref.watch(trueBlackThemeNotifierProvider),
ref.watch(localeNotifierProvider).preferredFontFamily,
);

View File

@@ -1,16 +1,38 @@
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:hiddify/core/prefs/locale_prefs.dart';
enum AppThemeMode {
system,
light,
dark,
black;
String present(TranslationsEn t) => switch (this) {
system => t.settings.general.themeModes.system,
light => t.settings.general.themeModes.light,
dark => t.settings.general.themeModes.dark,
black => t.settings.general.themeModes.black,
};
ThemeMode get flutterThemeMode => switch (this) {
system => ThemeMode.system,
light => ThemeMode.light,
dark => ThemeMode.dark,
black => ThemeMode.dark,
};
bool get trueBlack => this == black;
}
// mostly exact copy of flex color scheme 7.1's fabulous 12 theme
class AppTheme {
AppTheme(
this.mode,
this.trueBlack,
this.fontFamily,
);
final ThemeMode mode;
final bool trueBlack;
final AppThemeMode mode;
final String fontFamily;
ThemeData light() {
@@ -81,7 +103,7 @@ class AppTheme {
useMaterial3: true,
swapLegacyOnMaterial3: true,
useMaterial3ErrorColors: true,
darkIsTrueBlack: trueBlack,
darkIsTrueBlack: mode.trueBlack,
surfaceMode: FlexSurfaceMode.highScaffoldLowSurface,
// blendLevel: 1,
subThemesData: const FlexSubThemesData(

View File

@@ -1,4 +1,4 @@
import 'package:flutter/material.dart';
import 'package:hiddify/core/prefs/app_theme.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/utils/pref_notifier.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -10,29 +10,15 @@ class ThemeModeNotifier extends _$ThemeModeNotifier {
late final _pref = Pref(
ref.watch(sharedPreferencesProvider),
"theme_mode",
ThemeMode.system,
mapFrom: ThemeMode.values.byName,
AppThemeMode.system,
mapFrom: AppThemeMode.values.byName,
mapTo: (value) => value.name,
);
@override
ThemeMode build() => _pref.getValue();
AppThemeMode build() => _pref.getValue();
Future<void> update(ThemeMode value) {
state = value;
return _pref.update(value);
}
}
@Riverpod(keepAlive: true)
class TrueBlackThemeNotifier extends _$TrueBlackThemeNotifier {
late final _pref =
Pref(ref.watch(sharedPreferencesProvider), "true_black_theme", false);
@override
bool build() => _pref.getValue();
Future<void> update(bool value) {
Future<void> update(AppThemeMode value) {
state = value;
return _pref.update(value);
}

View File

@@ -1,6 +1,6 @@
// ignore_for_file: avoid_manual_providers_as_generated_provider_dependency
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/singbox/config_options.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/utils/pref_notifier.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -67,6 +67,42 @@ final enableTunStore = PrefNotifier.provider("enable-tun", _default.enableTun);
final setSystemProxyStore =
PrefNotifier.provider("set-system-proxy", _default.setSystemProxy);
// HACK temporary
@riverpod
List<Rule> rules(RulesRef ref) => switch (ref.watch(regionNotifierProvider)) {
Region.ir => [
const Rule(
id: "id",
name: "name",
enabled: true,
domains: "domain:.ir",
ip: "geoip:ir",
outbound: RuleOutbound.bypass,
),
],
Region.cn => [
const Rule(
id: "id",
name: "name",
enabled: true,
domains: "domain:.cn,geosite:cn",
ip: "geoip:cn",
outbound: RuleOutbound.bypass,
),
],
Region.ru => [
const Rule(
id: "id",
name: "name",
enabled: true,
domains: "domain:.ru",
ip: "geoip:ru",
outbound: RuleOutbound.bypass,
),
],
_ => [],
};
@riverpod
ConfigOptions configOptions(ConfigOptionsRef ref) => ConfigOptions(
executeConfigAsIs:
@@ -88,4 +124,5 @@ ConfigOptions configOptions(ConfigOptionsRef ref) => ConfigOptions(
clashApiPort: ref.watch(clashApiPortStore),
enableTun: ref.watch(enableTunStore),
setSystemProxy: ref.watch(setSystemProxyStore),
rules: ref.watch(rulesProvider),
);

View File

@@ -179,10 +179,21 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
}
@override
Stream<Either<CoreServiceFailure, String>> watchLogs() {
return singbox
.watchLogs(filesEditor.coreLogsPath)
.handleExceptions(CoreServiceFailure.unexpected);
Stream<Either<CoreServiceFailure, List<String>>> watchLogs() {
return singbox.watchLogs(filesEditor.coreLogsPath).handleExceptions(
(error, stackTrace) {
loggy.warning("error watching logs", error, stackTrace);
return CoreServiceFailure.unexpected(error, stackTrace);
},
);
}
@override
TaskEither<CoreServiceFailure, Unit> clearLogs() {
return exceptionHandler(
() => singbox.clearLogs().mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected,
);
}
@override

View File

@@ -0,0 +1,62 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:tint/tint.dart';
part 'box_log.freezed.dart';
enum LogLevel {
trace,
debug,
info,
warn,
error,
fatal,
panic;
static List<LogLevel> get choices => values.takeFirst(4);
Color? get color => switch (this) {
trace => Colors.lightBlueAccent,
debug => Colors.grey,
info => Colors.lightGreen,
warn => Colors.orange,
error => Colors.redAccent,
fatal => Colors.red,
panic => Colors.red,
};
}
@freezed
class BoxLog with _$BoxLog {
const factory BoxLog({
LogLevel? level,
DateTime? time,
required String message,
}) = _BoxLog;
factory BoxLog.parse(String log) {
log = log.strip();
DateTime? time;
if (log.length > 25) {
time = DateTime.tryParse(log.substring(6, 25));
}
if (time != null) {
log = log.substring(26);
}
final level = LogLevel.values.firstOrNullWhere(
(e) {
if (log.startsWith(e.name.toUpperCase())) {
log = log.removePrefix(e.name.toUpperCase());
return true;
}
return false;
},
);
return BoxLog(
level: level,
time: time,
message: log.trim(),
);
}
}

View File

@@ -2,6 +2,8 @@ import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/singbox/box_log.dart';
import 'package:hiddify/domain/singbox/rules.dart';
import 'package:hiddify/utils/platform_utils.dart';
part 'config_options.freezed.dart';
@@ -19,13 +21,13 @@ class ConfigOptions with _$ConfigOptions {
@Default(IPv6Mode.disable) IPv6Mode ipv6Mode,
@Default("tcp://8.8.8.8") String remoteDnsAddress,
@Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy,
@Default("8.8.8.8") String directDnsAddress,
@Default("local") String directDnsAddress,
@Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy,
@Default(2334) int mixedPort,
@Default(6450) int localDnsPort,
@Default(TunImplementation.mixed) TunImplementation tunImplementation,
@Default(9000) int mtu,
@Default("https://www.gstatic.com/generate_204") String connectionTestUrl,
@Default("http://cp.cloudflare.com/") String connectionTestUrl,
@IntervalConverter()
@Default(Duration(minutes: 10))
Duration urlTestInterval,
@@ -33,6 +35,9 @@ class ConfigOptions with _$ConfigOptions {
@Default(6756) int clashApiPort,
@Default(false) bool enableTun,
@Default(true) bool setSystemProxy,
@Default(false) bool bypassLan,
@Default(false) bool enableFakeDns,
List<Rule>? rules,
}) = _ConfigOptions;
static ConfigOptions initial = ConfigOptions(
@@ -49,13 +54,6 @@ class ConfigOptions with _$ConfigOptions {
_$ConfigOptionsFromJson(json);
}
enum LogLevel {
warn,
info,
debug,
trace,
}
@JsonEnum(valueField: 'key')
enum IPv6Mode {
disable("ipv4_only"),

View File

@@ -1,5 +1,40 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/prefs/locale_prefs.dart';
part 'rules.freezed.dart';
part 'rules.g.dart';
@freezed
class Rule with _$Rule {
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory Rule({
required String id,
required String name,
@Default(false) bool enabled,
String? domains,
String? ip,
String? port,
String? protocol,
@Default(RuleNetwork.tcpAndUdp) RuleNetwork network,
@Default(RuleOutbound.proxy) RuleOutbound outbound,
}) = _Rule;
factory Rule.fromJson(Map<String, dynamic> json) => _$RuleFromJson(json);
}
enum RuleOutbound { proxy, bypass, block }
@JsonEnum(valueField: 'key')
enum RuleNetwork {
tcpAndUdp(""),
tcp("tcp"),
udp("udp");
const RuleNetwork(this.key);
final String? key;
}
enum PerAppProxyMode {
off,
include,
@@ -26,11 +61,13 @@ enum PerAppProxyMode {
enum Region {
ir,
cn,
ru,
other;
String present(TranslationsEn t) => switch (this) {
ir => t.settings.general.regions.ir,
cn => t.settings.general.regions.cn,
ru => t.settings.general.regions.ru,
other => t.settings.general.regions.other,
};
}

View File

@@ -1,3 +1,4 @@
export 'box_log.dart';
export 'config_options.dart';
export 'core_status.dart';
export 'outbounds.dart';

View File

@@ -37,5 +37,7 @@ abstract interface class SingboxFacade {
Stream<Either<CoreServiceFailure, CoreStatus>> watchCoreStatus();
Stream<Either<CoreServiceFailure, String>> watchLogs();
Stream<Either<CoreServiceFailure, List<String>>> watchLogs();
TaskEither<CoreServiceFailure, Unit> clearLogs();
}

View File

@@ -2,7 +2,6 @@ import 'package:hiddify/core/prefs/general_prefs.dart';
import 'package:hiddify/features/common/app_update_notifier.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/common/window/window_controller.dart';
import 'package:hiddify/features/logs/notifier/notifier.dart';
import 'package:hiddify/features/profiles/notifier/notifier.dart';
import 'package:hiddify/features/system_tray/controller/system_tray_controller.dart';
import 'package:hiddify/services/service_providers.dart';
@@ -24,10 +23,6 @@ void commonControllers(CommonControllersRef ref) {
},
fireImmediately: true,
);
ref.listen(
logsNotifierProvider,
(previous, next) {},
);
ref.listen(
connectivityControllerProvider,
(previous, next) {},

View File

@@ -1,66 +1,133 @@
import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/features/logs/notifier/logs_state.dart';
import 'package:hiddify/utils/riverpod_utils.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
part 'logs_notifier.g.dart';
// TODO: rewrite
@Riverpod(keepAlive: true)
@riverpod
class LogsNotifier extends _$LogsNotifier with AppLogger {
static const maxLength = 1000;
@override
Stream<LogsState> build() {
state = const AsyncData(LogsState());
return ref.read(coreFacadeProvider).watchLogs().asyncMap(
(event) async {
_logs = [
event.getOrElse((l) => throw l),
..._logs.takeFirst(maxLength - 1),
];
return switch (state) {
// ignore: unused_result
AsyncData(:final value) => value.copyWith(logs: await _computeLogs()),
_ => LogsState(logs: await _computeLogs()),
};
LogsState build() {
ref.disposeDelay(const Duration(seconds: 20));
state = const LogsState();
ref.onDispose(
() {
loggy.debug("disposing");
_listener?.cancel();
_listener = null;
},
);
ref.onCancel(
() {
if (_listener?.isPaused != true) {
loggy.debug("pausing");
_listener?.pause();
}
},
);
ref.onResume(
() {
if (!state.paused && (_listener?.isPaused ?? false)) {
loggy.debug("resuming");
_listener?.resume();
}
},
);
_addListeners();
return const LogsState();
}
var _logs = <String>[];
StreamSubscription? _listener;
Future<void> _addListeners() async {
loggy.debug("adding listeners");
await _listener?.cancel();
_listener = ref
.read(coreFacadeProvider)
.watchLogs()
.throttle(
(_) => Stream.value(_listener?.isPaused ?? false),
leading: false,
trailing: true,
)
.throttleTime(
const Duration(milliseconds: 250),
leading: false,
trailing: true,
)
.asyncMap(
(event) async {
await event.fold(
(f) {
_logs = [];
state = state.copyWith(logs: AsyncError(f, StackTrace.current));
},
(a) async {
_logs = a.reversed;
state = state.copyWith(logs: AsyncData(await _computeLogs()));
},
);
},
).listen((event) {});
}
Iterable<String> _logs = [];
final _debouncer = CallbackDebouncer(const Duration(milliseconds: 200));
LogLevel? _levelFilter;
String _filter = "";
Future<List<String>> _computeLogs() async {
if (_levelFilter == null && _filter.isEmpty) return _logs;
return _logs.where((e) {
return _filter.isEmpty || e.contains(_filter);
Future<List<BoxLog>> _computeLogs() async {
final logs = _logs.map(BoxLog.parse);
if (_levelFilter == null && _filter.isEmpty) return logs.toList();
return logs.where((e) {
return (_filter.isEmpty || e.message.contains(_filter)) &&
(_levelFilter == null ||
e.level == null ||
e.level!.index >= _levelFilter!.index);
}).toList();
}
void clear() {
if (state case AsyncData(:final value)) {
state = AsyncData(value.copyWith(logs: [])).copyWithPrevious(state);
}
void pause() {
loggy.debug("pausing");
_listener?.pause();
state = state.copyWith(paused: true);
}
void resume() {
loggy.debug("resuming");
_listener?.resume();
state = state.copyWith(paused: false);
}
Future<void> clear() async {
loggy.debug("clearing");
await ref.read(coreFacadeProvider).clearLogs().match(
(l) {
loggy.warning("error clearing logs", l);
},
(_) {
_logs = [];
state = state.copyWith(logs: const AsyncData([]));
},
).run();
}
void filterMessage(String? filter) {
_filter = filter ?? '';
_debouncer(
() async {
if (state case AsyncData(:final value)) {
state = AsyncData(
value.copyWith(
filter: _filter,
logs: await _computeLogs(),
),
).copyWithPrevious(state);
if (state.logs case AsyncData()) {
state = state.copyWith(
filter: _filter,
logs: AsyncData(await _computeLogs()),
);
}
},
);
@@ -68,13 +135,11 @@ class LogsNotifier extends _$LogsNotifier with AppLogger {
Future<void> filterLevel(LogLevel? level) async {
_levelFilter = level;
if (state case AsyncData(:final value)) {
state = AsyncData(
value.copyWith(
levelFilter: _levelFilter,
logs: await _computeLogs(),
),
).copyWithPrevious(state);
if (state.logs case AsyncData()) {
state = state.copyWith(
levelFilter: _levelFilter,
logs: AsyncData(await _computeLogs()),
);
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'logs_state.freezed.dart';
@@ -8,7 +9,8 @@ class LogsState with _$LogsState {
const LogsState._();
const factory LogsState({
@Default([]) List<String> logs,
@Default(AsyncLoading()) AsyncValue<List<BoxLog>> logs,
@Default(false) bool paused,
@Default("") String filter,
LogLevel? levelFilter,
}) = _LogsState;

View File

@@ -1,31 +1,31 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fpdart/fpdart.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/failures.dart';
import 'package:hiddify/features/common/common.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/features/logs/notifier/notifier.dart';
import 'package:hiddify/services/service_providers.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tint/tint.dart';
class LogsPage extends HookConsumerWidget with PresLogger {
const LogsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final asyncState = ref.watch(logsNotifierProvider);
final state = ref.watch(logsNotifierProvider);
final notifier = ref.watch(logsNotifierProvider.notifier);
final debug = ref.watch(debugModeNotifierProvider);
final filesEditor = ref.watch(filesEditorServiceProvider);
final filterController = useTextEditingController(text: state.filter);
final List<PopupMenuEntry> popupButtons = debug || PlatformUtils.isDesktop
? [
PopupMenuItem(
@@ -49,115 +49,146 @@ class LogsPage extends HookConsumerWidget with PresLogger {
]
: [];
switch (asyncState) {
case AsyncData(value: final state):
return Scaffold(
appBar: AppBar(
// TODO: fix height
toolbarHeight: 90,
title: Text(t.logs.pageTitle),
actions: [
if (popupButtons.isNotEmpty)
PopupMenuButton(
itemBuilder: (context) {
return popupButtons;
},
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(36),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Flexible(
child: TextFormField(
onChanged: notifier.filterMessage,
decoration: InputDecoration(
isDense: true,
hintText: t.logs.filterHint,
),
),
return Scaffold(
appBar: AppBar(
// TODO: fix height
toolbarHeight: 90,
title: Text(t.logs.pageTitle),
actions: [
if (state.paused)
IconButton(
onPressed: notifier.resume,
icon: const Icon(Icons.play_arrow),
tooltip: t.logs.resumeTooltip,
)
else
IconButton(
onPressed: notifier.pause,
icon: const Icon(Icons.pause),
tooltip: t.logs.pauseTooltip,
),
IconButton(
onPressed: notifier.clear,
icon: const Icon(Icons.clear_all),
tooltip: t.logs.clearTooltip,
),
if (popupButtons.isNotEmpty)
PopupMenuButton(
itemBuilder: (context) {
return popupButtons;
},
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(36),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Flexible(
child: TextFormField(
controller: filterController,
onChanged: notifier.filterMessage,
decoration: InputDecoration(
isDense: true,
hintText: t.logs.filterHint,
),
const Gap(16),
DropdownButton<Option<LogLevel>>(
value: optionOf(state.levelFilter),
onChanged: (v) {
if (v == null) return;
notifier.filterLevel(v.toNullable());
},
padding: const EdgeInsets.symmetric(horizontal: 8),
borderRadius: BorderRadius.circular(4),
items: [
DropdownMenuItem(
value: none(),
child: Text(t.logs.allLevelsFilter),
),
...LogLevel.values.takeFirst(3).map(
(e) => DropdownMenuItem(
value: some(e),
child: Text(e.name),
),
),
],
),
),
const Gap(16),
DropdownButton<Option<LogLevel>>(
value: optionOf(state.levelFilter),
onChanged: (v) {
if (v == null) return;
notifier.filterLevel(v.toNullable());
},
padding: const EdgeInsets.symmetric(horizontal: 8),
borderRadius: BorderRadius.circular(4),
items: [
DropdownMenuItem(
value: none(),
child: Text(t.logs.allLevelsFilter),
),
...LogLevel.choices.map(
(e) => DropdownMenuItem(
value: some(e),
child: Text(e.name),
),
),
],
),
),
],
),
),
body: ListView.builder(
itemCount: state.logs.length,
reverse: true,
itemBuilder: (context, index) {
final log = state.logs[index];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
dense: true,
subtitle: Text(log.strip()),
),
if (index != 0)
const Divider(
indent: 16,
endIndent: 16,
height: 4,
),
),
body: switch (state.logs) {
AsyncData(value: final logs) => SelectionArea(
child: ListView.builder(
itemCount: logs.length,
reverse: true,
itemBuilder: (context, index) {
final log = logs[index];
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (log.level != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
log.level!.name.toUpperCase(),
style: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(color: log.level!.color),
),
if (log.time != null)
Text(
log.time!.toString(),
style:
Theme.of(context).textTheme.labelSmall,
),
],
),
Text(
log.message,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
);
},
if (index != 0)
const Divider(
indent: 16,
endIndent: 16,
height: 4,
),
],
);
},
),
),
);
case AsyncError(:final error):
return Scaffold(
body: CustomScrollView(
AsyncError(:final error) => CustomScrollView(
slivers: [
NestedTabAppBar(
title: Text(t.logs.pageTitle),
),
SliverErrorBodyPlaceholder(t.presentShortError(error)),
],
),
);
case AsyncLoading():
return Scaffold(
body: CustomScrollView(
_ => const CustomScrollView(
slivers: [
NestedTabAppBar(
title: Text(t.logs.pageTitle),
),
const SliverLoadingBodyPlaceholder(),
SliverLoadingBodyPlaceholder(),
],
),
);
// TODO: remove
default:
return const Scaffold();
}
},
);
}
}

View File

@@ -52,13 +52,13 @@ class ConfigOptionsPage extends HookConsumerWidget {
),
ListTile(
title: Text(t.settings.config.logLevel),
subtitle: Text(options.logLevel.name),
subtitle: Text(options.logLevel.name.toUpperCase()),
onTap: () async {
final logLevel = await SettingsPickerDialog(
title: t.settings.config.logLevel,
selected: options.logLevel,
options: LogLevel.values,
getTitle: (e) => e.name,
options: LogLevel.choices,
getTitle: (e) => e.name.toUpperCase(),
resetValue: _default.logLevel,
).show(context);
if (logLevel == null) return;

View File

@@ -6,6 +6,7 @@ import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/core/router/routes/routes.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/features/common/common.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class AdvancedSettingTiles extends HookConsumerWidget {
@@ -20,6 +21,7 @@ class AdvancedSettingTiles extends HookConsumerWidget {
return Column(
children: [
const RegionPrefTile(),
ListTile(
title: Text(t.settings.config.pageTitle),
leading: const Icon(Icons.edit_document),

View File

@@ -3,7 +3,6 @@ import 'package:go_router/go_router.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/features/common/common.dart';
import 'package:hiddify/features/settings/widgets/theme_mode_switch_button.dart';
import 'package:hiddify/services/auto_start_service.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -44,31 +43,34 @@ class GeneralSettingTiles extends HookConsumerWidget {
),
ListTile(
title: Text(t.settings.general.themeMode),
subtitle: Text(
switch (theme.mode) {
ThemeMode.system => t.settings.general.themeModes.system,
ThemeMode.light => t.settings.general.themeModes.light,
ThemeMode.dark => t.settings.general.themeModes.dark,
},
),
trailing: ThemeModeSwitch(
themeMode: theme.mode,
onChanged: ref.read(themeModeNotifierProvider.notifier).update,
),
subtitle: Text(theme.mode.present(t)),
leading: const Icon(Icons.light_mode),
onTap: () async {
await ref.read(themeModeNotifierProvider.notifier).update(
Theme.of(context).brightness == Brightness.light
? ThemeMode.dark
: ThemeMode.light,
final selectedThemeMode = await showDialog<AppThemeMode>(
context: context,
builder: (context) {
return SimpleDialog(
title: Text(t.settings.general.themeMode),
children: AppThemeMode.values
.map(
(e) => RadioListTile(
title: Text(e.present(t)),
value: e,
groupValue: theme.mode,
onChanged: (e) => context.pop(e),
),
)
.toList(),
);
},
);
if (selectedThemeMode != null) {
await ref
.read(themeModeNotifierProvider.notifier)
.update(selectedThemeMode);
}
},
),
SwitchListTile(
title: Text(t.settings.general.trueBlack),
value: theme.trueBlack,
onChanged: ref.read(trueBlackThemeNotifierProvider.notifier).update,
),
if (PlatformUtils.isDesktop) ...[
SwitchListTile(
title: Text(t.settings.general.autoStart),

View File

@@ -1,51 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ThemeModeSwitch extends HookConsumerWidget {
const ThemeModeSwitch({
super.key,
required this.themeMode,
required this.onChanged,
});
final ThemeMode themeMode;
final ValueChanged<ThemeMode> onChanged;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final List<bool> isSelected = <bool>[
themeMode == ThemeMode.light,
themeMode == ThemeMode.system,
themeMode == ThemeMode.dark,
];
return ToggleButtons(
isSelected: isSelected,
onPressed: (int newIndex) {
if (newIndex == 0) {
onChanged(ThemeMode.light);
} else if (newIndex == 1) {
onChanged(ThemeMode.system);
} else {
onChanged(ThemeMode.dark);
}
},
children: <Widget>[
Icon(
Icons.wb_sunny,
semanticLabel: t.settings.general.themeModes.light,
),
Icon(
Icons.phone_iphone,
semanticLabel: t.settings.general.themeModes.system,
),
Icon(
Icons.bedtime,
semanticLabel: t.settings.general.themeModes.dark,
),
],
);
}
}

View File

@@ -8,7 +8,7 @@ import 'package:combine/combine.dart';
import 'package:ffi/ffi.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/singbox/config_options.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/gen/singbox_generated_bindings.dart';
import 'package:hiddify/services/singbox/shared.dart';
import 'package:hiddify/services/singbox/singbox_service.dart';
@@ -16,6 +16,7 @@ import 'package:hiddify/utils/utils.dart';
import 'package:loggy/loggy.dart';
import 'package:path/path.dart' as p;
import 'package:rxdart/rxdart.dart';
import 'package:watcher/watcher.dart';
final _logger = Loggy('FFISingboxService');
@@ -301,33 +302,47 @@ class FFISingboxService
);
}
final _logBuffer = <String>[];
int _logFilePosition = 0;
@override
Stream<String> watchLogs(String path) {
var linesRead = 0;
return Stream.periodic(
const Duration(seconds: 1),
).asyncMap((_) async {
final result = await _readLogs(path, linesRead);
linesRead = result.$2;
return result.$1;
}).transform(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
for (final item in data) {
sink.add(item);
}
},
),
);
Stream<List<String>> watchLogs(String path) async* {
yield await _readLogFile(File(path));
yield* Watcher(path, pollingDelay: const Duration(seconds: 1))
.events
.asyncMap((event) async {
if (event.type == ChangeType.MODIFY) {
await _readLogFile(File(path));
}
return _logBuffer;
});
}
Future<(List<String>, int)> _readLogs(String path, int from) async {
return CombineWorker().execute(
@override
TaskEither<String, Unit> clearLogs() {
return TaskEither(
() async {
final lines = await File(path).readAsLines();
final to = lines.length;
return (lines.sublist(from), to);
_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();
_logFilePosition = file.lengthSync();
final lines = const LineSplitter().convert(content);
if (lines.length > 300) {
lines.removeRange(0, lines.length - 300);
}
for (final line in lines) {
_logBuffer.add(line);
if (_logBuffer.length > 300) {
_logBuffer.removeAt(0);
}
}
return _logBuffer;
}
}

View File

@@ -163,11 +163,18 @@ class MobileSingboxService
}
@override
Stream<String> watchLogs(String path) {
return _logsChannel.receiveBroadcastStream().map(
(event) {
// loggy.debug("received log: $event");
return event as String;
Stream<List<String>> watchLogs(String path) async* {
yield* _logsChannel
.receiveBroadcastStream()
.map((event) => (event as List).map((e) => e as String).toList());
}
@override
TaskEither<String, Unit> clearLogs() {
return TaskEither(
() async {
await _methodChannel.invokeMethod("clear_logs");
return right(unit);
},
);
}

View File

@@ -48,5 +48,7 @@ abstract interface class SingboxService {
Stream<String> watchStats();
Stream<String> watchLogs(String path);
Stream<List<String>> watchLogs(String path);
TaskEither<String, Unit> clearLogs();
}

Submodule libcore updated: d410fe1c4b...7b367fe70c

View File

@@ -1555,7 +1555,7 @@ packages:
source: hosted
version: "11.10.0"
watcher:
dependency: transitive
dependency: "direct main"
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"

View File

@@ -10,8 +10,6 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.6
# internationalization
flutter_localizations:
sdk: flutter
intl: ^0.18.1
@@ -19,28 +17,18 @@ dependencies:
slang_flutter: ^3.24.0
timeago: ^3.5.0
flutter_localized_locales: ^2.0.5
# data & serialization
fpdart: ^1.1.0
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
# state management
hooks_riverpod: ^2.4.3
flutter_hooks: ^0.20.3
riverpod_annotation: ^2.2.0
rxdart: ^0.27.7
# persistence
drift: ^2.12.1
sqlite3_flutter_libs: ^0.5.16
shared_preferences: ^2.2.2
# networking
dio: ^5.3.3
web_socket_channel: ^2.4.0
# native
ffi: ^2.1.0
path_provider: ^2.1.1
flutter_local_notifications: ^15.1.1
@@ -54,13 +42,9 @@ dependencies:
url_launcher: ^6.1.14
vclibs: ^0.1.0
launch_at_startup: ^0.2.2
# analytics
sentry_flutter: ^7.10.1
sentry_dart_plugin: ^1.6.2
sentry_dio: ^7.10.1
# utils
combine: ^0.5.6
path: ^1.8.3
loggy: ^2.0.3
@@ -73,8 +57,7 @@ dependencies:
accessibility_tools: ^1.0.0
neat_periodic_task: ^2.0.1
retry: ^3.1.2
# widgets
watcher: ^1.1.0
go_router: ^11.1.4
flex_color_scheme: ^7.3.1
flutter_animate: ^4.2.0+1
@@ -170,3 +153,8 @@ sentry:
upload_sources: true
log_level: info
ignore_missing: true
cider:
link_template:
tag: https://github.com/hiddify/hiddify-next/releases/tag/%tag%
diff: https://github.com/hiddify/hiddify-next/compare/%from%...%to%