Merge branch 'main' of hiddify-github:hiddify/hiddify-next
This commit is contained in:
20
CHANGELOG.md
Normal file
20
CHANGELOG.md
Normal 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
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
# Attribution-NonCommercial-ShareAlike 4.0 International
|
# Attribution-NonCommercial-ShareAlike 4.0 International
|
||||||
|
|
||||||
## Summary:
|
## 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:
|
- You are free to:
|
||||||
- Share — copy and redistribute the material in any medium or format
|
- Share — copy and redistribute the material in any medium or format
|
||||||
- Adapt — remix, transform, and build upon the material
|
- Adapt — remix, transform, and build upon the material
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -119,9 +119,8 @@ release: # Create a new tag for release.
|
|||||||
echo "version: $${VERSION_STR}+$${BUILD_NUMBER}" && \
|
echo "version: $${VERSION_STR}+$${BUILD_NUMBER}" && \
|
||||||
sed -i "s/version: .*/version: $${VERSION_STR}\+$${BUILD_NUMBER}/g" pubspec.yaml && \
|
sed -i "s/version: .*/version: $${VERSION_STR}\+$${BUILD_NUMBER}/g" pubspec.yaml && \
|
||||||
git tag $${TAG} > /dev/null && \
|
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 tag -d $${TAG} > /dev/null && \
|
||||||
git add pubspec.yaml changelog.md && \
|
git add pubspec.yaml CHANGELOG.md && \
|
||||||
make sync_translate && \
|
make sync_translate && \
|
||||||
git add assets/translations/* && \
|
git add assets/translations/* && \
|
||||||
git commit -m "release: version $${TAG}" && \
|
git commit -m "release: version $${TAG}" && \
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -13,11 +13,6 @@
|
|||||||
|
|
||||||
</div>
|
</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?
|
## 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.
|
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
|
<div align=center>
|
||||||
You can easily contribute to this project by using the following links to improve the translations:
|
<img width=90% alt="English Demo" src="https://github.com/hiddify/hiddify-next/assets/125398461/ffe5346d-3404-470f-b5e0-4364e23743d2">
|
||||||
- [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>
|
||||||
|
|
||||||
|
|
||||||
## 🚀 Main features
|
## 🚀 Main features
|
||||||
@@ -80,9 +72,9 @@ You can easily contribute to this project by using the following links to improv
|
|||||||
<td>Android</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://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-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-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-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-x86_64.apk"><img src="https://img.shields.io/badge/APK-x64-96ed89.svg?logo=github"></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -97,7 +89,7 @@ You can easily contribute to this project by using the following links to improv
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Linux</td>
|
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -108,6 +100,15 @@ You can easily contribute to this project by using the following links to improv
|
|||||||
## Installation and tutorials
|
## Installation and tutorials
|
||||||
Please find tutorial information on the [wiki page](https://github.com/hiddify/hiddify-next/wiki).
|
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
|
## Acknowledgements
|
||||||
|
|
||||||
- [Sing-box](https://github.com/SagerNet/sing-box)
|
- [Sing-box](https://github.com/SagerNet/sing-box)
|
||||||
|
|||||||
28
README_cn.md
28
README_cn.md
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
## 什么是 Hiddify-Next?
|
## Hiddify-Next 是什么?
|
||||||
基于 [Sing-box](https://github.com/SagerNet/sing-box) 的多平台客户端,用作通用代理工具链。 该应用程序提供了广泛的功能,如下所列。 它还支持大量协议。 该应用程序免费使用、无广告且开源。 它提供了一个安全且私密的工具来访问免费互联网。
|
基于 [Sing-box](https://github.com/SagerNet/sing-box) 的多平台客户端,用作通用代理工具链。 该应用程序提供了广泛的功能,如下所列。 它还支持大量协议。 该应用程序免费使用、无广告且开源。 它提供了一个安全且私密的工具来访问免费互联网。
|
||||||
|
|
||||||
该应用程序是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 欲了解更多信息,您可以阅读我们的开发贡献指南。
|
该应用程序是使用 [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 上获取
|
📱 可在 Google Play 上获取
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody align=left>
|
<tbody align=left>
|
||||||
<tr>
|
<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://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-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-arm64.apk"><img src="https://img.shields.io/badge/APK-ArmV8-168039.svg?logo=github"></a><br>
|
||||||
@@ -72,13 +72,13 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
<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>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -95,16 +95,16 @@
|
|||||||
|
|
||||||
## 致谢
|
## 致谢
|
||||||
- [Sing-box](https://github.com/SagerNet/sing-box)
|
- [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](https://github.com/Dreamacro/clash)
|
||||||
- [Clash Meta](https://github.com/MetaCubeX/Clash.Meta)
|
- [Clash Meta](https://github.com/MetaCubeX/Clash.Meta)
|
||||||
- [FClash](https://github.com/Fclash/Fclash)
|
- [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>
|
<div align=center>
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align=center>
|
<p align=center>
|
||||||
我们感谢所有参与该项目的人。 这里有一些人,还有 Github 之外的更多人。 这对我们来说意义重大。 ♥
|
感谢所有参与该项目的人。包括以下列出的人,和更多其他来自 Github 的人。你们对我们的意义非常重大。 ♥ </p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align=center>
|
<p align=center>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p align=center>
|
<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>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.hiddify.hiddify
|
package com.hiddify.hiddify
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
|
||||||
@@ -18,13 +19,15 @@ class LogHandler : FlutterPlugin {
|
|||||||
|
|
||||||
logsChannel.setStreamHandler(object : EventChannel.StreamHandler {
|
logsChannel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||||
MainActivity.instance.serviceLogs.observeForever {
|
val activity = MainActivity.instance
|
||||||
if (it == null) return@observeForever
|
events?.success(activity.logList)
|
||||||
events?.success(it)
|
activity.logCallback = {
|
||||||
|
events?.success(activity.logList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {
|
override fun onCancel(arguments: Any?) {
|
||||||
|
MainActivity.instance.logCallback = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
|
|||||||
var logCallback: ((Boolean) -> Unit)? = null
|
var logCallback: ((Boolean) -> Unit)? = null
|
||||||
val serviceStatus = MutableLiveData(Status.Stopped)
|
val serviceStatus = MutableLiveData(Status.Stopped)
|
||||||
val serviceAlerts = MutableLiveData<ServiceEvent?>(null)
|
val serviceAlerts = MutableLiveData<ServiceEvent?>(null)
|
||||||
val serviceLogs = MutableLiveData<String?>(null)
|
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
@@ -102,37 +101,18 @@ class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
|
|||||||
serviceAlerts.postValue(ServiceEvent(Status.Stopped, type, message))
|
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?) {
|
override fun onServiceWriteLog(message: String?) {
|
||||||
if (paused) {
|
if (logList.size > 300) {
|
||||||
if (logList.size > 300) {
|
logList.removeFirst()
|
||||||
logList.removeFirst()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
logList.addLast(message)
|
logList.addLast(message)
|
||||||
if (!paused) {
|
logCallback?.invoke(false)
|
||||||
logCallback?.invoke(false)
|
|
||||||
serviceLogs.postValue(message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceResetLogs(messages: MutableList<String>) {
|
override fun onServiceResetLogs(messages: MutableList<String>) {
|
||||||
logList.clear()
|
logList.clear()
|
||||||
logList.addAll(messages)
|
logList.addAll(messages)
|
||||||
if (!paused) logCallback?.invoke(true)
|
logCallback?.invoke(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
|
|||||||
Restart("restart"),
|
Restart("restart"),
|
||||||
SelectOutbound("select_outbound"),
|
SelectOutbound("select_outbound"),
|
||||||
UrlTest("url_test"),
|
UrlTest("url_test"),
|
||||||
|
ClearLogs("clear_logs"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,38 +64,44 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
|
|||||||
}
|
}
|
||||||
|
|
||||||
Trigger.ChangeConfigOptions.method -> {
|
Trigger.ChangeConfigOptions.method -> {
|
||||||
result.runCatching {
|
scope.launch {
|
||||||
val args = call.arguments as String
|
result.runCatching {
|
||||||
Settings.configOptions = args
|
val args = call.arguments as String
|
||||||
success(true)
|
Settings.configOptions = args
|
||||||
|
success(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Trigger.Start.method -> {
|
Trigger.Start.method -> {
|
||||||
result.runCatching {
|
scope.launch {
|
||||||
val args = call.arguments as Map<*, *>
|
result.runCatching {
|
||||||
Settings.activeConfigPath = args["path"] as String? ?: ""
|
val args = call.arguments as Map<*, *>
|
||||||
val mainActivity = MainActivity.instance
|
Settings.activeConfigPath = args["path"] as String? ?: ""
|
||||||
val started = mainActivity.serviceStatus.value == Status.Started
|
val mainActivity = MainActivity.instance
|
||||||
if (started) {
|
val started = mainActivity.serviceStatus.value == Status.Started
|
||||||
Log.w(TAG, "service is already running")
|
if (started) {
|
||||||
return success(true)
|
Log.w(TAG, "service is already running")
|
||||||
|
return@launch success(true)
|
||||||
|
}
|
||||||
|
mainActivity.startService()
|
||||||
|
success(true)
|
||||||
}
|
}
|
||||||
mainActivity.startService()
|
|
||||||
success(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Trigger.Stop.method -> {
|
Trigger.Stop.method -> {
|
||||||
result.runCatching {
|
scope.launch {
|
||||||
val mainActivity = MainActivity.instance
|
result.runCatching {
|
||||||
val started = mainActivity.serviceStatus.value == Status.Started
|
val mainActivity = MainActivity.instance
|
||||||
if (!started) {
|
val started = mainActivity.serviceStatus.value == Status.Started
|
||||||
Log.w(TAG, "service is not running")
|
if (!started) {
|
||||||
return success(true)
|
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()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"remainingDuration": "${duration} Days Remaining",
|
"remainingDuration": "${duration} Days Remaining",
|
||||||
"remainingTrafficSemanticLabel": "${consumed} of ${total} traffic consumed.",
|
"remainingTrafficSemanticLabel": "${consumed} of ${total} traffic consumed.",
|
||||||
"expired": "Expired",
|
"expired": "Expired",
|
||||||
"noTraffic": "No more traffic"
|
"noTraffic": "Out of Quota"
|
||||||
},
|
},
|
||||||
"sortBy": {
|
"sortBy": {
|
||||||
"lastUpdate": "Recently updated",
|
"lastUpdate": "Recently updated",
|
||||||
@@ -106,11 +106,13 @@
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"pageTitle": "Logs",
|
"pageTitle": "Logs",
|
||||||
"clearLogsButtonText": "Clear Logs",
|
|
||||||
"filterHint": "Filter",
|
"filterHint": "Filter",
|
||||||
"allLevelsFilter": "All",
|
"allLevelsFilter": "All",
|
||||||
"shareCoreLogs": "Share Core Logs",
|
"shareCoreLogs": "Share Core Logs",
|
||||||
"shareAppLogs": "Share App logs"
|
"shareAppLogs": "Share App logs",
|
||||||
|
"pauseTooltip": "Pause",
|
||||||
|
"resumeTooltip": "Resume",
|
||||||
|
"clearTooltip": "Clear"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"pageTitle": "Settings",
|
"pageTitle": "Settings",
|
||||||
@@ -123,17 +125,18 @@
|
|||||||
"regions": {
|
"regions": {
|
||||||
"ir": "Iran (ir)",
|
"ir": "Iran (ir)",
|
||||||
"cn": "China (cn)",
|
"cn": "China (cn)",
|
||||||
|
"ru": "Russia (ru)",
|
||||||
"other": "Other"
|
"other": "Other"
|
||||||
},
|
},
|
||||||
"themeMode": "Theme Mode",
|
"themeMode": "Theme Mode",
|
||||||
"themeModes": {
|
"themeModes": {
|
||||||
"system": "Follow system theme",
|
"system": "Follow system theme",
|
||||||
"dark": "Dark mode",
|
"dark": "Dark mode",
|
||||||
"light": "Light mode"
|
"light": "Light mode",
|
||||||
|
"black": "Black mode"
|
||||||
},
|
},
|
||||||
"enableAnalytics": "Enable Analytics",
|
"enableAnalytics": "Enable Analytics",
|
||||||
"enableAnalyticsMsg": "Give permission to collect analytics and send crash reports to improve the app",
|
"enableAnalyticsMsg": "Give permission to collect analytics and send crash reports to improve the app",
|
||||||
"trueBlack": "Pure Black",
|
|
||||||
"autoStart": "Start on Boot",
|
"autoStart": "Start on Boot",
|
||||||
"silentStart": "Silent Start",
|
"silentStart": "Silent Start",
|
||||||
"openWorkingDir": "Open Working Directory",
|
"openWorkingDir": "Open Working Directory",
|
||||||
@@ -257,4 +260,4 @@
|
|||||||
"short_description": "Auto, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,11 +106,13 @@
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"pageTitle": "لاگها",
|
"pageTitle": "لاگها",
|
||||||
"clearLogsButtonText": "پاکسازی",
|
|
||||||
"filterHint": "فیلتر",
|
"filterHint": "فیلتر",
|
||||||
"allLevelsFilter": "همه",
|
"allLevelsFilter": "همه",
|
||||||
"shareCoreLogs": "اشتراکگذاری لاگ هسته",
|
"shareCoreLogs": "اشتراکگذاری لاگ هسته",
|
||||||
"shareAppLogs": "اشتراکگذاری لاگ برنامه"
|
"shareAppLogs": "اشتراکگذاری لاگ برنامه",
|
||||||
|
"pauseTooltip": "مکث",
|
||||||
|
"resumeTooltip": "از سرگیری",
|
||||||
|
"clearTooltip": "پاکسازی"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"pageTitle": "تنظیمات",
|
"pageTitle": "تنظیمات",
|
||||||
@@ -123,17 +125,18 @@
|
|||||||
"regions": {
|
"regions": {
|
||||||
"ir": "ایران (ir)",
|
"ir": "ایران (ir)",
|
||||||
"cn": "چین (cn)",
|
"cn": "چین (cn)",
|
||||||
|
"ru": "روسیه (ru)",
|
||||||
"other": "سایر"
|
"other": "سایر"
|
||||||
},
|
},
|
||||||
"themeMode": "تم مود",
|
"themeMode": "تم مود",
|
||||||
"themeModes": {
|
"themeModes": {
|
||||||
"system": "پیروی از تم دستگاه",
|
"system": "پیروی از تم دستگاه",
|
||||||
"dark": "تم تیره",
|
"dark": "تم تیره",
|
||||||
"light": "تم روشن"
|
"light": "تم روشن",
|
||||||
|
"black": "تم سیاه"
|
||||||
},
|
},
|
||||||
"enableAnalytics": "فعالسازی آنالیتیکز",
|
"enableAnalytics": "فعالسازی آنالیتیکز",
|
||||||
"enableAnalyticsMsg": "ارائه دسترسی آنالیز و گزارش خطا برای بهبود عملکرد برنامه",
|
"enableAnalyticsMsg": "ارائه دسترسی آنالیز و گزارش خطا برای بهبود عملکرد برنامه",
|
||||||
"trueBlack": "کاملا سیاه",
|
|
||||||
"autoStart": "اجرا با روشن شدن سیستم",
|
"autoStart": "اجرا با روشن شدن سیستم",
|
||||||
"silentStart": "اجرای ساکت",
|
"silentStart": "اجرای ساکت",
|
||||||
"openWorkingDir": "باز کردن دایرکتوری کاری",
|
"openWorkingDir": "باز کردن دایرکتوری کاری",
|
||||||
@@ -257,4 +260,4 @@
|
|||||||
"short_description": "Auto, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
|
"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- این برنامه بدون تبلیغات است. تجزیه و تحلیل و داده های اشکال فقط با رضایت صریح کاربر در اولین استفاده از برنامه اتفاق می افتد."
|
"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- این برنامه بدون تبلیغات است. تجزیه و تحلیل و داده های اشکال فقط با رضایت صریح کاربر در اولین استفاده از برنامه اتفاق می افتد."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"appTitle": "HiddifyNext",
|
"appTitle": "Hiddify Next",
|
||||||
"reset": "Сброс",
|
"reset": "Сброс",
|
||||||
"toggle": {
|
"toggle": {
|
||||||
"enabled": "Включено",
|
"enabled": "Включено",
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
"stats": {
|
"stats": {
|
||||||
"traffic": "Скорость",
|
"traffic": "Скорость",
|
||||||
"trafficTotal": "Трафик",
|
"trafficTotal": "Трафик",
|
||||||
"uplink": "Входящий канал",
|
"uplink": "Исходящий канал",
|
||||||
"downlink": "Исходящий канал"
|
"downlink": "Входящий канал"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
@@ -106,11 +106,13 @@
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"pageTitle": "Журналы",
|
"pageTitle": "Журналы",
|
||||||
"clearLogsButtonText": "Очистить журналы",
|
|
||||||
"filterHint": "Фильтр",
|
"filterHint": "Фильтр",
|
||||||
"allLevelsFilter": "Все",
|
"allLevelsFilter": "Все",
|
||||||
"shareCoreLogs": "Поделиться журналами ядра",
|
"shareCoreLogs": "Поделиться журналами ядра",
|
||||||
"shareAppLogs": "Поделиться журналами приложения"
|
"shareAppLogs": "Поделиться журналами приложения",
|
||||||
|
"pauseTooltip": "Приостановить",
|
||||||
|
"resumeTooltip": "Возобновить",
|
||||||
|
"clearTooltip": "Очистить"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"pageTitle": "Настройки",
|
"pageTitle": "Настройки",
|
||||||
@@ -123,17 +125,18 @@
|
|||||||
"regions": {
|
"regions": {
|
||||||
"ir": "Иран (ir)",
|
"ir": "Иран (ir)",
|
||||||
"cn": "Китай (cn)",
|
"cn": "Китай (cn)",
|
||||||
|
"ru": "Россия (ru)",
|
||||||
"other": "Другой"
|
"other": "Другой"
|
||||||
},
|
},
|
||||||
"themeMode": "Оформление",
|
"themeMode": "Оформление",
|
||||||
"themeModes": {
|
"themeModes": {
|
||||||
"system": "Системная тема",
|
"system": "Системная тема",
|
||||||
"dark": "Тёмная тема",
|
"dark": "Тёмная тема",
|
||||||
"light": "Светлая тема"
|
"light": "Светлая тема",
|
||||||
|
"black": "Чёрная тема"
|
||||||
},
|
},
|
||||||
"enableAnalytics": "Сбор аналитики",
|
"enableAnalytics": "Сбор аналитики",
|
||||||
"enableAnalyticsMsg": "Сбор аналитических данных и отправка отчётов о сбоях для улучшения приложения.",
|
"enableAnalyticsMsg": "Сбор аналитических данных и отправка отчётов о сбоях для улучшения приложения.",
|
||||||
"trueBlack": "Чистый чёрный цвет",
|
|
||||||
"autoStart": "Запуск при загрузке",
|
"autoStart": "Запуск при загрузке",
|
||||||
"silentStart": "Тихий запуск",
|
"silentStart": "Тихий запуск",
|
||||||
"openWorkingDir": "Открыть рабочую папку",
|
"openWorkingDir": "Открыть рабочую папку",
|
||||||
@@ -198,7 +201,7 @@
|
|||||||
"pageTitle": "О программе",
|
"pageTitle": "О программе",
|
||||||
"version": "Версия",
|
"version": "Версия",
|
||||||
"sourceCode": "Исходный код",
|
"sourceCode": "Исходный код",
|
||||||
"telegramChannel": "Telegram канал",
|
"telegramChannel": "Telegram-канал",
|
||||||
"checkForUpdate": "Проверка обновления",
|
"checkForUpdate": "Проверка обновления",
|
||||||
"privacyPolicy": "Политика конфиденциальности",
|
"privacyPolicy": "Политика конфиденциальности",
|
||||||
"termsAndConditions": "Условия и положения"
|
"termsAndConditions": "Условия и положения"
|
||||||
@@ -209,7 +212,7 @@
|
|||||||
"updateMsg": "Доступна новая версия @:general.appTitle. Обновить сейчас?",
|
"updateMsg": "Доступна новая версия @:general.appTitle. Обновить сейчас?",
|
||||||
"currentVersionLbl": "Текущая версия",
|
"currentVersionLbl": "Текущая версия",
|
||||||
"newVersionLbl": "Новая версия",
|
"newVersionLbl": "Новая версия",
|
||||||
"updateNowBtnTxt": "Обновить сейчас",
|
"updateNowBtnTxt": "Обновить",
|
||||||
"laterBtnTxt": "Позже",
|
"laterBtnTxt": "Позже",
|
||||||
"ignoreBtnTxt": "Пропустить"
|
"ignoreBtnTxt": "Пропустить"
|
||||||
},
|
},
|
||||||
@@ -255,6 +258,6 @@
|
|||||||
"play": {
|
"play": {
|
||||||
"title": "Hiddify Next (Preview)",
|
"title": "Hiddify Next (Preview)",
|
||||||
"short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
|
"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• Все протоколы, поддерживаемые 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Ядро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n- ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения."
|
"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• Все протоколы, поддерживаемые 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Ядро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,11 +106,13 @@
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"pageTitle": "日志",
|
"pageTitle": "日志",
|
||||||
"clearLogsButtonText": "清除日志",
|
|
||||||
"filterHint": "筛选",
|
"filterHint": "筛选",
|
||||||
"allLevelsFilter": "全部",
|
"allLevelsFilter": "全部",
|
||||||
"shareCoreLogs": "分享核心日志",
|
"shareCoreLogs": "分享核心日志",
|
||||||
"shareAppLogs": "分享日志"
|
"shareAppLogs": "分享日志",
|
||||||
|
"pauseTooltip": "暂停",
|
||||||
|
"resumeTooltip": "恢复",
|
||||||
|
"clearTooltip": "清除"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"pageTitle": "设置",
|
"pageTitle": "设置",
|
||||||
@@ -123,17 +125,18 @@
|
|||||||
"regions": {
|
"regions": {
|
||||||
"ir": "伊朗 (ir)",
|
"ir": "伊朗 (ir)",
|
||||||
"cn": "中国 (cn)",
|
"cn": "中国 (cn)",
|
||||||
|
"ru": "俄罗斯 (ru)",
|
||||||
"other": "其他"
|
"other": "其他"
|
||||||
},
|
},
|
||||||
"themeMode": "主题模式",
|
"themeMode": "主题模式",
|
||||||
"themeModes": {
|
"themeModes": {
|
||||||
"system": "遵循系统主题",
|
"system": "遵循系统主题",
|
||||||
"dark": "深色模式",
|
"dark": "深色模式",
|
||||||
"light": "灯光模式"
|
"light": "灯光模式",
|
||||||
|
"black": "黑色模式"
|
||||||
},
|
},
|
||||||
"enableAnalytics": "启用分析",
|
"enableAnalytics": "启用分析",
|
||||||
"enableAnalyticsMsg": "授予收集分析并发送崩溃报告以改进应用程序的权限",
|
"enableAnalyticsMsg": "授予收集分析并发送崩溃报告以改进应用程序的权限",
|
||||||
"trueBlack": "纯黑",
|
|
||||||
"autoStart": "开机启动",
|
"autoStart": "开机启动",
|
||||||
"silentStart": "无声启动",
|
"silentStart": "无声启动",
|
||||||
"openWorkingDir": "打开工作目录",
|
"openWorkingDir": "打开工作目录",
|
||||||
@@ -257,4 +260,4 @@
|
|||||||
"short_description": "自动,SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
|
"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此应用程序没有广告。分析和崩溃数据仅在用户在首次使用应用程序时明确同意的情况下发生。"
|
"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此应用程序没有广告。分析和崩溃数据仅在用户在首次使用应用程序时明确同意的情况下发生。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1265
changelog.md
1265
changelog.md
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ class AppView extends HookConsumerWidget with PresLogger {
|
|||||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
themeMode: theme.mode,
|
themeMode: theme.mode.flutterThemeMode,
|
||||||
theme: theme.light(),
|
theme: theme.light(),
|
||||||
darkTheme: theme.dark(),
|
darkTheme: theme.dark(),
|
||||||
title: Constants.appName,
|
title: Constants.appName,
|
||||||
|
|||||||
@@ -19,6 +19,5 @@ TranslationsEn translations(TranslationsRef ref) =>
|
|||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
AppTheme theme(ThemeRef ref) => AppTheme(
|
AppTheme theme(ThemeRef ref) => AppTheme(
|
||||||
ref.watch(themeModeNotifierProvider),
|
ref.watch(themeModeNotifierProvider),
|
||||||
ref.watch(trueBlackThemeNotifierProvider),
|
|
||||||
ref.watch(localeNotifierProvider).preferredFontFamily,
|
ref.watch(localeNotifierProvider).preferredFontFamily,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,38 @@
|
|||||||
import 'package:flex_color_scheme/flex_color_scheme.dart';
|
import 'package:flex_color_scheme/flex_color_scheme.dart';
|
||||||
import 'package:flutter/material.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
|
// mostly exact copy of flex color scheme 7.1's fabulous 12 theme
|
||||||
class AppTheme {
|
class AppTheme {
|
||||||
AppTheme(
|
AppTheme(
|
||||||
this.mode,
|
this.mode,
|
||||||
this.trueBlack,
|
|
||||||
this.fontFamily,
|
this.fontFamily,
|
||||||
);
|
);
|
||||||
|
|
||||||
final ThemeMode mode;
|
final AppThemeMode mode;
|
||||||
final bool trueBlack;
|
|
||||||
final String fontFamily;
|
final String fontFamily;
|
||||||
|
|
||||||
ThemeData light() {
|
ThemeData light() {
|
||||||
@@ -81,7 +103,7 @@ class AppTheme {
|
|||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
swapLegacyOnMaterial3: true,
|
swapLegacyOnMaterial3: true,
|
||||||
useMaterial3ErrorColors: true,
|
useMaterial3ErrorColors: true,
|
||||||
darkIsTrueBlack: trueBlack,
|
darkIsTrueBlack: mode.trueBlack,
|
||||||
surfaceMode: FlexSurfaceMode.highScaffoldLowSurface,
|
surfaceMode: FlexSurfaceMode.highScaffoldLowSurface,
|
||||||
// blendLevel: 1,
|
// blendLevel: 1,
|
||||||
subThemesData: const FlexSubThemesData(
|
subThemesData: const FlexSubThemesData(
|
||||||
|
|||||||
@@ -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/data/data_providers.dart';
|
||||||
import 'package:hiddify/utils/pref_notifier.dart';
|
import 'package:hiddify/utils/pref_notifier.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -10,29 +10,15 @@ class ThemeModeNotifier extends _$ThemeModeNotifier {
|
|||||||
late final _pref = Pref(
|
late final _pref = Pref(
|
||||||
ref.watch(sharedPreferencesProvider),
|
ref.watch(sharedPreferencesProvider),
|
||||||
"theme_mode",
|
"theme_mode",
|
||||||
ThemeMode.system,
|
AppThemeMode.system,
|
||||||
mapFrom: ThemeMode.values.byName,
|
mapFrom: AppThemeMode.values.byName,
|
||||||
mapTo: (value) => value.name,
|
mapTo: (value) => value.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ThemeMode build() => _pref.getValue();
|
AppThemeMode build() => _pref.getValue();
|
||||||
|
|
||||||
Future<void> update(ThemeMode value) {
|
Future<void> update(AppThemeMode 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) {
|
|
||||||
state = value;
|
state = value;
|
||||||
return _pref.update(value);
|
return _pref.update(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// ignore_for_file: avoid_manual_providers_as_generated_provider_dependency
|
// ignore_for_file: avoid_manual_providers_as_generated_provider_dependency
|
||||||
import 'package:hiddify/core/prefs/prefs.dart';
|
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:hiddify/utils/pref_notifier.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
@@ -67,6 +67,42 @@ final enableTunStore = PrefNotifier.provider("enable-tun", _default.enableTun);
|
|||||||
final setSystemProxyStore =
|
final setSystemProxyStore =
|
||||||
PrefNotifier.provider("set-system-proxy", _default.setSystemProxy);
|
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
|
@riverpod
|
||||||
ConfigOptions configOptions(ConfigOptionsRef ref) => ConfigOptions(
|
ConfigOptions configOptions(ConfigOptionsRef ref) => ConfigOptions(
|
||||||
executeConfigAsIs:
|
executeConfigAsIs:
|
||||||
@@ -88,4 +124,5 @@ ConfigOptions configOptions(ConfigOptionsRef ref) => ConfigOptions(
|
|||||||
clashApiPort: ref.watch(clashApiPortStore),
|
clashApiPort: ref.watch(clashApiPortStore),
|
||||||
enableTun: ref.watch(enableTunStore),
|
enableTun: ref.watch(enableTunStore),
|
||||||
setSystemProxy: ref.watch(setSystemProxyStore),
|
setSystemProxy: ref.watch(setSystemProxyStore),
|
||||||
|
rules: ref.watch(rulesProvider),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -179,10 +179,21 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Either<CoreServiceFailure, String>> watchLogs() {
|
Stream<Either<CoreServiceFailure, List<String>>> watchLogs() {
|
||||||
return singbox
|
return singbox.watchLogs(filesEditor.coreLogsPath).handleExceptions(
|
||||||
.watchLogs(filesEditor.coreLogsPath)
|
(error, stackTrace) {
|
||||||
.handleExceptions(CoreServiceFailure.unexpected);
|
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
|
@override
|
||||||
|
|||||||
62
lib/domain/singbox/box_log.dart
Normal file
62
lib/domain/singbox/box_log.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hiddify/core/prefs/prefs.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';
|
import 'package:hiddify/utils/platform_utils.dart';
|
||||||
|
|
||||||
part 'config_options.freezed.dart';
|
part 'config_options.freezed.dart';
|
||||||
@@ -19,13 +21,13 @@ class ConfigOptions with _$ConfigOptions {
|
|||||||
@Default(IPv6Mode.disable) IPv6Mode ipv6Mode,
|
@Default(IPv6Mode.disable) IPv6Mode ipv6Mode,
|
||||||
@Default("tcp://8.8.8.8") String remoteDnsAddress,
|
@Default("tcp://8.8.8.8") String remoteDnsAddress,
|
||||||
@Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy,
|
@Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy,
|
||||||
@Default("8.8.8.8") String directDnsAddress,
|
@Default("local") String directDnsAddress,
|
||||||
@Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy,
|
@Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy,
|
||||||
@Default(2334) int mixedPort,
|
@Default(2334) int mixedPort,
|
||||||
@Default(6450) int localDnsPort,
|
@Default(6450) int localDnsPort,
|
||||||
@Default(TunImplementation.mixed) TunImplementation tunImplementation,
|
@Default(TunImplementation.mixed) TunImplementation tunImplementation,
|
||||||
@Default(9000) int mtu,
|
@Default(9000) int mtu,
|
||||||
@Default("https://www.gstatic.com/generate_204") String connectionTestUrl,
|
@Default("http://cp.cloudflare.com/") String connectionTestUrl,
|
||||||
@IntervalConverter()
|
@IntervalConverter()
|
||||||
@Default(Duration(minutes: 10))
|
@Default(Duration(minutes: 10))
|
||||||
Duration urlTestInterval,
|
Duration urlTestInterval,
|
||||||
@@ -33,6 +35,9 @@ class ConfigOptions with _$ConfigOptions {
|
|||||||
@Default(6756) int clashApiPort,
|
@Default(6756) int clashApiPort,
|
||||||
@Default(false) bool enableTun,
|
@Default(false) bool enableTun,
|
||||||
@Default(true) bool setSystemProxy,
|
@Default(true) bool setSystemProxy,
|
||||||
|
@Default(false) bool bypassLan,
|
||||||
|
@Default(false) bool enableFakeDns,
|
||||||
|
List<Rule>? rules,
|
||||||
}) = _ConfigOptions;
|
}) = _ConfigOptions;
|
||||||
|
|
||||||
static ConfigOptions initial = ConfigOptions(
|
static ConfigOptions initial = ConfigOptions(
|
||||||
@@ -49,13 +54,6 @@ class ConfigOptions with _$ConfigOptions {
|
|||||||
_$ConfigOptionsFromJson(json);
|
_$ConfigOptionsFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LogLevel {
|
|
||||||
warn,
|
|
||||||
info,
|
|
||||||
debug,
|
|
||||||
trace,
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonEnum(valueField: 'key')
|
@JsonEnum(valueField: 'key')
|
||||||
enum IPv6Mode {
|
enum IPv6Mode {
|
||||||
disable("ipv4_only"),
|
disable("ipv4_only"),
|
||||||
|
|||||||
@@ -1,5 +1,40 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hiddify/core/prefs/locale_prefs.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 {
|
enum PerAppProxyMode {
|
||||||
off,
|
off,
|
||||||
include,
|
include,
|
||||||
@@ -26,11 +61,13 @@ enum PerAppProxyMode {
|
|||||||
enum Region {
|
enum Region {
|
||||||
ir,
|
ir,
|
||||||
cn,
|
cn,
|
||||||
|
ru,
|
||||||
other;
|
other;
|
||||||
|
|
||||||
String present(TranslationsEn t) => switch (this) {
|
String present(TranslationsEn t) => switch (this) {
|
||||||
ir => t.settings.general.regions.ir,
|
ir => t.settings.general.regions.ir,
|
||||||
cn => t.settings.general.regions.cn,
|
cn => t.settings.general.regions.cn,
|
||||||
|
ru => t.settings.general.regions.ru,
|
||||||
other => t.settings.general.regions.other,
|
other => t.settings.general.regions.other,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export 'box_log.dart';
|
||||||
export 'config_options.dart';
|
export 'config_options.dart';
|
||||||
export 'core_status.dart';
|
export 'core_status.dart';
|
||||||
export 'outbounds.dart';
|
export 'outbounds.dart';
|
||||||
|
|||||||
@@ -37,5 +37,7 @@ abstract interface class SingboxFacade {
|
|||||||
|
|
||||||
Stream<Either<CoreServiceFailure, CoreStatus>> watchCoreStatus();
|
Stream<Either<CoreServiceFailure, CoreStatus>> watchCoreStatus();
|
||||||
|
|
||||||
Stream<Either<CoreServiceFailure, String>> watchLogs();
|
Stream<Either<CoreServiceFailure, List<String>>> watchLogs();
|
||||||
|
|
||||||
|
TaskEither<CoreServiceFailure, Unit> clearLogs();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/app_update_notifier.dart';
|
||||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||||
import 'package:hiddify/features/common/window/window_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/profiles/notifier/notifier.dart';
|
||||||
import 'package:hiddify/features/system_tray/controller/system_tray_controller.dart';
|
import 'package:hiddify/features/system_tray/controller/system_tray_controller.dart';
|
||||||
import 'package:hiddify/services/service_providers.dart';
|
import 'package:hiddify/services/service_providers.dart';
|
||||||
@@ -24,10 +23,6 @@ void commonControllers(CommonControllersRef ref) {
|
|||||||
},
|
},
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
);
|
);
|
||||||
ref.listen(
|
|
||||||
logsNotifierProvider,
|
|
||||||
(previous, next) {},
|
|
||||||
);
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
connectivityControllerProvider,
|
connectivityControllerProvider,
|
||||||
(previous, next) {},
|
(previous, next) {},
|
||||||
|
|||||||
@@ -1,66 +1,133 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dartx/dartx.dart';
|
|
||||||
import 'package:hiddify/data/data_providers.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/features/logs/notifier/logs_state.dart';
|
||||||
|
import 'package:hiddify/utils/riverpod_utils.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
part 'logs_notifier.g.dart';
|
part 'logs_notifier.g.dart';
|
||||||
|
|
||||||
// TODO: rewrite
|
@riverpod
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
class LogsNotifier extends _$LogsNotifier with AppLogger {
|
class LogsNotifier extends _$LogsNotifier with AppLogger {
|
||||||
static const maxLength = 1000;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<LogsState> build() {
|
LogsState build() {
|
||||||
state = const AsyncData(LogsState());
|
ref.disposeDelay(const Duration(seconds: 20));
|
||||||
return ref.read(coreFacadeProvider).watchLogs().asyncMap(
|
state = const LogsState();
|
||||||
(event) async {
|
ref.onDispose(
|
||||||
_logs = [
|
() {
|
||||||
event.getOrElse((l) => throw l),
|
loggy.debug("disposing");
|
||||||
..._logs.takeFirst(maxLength - 1),
|
_listener?.cancel();
|
||||||
];
|
_listener = null;
|
||||||
return switch (state) {
|
|
||||||
// ignore: unused_result
|
|
||||||
AsyncData(:final value) => value.copyWith(logs: await _computeLogs()),
|
|
||||||
_ => LogsState(logs: await _computeLogs()),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
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));
|
final _debouncer = CallbackDebouncer(const Duration(milliseconds: 200));
|
||||||
LogLevel? _levelFilter;
|
LogLevel? _levelFilter;
|
||||||
String _filter = "";
|
String _filter = "";
|
||||||
|
|
||||||
Future<List<String>> _computeLogs() async {
|
Future<List<BoxLog>> _computeLogs() async {
|
||||||
if (_levelFilter == null && _filter.isEmpty) return _logs;
|
final logs = _logs.map(BoxLog.parse);
|
||||||
return _logs.where((e) {
|
if (_levelFilter == null && _filter.isEmpty) return logs.toList();
|
||||||
return _filter.isEmpty || e.contains(_filter);
|
return logs.where((e) {
|
||||||
|
return (_filter.isEmpty || e.message.contains(_filter)) &&
|
||||||
|
(_levelFilter == null ||
|
||||||
|
e.level == null ||
|
||||||
|
e.level!.index >= _levelFilter!.index);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void pause() {
|
||||||
if (state case AsyncData(:final value)) {
|
loggy.debug("pausing");
|
||||||
state = AsyncData(value.copyWith(logs: [])).copyWithPrevious(state);
|
_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) {
|
void filterMessage(String? filter) {
|
||||||
_filter = filter ?? '';
|
_filter = filter ?? '';
|
||||||
_debouncer(
|
_debouncer(
|
||||||
() async {
|
() async {
|
||||||
if (state case AsyncData(:final value)) {
|
if (state.logs case AsyncData()) {
|
||||||
state = AsyncData(
|
state = state.copyWith(
|
||||||
value.copyWith(
|
filter: _filter,
|
||||||
filter: _filter,
|
logs: AsyncData(await _computeLogs()),
|
||||||
logs: await _computeLogs(),
|
);
|
||||||
),
|
|
||||||
).copyWithPrevious(state);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -68,13 +135,11 @@ class LogsNotifier extends _$LogsNotifier with AppLogger {
|
|||||||
|
|
||||||
Future<void> filterLevel(LogLevel? level) async {
|
Future<void> filterLevel(LogLevel? level) async {
|
||||||
_levelFilter = level;
|
_levelFilter = level;
|
||||||
if (state case AsyncData(:final value)) {
|
if (state.logs case AsyncData()) {
|
||||||
state = AsyncData(
|
state = state.copyWith(
|
||||||
value.copyWith(
|
levelFilter: _levelFilter,
|
||||||
levelFilter: _levelFilter,
|
logs: AsyncData(await _computeLogs()),
|
||||||
logs: await _computeLogs(),
|
);
|
||||||
),
|
|
||||||
).copyWithPrevious(state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
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';
|
part 'logs_state.freezed.dart';
|
||||||
|
|
||||||
@@ -8,7 +9,8 @@ class LogsState with _$LogsState {
|
|||||||
const LogsState._();
|
const LogsState._();
|
||||||
|
|
||||||
const factory LogsState({
|
const factory LogsState({
|
||||||
@Default([]) List<String> logs,
|
@Default(AsyncLoading()) AsyncValue<List<BoxLog>> logs,
|
||||||
|
@Default(false) bool paused,
|
||||||
@Default("") String filter,
|
@Default("") String filter,
|
||||||
LogLevel? levelFilter,
|
LogLevel? levelFilter,
|
||||||
}) = _LogsState;
|
}) = _LogsState;
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import 'package:dartx/dartx.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fpdart/fpdart.dart';
|
import 'package:fpdart/fpdart.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hiddify/core/core_providers.dart';
|
import 'package:hiddify/core/core_providers.dart';
|
||||||
import 'package:hiddify/core/prefs/prefs.dart';
|
import 'package:hiddify/core/prefs/prefs.dart';
|
||||||
import 'package:hiddify/domain/clash/clash.dart';
|
|
||||||
import 'package:hiddify/domain/failures.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/features/logs/notifier/notifier.dart';
|
||||||
import 'package:hiddify/services/service_providers.dart';
|
import 'package:hiddify/services/service_providers.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:tint/tint.dart';
|
|
||||||
|
|
||||||
class LogsPage extends HookConsumerWidget with PresLogger {
|
class LogsPage extends HookConsumerWidget with PresLogger {
|
||||||
const LogsPage({super.key});
|
const LogsPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final t = ref.watch(translationsProvider);
|
final t = ref.watch(translationsProvider);
|
||||||
final asyncState = ref.watch(logsNotifierProvider);
|
final state = ref.watch(logsNotifierProvider);
|
||||||
final notifier = ref.watch(logsNotifierProvider.notifier);
|
final notifier = ref.watch(logsNotifierProvider.notifier);
|
||||||
|
|
||||||
final debug = ref.watch(debugModeNotifierProvider);
|
final debug = ref.watch(debugModeNotifierProvider);
|
||||||
final filesEditor = ref.watch(filesEditorServiceProvider);
|
final filesEditor = ref.watch(filesEditorServiceProvider);
|
||||||
|
|
||||||
|
final filterController = useTextEditingController(text: state.filter);
|
||||||
|
|
||||||
final List<PopupMenuEntry> popupButtons = debug || PlatformUtils.isDesktop
|
final List<PopupMenuEntry> popupButtons = debug || PlatformUtils.isDesktop
|
||||||
? [
|
? [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@@ -49,115 +49,146 @@ class LogsPage extends HookConsumerWidget with PresLogger {
|
|||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
switch (asyncState) {
|
return Scaffold(
|
||||||
case AsyncData(value: final state):
|
appBar: AppBar(
|
||||||
return Scaffold(
|
// TODO: fix height
|
||||||
appBar: AppBar(
|
toolbarHeight: 90,
|
||||||
// TODO: fix height
|
title: Text(t.logs.pageTitle),
|
||||||
toolbarHeight: 90,
|
actions: [
|
||||||
title: Text(t.logs.pageTitle),
|
if (state.paused)
|
||||||
actions: [
|
IconButton(
|
||||||
if (popupButtons.isNotEmpty)
|
onPressed: notifier.resume,
|
||||||
PopupMenuButton(
|
icon: const Icon(Icons.play_arrow),
|
||||||
itemBuilder: (context) {
|
tooltip: t.logs.resumeTooltip,
|
||||||
return popupButtons;
|
)
|
||||||
},
|
else
|
||||||
),
|
IconButton(
|
||||||
],
|
onPressed: notifier.pause,
|
||||||
bottom: PreferredSize(
|
icon: const Icon(Icons.pause),
|
||||||
preferredSize: const Size.fromHeight(36),
|
tooltip: t.logs.pauseTooltip,
|
||||||
child: Padding(
|
),
|
||||||
padding:
|
IconButton(
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
onPressed: notifier.clear,
|
||||||
child: Row(
|
icon: const Icon(Icons.clear_all),
|
||||||
children: [
|
tooltip: t.logs.clearTooltip,
|
||||||
Flexible(
|
),
|
||||||
child: TextFormField(
|
if (popupButtons.isNotEmpty)
|
||||||
onChanged: notifier.filterMessage,
|
PopupMenuButton(
|
||||||
decoration: InputDecoration(
|
itemBuilder: (context) {
|
||||||
isDense: true,
|
return popupButtons;
|
||||||
hintText: t.logs.filterHint,
|
},
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
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),
|
const Gap(16),
|
||||||
onChanged: (v) {
|
DropdownButton<Option<LogLevel>>(
|
||||||
if (v == null) return;
|
value: optionOf(state.levelFilter),
|
||||||
notifier.filterLevel(v.toNullable());
|
onChanged: (v) {
|
||||||
},
|
if (v == null) return;
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
notifier.filterLevel(v.toNullable());
|
||||||
borderRadius: BorderRadius.circular(4),
|
},
|
||||||
items: [
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
DropdownMenuItem(
|
borderRadius: BorderRadius.circular(4),
|
||||||
value: none(),
|
items: [
|
||||||
child: Text(t.logs.allLevelsFilter),
|
DropdownMenuItem(
|
||||||
),
|
value: none(),
|
||||||
...LogLevel.values.takeFirst(3).map(
|
child: Text(t.logs.allLevelsFilter),
|
||||||
(e) => DropdownMenuItem(
|
),
|
||||||
value: some(e),
|
...LogLevel.choices.map(
|
||||||
child: Text(e.name),
|
(e) => DropdownMenuItem(
|
||||||
),
|
value: some(e),
|
||||||
),
|
child: Text(e.name),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
),
|
||||||
itemCount: state.logs.length,
|
),
|
||||||
reverse: true,
|
body: switch (state.logs) {
|
||||||
itemBuilder: (context, index) {
|
AsyncData(value: final logs) => SelectionArea(
|
||||||
final log = state.logs[index];
|
child: ListView.builder(
|
||||||
return Column(
|
itemCount: logs.length,
|
||||||
mainAxisSize: MainAxisSize.min,
|
reverse: true,
|
||||||
children: [
|
itemBuilder: (context, index) {
|
||||||
ListTile(
|
final log = logs[index];
|
||||||
dense: true,
|
return Column(
|
||||||
subtitle: Text(log.strip()),
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
if (index != 0)
|
children: [
|
||||||
const Divider(
|
Padding(
|
||||||
indent: 16,
|
padding: const EdgeInsets.symmetric(
|
||||||
endIndent: 16,
|
horizontal: 16,
|
||||||
height: 4,
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
AsyncError(:final error) => CustomScrollView(
|
||||||
|
|
||||||
case AsyncError(:final error):
|
|
||||||
return Scaffold(
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
slivers: [
|
||||||
NestedTabAppBar(
|
|
||||||
title: Text(t.logs.pageTitle),
|
|
||||||
),
|
|
||||||
SliverErrorBodyPlaceholder(t.presentShortError(error)),
|
SliverErrorBodyPlaceholder(t.presentShortError(error)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
_ => const CustomScrollView(
|
||||||
|
|
||||||
case AsyncLoading():
|
|
||||||
return Scaffold(
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
slivers: [
|
||||||
NestedTabAppBar(
|
SliverLoadingBodyPlaceholder(),
|
||||||
title: Text(t.logs.pageTitle),
|
|
||||||
),
|
|
||||||
const SliverLoadingBodyPlaceholder(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
},
|
||||||
|
);
|
||||||
// TODO: remove
|
|
||||||
default:
|
|
||||||
return const Scaffold();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,13 +52,13 @@ class ConfigOptionsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(t.settings.config.logLevel),
|
title: Text(t.settings.config.logLevel),
|
||||||
subtitle: Text(options.logLevel.name),
|
subtitle: Text(options.logLevel.name.toUpperCase()),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final logLevel = await SettingsPickerDialog(
|
final logLevel = await SettingsPickerDialog(
|
||||||
title: t.settings.config.logLevel,
|
title: t.settings.config.logLevel,
|
||||||
selected: options.logLevel,
|
selected: options.logLevel,
|
||||||
options: LogLevel.values,
|
options: LogLevel.choices,
|
||||||
getTitle: (e) => e.name,
|
getTitle: (e) => e.name.toUpperCase(),
|
||||||
resetValue: _default.logLevel,
|
resetValue: _default.logLevel,
|
||||||
).show(context);
|
).show(context);
|
||||||
if (logLevel == null) return;
|
if (logLevel == null) return;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:hiddify/core/core_providers.dart';
|
|||||||
import 'package:hiddify/core/prefs/prefs.dart';
|
import 'package:hiddify/core/prefs/prefs.dart';
|
||||||
import 'package:hiddify/core/router/routes/routes.dart';
|
import 'package:hiddify/core/router/routes/routes.dart';
|
||||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||||
|
import 'package:hiddify/features/common/common.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
class AdvancedSettingTiles extends HookConsumerWidget {
|
class AdvancedSettingTiles extends HookConsumerWidget {
|
||||||
@@ -20,6 +21,7 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
const RegionPrefTile(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(t.settings.config.pageTitle),
|
title: Text(t.settings.config.pageTitle),
|
||||||
leading: const Icon(Icons.edit_document),
|
leading: const Icon(Icons.edit_document),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hiddify/core/core_providers.dart';
|
import 'package:hiddify/core/core_providers.dart';
|
||||||
import 'package:hiddify/core/prefs/prefs.dart';
|
import 'package:hiddify/core/prefs/prefs.dart';
|
||||||
import 'package:hiddify/features/common/common.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/services/auto_start_service.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -44,31 +43,34 @@ class GeneralSettingTiles extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(t.settings.general.themeMode),
|
title: Text(t.settings.general.themeMode),
|
||||||
subtitle: Text(
|
subtitle: Text(theme.mode.present(t)),
|
||||||
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,
|
|
||||||
),
|
|
||||||
leading: const Icon(Icons.light_mode),
|
leading: const Icon(Icons.light_mode),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await ref.read(themeModeNotifierProvider.notifier).update(
|
final selectedThemeMode = await showDialog<AppThemeMode>(
|
||||||
Theme.of(context).brightness == Brightness.light
|
context: context,
|
||||||
? ThemeMode.dark
|
builder: (context) {
|
||||||
: ThemeMode.light,
|
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) ...[
|
if (PlatformUtils.isDesktop) ...[
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.general.autoStart),
|
title: Text(t.settings.general.autoStart),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import 'package:combine/combine.dart';
|
|||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:fpdart/fpdart.dart';
|
import 'package:fpdart/fpdart.dart';
|
||||||
import 'package:hiddify/domain/connectivity/connectivity.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/gen/singbox_generated_bindings.dart';
|
||||||
import 'package:hiddify/services/singbox/shared.dart';
|
import 'package:hiddify/services/singbox/shared.dart';
|
||||||
import 'package:hiddify/services/singbox/singbox_service.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:loggy/loggy.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:watcher/watcher.dart';
|
||||||
|
|
||||||
final _logger = Loggy('FFISingboxService');
|
final _logger = Loggy('FFISingboxService');
|
||||||
|
|
||||||
@@ -301,33 +302,47 @@ class FFISingboxService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _logBuffer = <String>[];
|
||||||
|
int _logFilePosition = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<String> watchLogs(String path) {
|
Stream<List<String>> watchLogs(String path) async* {
|
||||||
var linesRead = 0;
|
yield await _readLogFile(File(path));
|
||||||
return Stream.periodic(
|
yield* Watcher(path, pollingDelay: const Duration(seconds: 1))
|
||||||
const Duration(seconds: 1),
|
.events
|
||||||
).asyncMap((_) async {
|
.asyncMap((event) async {
|
||||||
final result = await _readLogs(path, linesRead);
|
if (event.type == ChangeType.MODIFY) {
|
||||||
linesRead = result.$2;
|
await _readLogFile(File(path));
|
||||||
return result.$1;
|
}
|
||||||
}).transform(
|
return _logBuffer;
|
||||||
StreamTransformer.fromHandlers(
|
});
|
||||||
handleData: (data, sink) {
|
|
||||||
for (final item in data) {
|
|
||||||
sink.add(item);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<(List<String>, int)> _readLogs(String path, int from) async {
|
@override
|
||||||
return CombineWorker().execute(
|
TaskEither<String, Unit> clearLogs() {
|
||||||
|
return TaskEither(
|
||||||
() async {
|
() async {
|
||||||
final lines = await File(path).readAsLines();
|
_logBuffer.clear();
|
||||||
final to = lines.length;
|
return right(unit);
|
||||||
return (lines.sublist(from), to);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,11 +163,18 @@ class MobileSingboxService
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<String> watchLogs(String path) {
|
Stream<List<String>> watchLogs(String path) async* {
|
||||||
return _logsChannel.receiveBroadcastStream().map(
|
yield* _logsChannel
|
||||||
(event) {
|
.receiveBroadcastStream()
|
||||||
// loggy.debug("received log: $event");
|
.map((event) => (event as List).map((e) => e as String).toList());
|
||||||
return event as String;
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<String, Unit> clearLogs() {
|
||||||
|
return TaskEither(
|
||||||
|
() async {
|
||||||
|
await _methodChannel.invokeMethod("clear_logs");
|
||||||
|
return right(unit);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,5 +48,7 @@ abstract interface class SingboxService {
|
|||||||
|
|
||||||
Stream<String> watchStats();
|
Stream<String> watchStats();
|
||||||
|
|
||||||
Stream<String> watchLogs(String path);
|
Stream<List<String>> watchLogs(String path);
|
||||||
|
|
||||||
|
TaskEither<String, Unit> clearLogs();
|
||||||
}
|
}
|
||||||
|
|||||||
2
libcore
2
libcore
Submodule libcore updated: d410fe1c4b...7b367fe70c
@@ -1555,7 +1555,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "11.10.0"
|
version: "11.10.0"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||||
|
|||||||
24
pubspec.yaml
24
pubspec.yaml
@@ -10,8 +10,6 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.6
|
cupertino_icons: ^1.0.6
|
||||||
|
|
||||||
# internationalization
|
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
intl: ^0.18.1
|
intl: ^0.18.1
|
||||||
@@ -19,28 +17,18 @@ dependencies:
|
|||||||
slang_flutter: ^3.24.0
|
slang_flutter: ^3.24.0
|
||||||
timeago: ^3.5.0
|
timeago: ^3.5.0
|
||||||
flutter_localized_locales: ^2.0.5
|
flutter_localized_locales: ^2.0.5
|
||||||
|
|
||||||
# data & serialization
|
|
||||||
fpdart: ^1.1.0
|
fpdart: ^1.1.0
|
||||||
freezed_annotation: ^2.4.1
|
freezed_annotation: ^2.4.1
|
||||||
json_annotation: ^4.8.1
|
json_annotation: ^4.8.1
|
||||||
|
|
||||||
# state management
|
|
||||||
hooks_riverpod: ^2.4.3
|
hooks_riverpod: ^2.4.3
|
||||||
flutter_hooks: ^0.20.3
|
flutter_hooks: ^0.20.3
|
||||||
riverpod_annotation: ^2.2.0
|
riverpod_annotation: ^2.2.0
|
||||||
rxdart: ^0.27.7
|
rxdart: ^0.27.7
|
||||||
|
|
||||||
# persistence
|
|
||||||
drift: ^2.12.1
|
drift: ^2.12.1
|
||||||
sqlite3_flutter_libs: ^0.5.16
|
sqlite3_flutter_libs: ^0.5.16
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
|
|
||||||
# networking
|
|
||||||
dio: ^5.3.3
|
dio: ^5.3.3
|
||||||
web_socket_channel: ^2.4.0
|
web_socket_channel: ^2.4.0
|
||||||
|
|
||||||
# native
|
|
||||||
ffi: ^2.1.0
|
ffi: ^2.1.0
|
||||||
path_provider: ^2.1.1
|
path_provider: ^2.1.1
|
||||||
flutter_local_notifications: ^15.1.1
|
flutter_local_notifications: ^15.1.1
|
||||||
@@ -54,13 +42,9 @@ dependencies:
|
|||||||
url_launcher: ^6.1.14
|
url_launcher: ^6.1.14
|
||||||
vclibs: ^0.1.0
|
vclibs: ^0.1.0
|
||||||
launch_at_startup: ^0.2.2
|
launch_at_startup: ^0.2.2
|
||||||
|
|
||||||
# analytics
|
|
||||||
sentry_flutter: ^7.10.1
|
sentry_flutter: ^7.10.1
|
||||||
sentry_dart_plugin: ^1.6.2
|
sentry_dart_plugin: ^1.6.2
|
||||||
sentry_dio: ^7.10.1
|
sentry_dio: ^7.10.1
|
||||||
|
|
||||||
# utils
|
|
||||||
combine: ^0.5.6
|
combine: ^0.5.6
|
||||||
path: ^1.8.3
|
path: ^1.8.3
|
||||||
loggy: ^2.0.3
|
loggy: ^2.0.3
|
||||||
@@ -73,8 +57,7 @@ dependencies:
|
|||||||
accessibility_tools: ^1.0.0
|
accessibility_tools: ^1.0.0
|
||||||
neat_periodic_task: ^2.0.1
|
neat_periodic_task: ^2.0.1
|
||||||
retry: ^3.1.2
|
retry: ^3.1.2
|
||||||
|
watcher: ^1.1.0
|
||||||
# widgets
|
|
||||||
go_router: ^11.1.4
|
go_router: ^11.1.4
|
||||||
flex_color_scheme: ^7.3.1
|
flex_color_scheme: ^7.3.1
|
||||||
flutter_animate: ^4.2.0+1
|
flutter_animate: ^4.2.0+1
|
||||||
@@ -170,3 +153,8 @@ sentry:
|
|||||||
upload_sources: true
|
upload_sources: true
|
||||||
log_level: info
|
log_level: info
|
||||||
ignore_missing: true
|
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%
|
||||||
|
|||||||
Reference in New Issue
Block a user