diff --git a/.github/release_message.md b/.github/release_message.md
index 1396d951..001fed44 100644
--- a/.github/release_message.md
+++ b/.github/release_message.md
@@ -1,25 +1,27 @@
[](https://img.shields.io/github/downloads/hiddify/hiddify-next/RELEASE_TAG/)
-
-
-
**Release Highlights:**
--
--
+-
+-
تغییرات اصلی به فارسی
--
--
-
+-
+-
+
**Download based on your OS:**
+
+
**List of all changes:** [ChangeLog](https://github.com/hiddify/hiddify-next/blob/main/CHANGELOG.md)
-
-
-
-
-
-
diff --git a/.github/sync_translate.sh b/.github/sync_translate.sh
index d67c2b09..d80daa1b 100644
--- a/.github/sync_translate.sh
+++ b/.github/sync_translate.sh
@@ -14,6 +14,7 @@ python3 auto_translator.py en fa
python3 auto_translator.py en zh
# python3 auto_translator.py en pt
python3 auto_translator.py en ru
+python3 auto_translator.py en tr
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 6c85dd45..945646d3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,12 +4,13 @@ on:
branches:
- main
tags:
- - "v*"
+ - 'v*'
paths-ignore:
- - "**.md"
- - "docs/**"
- - ".github/**"
- - "!.github/workflows/build.yml"
+ - '**.md'
+ - 'docs/**'
+ - '.github/**'
+ - '!.github/workflows/build.yml'
+ - 'appcast.xml'
# pull_request:
# branches:
# - main
@@ -77,15 +78,15 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
- flutter-version: "3.13.x"
- channel: "stable"
+ flutter-version: '3.16.x'
+ channel: 'stable'
cache: true
- name: Setup Java
if: startsWith(matrix.platform,'android')
uses: actions/setup-java@v3
with:
- distribution: "zulu"
+ distribution: 'zulu'
java-version: 11
- name: Setup NDK
@@ -305,7 +306,7 @@ jobs:
uses: 8Mi-Tech/delete-release-assets-action@main
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- tag: "draft"
+ tag: 'draft'
deleteOnlyFromDrafts: false
- name: Create or Update Draft Release
@@ -315,8 +316,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: ./out/*
- name: "draft"
- tag_name: "draft"
+ name: 'draft'
+ tag_name: 'draft'
prerelease: true
upload-release:
@@ -356,7 +357,7 @@ jobs:
with:
prerelease: ${{ env.CHANNEL == 'dev' }}
tag_name: ${{ github.ref_name }}
- body_path: "./release.md"
+ body_path: './release.md'
files: ./out/*
- name: Create service_account.json
@@ -369,7 +370,8 @@ jobs:
packageName: app.hiddify.com
releaseName: ${{ github.ref }}
releaseFiles: ./hiddify-android-market.aab
- track: ${{ env.CHANNEL == 'dev' && 'beta' || 'internal' }}
+ # track: ${{ env.CHANNEL == 'dev' && 'beta' || 'internal' }}
+ track: 'beta'
# - name: "Upload app to TestFlight"
# uses: apple-actions/upload-testflight-build@v1
@@ -389,28 +391,27 @@ jobs:
id: version
uses: ashley-taylor/regex-property-action@v1.3
with:
- value: "${{ github.ref_name }}"
- regex: "^v|.dev$"
- flags: "" # Optional, defaults to "g"
- replacement: ""
+ value: '${{ github.ref_name }}'
+ regex: '^v|.dev$'
+ flags: 'gi' # Optional, defaults to "g"
+ replacement: ''
- name: Winget Publish
if: ${{ env.CHANNEL != 'dev' }}
uses: isaacrlevin/winget-publish-action@v.5
with:
- publish-type: "Update"
- user: "Hiddify"
- package: "Next"
+ publish-type: 'Update'
+ user: 'Hiddify'
+ package: 'Next'
version: ${{ steps.version.outputs.value }}
- url: "https://github.com/hiddify/hiddify-next/releases/download/${{ github.ref_name }}/hiddify-windows-x64-setup.zip"
+ url: 'https://github.com/hiddify/hiddify-next/releases/download/${{ github.ref_name }}/hiddify-windows-x64-setup.zip'
token: ${{ secrets.WINGET_TOKEN }}
-
- name: Winget Publish Beta
uses: isaacrlevin/winget-publish-action@v.5
with:
- publish-type: "Update"
- user: "Hiddify"
- package: "Next"
+ publish-type: 'Update'
+ user: 'Hiddify'
+ package: 'Next.Beta'
version: ${{ steps.version.outputs.value }}
- url: "https://github.com/hiddify/hiddify-next/releases/download/${{ github.ref_name }}/hiddify-windows-x64-setup.zip"
+ url: 'https://github.com/hiddify/hiddify-next/releases/download/${{ github.ref_name }}/hiddify-windows-x64-setup.zip'
token: ${{ secrets.WINGET_TOKEN }}
diff --git a/.github/workflows/dev-i.yml b/.github/workflows/dev-i.yml
new file mode 100644
index 00000000..1fad9d30
--- /dev/null
+++ b/.github/workflows/dev-i.yml
@@ -0,0 +1,196 @@
+name: dev i
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - 'v*'
+ paths-ignore:
+ - '**.md'
+ - 'docs/**'
+ - '.github/**'
+ - '!.github/workflows/*'
+ - 'appcast.xml'
+ # pull_request:
+ # branches:
+ # - main
+concurrency:
+ group: ${{ github.ref }}-${{ github.workflow }}
+ cancel-in-progress: true
+
+env:
+ CHANNEL: ${{ github.ref_type == 'tag' && endsWith(github.ref_name, 'dev') && 'dev' || github.ref_type != 'tag' && 'dev' || 'prod' }}
+ NDK_VERSION: r26b
+
+jobs:
+ build:
+ permissions: write-all
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # - platform: android-apk
+ # os: ubuntu-latest
+ # targets: apk
+
+ # - platform: android-aab
+ # os: ubuntu-latest
+ # targets: aab
+
+ # - platform: windows
+ # os: windows-latest
+ # aarch: amd64
+ # targets: exe
+ # filename: hiddify-windows-x64
+
+ # - platform: linux
+ # os: ubuntu-latest
+ # aarch: amd64
+ # targets: AppImage
+ # filename: hiddify-linux-x64
+
+ # - platform: macos
+ # os: macos-13
+ # aarch: universal
+ # targets: dmg
+ # filename: hiddify-macos-universal
+
+ - platform: ios
+ os: macos-13
+ aarch: universal
+ filename: hiddify-ios
+ targets: ipa
+
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: checkout
+ uses: actions/checkout@v3
+ - name: Install macos dmg needed tools
+ if: matrix.platform == 'macos' || matrix.platform == 'ios'
+ run: |
+ # xcode-select --install || softwareupdate --all --install --force
+ # brew uninstall --force $(brew list | grep python@) && brew cleanup || echo "python not installed"
+ brew uninstall --ignore-dependencies python@3.12
+ brew reinstall python@3.10
+ python3 -m pip install --upgrade setuptools pip
+ brew install create-dmg tree
+ npm install -g appdmg
+ - uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: '15.0.1'
+ - name: Setup Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ flutter-version: '3.16.x'
+ channel: 'stable'
+ cache: true
+
+
+ - name: Setup Flutter Distributor
+ if: ${{ !startsWith(matrix.platform,'android') }}
+ run: |
+ dart pub global activate flutter_distributor
+
+
+ - name: Get Geo Assets
+ run: |
+ make get-geo-assets
+
+ - name: Get Dependencies
+ run: |
+ make get
+
+ - name: Generate
+ run: |
+ make translate
+ make gen
+
+ - name: Get Libs ${{ matrix.platform }}
+ run: |
+ make ${{ matrix.platform }}-libs
+
+
+ - name: Setup Apple certificate and provisioning profile
+ if: startsWith(matrix.os,'macos')
+ env:
+ BUILD_CERTIFICATE_BASE64: ${{ secrets.APPLE_BUILD_CERTIFICATE_BASE64 }}
+ P12_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
+ BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.APPLE_BUILD_PROVISION_PROFILE_BASE64 }}
+ BUILD_PACKET_TUNNEL_PROVISION_PROFILE_BASE64: ${{ secrets.APPLE_BUILD_PACKET_TUNNEL_PROVISION_PROFILE_BASE64 }}
+ KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
+ run: |
+ # create variables
+ CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
+ PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
+ PP_PACKET_TUNNEL_PATH=$RUNNER_TEMP/build_pppt.mobileprovision
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
+
+ # import certificate and provisioning profile from secrets
+ echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
+ echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
+ echo -n "$BUILD_PACKET_TUNNEL_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PACKET_TUNNEL_PATH
+
+ # create temporary keychain
+ security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
+ security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
+
+ # import certificate to keychain
+ security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
+ security list-keychain -d user -s $KEYCHAIN_PATH
+
+ # apply provisioning profile
+ mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
+ cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
+ cp $PP_PACKET_TUNNEL_PATH ~/Library/MobileDevice/Provisioning\ Profiles
+
+ - name: Release ${{ matrix.platform }}
+ env:
+ SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
+ run: |
+ make ${{ matrix.platform }}-release
+
+ - name: Upload Debug Symbols
+ if: ${{ github.ref_type == 'tag' }}
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
+ SENTRY_DIST: ${{ matrix.platform == 'android-aab' && 'google-play' || 'general' }}
+ run: |
+ flutter packages pub run sentry_dart_plugin
+
+
+ - name: Copy to out unix
+ if: matrix.platform == 'linux' || matrix.platform == 'macos' || matrix.platform == 'ios'
+ run: |
+ ls -R dist/
+ mkdir out
+ mkdir tmp_out
+ EXT="${{ matrix.targets }}"
+ mv dist/*/*.$EXT tmp_out/${{matrix.filename}}.$EXT
+ chmod +x tmp_out/${{matrix.filename}}.$EXT
+ if [ "${{matrix.platform}}" == "linux" ];then
+ cp ./.github/help/linux/* tmp_out/
+ else
+ cp ./.github/help/mac-windows/* tmp_out/
+ fi
+ if [[ "${{matrix.platform}}" == 'ios' ]];then
+ mv tmp_out/${{matrix.filename}}.ipa bin/${{matrix.filename}}.ipa
+ else
+ cd tmp_out
+ 7z a ${{matrix.filename}}.zip ./
+ mv *.zip ../out/
+ fi
+
+ - name: Clean up keychain and provisioning profile
+ if: ${{ always() && startsWith(matrix.os,'macos')}}
+ run: |
+ security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
+ rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: artifact
+ path: ./out
+ retention-days: 2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfbff204..8592e5e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,21 +1,52 @@
# Changelog
-## Unreleased
+## [0.12.0.dev] - 2023-12-01
+
+### New Features and Improvements
+
+- Added soffchen to recommended geo assets
+
+### Bug Fixes
+
+- Refactored significant portions of the app
+- Fixed incorrect profile parsing when missing headers
+- Fixed geo assets bug where assets were deactivated
+- Updated sing-box to version 1.7.0
+- Fixed Chinese typography bug (thanks to [betaxab](https://github.com/betaxab))
+- Fixed localization mistakes in Russian. [PR#189](https://github.com/hiddify/hiddify-next/pull/189) by [jomertix](https://github.com/jomertix)
+
+## [0.11.1] - 2023-11-19
+
+### Bug Fixes
+
+- Fixed Android manifest bug.
+
+## [0.11.0] - 2023-11-19
### New Features and Improvements
- Changed Responsive UI Behavior
- Now app is responsive on all platforms with appropriate routing setup.
+- Added Simplified Service Modes
+ - Choose between VPN(Tun), System Proxy and Proxy only modes. (System Proxy available on desktop)
+- Added Share Functionality
+ - Share configuration as json(export to clipboard) or share subscription link as QR code.
- Redesigned System Tray on Desktop
- - Options have been simplified and a new mode selector is added for easier access to TUN and Proxy modes.
+ - Options have been simplified and a new mode selector and navigation options are added.
+- Added Privilege Checks for VPN(TUN) on Desktop
- Added Auto Connect on Start
- On desktop, app will try to connect to the last used profile on startup. (if last session was not explicitly disconnected by the user)
- Added AppCast Update Checker
- Checking for new versions of the app will use a more reliable approach on all platforms.
+- Added Geo Asset Settings
+ - Update geo assets and use recommended providers
- Added **winget** Release
- Now you're able to install and update Hiddify Next on Windows using [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/).
+- Added Turkish Translations. [PR#173](https://github.com/hiddify/hiddify-next/pull/173) by [Hasan Karlı](https://github.com/hasankarli)
- Changed in-app Toasts
- Updated Core Sing-box Version to 1.7.0
+- Improved Network Reliability While Adding/Updating Subscriptions
+- Improved QR Code Scanner
### Bug Fixes
@@ -25,7 +56,12 @@
- Fixed translator script. [PR#108](https://github.com/hiddify/hiddify-next/pull/108) by [Hirad Rasoolinejad](https://github.com/Hiiirad)
- Fixed localization mistakes in Chinese. [PR#113](https://github.com/hiddify/hiddify-next/pull/113) and [PR#123](https://github.com/hiddify/hiddify-next/pull/123) by [Nyar233](https://github.com/Nyar233)
- Fixed localization mistakes in Chinese Readme. [PR#137](https://github.com/hiddify/hiddify-next/pull/137) by [wldjdjsks](https://github.com/huajizhige)
-- Fixed localization mistakes in Chinese. [PR#138](https://github.com/hiddify/hiddify-next/pull/138) by [wldjdjsks](https://github.com/huajizhige)
+- Fixed localization mistakes in Chinese. [PR#138](https://github.com/hiddify/hiddify-next/pull/138) and [PR#165](https://github.com/hiddify/hiddify-next/pull/165) by [wldjdjsks](https://github.com/huajizhige)
+- Fixed localization mistakes in Russian. [PR#155](https://github.com/hiddify/hiddify-next/pull/155), [PR#162](https://github.com/hiddify/hiddify-next/pull/162) and [PR#169](https://github.com/hiddify/hiddify-next/pull/169) by [solokot](https://github.com/solokot)
+- Fixed linux build libs command. [PR#161](https://github.com/hiddify/hiddify-next/pull/161) by [Aloxaf](https://github.com/Aloxaf)
+- Fixed localization mistakes in Russian. [PR#164](https://github.com/hiddify/hiddify-next/pull/164) and [PR#168](https://github.com/hiddify/hiddify-next/pull/168) by [jomertix](https://github.com/jomertix)
+- Fixed localization mistakes in Chinese. [PR#179](https://github.com/hiddify/hiddify-next/pull/179) by [betaxab](https://github.com/betaxab)
+- Fixed localization mistakes in Chinese Readme. [PR#172](https://github.com/hiddify/hiddify-next/pull/172) by [Locas](https://github.com/Locas56227)
## [0.10.0] - 2023-10-27
@@ -52,4 +88,7 @@
- Fixed localization mistakes in Russian. [PR#95](https://github.com/hiddify/hiddify-next/pull/95) by [solokot](https://github.com/solokot)
- Fixed localization mistakes in Russian. [PR#74](https://github.com/hiddify/hiddify-next/pull/74) by [Elshad Guseynov](https://github.com/lifeindarkside)
-[0.10.0]: https://github.com/hiddify/hiddify-next/releases/tag/0.10.0
+[0.12.0.dev]: https://github.com/hiddify/hiddify-next/releases/tag/v0.12.0.dev
+[0.11.1]: https://github.com/hiddify/hiddify-next/releases/tag/v0.11.1
+[0.11.0]: https://github.com/hiddify/hiddify-next/releases/tag/v0.11.0
+[0.10.0]: https://github.com/hiddify/hiddify-next/releases/tag/v0.10.0
diff --git a/Makefile b/Makefile
index 9dbd4bbf..333c0284 100644
--- a/Makefile
+++ b/Makefile
@@ -114,7 +114,7 @@ build-windows-libs:
make -C libcore -f Makefile windows-amd64 && mv $(BINDIR)/$(CORE_NAME)-windows-amd64.dll $(DESKTOP_OUT)/libcore.dll
build-linux-libs:
- make -C libcore -f Makefile linux-amd64 && mv $(BINDIR)/$(CORE_NAME)-linux-amd64.dll $(DESKTOP_OUT)/libcore.so
+ make -C libcore -f Makefile linux-amd64 && mv $(BINDIR)/$(CORE_NAME)-linux-amd64.so $(DESKTOP_OUT)/libcore.so
build-macos-libs:
make -C libcore -f Makefile macos-universal && mv $(BINDIR)/$(CORE_NAME)-macos-universal.dylib $(DESKTOP_OUT)/libcore.dylib
@@ -133,12 +133,10 @@ release: # Create a new tag for release.
VERSION_STR="$${VERSION_ARRAY[0]}.$${VERSION_ARRAY[1]}.$${VERSION_ARRAY[2]}" && \
BUILD_NUMBER=$$(( $${VERSION_ARRAY[0]} * 10000 + $${VERSION_ARRAY[1]} * 100 + $${VERSION_ARRAY[2]} )) && \
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 -d $${TAG} > /dev/null && \
git add pubspec.yaml CHANGELOG.md && \
- make sync_translate && \
- git add assets/translations/* && \
git commit -m "release: version $${TAG}" && \
echo "creating git tag : v$${TAG}" && \
git tag v$${TAG} && \
diff --git a/README.md b/README.md
index fa5b6f06..476871d9 100644
--- a/README.md
+++ b/README.md
@@ -6,9 +6,14 @@

+
+
+
-[](https://play.google.com/store/apps/details?id=app.hiddify.com) [](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/)[](https://github.com/hiddify/hiddify-next/)[](https://github.com/hiddify/hiddify-next/)
+[](https://play.google.com/store/apps/details?id=app.hiddify.com) [](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/)
[](https://www.youtube.com/@hiddify)[](https://telegram.dog/hiddify)[](https://telegram.dog/hiddify_board/5)
@@ -18,9 +23,9 @@
## What is Hiddify-Next?
-
A multi-platform client based on Sing-box that serves as a universal proxy tool-chain. This app offers a wide range of capabilities, which are listed below. It also supports a large number of protocols. The app is free to use, ad-free, and open-source. It provides a secure and private tool for getting access to the free internet.
+
A multi-platform auto-client based on Sing-box that serves as a universal proxy tool-chain. This app offers a wide range of capabilities, which are listed below. It also supports a large number of protocols. The app is free to use, ad-free, and open-source. It provides a secure and private tool for getting access to the free internet.
-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 about development, you can read through our [Contribution Guidelines](CONTRIBUTING.md) .

diff --git a/README_cn.md b/README_cn.md
index 9af45c8c..f92eff6c 100644
--- a/README_cn.md
+++ b/README_cn.md
@@ -1,55 +1,61 @@
-
-
-[** فارسی**](README_fa.md) [**Русский 🇷🇺**](README_ru.md) [**English 🇺🇸**](README.md)
+
+
+[** فارسی**](README_fa.md) [**Русский 🇷🇺**](README_ru.md) [**English 🇺🇸**](README.md)
+

-
-[](https://play.google.com/store/apps/details?id=app.hiddify.com) [](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/)[](https://github.com/hiddify/hiddify-next/)[](https://github.com/hiddify/hiddify-next/)
+
+[](https://play.google.com/store/apps/details?id=app.hiddify.com) [](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/)
[](https://www.youtube.com/@hiddify)[](https://telegram.dog/hiddify)[](https://telegram.dog/hiddify_board/5)
+
+
## Hiddify-Next 是什么?
-基于 [Sing-box](https://github.com/SagerNet/sing-box) 的多平台客户端,用作通用代理工具链。 该应用程序提供了广泛的功能,如下所列。 它还支持大量协议。 该应用程序免费使用、无广告且开源。 它提供了一个安全且私密的工具来访问免费互联网。
-该应用程序是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 欲了解更多信息,您可以阅读我们的开发贡献指南。
+
一个基于 Sing-box 的跨平台自动客户端,用作通用代理工具链。该应用提供了广泛的功能,如下所列。它还支持大量协议。该应用免费使用、无广告且开源。它为访问自由互联网提供了一个安全且私密的工具。
+该应用是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 有关开发的更多信息,您可以阅读我们的[贡献指南](CONTRIBUTING.md)。
-

+

+
-## 🚀 主要特点
-⭐ 简单的用户界面易于使用
-✈️ 多平台:Android、Windows、Linux 和 macOS(欢迎 iOS 的 PR)
+## 🚀 主要功能
+
+⭐ 简单易用的用户界面
+
+✈️ 跨平台:Android、Windows、Linux 和 macOS(欢迎 iOS 的 PR)
🔍 基于延迟的自动选择
-🟡 广泛的协议支持:ECH、Sing-box、V2ray、Xray、Vless、Vmess、Trojan、Trojan with websocket、Reality、TUIC、Hysteria、Hysteria2、ShadowTLS、SSH、Clash、Clash meta
+🟡 广泛的协议支持:**ECH, Sing-box, V2ray, Xray, Vless, Vmess, Reality, TUIC, Hysteria, ShadowTLS, SSH, Clash, Clash meta**
-🟡 支持多种订阅链接导入:Clash、Clash meta、Sing-box 和 Shadowsocks
+🟡 支持多种订阅链接导入: **Clash, Clash meta, Sing-box and Shadowsocks**
-🔄 自动订阅更新
+🔄 自动更新订阅
-🔎 显示个人资料信息,包括剩余天数和流量使用情况
+🔎 显示包含了剩余天数和流量使用情况的配置文件信息
💻 完全免费,没有任何广告和干扰
-🛡 开源、安全且社区驱动
+🛡 开源、安全且由社区驱动
🌙 深色和浅色模式
-⚙ 与所有代理管理面板的节点兼容
+⚙ 兼容所有的代理管理面板
-⭐ 适用于伊朗、中国、俄罗斯等国家配置
+⭐ 适用于伊朗、中国、俄罗斯或其他国家的配置
-📱 可在 Google Play 上获取
+📱 可在 [Google Play](https://play.google.com/store/apps/details?id=app.hiddify.com) 上获取
## 下载
@@ -58,7 +64,7 @@
| 操作系统 |
- 下载 |
+ 下载链接 |
@@ -66,9 +72,9 @@
Android |


- 
- 
-
+ 
+ 
+
|
@@ -83,58 +89,58 @@
| Linux |
- |
+ |
+
+
## 安装和教程
-请在 [wiki 页面](https://github.com/hiddify/hiddify-next/wiki) 上查找教程信息。
+请在 [wiki 页面](https://github.com/hiddify/hiddify-next/wiki) 上获取教程信息。
## 改进翻译
-您可以使用以下链接轻松地为该项目做出贡献以改进翻译:
-- [简体中文](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=zh)
-- [英语](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en)
+您可以使用以下链接轻松地为该项目改进翻译以做出贡献:
+ - [英语](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en)
- [波斯语](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=fa)
- [俄语](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=ru)
+- [简体中文](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=zh)
## 致谢
+
- [Sing-box](https://github.com/SagerNet/sing-box)
- [Sing-box for Android](https://github.com/SagerNet/sing-box-for-android)
- [Clash](https://github.com/Dreamacro/clash)
- [Clash Meta](https://github.com/MetaCubeX/Clash.Meta)
- [FClash](https://github.com/Fclash/Fclash)
-- [其他](./pubspec.yaml)
-## 捐赠与支持
+- [Others](./pubspec.yaml)
-支持我们的最简单方法是单击此页面顶部的 Star (⭐)。
+## 捐赠和支持
+
+支持我们的最简单方法是单击此页面顶部的Star (⭐) 。
-
+
-我们的服务也需要资金支持。我们所有的活动都是自愿进行的,资金支持将用于项目的开发和维护。您可以在 [此处](https://github.com/hiddify/hiddify-manager/wiki/support) 查看我们的支持地址。
+我们的服务也需要经济支持。我们所有的活动都是志愿性质的,经济支持将被用于项目的发展。您可以在 [这里](https://github.com/hiddify/hiddify-server/wiki/support) 查看我们的支持地址。
+## 合作与联系信息
+我们需要您的合作来推动这个项目的发展。如果您在这些领域是专家,请不要犹豫联系我们并提及您的技能。
+
+- Flutter 开发
+- Swift 开发
+- Kotlin 开发
+- Go 开发
+
-## 协作和联系信息
-我们需要您的协作才能继续开发并维护此项目。如果您是这些领域的专家,请随时与我们联系 并提及你的技能。
-
-* Flutter 开发
-* Swift 开发
-* Kotlin 开发
-* Go 开发
-
-
-
-
-
[](mailto:contribute@hiddify.com)
[](https://telegram.dog/hiddify)
[](https://telegram.dog/hiddify_board)
@@ -144,9 +150,10 @@
- 感谢所有参与该项目的人。包括以下列出的人,和更多其他来自 Github 的人。你们对我们的意义非常重大。 ♥
+我们非常感谢所有参与此项目的人,包括在这里的一些人和在Github之外的。这对我们来说意义重大。♥
-
+
+
@@ -156,4 +163,3 @@
使用 Contrib.Rocks 制作
-
diff --git a/README_fa.md b/README_fa.md
index 060fd675..d8d23408 100644
--- a/README_fa.md
+++ b/README_fa.md
@@ -7,18 +7,17 @@

-
-
-[](https://play.google.com/store/apps/details?id=app.hiddify.com) [](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/)[](https://github.com/hiddify/hiddify-next/)[](https://github.com/hiddify/hiddify-next/)
-[](https://www.youtube.com/@hiddify)[](https://telegram.dog/hiddify)[](https://telegram.dog/hiddify_board)
+
+[](https://play.google.com/store/apps/details?id=app.hiddify.com) [](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/)
+[](https://www.youtube.com/@hiddify)[](https://telegram.dog/hiddify)[](https://telegram.dog/hiddify_board/5)
## هیدیفاینکست چیست؟
-یک کلاینت مالتیپلتفرم مبتنی بر [سینگباکس](https://github.com/SagerNet/sing-box) که به عنوان یک ابزار عمومی برای پروکسی عمل میکند. این برنامه طیف گستردهای از قابلیتها را ارائه میدهد که در زیر لیست شده است. همچنین از تعداد زیادی پروتکل پشتیبانی میکند. این برنامه رایگان، بدون آگهی و منبع باز است. این یک ابزار امن و مطمئن برای دسترسی به اینترنت رایگان فراهم میکند.
+یک کلاینت خودکار مالتیپلتفرم مبتنی بر [سینگباکس](https://github.com/SagerNet/sing-box) که به عنوان یک ابزار عمومی برای پروکسی عمل میکند. این برنامه طیف گستردهای از قابلیتها را ارائه میدهد که در زیر لیست شده است. همچنین از تعداد زیادی پروتکل پشتیبانی میکند. این برنامه رایگان، بدون آگهی و منبع باز است. این یک ابزار امن و مطمئن برای دسترسی به اینترنت رایگان فراهم میکند.
-این برنامه با استفاده از [Flutter](https://flutter.dev/) و [Go](https://go.dev/) توسعه یافته است. برای اطلاعات بیشتر در خصوص توسعه میتوانید [دستورالعملهای مشارکت](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) در پروژه ما را مطالعه نمایید.
+این برنامه با استفاده از [Flutter](https://flutter.dev/) و [Go](https://go.dev/) توسعه یافته است. برای اطلاعات بیشتر در خصوص توسعه میتوانید [دستورالعملهای مشارکت](CONTRIBUTING.md) در پروژه ما را مطالعه نمایید.
diff --git a/README_ru.md b/README_ru.md
index c828d642..7a1bf5d3 100644
--- a/README_ru.md
+++ b/README_ru.md
@@ -9,15 +9,15 @@
-[](https://play.google.com/store/apps/details?id=app.hiddify.com) [](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/)[](https://github.com/hiddify/hiddify-next/)[](https://github.com/hiddify/hiddify-next/)
+[](https://play.google.com/store/apps/details?id=app.hiddify.com) [](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/releases/)[](https://github.com/hiddify/hiddify-next/)
[](https://www.youtube.com/@hiddify)[](https://telegram.dog/hiddify)[](https://telegram.dog/hiddify_board/5)
## Что такое 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/). Для получения дополнительной информации о разработке вы можете прочитать наши [Рекомендации по участию](CONTRIBUTING.md) .

diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ced8b599..18fd9dd8 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,7 @@
-
+
+
@@ -115,4 +116,4 @@
android:name="flutterEmbedding"
android:value="2" />
-
+
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt
index cf5717ad..95a58b42 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt
@@ -6,6 +6,7 @@ import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
+import io.flutter.plugin.common.JSONMethodCodec
class EventHandler : FlutterPlugin {
@@ -22,8 +23,8 @@ class EventHandler : FlutterPlugin {
private var alertsObserver: Observer
? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
- statusChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_STATUS)
- alertsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_ALERTS)
+ statusChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_STATUS, JSONMethodCodec.INSTANCE)
+ alertsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_ALERTS, JSONMethodCodec.INSTANCE)
statusChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt
index d40ae0ba..f2656c25 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt
@@ -2,6 +2,7 @@ package com.hiddify.hiddify
import android.util.Log
import com.hiddify.hiddify.bg.BoxService
+import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
@@ -24,6 +25,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
enum class Trigger(val method: String) {
ParseConfig("parse_config"),
ChangeConfigOptions("change_config_options"),
+ GenerateConfig("generate_config"),
Start("start"),
Stop("stop"),
Restart("restart"),
@@ -70,6 +72,21 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
}
}
+ Trigger.GenerateConfig.method -> {
+ scope.launch {
+ result.runCatching {
+ val args = call.arguments as Map<*, *>
+ val path = args["path"] as String
+ val options = Settings.configOptions
+ if (options.isBlank() || path.isBlank()) {
+ error("blank properties")
+ }
+ val config = BoxService.buildConfig(path, options)
+ success(config)
+ }
+ }
+ }
+
Trigger.Start.method -> {
scope.launch {
result.runCatching {
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt
index 41fa05e1..6268b739 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt
@@ -72,6 +72,10 @@ class BoxService(
}
}
+ fun buildConfig(path: String, options: String):String {
+ return Mobile.buildConfig(path, options)
+ }
+
fun start() {
val intent = runBlocking {
withContext(Dispatchers.IO) {
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt
index 9345bef7..7ce41726 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt
@@ -10,6 +10,7 @@ import io.nekohasekai.libbox.NetworkInterfaceIterator
import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.StringIterator
import io.nekohasekai.libbox.TunOptions
+import io.nekohasekai.libbox.WIFIState
import java.net.Inet6Address
import java.net.InetSocketAddress
import java.net.InterfaceAddress
@@ -101,6 +102,10 @@ interface PlatformInterfaceWrapper : PlatformInterface {
override fun clearDNSCache() {
}
+ override fun readWIFIState(): WIFIState? {
+ return null
+ }
+
private class InterfaceArray(private val iterator: Enumeration) :
NetworkInterfaceIterator {
diff --git a/appcast.xml b/appcast.xml
index 8ccbcb8d..ad44f335 100644
--- a/appcast.xml
+++ b/appcast.xml
@@ -3,31 +3,32 @@
Release
-
- Version 0.10.0
- Sat, 28 Oct 2023 12:00:00 +0000
-
+ Version 0.11.1
+ Sun, 20 Nov 2023 22:00:00 +0000
+
-
- Version 0.10.0
- Sat, 28 Oct 2023 12:00:00 +0000
+ Version 0.11.1
+ Sun, 20 Nov 2023 22:00:00 +0000
+ url="https://github.com/hiddify/hiddify-next/releases/download/v0.11.1/hiddify-windows-x64-setup.zip"
+ sparkle:version="0.11.1" sparkle:os="windows" />
-
- Version 0.10.0
- Sat, 28 Oct 2023 12:00:00 +0000
+ Version 0.11.1
+ Sun, 20 Nov 2023 22:00:00 +0000
+ url="https://github.com/hiddify/hiddify-next/releases/download/v0.11.1/hiddify-macos-universal.zip"
+ sparkle:version="0.11.1" sparkle:os="macos" />
-
- Version 0.10.0
- Sat, 28 Oct 2023 12:00:00 +0000
+ Version 0.11.1
+ Sun, 20 Nov 2023 22:00:00 +0000
+ url="https://github.com/hiddify/hiddify-next/releases/download/v0.11.1/hiddify-linux-x64.zip"
+ sparkle:version="0.11.1" sparkle:os="linux" />
\ No newline at end of file
diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json
index ad62901c..6bb29c05 100644
--- a/assets/translations/strings_en.i18n.json
+++ b/assets/translations/strings_en.i18n.json
@@ -57,6 +57,12 @@
"shortBtnTxt": "New Profile",
"fromClipboard": "Add From Clipboard",
"scanQr": "Scan QR code",
+ "qrScanner": {
+ "permissionDeniedError": "Permission denied",
+ "unexpectedError": "Something went wrong",
+ "torchSemanticLabel": "Flash light",
+ "facingSemanticLabel": "Camera facing"
+ },
"manually": "Manual Entry",
"addingProfileMsg": "Adding Profile",
"failureMsg": "Failed to add profile"
@@ -67,6 +73,14 @@
"failureMsg": "Failed to update profile",
"successMsg": "Profile updated successfully"
},
+ "share": {
+ "buttonText": "Share",
+ "exportToClipboardSuccess": "Exported to clipboard",
+ "exportSubLinkToClipboard": "Export subscription link to clipboard",
+ "subLinkQrCode": "Subscription link QR code",
+ "exportConfigToClipboard": "Export configuration to clipboard",
+ "exportConfigToClipboardSuccess": "Configuration copied to clipboard"
+ },
"edit": {
"buttonTxt": "Edit",
"selectActiveTxt": "Select active profile"
@@ -203,6 +217,16 @@
"enableFakeDns": "Enable Fake DNS",
"bypassLan": "Bypass Lan",
"strictRoute": "Strict Route"
+ },
+ "geoAssets": {
+ "pageTitle": "Routing Assets",
+ "version": "Version ${version}",
+ "fileMissing": "File Missing",
+ "update": "Update",
+ "download": "Download",
+ "failureMsg": "Failed to update asset",
+ "successMsg": "Successfully updated asset",
+ "addRecommended": "Add Recommended Assets"
}
},
"about": {
@@ -227,6 +251,7 @@
"tray": {
"dashboard": "Dashboard",
"quit": "Quit",
+ "open": "Open",
"status": {
"connect": "Connect",
"connecting": "Connecting",
@@ -245,6 +270,8 @@
"serviceNotRunning": "Service is not running",
"missingPrivilege": "Missing Privilege",
"missingPrivilegeMsg": "VPN mode requires administrator privileges. Either relaunch the app as administrator or change service mode.",
+ "missingGeoAssets": "Missing Geo Assets",
+ "missingGeoAssetsMsg": "Geo assets are missing. consider changing active asset or download selected one in the settings.",
"invalidConfigOptions": "Invalid configuration options",
"invalidConfig": "Invalid Configuration",
"create": "Service creation error",
@@ -268,6 +295,11 @@
"badResponse": "Bad response",
"connectionError": "Connection error",
"badCertificate": "Bad certificate"
+ },
+ "geoAssets": {
+ "unexpected": "Unexpected Error",
+ "notUpdate": "No Update Available",
+ "activeNotFound": "Active Geo Asset Not Found"
}
},
"play": {
diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json
index 6e4f06de..24a7dfce 100644
--- a/assets/translations/strings_fa.i18n.json
+++ b/assets/translations/strings_fa.i18n.json
@@ -57,6 +57,12 @@
"shortBtnTxt": "افزودن",
"fromClipboard": "افزودن از کلیپبورد",
"scanQr": "اسکن QR کد",
+ "qrScanner": {
+ "permissionDeniedError": "اجازه رد شد",
+ "unexpectedError": "خطایی رخ داده",
+ "torchSemanticLabel": "چراغ فلاش",
+ "facingSemanticLabel": "جهت دوربین"
+ },
"manually": "افزودن دستی",
"addingProfileMsg": "در حال افزودن پروفایل",
"failureMsg": "در افزودن پروفایل خطایی رخ داد"
@@ -67,6 +73,14 @@
"failureMsg": "در بروزرسانی پروفایل خطایی رخ داد",
"successMsg": "پروفایل با موفقیت بروزرسانی شد"
},
+ "share": {
+ "buttonText": "اشتراک گذاری",
+ "exportToClipboardSuccess": "به کلیپ بورد اضافه شد",
+ "exportSubLinkToClipboard": "افزودن لینک اشتراک به کلیپ بورد",
+ "subLinkQrCode": "کد QR لینک اشتراک",
+ "exportConfigToClipboard": "افزودن پیکربندی به کلیپ بورد",
+ "exportConfigToClipboardSuccess": "پیکربندی در کلیپ بورد کپی شد"
+ },
"edit": {
"buttonTxt": "ویرایش",
"selectActiveTxt": "انتخاب پروفایل فعال"
@@ -203,6 +217,16 @@
"enableFakeDns": "Enable Fake DNS",
"bypassLan": "Bypass Lan",
"strictRoute": "Strict Route"
+ },
+ "geoAssets": {
+ "pageTitle": "فایلهای مسیریابی",
+ "version": "نسخه ${version}",
+ "fileMissing": "فایل موجود نیست",
+ "update": "به روز رسانی",
+ "download": "دانلود",
+ "failureMsg": "دارایی به روز نشد",
+ "successMsg": "دارایی با موفقیت به روز شد",
+ "addRecommended": "اضافه کردن دارایی های توصیه شده"
}
},
"about": {
@@ -227,6 +251,7 @@
"tray": {
"dashboard": "داشبورد",
"quit": "خروج",
+ "open": "باز کن",
"status": {
"connect": "اتصال",
"connecting": "در حال اتصال",
@@ -248,7 +273,9 @@
"invalidConfigOptions": "تنظیمات کانفیگ نامعتبر",
"invalidConfig": "کانفیگ غیر معتبر",
"create": "در ایجاد سرویس خطایی رخ داده",
- "start": "در راهاندازی سرویس خطایی رخ داده"
+ "start": "در راهاندازی سرویس خطایی رخ داده",
+ "missingGeoAssets": "دارایی های جغرافیایی از دست رفته",
+ "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید."
},
"connectivity": {
"unexpected": "خطای غیرمنتظره",
@@ -268,6 +295,11 @@
"badResponse": "پاسخ نامعتبر",
"connectionError": "خطای اتصال",
"badCertificate": "خطای اعتبار سنجی"
+ },
+ "geoAssets": {
+ "unexpected": "خطای غیرمنتظره",
+ "notUpdate": "به روز رسانی موجود نیست",
+ "activeNotFound": "Active Geo Asset یافت نشد"
}
},
"play": {
@@ -275,4 +307,4 @@
"short_description": "Auto, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
"full_description": "هدف اصلی HiddifyNext ارائه یک کلاینت تونل زنی ایمن، کاربرپسند و کارآمد است. این به شما امکان می دهد تا با استفاده از مجوز VPN-Service، تمام ترافیک یا ترافیک برنامه انتخابی را به یک سرور راه دور مورد نظر خود هدایت کنید.\n\nتوجه: ما هیچ سروری ارائه نمی دهیم. کاربران موظفند با استفاده از سرورهای خود میزبان یا سرورهای مورد اعتماد، فعالیتهای آنلاین خود را خصوصی نگه دارند.\n \nما از سرورهایی با موارد زیر پشتیبانی می کنیم:\n- لینک اشتراک V2ray/Xray معمولی\n- لینک اشتراک کلش\n- لینک اشتراک Sing-Box\n\nویژگی های منحصر به فرد ما چیست؟\n - کاربر پسند\n - بهینه و سریع\n - به طور خودکار LowestPing را انتخاب کنید\n - نمایش اطلاعات استفاده کاربر\n - به راحتی لینک فرعی را با یک کلیک با استفاده از دیپ لینک وارد کنید\n - رایگان و بدون تبلیغات\n - به راحتی پیوندهای فرعی کاربر را تغییر دهید\n - بیشتر و بیشتر\n\nحمایت کردن:\n- تمام پروتکل های پشتیبانی شده توسط Sing-Box\n- VLESS + xtls \n- VMESS\n- تروجان\n- ShoadowSocks\n- ریالیتی\n- V2ray\n- هیستریا 2\n- TUIC\n- SSH\n- ShadowTLS\n\n\nکد منبع در https://github.com/hiddify/Hiddify-Next وجود دارد\nهسته برنامه بر اساس sing-box منبع باز است.\n\nتوضیحات مجوز:\n- سرویس VPN: از آنجا که هدف این برنامه ارائه یک کلاینت تونل زنی ایمن، کاربر پسند و کارآمد است، ما به این مجوز نیاز داریم تا بتوانیم ترافیک را از طریق تونل به سرور راه دور هدایت کنیم.\n- QUERY ALL PACKAGES: این مجوز برای اجازه دادن به کاربران برای گنجاندن یا حذف برنامه های کاربردی خاص برای تونل زدن استفاده می شود.\n- دریافت بوت تکمیل شد: این مجوز را می توان از تنظیمات برنامه فعال یا غیرفعال کرد تا این برنامه پس از بوت شدن دستگاه فعال شود.\n- اعلان های ارسالی: این مجوز ضروری است زیرا ما از یک سرویس پیش زمینه برای اطمینان از عملکرد مداوم سرویس VPN استفاده می کنیم.\n- این برنامه بدون تبلیغات است. تجزیه و تحلیل و داده های اشکال فقط با رضایت صریح کاربر در اولین استفاده از برنامه اتفاق می افتد."
}
-}
\ No newline at end of file
+}
diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json
index b7d6a63a..e8223be9 100644
--- a/assets/translations/strings_ru.i18n.json
+++ b/assets/translations/strings_ru.i18n.json
@@ -14,7 +14,7 @@
"addToClipboard": "Копировать в буфер обмена"
},
"intro": {
- "termsAndPolicyCaution(rich)": "Продолжая, вы соглашаетесь с ${tap(@:about.termsAndConditions)}",
+ "termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}",
"start": "Начать"
},
"home": {
@@ -28,10 +28,10 @@
"connected": "Подключено"
},
"stats": {
- "traffic": "Скорость",
+ "traffic": "Текущий трафик",
"trafficTotal": "Трафик",
- "uplink": "Исходящий канал",
- "downlink": "Входящий канал"
+ "uplink": "Скорость отправки",
+ "downlink": "Скорость загрузки"
}
},
"profile": {
@@ -44,29 +44,43 @@
"traffic": "Трафик",
"updatedTimeAgo": "Обновлено ${timeago}",
"remainingDuration": "Ещё ${duration} дн.",
- "remainingTrafficSemanticLabel": "${consumed} из ${total} использованного трафика.",
+ "remainingTrafficSemanticLabel": "Использовано ${consumed} трафика из ${total}.",
"expired": "Истекло",
"noTraffic": "Нет доступного трафика"
},
"sortBy": {
- "lastUpdate": "Последнее обновление",
- "name": "По алфавиту"
+ "lastUpdate": "по последнему обновлению",
+ "name": "по названию"
},
"add": {
"buttonText": "Новый профиль",
"shortBtnTxt": "Новый профиль",
"fromClipboard": "Добавить из буфера обмена",
"scanQr": "Сканировать QR-код",
- "manually": "Ручной ввод",
+ "qrScanner": {
+ "permissionDeniedError": "Нет прав",
+ "unexpectedError": "Неизвестная ошибка",
+ "torchSemanticLabel": "Вспышка",
+ "facingSemanticLabel": "Фронтальная камера"
+ },
+ "manually": "Ввести вручную",
"addingProfileMsg": "Добавление профиля",
- "failureMsg": "Невозможно добавить профиль"
+ "failureMsg": "Не удалось добавить профиль"
},
"update": {
"buttonTxt": "Обновить",
"tooltip": "Обновить профиль",
- "failureMsg": "Ошибка обновления",
+ "failureMsg": "Не удалось обновить профиль",
"successMsg": "Профиль успешно обновлён"
},
+ "share": {
+ "buttonText": "Поделиться",
+ "exportToClipboardSuccess": "Ссылка скопирована в буфер обмена",
+ "exportSubLinkToClipboard": "Скопировать ссылку на подписку в буфер обмена",
+ "subLinkQrCode": "QR-код ссылки на подписку",
+ "exportConfigToClipboard": "Скопировать конфигурацию в буфер обмена",
+ "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена"
+ },
"edit": {
"buttonTxt": "Изменить",
"selectActiveTxt": "Выберите активный профиль"
@@ -79,7 +93,7 @@
"save": {
"buttonText": "Сохранить",
"successMsg": "Профиль успешно сохранён",
- "failureMsg": "Невозможно сохранить профиль"
+ "failureMsg": "Не удалось сохранить профиль"
},
"detailsForm": {
"nameLabel": "Имя",
@@ -116,7 +130,7 @@
},
"settings": {
"pageTitle": "Настройки",
- "requiresRestartMsg": "Для применения перезапустите приложение.",
+ "requiresRestartMsg": "Чтобы применить изменения, перезапустите приложение.",
"general": {
"sectionTitle": "Основные",
"locale": "Язык",
@@ -136,7 +150,7 @@
"black": "Чёрная тема"
},
"enableAnalytics": "Сбор аналитики",
- "enableAnalyticsMsg": "Сбор аналитических данных и отправка отчётов о сбоях для улучшения приложения.",
+ "enableAnalyticsMsg": "Сбор данных аналитики и отправка отчётов о сбоях для улучшения приложения",
"autoStart": "Запуск при загрузке",
"silentStart": "Тихий запуск",
"openWorkingDir": "Открыть рабочую папку",
@@ -146,7 +160,7 @@
"advanced": {
"sectionTitle": "Расширенные",
"debugMode": "Режим отладки",
- "debugModeMsg": "Для применения перезапустите приложение.",
+ "debugModeMsg": "Чтобы применить изменения, перезапустите приложение.",
"memoryLimit": "Ограничение памяти"
},
"network": {
@@ -164,7 +178,7 @@
"clearSelection": "Очистить выбор"
},
"config": {
- "serviceMode": "Режим сервиса",
+ "serviceMode": "Режим работы",
"serviceModes": {
"proxy": "Прокси",
"systemProxy": "Системный прокси",
@@ -184,11 +198,11 @@
"disable": "Отключено",
"enable": "Включено",
"prefer": "Предпочтительно",
- "only": "Эксклюзивно"
+ "only": "Исключительно"
},
- "remoteDnsAddress": "Удалённая DNS",
+ "remoteDnsAddress": "Удалённый DNS",
"remoteDnsDomainStrategy": "Стратегия удалённого домена DNS",
- "directDnsAddress": "Прямая DNS",
+ "directDnsAddress": "Прямой DNS",
"directDnsDomainStrategy": "Стратегия прямого домена DNS",
"mixedPort": "Смешанный порт",
"localDnsPort": "Локальный порт DNS",
@@ -203,6 +217,16 @@
"enableFakeDns": "Использовать поддельную DNS",
"bypassLan": "Обход локальной сети",
"strictRoute": "Строгая маршрутизация"
+ },
+ "geoAssets": {
+ "pageTitle": "Активы маршрутизации",
+ "version": "Версия ${version}",
+ "fileMissing": "Файл отсутствует",
+ "update": "Обновить",
+ "download": "Скачать",
+ "failureMsg": "Не удалось обновить объект",
+ "successMsg": "Объект успешно обновлен",
+ "addRecommended": "Добавить рекомендуемые активы"
}
},
"about": {
@@ -227,37 +251,40 @@
"tray": {
"dashboard": "Панель",
"quit": "Выход",
+ "open": "Открыть",
"status": {
- "connect": "Подключено",
+ "connect": "Подключиться",
"connecting": "Подключение",
- "disconnect": "Отключено",
+ "disconnect": "Отключиться",
"disconnecting": "Отключение"
}
},
"failure": {
- "unexpected": "Неожиданная ошибка",
+ "unexpected": "Непредвиденная ошибка",
"clash": {
- "unexpected": "Неожиданная ошибка (Clash)",
- "core": "Ошибка ${reason}"
+ "unexpected": "Непредвиденная ошибка (Clash)",
+ "core": "Ошибка ${reason}"
},
"singbox": {
- "unexpected": "Неожиданная ошибка (SingBox)",
+ "unexpected": "Непредвиденная ошибка (SingBox)",
"serviceNotRunning": "Сервис не запущен",
+ "missingPrivilege": "Отсутствие прав",
+ "missingPrivilegeMsg": "Режим VPN требует прав администратора. Перезапустите приложение от имени администратора или измените режим работы приложения.",
+ "missingGeoAssets": "Отсутствуют географические ресурсы",
+ "missingGeoAssetsMsg": "Георесурсы отсутствуют. Изменените выбранный ресурс или загрузите собственный в настройках.",
"invalidConfigOptions": "Неправильные параметры конфигурации",
"invalidConfig": "Неправильная конфигурация",
"create": "Ошибка создания сервиса",
- "start": "Ошибка запуска сервиса",
- "missingPrivilege": "Отсутствующие привилегии",
- "missingPrivilegeMsg": "Режим VPN требует прав администратора. Либо перезапустите приложение от имени администратора, либо измените сервисный режим."
+ "start": "Ошибка запуска сервиса"
},
"connectivity": {
- "unexpected": "Неожиданная ошибка",
+ "unexpected": "Непредвиденная ошибка",
"missingVpnPermission": "Отсутствует разрешение VPN",
- "missingNotificationPermission": "Отсутствует разрешение на уведомление",
+ "missingNotificationPermission": "Отсутствует разрешение на показ уведомлений",
"core": "Ошибка ядра"
},
"profiles": {
- "unexpected": "Неожиданная ошибка",
+ "unexpected": "Непредвиденная ошибка",
"notFound": "Профиль не найден",
"invalidConfig": "Неправильная конфигурация",
"invalidUrl": "Неправильный URL"
@@ -268,11 +295,16 @@
"badResponse": "Неправильный ответ",
"connectionError": "Ошибка подключения",
"badCertificate": "Неправильный сертификат"
+ },
+ "geoAssets": {
+ "unexpected": "Неожиданная ошибка",
+ "notUpdate": "Нет доступных обновлений",
+ "activeNotFound": "Активный географический актив не найден"
}
},
"play": {
"title": "Hiddify Next (Preview)",
"short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
- "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPN–сервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на 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 — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на указанный Вами удалённый сервер.\nПримечание: мы не предоставляем серверы, пользователи должны сами обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы. Поддерживаются сервера с:— Обычной ссылка на подписку V2ray/Xray— Ссылкой на подписку Clash— Ссылко на подписку на Sing–Box\nВ чём уникальные особенности? — Удобство — Оптимизация и скорость — Автоматический выбор минимальной задержки — Отображение информации об использовании — Простой импорт ссылок одним щелчком мыши — Бесплатно и без рекламы — Простое переключение ссылок — …и много больше\nПоддерживаются:• Все протоколы, поддерживаемые Sing-Box• VLESS + xtls reality, vision• VMESS• Trojan• ShoadowSocks• Reality• V2ray• Hystria2• TUIC• SSH• ShadowTLS\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.Ядро приложения основано на открытом исходном коде Sing–Box.\nОписание разрешений:— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет добавлять или удалять определённые приложения из списка для туннелирования.— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, так как используется приоритетная служба для обеспечения непрерывной работы VPN.— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения."
}
-}
+}
\ No newline at end of file
diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json
new file mode 100644
index 00000000..f381d98e
--- /dev/null
+++ b/assets/translations/strings_tr.i18n.json
@@ -0,0 +1,310 @@
+{
+ "general": {
+ "appTitle": "Hiddify Next",
+ "reset": "Sıfırla",
+ "toggle": {
+ "enabled": "Etkin",
+ "disabled": "Devre dışı"
+ },
+ "state": {
+ "disable": "Devre dışı bırak"
+ },
+ "sort": "Sırala",
+ "sortBy": "Sırala",
+ "addToClipboard": "Panoya ekle"
+ },
+ "intro": {
+ "termsAndPolicyCaution(rich)": "devam ederek ${tap(@:about.termsAndConditions)} kabul etmiş olursunuz",
+ "start": "Başla"
+ },
+ "home": {
+ "pageTitle": "Ana Sayfa",
+ "emptyProfilesMsg": "Aboneliği profili ekleyerek başlayın",
+ "noActiveProfileMsg": "Profil seçin",
+ "connection": {
+ "tapToConnect": "Bağlanmak için dokunun",
+ "connecting": "Bağlanıyor",
+ "disconnecting": "Bağlantı kesiliyor",
+ "connected": "Bağlandı"
+ },
+ "stats": {
+ "traffic": "Canlı Trafik",
+ "trafficTotal": "Toplam Trafik",
+ "uplink": "Çıkış Yolu",
+ "downlink": "Giriş Yolu"
+ }
+ },
+ "profile": {
+ "overviewPageTitle": "Profiller",
+ "detailsPageTitle": "Profil",
+ "activeProfileNameSemanticLabel": "Aktif profil adı: \"${name}\".",
+ "activeProfileBtnSemanticLabel": "Tüm profilleri görüntüleyin.",
+ "nonActiveProfileBtnSemanticLabel": "Aktif profil olarak \"${name}\" seçeneğini seçin.",
+ "subscription": {
+ "traffic": "Trafik",
+ "updatedTimeAgo": "${timeago} güncellendi",
+ "remainingDuration": "${duration} Gün Kaldı",
+ "remainingTrafficSemanticLabel": "${consumed}/${total} trafik tüketildi.",
+ "expired": "Süresi Doldu",
+ "noTraffic": "Kotal Doldu"
+ },
+ "sortBy": {
+ "lastUpdate": "Yakın zamanda güncellendi",
+ "name": "Alfabetik"
+ },
+ "add": {
+ "buttonText": "Yeni profil",
+ "shortBtnTxt": "Yeni profil",
+ "fromClipboard": "Panodan Ekle",
+ "scanQr": "QR kodunu tarayın",
+ "qrScanner": {
+ "permissionDeniedError": "İzin reddedildi",
+ "unexpectedError": "Bir şeyler yanlış gitti",
+ "torchSemanticLabel": "El feneri",
+ "facingSemanticLabel": "Kameraya önü"
+ },
+ "manually": "Manuel giriş",
+ "addingProfileMsg": "Profil Ekleniyor",
+ "failureMsg": "Profil eklenemedi"
+ },
+ "update": {
+ "buttonTxt": "Güncelle",
+ "tooltip": "Profili Güncelle",
+ "failureMsg": "Profil güncellenemedi",
+ "successMsg": "Profil başarıyla güncellendi"
+ },
+ "share": {
+ "buttonText": "Paylaş",
+ "exportToClipboardSuccess": "Panoya aktarıldı",
+ "exportSubLinkToClipboard": "Abonelik bağlantısını panoya aktar",
+ "subLinkQrCode": "QR kodun abonelik bağlantısı ",
+ "exportConfigToClipboard": "Yapılandırmayı panoya aktar",
+ "exportConfigToClipboardSuccess": "Yapılandırma panoya kopyalandı"
+ },
+ "edit": {
+ "buttonTxt": "Düzenle",
+ "selectActiveTxt": "Etkin profili seçin"
+ },
+ "delete": {
+ "buttonTxt": "Sil",
+ "confirmationMsg": "Profil kalıcı olarak silinsin mi?",
+ "successMsg": "Profil başarıyla silindi"
+ },
+ "save": {
+ "buttonText": "Kaydet",
+ "successMsg": "Profil başarıyla kaydedildi",
+ "failureMsg": "Profil kaydedilemedi"
+ },
+ "detailsForm": {
+ "nameLabel": "İsim",
+ "nameHint": "Profil ismi",
+ "urlLabel": "URL",
+ "urlHint": "Tam yapılandırma URL'i",
+ "emptyNameMsg": "İsim gerekli",
+ "invalidUrlMsg": "Geçersiz URL",
+ "lastUpdate": "Son Güncelleme",
+ "updateInterval": "Otomatik güncelleme",
+ "updateIntervalDialogTitle": "Otomatik Güncelleme Aralığı (saat olarak)"
+ }
+ },
+ "proxies": {
+ "pageTitle": "Proxyler",
+ "emptyProxiesMsg": "Kullanılabilir proxy yok",
+ "delayTestTooltip": "Test Gecikmesi",
+ "sortTooltip": "Proxy'leri Sırala",
+ "sortOptions": {
+ "unsorted": "Varsayılan",
+ "name": "Alfabetik olarak",
+ "delay": "Gecikmeyle"
+ }
+ },
+ "logs": {
+ "pageTitle": "Log",
+ "filterHint": "Filtre",
+ "allLevelsFilter": "Tüm",
+ "shareCoreLogs": "Çekirdek Loglarını Paylaş",
+ "shareAppLogs": "Uygulama Loglarını paylaş",
+ "pauseTooltip": "Duraklat",
+ "resumeTooltip": "Devam et",
+ "clearTooltip": "Temizle"
+ },
+ "settings": {
+ "pageTitle": "Ayarlar",
+ "requiresRestartMsg": "Bunun etkili olması için uygulamayı yeniden başlatın",
+ "general": {
+ "sectionTitle": "Genel",
+ "locale": "Dil",
+ "region": "Bölge",
+ "regionMsg": "Yerel adresleri atlamak için varsayılan seçeneği seçebilirsin",
+ "regions": {
+ "ir": "İran (ir)",
+ "cn": "Çin (cn)",
+ "ru": "Rusya (ru)",
+ "other": "Diğer"
+ },
+ "themeMode": "Tema Modu",
+ "themeModes": {
+ "system": "Sistem temasını takip et",
+ "dark": "Karanlık mod",
+ "light": "Işık modu",
+ "black": "Siyah mod"
+ },
+ "enableAnalytics": "Analitikleri Etkinleştir",
+ "enableAnalyticsMsg": "Uygulamayı iyileştirmek için analiz toplama ve kilitlenme raporları göndermeye izni verin",
+ "autoStart": "Önyüklemede Başlat",
+ "silentStart": "Sessiz Başlatma",
+ "openWorkingDir": "Çalışma Dizinini Aç",
+ "ignoreBatteryOptimizations": "Pil Optimizasyonunu Devre Dışı Bırak",
+ "ignoreBatteryOptimizationsMsg": "Optimum VPN performansı için kısıtlamaları kaldırın"
+ },
+ "advanced": {
+ "sectionTitle": "Gelişmiş",
+ "debugMode": "Hata ayıklama modu",
+ "debugModeMsg": "Bu değişikliği uygulamak için uygulamayı yeniden başlatın",
+ "memoryLimit": "Bellek Sınırı"
+ },
+ "network": {
+ "perAppProxyPageTitle": "Uygulama başına Proxy",
+ "perAppProxyModes": {
+ "off": "Tümü",
+ "offMsg": "Proxy tüm uygulamalar",
+ "include": "Proxy",
+ "includeMsg": "Yalnızca proxy seçilen uygulamalar",
+ "exclude": "Atlatma",
+ "excludeMsg": "Seçilen uygulamalara proxy uygulama"
+ },
+ "showSystemApps": "Sistem uygulamalarını göster",
+ "hideSystemApps": "Sistem uygulamalarını gizle",
+ "clearSelection": "Seçimi temizle"
+ },
+ "config": {
+ "serviceMode": "Servis modu",
+ "serviceModes": {
+ "proxy": "Proxy",
+ "systemProxy": "Sistem Proxy",
+ "tun": "VPN"
+ },
+ "section": {
+ "route": "Rota Seçenekleri",
+ "dns": "DNS Seçenekleri",
+ "inbound": "Gelen Seçenekler",
+ "misc": "Çeşitli Seçenekler"
+ },
+ "pageTitle": "Yapılandırma Seçenekleri",
+ "logLevel": "Log Seviyesi",
+ "resolveDestination": "Hedefi Çöz",
+ "ipv6Mode": "IPv6 Rotası",
+ "ipv6Modes": {
+ "disable": "Devre dışı bırak",
+ "enable": "Etkinleştir",
+ "prefer": "Tercih edilen",
+ "only": "Özel"
+ },
+ "remoteDnsAddress": "Uzak DNS",
+ "remoteDnsDomainStrategy": "Uzak DNS Domain Stratejisi",
+ "directDnsAddress": "Doğrudan DNS",
+ "directDnsDomainStrategy": "Doğrudan DNS Domain Stratejisi",
+ "mixedPort": "Mixed Port",
+ "localDnsPort": "Yerel DNS Bağlantı Noktası",
+ "tunImplementation": "TUN İmplementasyonu",
+ "mtu": "MTU",
+ "connectionTestUrl": "Bağlantı Testi URL'i",
+ "urlTestInterval": "URL Test Aralığı",
+ "enableClashApi": "Clash API'yi etkinleştir",
+ "clashApiPort": "Clash API Bağlantı Noktası",
+ "enableTun": "TUN'u etkinleştir",
+ "setSystemProxy": "Sistem Proxy'sini Ayarla",
+ "enableFakeDns": "Sahte DNS'yi Etkinleştir",
+ "bypassLan": "Lan'ı Atla",
+ "strictRoute": "Kesin Rota"
+ },
+ "geoAssets": {
+ "pageTitle": "Varlıkları Yönlendirme",
+ "version": "Sürüm ${version}",
+ "fileMissing": "Eksik dosya",
+ "update": "Güncelleme",
+ "download": "İndirmek",
+ "failureMsg": "Öğe güncellenemedi",
+ "successMsg": "Öğe başarıyla güncellendi",
+ "addRecommended": "Önerilen Varlıkları Ekle"
+ }
+ },
+ "about": {
+ "pageTitle": "Hakkında",
+ "version": "Sürüm",
+ "sourceCode": "Kaynak kodu",
+ "telegramChannel": "Telegram Kanalı",
+ "checkForUpdate": "Güncellemeleri kontrol et",
+ "privacyPolicy": "Gizlilik Politikası",
+ "termsAndConditions": "Şartlar ve koşullar"
+ },
+ "appUpdate": {
+ "notAvailableMsg": " En son sürümü kullanıyorsunuz",
+ "dialogTitle": "Yeni Güncell",
+ "updateMsg": "@:general.appTitle'ın yeni bir sürümü mevcut. Şimdi güncellemek ister misiniz?",
+ "currentVersionLbl": "Şimdiki versiyon",
+ "newVersionLbl": "Yeni versiyon",
+ "updateNowBtnTxt": "Şimdi güncelle",
+ "laterBtnTxt": "Daha sonra",
+ "ignoreBtnTxt": "Görmezden gel"
+ },
+ "tray": {
+ "dashboard": "Gösterge Paneli",
+ "quit": "Çıkış yap",
+ "open": "Açık",
+ "status": {
+ "connect": "Bağlan",
+ "connecting": "Bağlanıyor",
+ "disconnect": "Bağlantıyı kes",
+ "disconnecting": "Bağlantı kesiliyor"
+ }
+ },
+ "failure": {
+ "unexpected": "Beklenmeyen hata",
+ "clash": {
+ "unexpected": "Beklenmeyen hata",
+ "core": "Çakışma Hatası ${reason}"
+ },
+ "singbox": {
+ "unexpected": "Beklenmedik Hizmet Hatası",
+ "serviceNotRunning": "Servis çalışmıyor",
+ "missingPrivilege": "Eksik Ayrıcalık",
+ "missingPrivilegeMsg": "VPN modu yönetici ayrıcalıkları gerektirir. Uygulamayı yönetici olarak yeniden başlatın veya hizmet modunu değiştirin.",
+ "invalidConfigOptions": "Geçersiz yapılandırma seçenekleri",
+ "invalidConfig": "Geçersiz Yapılandırma",
+ "create": "Servis oluşturma hatası",
+ "start": "Servis başlatma hatası",
+ "missingGeoAssets": "Eksik Coğrafi Varlıklar",
+ "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün."
+ },
+ "connectivity": {
+ "unexpected": "Beklenmedik Hata",
+ "missingVpnPermission": "Eksik VPN İzni",
+ "missingNotificationPermission": "Eksik Bildirim İzni",
+ "core": "Temel Hata"
+ },
+ "profiles": {
+ "unexpected": "Beklenmedik hata",
+ "notFound": "Profil bulunamadı",
+ "invalidConfig": "Geçersiz Yapılandırmalar",
+ "invalidUrl": "Geçersiz URL"
+ },
+ "connection": {
+ "unexpected": "Beklenmeyen bağlantı hatası",
+ "timeout": "Bağlantı zamanaşımına uğradı",
+ "badResponse": "Kötü yanıt",
+ "connectionError": "Bağlantı hatası",
+ "badCertificate": "Kötü sertifika"
+ },
+ "geoAssets": {
+ "unexpected": "Beklenmeyen hata",
+ "notUpdate": "Güncelleme mevcut değil",
+ "activeNotFound": "Etkin Coğrafi Varlık Bulunamadı"
+ }
+ },
+ "play": {
+ "title": "Hiddify Next (Önizleme)",
+ "short_description": "Otomatik, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks",
+ "full_description": "HiddifyNext'in temel hedefi güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamaktır. VPN Hizmeti iznini kullanarak tüm trafiği veya seçilen uygulama trafiğini seçtiğiniz uzak bir sunucuya yönlendirmenizi sağlar. Not: Herhangi bir sunucu sağlamıyoruz; kullanıcıların kendi barındırılan sunucularını veya güvenilir sunucularını kullanarak çevrimiçi etkinliklerinin gizli kalmasını sağlamaları gerekir. Sunucuları aşağıdakilerle destekliyoruz: - Normal V2ray/Xray Abonelik Bağlantısı - Clash Abonelik Bağlantısı - Sing-Box Abonelik Bağlantısı Benzersiz özelliklerimiz nelerdir? - Kullanıcı Dostu - Optimize Edilmiş ve Hızlı - En Düşük Ping'i otomatik olarak seçin - Kullanıcı kullanım bilgilerini gösterin - Derin bağlantı kullanarak tek tıklamayla alt bağlantıyı kolayca içe aktarın - Ücretsiz ve ADS Yok - Kullanıcı alt bağlantılarını kolayca değiştirin - giderek daha fazla Destek: - Sing-Box tarafından desteklenen tüm Protokoller - VLESS + xtls gerçeklik, vizyon - VMESS - Trojan - ShoadowSocks - Reality - V2ray - Hystria2 - TUIC - SSH - ShadowTLS Kaynak kodu https://github.com/hiddify/Hiddify-Next adresinde mevcuttur. Uygulama çekirdeği açık tabanlıdır. kaynak şarkı kutusu. İzin Açıklaması: - VPN Hizmeti: Bu uygulamanın amacı güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamak olduğundan, trafiği tünel aracılığıyla uzak sunucuya yönlendirebilmek için bu izne ihtiyacımız var. - TÜM PAKETLERİ SORGULAYIN: Bu izin, kullanıcıların tünelleme için belirli uygulamaları dahil etmesine veya hariç tutmasına izin vermek için kullanılır. - ALMA ÖNYÜKLEME TAMAMLANDI: Bu izin, cihaz önyüklemesi sırasında bu uygulamayı etkinleştirmek için uygulama ayarlarından etkinleştirilebilir veya devre dışı bırakılabilir. - BİLDİRİMLER SONRASI: VPN hizmetinin sürekli çalışmasını sağlamak için bir ön plan hizmeti kullandığımız için bu izin önemlidir. - Bu uygulama reklam içermez. Analitik ve kilitlenme verileri yalnızca uygulamanın ilk kullanımında kullanıcının açık rızası ile gerçekleşir."
+ }
+}
diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh-CN.i18n.json
similarity index 72%
rename from assets/translations/strings_zh.i18n.json
rename to assets/translations/strings_zh-CN.i18n.json
index 54e5030d..b4152345 100644
--- a/assets/translations/strings_zh.i18n.json
+++ b/assets/translations/strings_zh-CN.i18n.json
@@ -45,7 +45,7 @@
"updatedTimeAgo": "更新 ${timeago}",
"remainingDuration": "剩余 ${duration} 天",
"remainingTrafficSemanticLabel": "已消耗 ${consumed} 流量,共 ${total} 流量。",
- "expired": "已到期",
+ "expired": "已过期",
"noTraffic": "超出配额"
},
"sortBy": {
@@ -56,7 +56,13 @@
"buttonText": "新的配置文件",
"shortBtnTxt": "新的配置文件",
"fromClipboard": "从剪贴板添加",
- "scanQr": "扫二维码",
+ "scanQr": "扫描二维码",
+ "qrScanner": {
+ "permissionDeniedError": "权限不足",
+ "unexpectedError": "出了些问题",
+ "torchSemanticLabel": "手电筒",
+ "facingSemanticLabel": "相机朝向"
+ },
"manually": "手动输入",
"addingProfileMsg": "添加配置文件",
"failureMsg": "添加配置文件失败"
@@ -67,9 +73,17 @@
"failureMsg": "更新配置文件失败",
"successMsg": "配置文件更新成功"
},
+ "share": {
+ "buttonText": "分享",
+ "exportToClipboardSuccess": "导出到剪贴板",
+ "exportSubLinkToClipboard": "将订阅链接导出到剪贴板",
+ "subLinkQrCode": "订阅链接二维码",
+ "exportConfigToClipboard": "将配置导出到剪贴板",
+ "exportConfigToClipboardSuccess": "配置已复制到剪贴板"
+ },
"edit": {
"buttonTxt": "编辑",
- "selectActiveTxt": "选择活动配置文件"
+ "selectActiveTxt": "选择并激活配置文件"
},
"delete": {
"buttonTxt": "删除",
@@ -79,7 +93,7 @@
"save": {
"buttonText": "保存",
"successMsg": "配置文件保存成功",
- "failureMsg": "保存配置文件失败"
+ "failureMsg": "配置文件保存失败"
},
"detailsForm": {
"nameLabel": "名称",
@@ -90,7 +104,7 @@
"invalidUrlMsg": "无效的网址",
"lastUpdate": "最后更新",
"updateInterval": "自动更新",
- "updateIntervalDialogTitle": "自动更新间隔(以小时为单位)"
+ "updateIntervalDialogTitle": "自动更新间隔(小时)"
}
},
"proxies": {
@@ -126,13 +140,13 @@
"ir": "伊朗 (ir)",
"cn": "中国 (cn)",
"ru": "俄罗斯 (ru)",
- "other": "其他"
+ "other": "其它"
},
"themeMode": "主题模式",
"themeModes": {
"system": "遵循系统主题",
"dark": "黑暗模式",
- "light": "明亮模式",
+ "light": "浅色模式",
"black": "深色模式"
},
"enableAnalytics": "启用分析",
@@ -166,7 +180,7 @@
"config": {
"serviceMode": "服务方式",
"serviceModes": {
- "proxy": "代理人",
+ "proxy": "仅代理",
"systemProxy": "系统代理",
"tun": "VPN"
},
@@ -174,7 +188,7 @@
"route": "路由选项",
"dns": "DNS 选项",
"inbound": "入站选项",
- "misc": "其他选项"
+ "misc": "其它选项"
},
"pageTitle": "配置选项",
"logLevel": "日志级别",
@@ -200,9 +214,19 @@
"clashApiPort": "Clash API 端口",
"enableTun": "启用 TUN",
"setSystemProxy": "设置系统代理",
- "enableFakeDns": "Enable Fake DNS",
- "bypassLan": "Bypass Lan",
- "strictRoute": "Strict Route"
+ "enableFakeDns": "启用 Fake DNS",
+ "bypassLan": "绕过局域网",
+ "strictRoute": "严格路由"
+ },
+ "geoAssets": {
+ "pageTitle": "路由资源文件",
+ "version": "版本 ${version}",
+ "fileMissing": "文件丢失",
+ "update": "更新",
+ "download": "下载",
+ "failureMsg": "更新资源文件失败",
+ "successMsg": "已成功更新资源文件",
+ "addRecommended": "添加建议的资源文件"
}
},
"about": {
@@ -221,12 +245,13 @@
"currentVersionLbl": "当前版本",
"newVersionLbl": "新版本",
"updateNowBtnTxt": "现在更新",
- "laterBtnTxt": "之后",
+ "laterBtnTxt": "以后再说",
"ignoreBtnTxt": "忽略"
},
"tray": {
"dashboard": "控制面板",
"quit": "退出",
+ "open": "打开",
"status": {
"connect": "连接",
"connecting": "正在连接",
@@ -238,17 +263,19 @@
"unexpected": "意外错误",
"clash": {
"unexpected": "意外错误",
- "core": "Clash错误 ${reason}"
+ "core": "Clash 错误 ${reason}"
},
"singbox": {
"unexpected": "意外服务错误",
"serviceNotRunning": "服务未运行",
+ "missingPrivilege": "缺少权限",
+ "missingPrivilegeMsg": "VPN 模式需要管理员权限。以管理员身份重新启动应用程序或更改服务模式",
+ "missingGeoAssets": "缺失 GEO 资源文件",
+ "missingGeoAssetsMsg": "缺失 GEO 资源文件。请考虑更改激活的资源文件或在设置中下载所选资源文件。",
"invalidConfigOptions": "配置选项无效",
"invalidConfig": "无效配置",
"create": "服务创建错误",
- "start": "服务启动错误",
- "missingPrivilege": "缺少特权",
- "missingPrivilegeMsg": "VPN 模式需要管理员权限。以管理员身份重新启动应用程序或更改服务模式"
+ "start": "服务启动错误"
},
"connectivity": {
"unexpected": "意外失败",
@@ -268,11 +295,16 @@
"badResponse": "错误响应",
"connectionError": "连接错误",
"badCertificate": "证书无效"
+ },
+ "geoAssets": {
+ "unexpected": "意外的错误",
+ "notUpdate": "无可用更新",
+ "activeNotFound": "未找到激活的 GEO 资源文件"
}
},
"play": {
"title": "Hiddify Next(预览)",
"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 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\n- VPN 服务:由于此应用程序的目标是提供安全、用户友好和高效的隧道客户端,我们需要此权限以能够通过隧道将流量路由到远程服务器。\n获取应用程序列表:此权限用于允许用户包括或排除特定应用程序以进行隧道传输。\n- 接收开机广播:可以从应用程序设置中启用或禁用此权限,以便在设备启动时激活此应用程序。\n- 发送通知:此权限是必需的,因为我们使用前台服务来确保 VPN 服务的持续运行。\n- 本应用程序没有广告。分析和崩溃数据仅在首次使用应用程序时经用户明确同意的情况下发生。"
}
}
\ No newline at end of file
diff --git a/build.yaml b/build.yaml
index b33aa2b2..c34cb667 100644
--- a/build.yaml
+++ b/build.yaml
@@ -2,8 +2,6 @@ targets:
$default:
builders:
drift_dev:
- generate_for:
- - "lib/data/local/**"
options:
store_date_time_values_as_text: true
slang_build_runner:
diff --git a/dependencies.properties b/dependencies.properties
index 2cb2a52d..ac07d8b0 100644
--- a/dependencies.properties
+++ b/dependencies.properties
@@ -1 +1 @@
-core.version=0.8.0
\ No newline at end of file
+core.version=0.8.4
\ No newline at end of file
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 7bfd0a6a..9f45281a 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1,4 +1,6 @@
PODS:
+ - cupertino_http (0.0.1):
+ - Flutter
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
@@ -52,7 +54,7 @@ PODS:
- GTMSessionFetcher/Core (< 3.0, >= 1.1)
- MLImage (= 1.0.0-beta4)
- MLKitCommon (~> 9.0)
- - mobile_scanner (3.5.2):
+ - mobile_scanner (3.5.5):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 4.0.0)
- nanopb (2.30909.1):
@@ -68,13 +70,13 @@ PODS:
- PromisesObjC (2.3.1)
- protocol_handler (0.0.1):
- Flutter
- - Sentry/HybridSDK (8.14.2):
- - SentryPrivate (= 8.14.2)
+ - Sentry/HybridSDK (8.15.2):
+ - SentryPrivate (= 8.15.2)
- sentry_flutter (0.0.1):
- Flutter
- FlutterMacOS
- - Sentry/HybridSDK (= 8.14.2)
- - SentryPrivate (8.14.2)
+ - Sentry/HybridSDK (= 8.15.2)
+ - SentryPrivate (8.15.2)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -99,6 +101,7 @@ PODS:
- Flutter
DEPENDENCIES:
+ - cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
@@ -131,6 +134,8 @@ SPEC REPOS:
- sqlite3
EXTERNAL SOURCES:
+ cupertino_http:
+ :path: ".symlinks/plugins/cupertino_http/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
@@ -157,6 +162,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
+ cupertino_http: 5f8b1161107fe6c8d94a0c618735a033d93fa7db
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
@@ -170,15 +176,15 @@ SPEC CHECKSUMS:
MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505
MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390
MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49
- mobile_scanner: 5090a13b7a35fc1c25b0d97e18e84f271a6eb605
+ mobile_scanner: 202ab6f652e40a9add68b10de4c4fb2a745c4348
nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
protocol_handler: ae9efcf3b307f3fdffcd9d5252775b9f7d9f0d09
- Sentry: e0ea366f95ebb68f26d6030d8c22d6b2e6d23dd0
- sentry_flutter: 9a04c51c373d76ee22167bf1e65bc468c0a91fed
- SentryPrivate: 949a21fa59872427edc73b524c3ec8456761d97f
+ Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923
+ sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b
+ SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273
diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart
index 230502ed..2458c244 100644
--- a/lib/bootstrap.dart
+++ b/lib/bootstrap.dart
@@ -4,23 +4,25 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
-import 'package:hiddify/core/app/app_view.dart';
-import 'package:hiddify/core/core_providers.dart';
-import 'package:hiddify/core/prefs/prefs.dart';
-import 'package:hiddify/data/data_providers.dart';
-import 'package:hiddify/data/repository/app_repository_impl.dart';
-import 'package:hiddify/domain/environment.dart';
-import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
+import 'package:hiddify/core/app_info/app_info_provider.dart';
+import 'package:hiddify/core/model/environment.dart';
+import 'package:hiddify/core/preferences/general_preferences.dart';
+import 'package:hiddify/core/preferences/preferences_provider.dart';
+import 'package:hiddify/features/app/widget/app.dart';
import 'package:hiddify/features/common/window/window_controller.dart';
+import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
+import 'package:hiddify/features/log/data/log_data_providers.dart';
+import 'package:hiddify/features/profile/data/profile_data_providers.dart';
+import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart';
import 'package:hiddify/features/system_tray/system_tray_controller.dart';
import 'package:hiddify/services/auto_start_service.dart';
import 'package:hiddify/services/deep_link_service.dart';
import 'package:hiddify/services/service_providers.dart';
+import 'package:hiddify/singbox/service/singbox_service_provider.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:loggy/loggy.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
-import 'package:shared_preferences/shared_preferences.dart';
import 'package:window_manager/window_manager.dart';
final _logger = Loggy('bootstrap');
@@ -38,15 +40,14 @@ Future lazyBootstrap(
_loggers.addPrinter(sentryLogger);
Loggy.initLoggy();
- final appInfo = await AppRepositoryImpl.getAppInfo(env);
- final sharedPreferences = await SharedPreferences.getInstance();
final container = ProviderContainer(
overrides: [
- appInfoProvider.overrideWithValue(appInfo),
- sharedPreferencesProvider.overrideWithValue(sharedPreferences),
+ environmentProvider.overrideWithValue(env),
],
);
+ final appInfo = await container.read(appInfoProvider.future);
+ await container.read(sharedPreferencesProvider.future);
final enableAnalytics = container.read(enableAnalyticsProvider);
await SentryFlutter.init(
@@ -86,27 +87,49 @@ Future _lazyBootstrap(
final filesEditor = container.read(filesEditorServiceProvider);
await filesEditor.init();
+ await container.read(logRepositoryProvider.future);
+ await container.read(geoAssetRepositoryProvider.future);
+ await container.read(profileRepositoryProvider.future);
initLoggers(container.read, debug);
- _logger.info(container.read(appInfoProvider).format());
+ _logger.info(container.read(appInfoProvider).requireValue.format());
final silentStart = container.read(silentStartNotifierProvider);
if (silentStart) {
FlutterNativeSplash.remove();
}
+
if (PlatformUtils.isDesktop) {
+ _logger.debug("initializing [Auto Start Service] and [Window Controller]");
await container.read(autoStartServiceProvider.future);
await container.read(windowControllerProvider.future);
}
- await initAppServices(container.read);
- await initControllers(container.read);
+ await container.read(singboxServiceProvider).init();
+ _logger.debug("initialized [Singbox Service]");
+
+ await container.read(activeProfileProvider.future);
+ await container.read(deepLinkServiceProvider.future);
+ if (PlatformUtils.isDesktop) {
+ try {
+ await container
+ .read(systemTrayControllerProvider.future)
+ .timeout(const Duration(seconds: 1));
+ _logger.debug("initialized [System Tray Controller]");
+ } catch (error, stackTrace) {
+ _logger.warning(
+ "error initializing [System Tray Controller]",
+ error,
+ stackTrace,
+ );
+ }
+ }
runApp(
ProviderScope(
parent: container,
child: SentryUserInteractionWidget(
- child: const AppView(),
+ child: const App(),
),
),
);
@@ -122,7 +145,7 @@ void initLoggers(
final logToFile = debug || (!Platform.isAndroid && !Platform.isIOS);
if (logToFile) {
_loggers.addPrinter(
- FileLogPrinter(read(filesEditorServiceProvider).appLogsFile.path),
+ FileLogPrinter(read(logPathResolverProvider).appFile().path),
);
}
Loggy.initLoggy(
@@ -130,29 +153,3 @@ void initLoggers(
logOptions: LogOptions(logLevel),
);
}
-
-Future initAppServices(
- Result Function(ProviderListenable) read,
-) async {
- _logger.debug("initializing app services");
- await Future.wait(
- [
- read(singboxServiceProvider).init(),
- ],
- );
- _logger.debug('initialized app services');
-}
-
-Future initControllers(
- Result Function(ProviderListenable) read,
-) async {
- _logger.debug("initializing controllers");
- await Future.wait(
- [
- read(activeProfileProvider.future),
- read(deepLinkServiceProvider.future),
- if (PlatformUtils.isDesktop) read(systemTrayControllerProvider.future),
- ],
- );
- _logger.debug("initialized base controllers");
-}
diff --git a/lib/core/app_info/app_info_provider.dart b/lib/core/app_info/app_info_provider.dart
new file mode 100644
index 00000000..48042618
--- /dev/null
+++ b/lib/core/app_info/app_info_provider.dart
@@ -0,0 +1,30 @@
+import 'dart:io';
+
+import 'package:hiddify/core/model/app_info_entity.dart';
+import 'package:hiddify/core/model/environment.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'app_info_provider.g.dart';
+
+@Riverpod(keepAlive: true)
+Environment environment(EnvironmentRef ref) =>
+ throw Exception("override environmentProvider");
+
+@Riverpod(keepAlive: true)
+class AppInfo extends _$AppInfo {
+ @override
+ Future build() async {
+ final packageInfo = await PackageInfo.fromPlatform();
+ final environment = ref.watch(environmentProvider);
+ return AppInfoEntity(
+ name: packageInfo.appName,
+ version: packageInfo.version,
+ buildNumber: packageInfo.buildNumber,
+ release: Release.read(),
+ operatingSystem: Platform.operatingSystem,
+ operatingSystemVersion: Platform.operatingSystemVersion,
+ environment: environment,
+ );
+ }
+}
diff --git a/lib/core/core_providers.dart b/lib/core/core_providers.dart
deleted file mode 100644
index 35ce17e4..00000000
--- a/lib/core/core_providers.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-import 'package:hiddify/core/prefs/prefs.dart';
-import 'package:hiddify/domain/app/app.dart';
-import 'package:hiddify/domain/environment.dart';
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-
-part 'core_providers.g.dart';
-
-@Riverpod(keepAlive: true)
-AppInfo appInfo(AppInfoRef ref) =>
- throw UnimplementedError('AppInfo must be overridden');
-
-@Riverpod(keepAlive: true)
-Environment env(EnvRef ref) => ref.watch(appInfoProvider).environment;
-
-@Riverpod(keepAlive: true)
-TranslationsEn translations(TranslationsRef ref) =>
- ref.watch(localeNotifierProvider).build();
-
-@Riverpod(keepAlive: true)
-AppTheme theme(ThemeRef ref) => AppTheme(
- ref.watch(themeModeNotifierProvider),
- ref.watch(localeNotifierProvider).preferredFontFamily,
- );
diff --git a/lib/core/database/app_database.dart b/lib/core/database/app_database.dart
new file mode 100644
index 00000000..5130d37a
--- /dev/null
+++ b/lib/core/database/app_database.dart
@@ -0,0 +1,59 @@
+import 'package:drift/drift.dart';
+import 'package:hiddify/core/database/connection/database_connection.dart';
+import 'package:hiddify/core/database/converters/duration_converter.dart';
+import 'package:hiddify/core/database/schema_versions.dart';
+import 'package:hiddify/core/database/tables/database_tables.dart';
+import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart';
+import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart';
+import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
+import 'package:hiddify/features/profile/model/profile_entity.dart';
+
+part 'app_database.g.dart';
+
+@DriftDatabase(tables: [ProfileEntries, GeoAssetEntries])
+class AppDatabase extends _$AppDatabase {
+ AppDatabase({required QueryExecutor connection}) : super(connection);
+
+ AppDatabase.connect() : super(openConnection());
+
+ @override
+ int get schemaVersion => 3;
+
+ @override
+ MigrationStrategy get migration {
+ return MigrationStrategy(
+ onCreate: (Migrator m) async {
+ await m.createAll();
+ await _prePopulateGeoAssets();
+ },
+ onUpgrade: stepByStep(
+ // add type column to profile entries table
+ // make url column nullable
+ from1To2: (m, schema) async {
+ await m.alterTable(
+ TableMigration(
+ schema.profileEntries,
+ columnTransformer: {
+ schema.profileEntries.type: const Constant("remote"),
+ },
+ newColumns: [schema.profileEntries.type],
+ ),
+ );
+ },
+ from2To3: (m, schema) async {
+ await m.createTable(schema.geoAssetEntries);
+ await _prePopulateGeoAssets();
+ },
+ ),
+ );
+ }
+
+ Future _prePopulateGeoAssets() async {
+ await transaction(() async {
+ final geoAssets = defaultGeoAssets.map((e) => e.toEntry());
+ for (final geoAsset in geoAssets) {
+ await into(geoAssetEntries).insert(geoAsset);
+ }
+ });
+ }
+}
diff --git a/lib/core/database/connection/database_connection.dart b/lib/core/database/connection/database_connection.dart
new file mode 100644
index 00000000..f05450e5
--- /dev/null
+++ b/lib/core/database/connection/database_connection.dart
@@ -0,0 +1,14 @@
+import 'dart:io';
+
+import 'package:drift/drift.dart';
+import 'package:drift/native.dart';
+import 'package:hiddify/services/files_editor_service.dart';
+import 'package:path/path.dart' as p;
+
+LazyDatabase openConnection() {
+ return LazyDatabase(() async {
+ final dbDir = await FilesEditorService.getDatabaseDirectory();
+ final file = File(p.join(dbDir.path, 'db.sqlite'));
+ return NativeDatabase.createInBackground(file);
+ });
+}
diff --git a/lib/data/local/type_converters.dart b/lib/core/database/converters/duration_converter.dart
similarity index 100%
rename from lib/data/local/type_converters.dart
rename to lib/core/database/converters/duration_converter.dart
diff --git a/lib/core/database/database_provider.dart b/lib/core/database/database_provider.dart
new file mode 100644
index 00000000..fb388e20
--- /dev/null
+++ b/lib/core/database/database_provider.dart
@@ -0,0 +1,7 @@
+import 'package:hiddify/core/database/app_database.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'database_provider.g.dart';
+
+@Riverpod(keepAlive: true)
+AppDatabase appDatabase(AppDatabaseRef ref) => AppDatabase.connect();
diff --git a/lib/data/local/schema_versions.dart b/lib/core/database/schema_versions.dart
similarity index 63%
rename from lib/data/local/schema_versions.dart
rename to lib/core/database/schema_versions.dart
index 7a743f02..da7a4d2b 100644
--- a/lib/data/local/schema_versions.dart
+++ b/lib/core/database/schema_versions.dart
@@ -111,8 +111,96 @@ i1.GeneratedColumn _column_11(String aliasedName) =>
i1.GeneratedColumn _column_12(String aliasedName) =>
i1.GeneratedColumn('support_url', aliasedName, true,
type: i1.DriftSqlType.string);
+
+final class _S3 extends i0.VersionedSchema {
+ _S3({required super.database}) : super(version: 3);
+ @override
+ late final List entities = [
+ profileEntries,
+ geoAssetEntries,
+ ];
+ late final Shape0 profileEntries = Shape0(
+ source: i0.VersionedTable(
+ entityName: 'profile_entries',
+ withoutRowId: false,
+ isStrict: false,
+ tableConstraints: [
+ 'PRIMARY KEY(id)',
+ ],
+ columns: [
+ _column_0,
+ _column_1,
+ _column_2,
+ _column_3,
+ _column_4,
+ _column_5,
+ _column_6,
+ _column_7,
+ _column_8,
+ _column_9,
+ _column_10,
+ _column_11,
+ _column_12,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null);
+ late final Shape1 geoAssetEntries = Shape1(
+ source: i0.VersionedTable(
+ entityName: 'geo_asset_entries',
+ withoutRowId: false,
+ isStrict: false,
+ tableConstraints: [
+ 'PRIMARY KEY(id)',
+ 'UNIQUE(name, provider_name)',
+ ],
+ columns: [
+ _column_0,
+ _column_1,
+ _column_2,
+ _column_3,
+ _column_13,
+ _column_14,
+ _column_15,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null);
+}
+
+class Shape1 extends i0.VersionedTable {
+ Shape1({required super.source, required super.alias}) : super.aliased();
+ i1.GeneratedColumn get id =>
+ columnsByName['id']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get type =>
+ columnsByName['type']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get active =>
+ columnsByName['active']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get name =>
+ columnsByName['name']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get providerName =>
+ columnsByName['provider_name']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get version =>
+ columnsByName['version']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get lastCheck =>
+ columnsByName['last_check']! as i1.GeneratedColumn;
+}
+
+i1.GeneratedColumn _column_13(String aliasedName) =>
+ i1.GeneratedColumn('provider_name', aliasedName, false,
+ additionalChecks: i1.GeneratedColumn.checkTextLength(
+ minTextLength: 1,
+ ),
+ type: i1.DriftSqlType.string);
+i1.GeneratedColumn _column_14(String aliasedName) =>
+ i1.GeneratedColumn('version', aliasedName, true,
+ type: i1.DriftSqlType.string);
+i1.GeneratedColumn _column_15(String aliasedName) =>
+ i1.GeneratedColumn('last_check', aliasedName, true,
+ type: i1.DriftSqlType.dateTime);
i0.MigrationStepWithVersion migrationSteps({
required Future Function(i1.Migrator m, _S2 schema) from1To2,
+ required Future Function(i1.Migrator m, _S3 schema) from2To3,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -121,6 +209,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from1To2(migrator, schema);
return 2;
+ case 2:
+ final schema = _S3(database: database);
+ final migrator = i1.Migrator(database, schema);
+ await from2To3(migrator, schema);
+ return 3;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -129,8 +222,10 @@ i0.MigrationStepWithVersion migrationSteps({
i1.OnUpgrade stepByStep({
required Future Function(i1.Migrator m, _S2 schema) from1To2,
+ required Future Function(i1.Migrator m, _S3 schema) from2To3,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
+ from2To3: from2To3,
));
diff --git a/lib/data/local/schemas/drift_schema_v1.json b/lib/core/database/schemas/drift_schema_v1.json
similarity index 100%
rename from lib/data/local/schemas/drift_schema_v1.json
rename to lib/core/database/schemas/drift_schema_v1.json
diff --git a/lib/data/local/schemas/drift_schema_v2.json b/lib/core/database/schemas/drift_schema_v2.json
similarity index 100%
rename from lib/data/local/schemas/drift_schema_v2.json
rename to lib/core/database/schemas/drift_schema_v2.json
diff --git a/lib/core/database/schemas/drift_schema_v3.json b/lib/core/database/schemas/drift_schema_v3.json
new file mode 100644
index 00000000..3cfc5fb9
--- /dev/null
+++ b/lib/core/database/schemas/drift_schema_v3.json
@@ -0,0 +1,286 @@
+{
+ "_meta": {
+ "description": "This file contains a serialized version of schema entities for drift.",
+ "version": "1.1.0"
+ },
+ "options": {
+ "store_date_time_values_as_text": true
+ },
+ "entities": [
+ {
+ "id": 0,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "profile_entries",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumNameConverter(ProfileType.values)",
+ "dart_type_name": "ProfileType"
+ }
+ },
+ {
+ "name": "active",
+ "getter_name": "active",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"active\" IN (0, 1))",
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "allowed-lengths": {
+ "min": 1,
+ "max": null
+ }
+ }
+ ]
+ },
+ {
+ "name": "url",
+ "getter_name": "url",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "last_update",
+ "getter_name": "lastUpdate",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "update_interval",
+ "getter_name": "updateInterval",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "DurationTypeConverter()",
+ "dart_type_name": "Duration"
+ }
+ },
+ {
+ "name": "upload",
+ "getter_name": "upload",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "download",
+ "getter_name": "download",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "total",
+ "getter_name": "total",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "expire",
+ "getter_name": "expire",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "web_page_url",
+ "getter_name": "webPageUrl",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "support_url",
+ "getter_name": "supportUrl",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": false,
+ "constraints": [],
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 1,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "geo_asset_entries",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumNameConverter(GeoAssetType.values)",
+ "dart_type_name": "GeoAssetType"
+ }
+ },
+ {
+ "name": "active",
+ "getter_name": "active",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"active\" IN (0, 1))",
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "allowed-lengths": {
+ "min": 1,
+ "max": null
+ }
+ }
+ ]
+ },
+ {
+ "name": "provider_name",
+ "getter_name": "providerName",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "allowed-lengths": {
+ "min": 1,
+ "max": null
+ }
+ }
+ ]
+ },
+ {
+ "name": "version",
+ "getter_name": "version",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "last_check",
+ "getter_name": "lastCheck",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": false,
+ "constraints": [],
+ "explicit_pk": [
+ "id"
+ ],
+ "unique_keys": [
+ [
+ "name",
+ "provider_name"
+ ]
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/lib/data/local/tables.dart b/lib/core/database/tables/database_tables.dart
similarity index 51%
rename from lib/data/local/tables.dart
rename to lib/core/database/tables/database_tables.dart
index 17bba834..469f33de 100644
--- a/lib/data/local/tables.dart
+++ b/lib/core/database/tables/database_tables.dart
@@ -1,6 +1,7 @@
import 'package:drift/drift.dart';
-import 'package:hiddify/data/local/type_converters.dart';
-import 'package:hiddify/domain/profiles/profiles.dart';
+import 'package:hiddify/core/database/converters/duration_converter.dart';
+import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
+import 'package:hiddify/features/profile/model/profile_entity.dart';
@DataClassName('ProfileEntry')
class ProfileEntries extends Table {
@@ -22,3 +23,22 @@ class ProfileEntries extends Table {
@override
Set get primaryKey => {id};
}
+
+@DataClassName('GeoAssetEntry')
+class GeoAssetEntries extends Table {
+ TextColumn get id => text()();
+ TextColumn get type => textEnum()();
+ BoolColumn get active => boolean()();
+ TextColumn get name => text().withLength(min: 1)();
+ TextColumn get providerName => text().withLength(min: 1)();
+ TextColumn get version => text().nullable()();
+ DateTimeColumn get lastCheck => dateTime().nullable()();
+
+ @override
+ Set get primaryKey => {id};
+
+ @override
+ List> get uniqueKeys => [
+ {name, providerName},
+ ];
+}
diff --git a/lib/core/http_client/http_client_provider.dart b/lib/core/http_client/http_client_provider.dart
new file mode 100644
index 00000000..0798a9d4
--- /dev/null
+++ b/lib/core/http_client/http_client_provider.dart
@@ -0,0 +1,28 @@
+import 'dart:io';
+
+import 'package:dio/dio.dart';
+import 'package:hiddify/core/app_info/app_info_provider.dart';
+import 'package:hiddify/core/preferences/general_preferences.dart';
+import 'package:native_dio_adapter/native_dio_adapter.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'http_client_provider.g.dart';
+
+@Riverpod(keepAlive: true)
+Dio httpClient(HttpClientRef ref) {
+ final dio = Dio(
+ BaseOptions(
+ connectTimeout: const Duration(seconds: 15),
+ sendTimeout: const Duration(seconds: 15),
+ receiveTimeout: const Duration(seconds: 15),
+ headers: {
+ "User-Agent": ref.watch(appInfoProvider).requireValue.userAgent,
+ },
+ ),
+ );
+ final debug = ref.read(debugModeNotifierProvider);
+ if (debug && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
+ dio.httpClientAdapter = NativeAdapter();
+ }
+ return dio;
+}
diff --git a/lib/core/localization/locale_extensions.dart b/lib/core/localization/locale_extensions.dart
new file mode 100644
index 00000000..e42d81e8
--- /dev/null
+++ b/lib/core/localization/locale_extensions.dart
@@ -0,0 +1,13 @@
+import 'package:flutter_localized_locales/flutter_localized_locales.dart';
+import 'package:hiddify/gen/fonts.gen.dart';
+import 'package:hiddify/gen/translations.g.dart';
+
+extension AppLocaleX on AppLocale {
+ String get preferredFontFamily =>
+ this == AppLocale.fa ? FontFamily.shabnam : "";
+
+ String get localeName =>
+ LocaleNamesLocalizationsDelegate
+ .nativeLocaleNames[flutterLocale.toString()] ??
+ name;
+}
diff --git a/lib/core/localization/locale_preferences.dart b/lib/core/localization/locale_preferences.dart
new file mode 100644
index 00000000..28da4eff
--- /dev/null
+++ b/lib/core/localization/locale_preferences.dart
@@ -0,0 +1,28 @@
+import 'package:hiddify/core/preferences/preferences_provider.dart';
+import 'package:hiddify/gen/translations.g.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'locale_preferences.g.dart';
+
+@Riverpod(keepAlive: true)
+class LocalePreferences extends _$LocalePreferences {
+ @override
+ AppLocale build() {
+ final persisted =
+ ref.watch(sharedPreferencesProvider).requireValue.getString("locale");
+ if (persisted == null) return AppLocaleUtils.findDeviceLocale();
+ // keep backward compatibility with chinese after changing zh to zh_CN
+ if (persisted == "zh") {
+ return AppLocale.zhCn;
+ }
+ return AppLocale.values.byName(persisted);
+ }
+
+ Future changeLocale(AppLocale value) async {
+ state = value;
+ await ref
+ .read(sharedPreferencesProvider)
+ .requireValue
+ .setString("locale", value.name);
+ }
+}
diff --git a/lib/core/localization/translations.dart b/lib/core/localization/translations.dart
new file mode 100644
index 00000000..461ffab2
--- /dev/null
+++ b/lib/core/localization/translations.dart
@@ -0,0 +1,11 @@
+import 'package:hiddify/core/localization/locale_preferences.dart';
+import 'package:hiddify/gen/translations.g.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+export 'package:hiddify/gen/translations.g.dart';
+
+part 'translations.g.dart';
+
+@Riverpod(keepAlive: true)
+TranslationsEn translations(TranslationsRef ref) =>
+ ref.watch(localePreferencesProvider).build();
diff --git a/lib/core/model/app_info_entity.dart b/lib/core/model/app_info_entity.dart
new file mode 100644
index 00000000..c239d7fa
--- /dev/null
+++ b/lib/core/model/app_info_entity.dart
@@ -0,0 +1,32 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:hiddify/core/model/environment.dart';
+
+part 'app_info_entity.freezed.dart';
+
+@freezed
+class AppInfoEntity with _$AppInfoEntity {
+ const AppInfoEntity._();
+
+ const factory AppInfoEntity({
+ required String name,
+ required String version,
+ required String buildNumber,
+ required Release release,
+ required String operatingSystem,
+ required String operatingSystemVersion,
+ required Environment environment,
+ }) = _AppInfoEntity;
+
+ String get userAgent =>
+ "HiddifyNext/$version ($operatingSystem) like ClashMeta v2ray sing-box";
+
+ String get presentVersion => environment == Environment.prod
+ ? version
+ : "$version ${environment.name}";
+
+ /// formats app info for sharing
+ String format() => '''
+$name v$version ($buildNumber) [${environment.name}]
+${release.name} release
+$operatingSystem [$operatingSystemVersion]''';
+}
diff --git a/lib/domain/constants.dart b/lib/core/model/constants.dart
similarity index 63%
rename from lib/domain/constants.dart
rename to lib/core/model/constants.dart
index da4e62dd..e2ffb59f 100644
--- a/lib/domain/constants.dart
+++ b/lib/core/model/constants.dart
@@ -1,9 +1,5 @@
abstract class Constants {
static const appName = "Hiddify Next";
- static const geoipFileName = "geoip.db";
- static const geositeFileName = "geosite.db";
- static const configsFolderName = "configs";
- static const localHost = "127.0.0.1";
static const githubUrl = "https://github.com/hiddify/hiddify-next";
static const githubReleasesApiUrl =
"https://api.github.com/repos/hiddify/hiddify-next/releases";
@@ -15,10 +11,3 @@ abstract class Constants {
static const privacyPolicyUrl = "https://hiddify.com/en/privacy-policy/";
static const termsAndConditionsUrl = "https://hiddify.com/terms/";
}
-
-abstract class Defaults {
- static const clashApiPort = 9090;
- static const mixedPort = 2334;
- static const connectionTestUrl = "https://www.gstatic.com/generate_204";
- static const concurrentTestCount = 5;
-}
diff --git a/lib/core/model/directories.dart b/lib/core/model/directories.dart
new file mode 100644
index 00000000..b940b75d
--- /dev/null
+++ b/lib/core/model/directories.dart
@@ -0,0 +1,7 @@
+import 'dart:io';
+
+typedef Directories = ({
+ Directory baseDir,
+ Directory workingDir,
+ Directory tempDir
+});
diff --git a/lib/domain/environment.dart b/lib/core/model/environment.dart
similarity index 100%
rename from lib/domain/environment.dart
rename to lib/core/model/environment.dart
diff --git a/lib/domain/failures.dart b/lib/core/model/failures.dart
similarity index 97%
rename from lib/domain/failures.dart
rename to lib/core/model/failures.dart
index 1640ac61..1f28b6d9 100644
--- a/lib/domain/failures.dart
+++ b/lib/core/model/failures.dart
@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
-import 'package:hiddify/core/prefs/prefs.dart';
+import 'package:hiddify/core/localization/translations.dart';
typedef PresentableError = ({String type, String? message});
diff --git a/lib/core/model/region.dart b/lib/core/model/region.dart
new file mode 100644
index 00000000..4b4a65eb
--- /dev/null
+++ b/lib/core/model/region.dart
@@ -0,0 +1,15 @@
+import 'package:hiddify/core/localization/translations.dart';
+
+enum Region {
+ ir,
+ cn,
+ ru,
+ other;
+
+ String present(TranslationsEn t) => switch (this) {
+ ir => t.settings.general.regions.ir,
+ cn => t.settings.general.regions.cn,
+ ru => t.settings.general.regions.ru,
+ other => t.settings.general.regions.other,
+ };
+}
diff --git a/lib/core/notification/in_app_notification_controller.dart b/lib/core/notification/in_app_notification_controller.dart
new file mode 100644
index 00000000..7ef2113c
--- /dev/null
+++ b/lib/core/notification/in_app_notification_controller.dart
@@ -0,0 +1,97 @@
+import 'package:flutter/material.dart';
+import 'package:hiddify/core/model/failures.dart';
+import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
+import 'package:hiddify/utils/utils.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'package:toastification/toastification.dart';
+
+part 'in_app_notification_controller.g.dart';
+
+@Riverpod(keepAlive: true)
+InAppNotificationController inAppNotificationController(
+ InAppNotificationControllerRef ref,
+) {
+ return InAppNotificationController();
+}
+
+enum NotificationType {
+ info,
+ error,
+ success,
+}
+
+class InAppNotificationController with AppLogger {
+ void showToast(
+ BuildContext context,
+ String message, {
+ NotificationType type = NotificationType.info,
+ Duration duration = const Duration(seconds: 3),
+ }) {
+ toastification.show(
+ context: context,
+ title: message,
+ type: type._toastificationType,
+ alignment: Alignment.bottomLeft,
+ autoCloseDuration: duration,
+ style: ToastificationStyle.fillColored,
+ pauseOnHover: true,
+ showProgressBar: false,
+ dragToClose: true,
+ closeOnClick: true,
+ closeButtonShowType: CloseButtonShowType.onHover,
+ );
+ }
+
+ void showErrorToast(String message) {
+ final context = RootScaffold.stateKey.currentContext;
+ if (context == null) {
+ loggy.warning("context is null");
+ return;
+ }
+ showToast(
+ context,
+ message,
+ type: NotificationType.error,
+ duration: const Duration(seconds: 5),
+ );
+ }
+
+ void showSuccessToast(String message) {
+ final context = RootScaffold.stateKey.currentContext;
+ if (context == null) {
+ loggy.warning("context is null");
+ return;
+ }
+ showToast(
+ context,
+ message,
+ type: NotificationType.success,
+ );
+ }
+
+ void showInfoToast(String message) {
+ final context = RootScaffold.stateKey.currentContext;
+ if (context == null) {
+ loggy.warning("context is null");
+ return;
+ }
+ showToast(context, message);
+ }
+
+ Future showErrorDialog(PresentableError error) async {
+ final context = RootScaffold.stateKey.currentContext;
+ if (context == null) {
+ loggy.warning("context is null");
+ return;
+ }
+ CustomAlertDialog.fromErr(error).show(context);
+ }
+}
+
+extension NotificationTypeX on NotificationType {
+ ToastificationType get _toastificationType => switch (this) {
+ NotificationType.success => ToastificationType.success,
+ NotificationType.error => ToastificationType.error,
+ NotificationType.info => ToastificationType.info,
+ };
+}
diff --git a/lib/core/prefs/general_prefs.dart b/lib/core/preferences/general_preferences.dart
similarity index 76%
rename from lib/core/prefs/general_prefs.dart
rename to lib/core/preferences/general_preferences.dart
index 656ffb41..af0a8dbe 100644
--- a/lib/core/prefs/general_prefs.dart
+++ b/lib/core/preferences/general_preferences.dart
@@ -1,19 +1,22 @@
import 'package:flutter/foundation.dart';
-import 'package:hiddify/core/core_providers.dart';
-import 'package:hiddify/data/data_providers.dart';
-import 'package:hiddify/domain/environment.dart';
-import 'package:hiddify/domain/singbox/singbox.dart';
+import 'package:hiddify/core/app_info/app_info_provider.dart';
+import 'package:hiddify/core/model/environment.dart';
+import 'package:hiddify/core/model/region.dart';
+import 'package:hiddify/core/preferences/preferences_provider.dart';
+import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
import 'package:hiddify/utils/pref_notifier.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
-part 'general_prefs.g.dart';
+part 'general_preferences.g.dart';
+
+// TODO refactor
bool _debugIntroPage = false;
@Riverpod(keepAlive: true)
class IntroCompleted extends _$IntroCompleted {
late final _pref = Pref(
- ref.watch(sharedPreferencesProvider),
+ ref.watch(sharedPreferencesProvider).requireValue,
"intro_completed",
false,
);
@@ -33,7 +36,7 @@ class IntroCompleted extends _$IntroCompleted {
@Riverpod(keepAlive: true)
class RegionNotifier extends _$RegionNotifier {
late final _pref = Pref(
- ref.watch(sharedPreferencesProvider),
+ ref.watch(sharedPreferencesProvider).requireValue,
"region",
Region.other,
mapFrom: Region.values.byName,
@@ -51,8 +54,11 @@ class RegionNotifier extends _$RegionNotifier {
@Riverpod(keepAlive: true)
class SilentStartNotifier extends _$SilentStartNotifier {
- late final _pref =
- Pref(ref.watch(sharedPreferencesProvider), "silent_start", false);
+ late final _pref = Pref(
+ ref.watch(sharedPreferencesProvider).requireValue,
+ "silent_start",
+ false,
+ );
@override
bool build() => _pref.getValue();
@@ -66,7 +72,7 @@ class SilentStartNotifier extends _$SilentStartNotifier {
@Riverpod(keepAlive: true)
class EnableAnalytics extends _$EnableAnalytics {
late final _pref = Pref(
- ref.watch(sharedPreferencesProvider),
+ ref.watch(sharedPreferencesProvider).requireValue,
"enable_analytics",
true,
);
@@ -83,7 +89,7 @@ class EnableAnalytics extends _$EnableAnalytics {
@Riverpod(keepAlive: true)
class DisableMemoryLimit extends _$DisableMemoryLimit {
late final _pref = Pref(
- ref.watch(sharedPreferencesProvider),
+ ref.watch(sharedPreferencesProvider).requireValue,
"disable_memory_limit",
false,
);
@@ -100,9 +106,9 @@ class DisableMemoryLimit extends _$DisableMemoryLimit {
@Riverpod(keepAlive: true)
class DebugModeNotifier extends _$DebugModeNotifier {
late final _pref = Pref(
- ref.watch(sharedPreferencesProvider),
+ ref.watch(sharedPreferencesProvider).requireValue,
"debug_mode",
- ref.read(envProvider) == Environment.dev,
+ ref.read(environmentProvider) == Environment.dev,
);
@override
@@ -117,7 +123,7 @@ class DebugModeNotifier extends _$DebugModeNotifier {
@Riverpod(keepAlive: true)
class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier {
late final _pref = Pref(
- ref.watch(sharedPreferencesProvider),
+ ref.watch(sharedPreferencesProvider).requireValue,
"per_app_proxy_mode",
PerAppProxyMode.off,
mapFrom: PerAppProxyMode.values.byName,
@@ -136,13 +142,13 @@ class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier {
@Riverpod(keepAlive: true)
class PerAppProxyList extends _$PerAppProxyList {
late final _include = Pref(
- ref.watch(sharedPreferencesProvider),
+ ref.watch(sharedPreferencesProvider).requireValue,
"per_app_proxy_include_list",
[],
);
late final _exclude = Pref(
- ref.watch(sharedPreferencesProvider),
+ ref.watch(sharedPreferencesProvider).requireValue,
"per_app_proxy_exclude_list",
[],
);
@@ -165,7 +171,7 @@ class PerAppProxyList extends _$PerAppProxyList {
@riverpod
class MarkNewProfileActive extends _$MarkNewProfileActive {
late final _pref = Pref(
- ref.watch(sharedPreferencesProvider),
+ ref.watch(sharedPreferencesProvider).requireValue,
"mark_new_profile_active",
true,
);
diff --git a/lib/core/preferences/preferences_provider.dart b/lib/core/preferences/preferences_provider.dart
new file mode 100644
index 00000000..533f6bd8
--- /dev/null
+++ b/lib/core/preferences/preferences_provider.dart
@@ -0,0 +1,8 @@
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+part 'preferences_provider.g.dart';
+
+@Riverpod(keepAlive: true)
+Future sharedPreferences(SharedPreferencesRef ref) async =>
+ SharedPreferences.getInstance();
diff --git a/lib/core/prefs/service_prefs.dart b/lib/core/preferences/service_preferences.dart
similarity index 63%
rename from lib/core/prefs/service_prefs.dart
rename to lib/core/preferences/service_preferences.dart
index d9cc2cbb..847c614f 100644
--- a/lib/core/prefs/service_prefs.dart
+++ b/lib/core/preferences/service_preferences.dart
@@ -1,14 +1,17 @@
-import 'package:hiddify/data/data_providers.dart';
+import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/utils/pref_notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
-part 'service_prefs.g.dart';
+part 'service_preferences.g.dart';
@Riverpod(keepAlive: true)
class StartedByUser extends _$StartedByUser with AppLogger {
- late final _pref =
- Pref(ref.watch(sharedPreferencesProvider), "started_by_user", false);
+ late final _pref = Pref(
+ ref.watch(sharedPreferencesProvider).requireValue,
+ "started_by_user",
+ false,
+ );
@override
bool build() => _pref.getValue();
diff --git a/lib/core/prefs/locale_prefs.dart b/lib/core/prefs/locale_prefs.dart
deleted file mode 100644
index 0f294743..00000000
--- a/lib/core/prefs/locale_prefs.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-import 'package:hiddify/data/data_providers.dart';
-import 'package:hiddify/gen/fonts.gen.dart';
-import 'package:hiddify/gen/translations.g.dart';
-import 'package:hiddify/utils/pref_notifier.dart';
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-
-export 'package:hiddify/gen/translations.g.dart';
-
-part 'locale_prefs.g.dart';
-
-@Riverpod(keepAlive: true)
-class LocaleNotifier extends _$LocaleNotifier {
- late final _pref = Pref(
- ref.watch(sharedPreferencesProvider),
- "locale",
- AppLocaleUtils.findDeviceLocale(),
- mapFrom: AppLocale.values.byName,
- mapTo: (value) => value.name,
- );
-
- @override
- AppLocale build() => _pref.getValue();
-
- Future update(AppLocale value) {
- state = value;
- return _pref.update(value);
- }
-}
-
-extension AppLocaleX on AppLocale {
- String get preferredFontFamily =>
- this == AppLocale.fa ? FontFamily.shabnam : "";
-}
diff --git a/lib/core/prefs/prefs.dart b/lib/core/prefs/prefs.dart
deleted file mode 100644
index 2d8fa8fc..00000000
--- a/lib/core/prefs/prefs.dart
+++ /dev/null
@@ -1,4 +0,0 @@
-export 'app_theme.dart';
-export 'general_prefs.dart';
-export 'locale_prefs.dart';
-export 'theme_prefs.dart';
diff --git a/lib/core/prefs/theme_prefs.dart b/lib/core/prefs/theme_prefs.dart
deleted file mode 100644
index a9f21b3b..00000000
--- a/lib/core/prefs/theme_prefs.dart
+++ /dev/null
@@ -1,25 +0,0 @@
-import 'package:hiddify/core/prefs/app_theme.dart';
-import 'package:hiddify/data/data_providers.dart';
-import 'package:hiddify/utils/pref_notifier.dart';
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-
-part 'theme_prefs.g.dart';
-
-@Riverpod(keepAlive: true)
-class ThemeModeNotifier extends _$ThemeModeNotifier {
- late final _pref = Pref(
- ref.watch(sharedPreferencesProvider),
- "theme_mode",
- AppThemeMode.system,
- mapFrom: AppThemeMode.values.byName,
- mapTo: (value) => value.name,
- );
-
- @override
- AppThemeMode build() => _pref.getValue();
-
- Future update(AppThemeMode value) {
- state = value;
- return _pref.update(value);
- }
-}
diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart
index 5de8f211..44e14eac 100644
--- a/lib/core/router/app_router.dart
+++ b/lib/core/router/app_router.dart
@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
-import 'package:hiddify/core/prefs/prefs.dart';
+import 'package:hiddify/core/preferences/general_preferences.dart';
import 'package:hiddify/core/router/routes.dart';
import 'package:hiddify/services/deep_link_service.dart';
import 'package:hiddify/utils/utils.dart';
@@ -38,7 +38,9 @@ GoRouter router(RouterRef ref) {
navigatorKey: rootNavigatorKey,
initialLocation: initialLocation,
debugLogDiagnostics: true,
- routes: useMobileRouter ? mobileRoutes : desktopRoutes,
+ routes: [
+ if (useMobileRouter) $mobileWrapperRoute else $desktopWrapperRoute,
+ ],
refreshListenable: notifier,
redirect: notifier.redirect,
observers: [
@@ -51,7 +53,7 @@ int getCurrentIndex(BuildContext context) {
final String location = GoRouterState.of(context).uri.path;
if (location == const HomeRoute().location) return 0;
if (location.startsWith(const ProxiesRoute().location)) return 1;
- if (location.startsWith(const LogsRoute().location)) return 2;
+ if (location.startsWith(const LogsOverviewRoute().location)) return 2;
if (location.startsWith(const SettingsRoute().location)) return 3;
if (location.startsWith(const AboutRoute().location)) return 4;
return 0;
@@ -64,7 +66,7 @@ void switchTab(int index, BuildContext context) {
case 1:
const ProxiesRoute().go(context);
case 2:
- const LogsRoute().go(context);
+ const LogsOverviewRoute().go(context);
case 3:
const SettingsRoute().go(context);
case 4:
@@ -90,6 +92,7 @@ class RouterListenable extends _$RouterListenable
});
}
+// ignore: avoid_build_context_in_providers
String? redirect(BuildContext context, GoRouterState state) {
// if (this.state.isLoading || this.state.hasError) return null;
diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart
index 5b9acd92..d5ec6869 100644
--- a/lib/core/router/routes.dart
+++ b/lib/core/router/routes.dart
@@ -1,16 +1,380 @@
-import 'package:hiddify/core/router/routes/desktop_routes.dart' as desktop;
-import 'package:hiddify/core/router/routes/mobile_routes.dart' as mobile;
-import 'package:hiddify/core/router/routes/shared_routes.dart' as shared;
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:hiddify/core/router/app_router.dart';
+import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
+import 'package:hiddify/features/config_option/overview/config_options_page.dart';
+import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart';
+import 'package:hiddify/features/home/widget/home_page.dart';
+import 'package:hiddify/features/intro/widget/intro_page.dart';
+import 'package:hiddify/features/log/overview/logs_overview_page.dart';
+import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_page.dart';
+import 'package:hiddify/features/profile/add/add_profile_modal.dart';
+import 'package:hiddify/features/profile/details/profile_details_page.dart';
+import 'package:hiddify/features/profile/overview/profiles_overview_page.dart';
+import 'package:hiddify/features/proxy/overview/proxies_overview_page.dart';
+import 'package:hiddify/features/settings/about/about_page.dart';
+import 'package:hiddify/features/settings/overview/settings_overview_page.dart';
+import 'package:hiddify/utils/utils.dart';
-export 'routes/mobile_routes.dart';
-export 'routes/shared_routes.dart' hide $appRoutes;
+part 'routes.g.dart';
-final mobileRoutes = [
- ...shared.$appRoutes,
- ...mobile.$appRoutes,
-];
+GlobalKey? _dynamicRootKey =
+ useMobileRouter ? rootNavigatorKey : null;
-final desktopRoutes = [
- ...shared.$appRoutes,
- ...desktop.$appRoutes,
-];
+@TypedShellRoute(
+ routes: [
+ TypedGoRoute(path: "/intro", name: IntroRoute.name),
+ TypedGoRoute(
+ path: "/",
+ name: HomeRoute.name,
+ routes: [
+ TypedGoRoute(
+ path: "add",
+ name: AddProfileRoute.name,
+ ),
+ TypedGoRoute(
+ path: "profiles",
+ name: ProfilesOverviewRoute.name,
+ ),
+ TypedGoRoute(
+ path: "profiles/new",
+ name: NewProfileRoute.name,
+ ),
+ TypedGoRoute(
+ path: "profiles/:id",
+ name: ProfileDetailsRoute.name,
+ ),
+ TypedGoRoute(
+ path: "logs",
+ name: LogsOverviewRoute.name,
+ ),
+ TypedGoRoute(
+ path: "settings",
+ name: SettingsRoute.name,
+ routes: [
+ TypedGoRoute(
+ path: "config-options",
+ name: ConfigOptionsRoute.name,
+ ),
+ TypedGoRoute(
+ path: "per-app-proxy",
+ name: PerAppProxyRoute.name,
+ ),
+ TypedGoRoute(
+ path: "routing-assets",
+ name: GeoAssetsRoute.name,
+ ),
+ ],
+ ),
+ TypedGoRoute(
+ path: "about",
+ name: AboutRoute.name,
+ ),
+ ],
+ ),
+ TypedGoRoute(
+ path: "/proxies",
+ name: ProxiesRoute.name,
+ ),
+ ],
+)
+class MobileWrapperRoute extends ShellRouteData {
+ const MobileWrapperRoute();
+
+ @override
+ Widget builder(BuildContext context, GoRouterState state, Widget navigator) {
+ return AdaptiveRootScaffold(navigator);
+ }
+}
+
+@TypedShellRoute(
+ routes: [
+ TypedGoRoute(path: "/intro", name: IntroRoute.name),
+ TypedGoRoute(
+ path: "/",
+ name: HomeRoute.name,
+ routes: [
+ TypedGoRoute(
+ path: "add",
+ name: AddProfileRoute.name,
+ ),
+ TypedGoRoute(
+ path: "profiles",
+ name: ProfilesOverviewRoute.name,
+ ),
+ TypedGoRoute(
+ path: "profiles/new",
+ name: NewProfileRoute.name,
+ ),
+ TypedGoRoute(
+ path: "profiles/:id",
+ name: ProfileDetailsRoute.name,
+ ),
+ ],
+ ),
+ TypedGoRoute(
+ path: "/proxies",
+ name: ProxiesRoute.name,
+ ),
+ TypedGoRoute(
+ path: "/logs",
+ name: LogsOverviewRoute.name,
+ ),
+ TypedGoRoute(
+ path: "/settings",
+ name: SettingsRoute.name,
+ routes: [
+ TypedGoRoute(
+ path: "config-options",
+ name: ConfigOptionsRoute.name,
+ ),
+ TypedGoRoute(
+ path: "routing-assets",
+ name: GeoAssetsRoute.name,
+ ),
+ ],
+ ),
+ TypedGoRoute(
+ path: "/about",
+ name: AboutRoute.name,
+ ),
+ ],
+)
+class DesktopWrapperRoute extends ShellRouteData {
+ const DesktopWrapperRoute();
+
+ @override
+ Widget builder(BuildContext context, GoRouterState state, Widget navigator) {
+ return AdaptiveRootScaffold(navigator);
+ }
+}
+
+class IntroRoute extends GoRouteData {
+ const IntroRoute();
+ static const name = "Intro";
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: IntroPage(),
+ );
+ }
+}
+
+class HomeRoute extends GoRouteData {
+ const HomeRoute();
+ static const name = "Home";
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ return const NoTransitionPage(
+ name: name,
+ child: HomePage(),
+ );
+ }
+}
+
+class ProxiesRoute extends GoRouteData {
+ const ProxiesRoute();
+ static const name = "Proxies";
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ return const NoTransitionPage(
+ name: name,
+ child: ProxiesOverviewPage(),
+ );
+ }
+}
+
+class AddProfileRoute extends GoRouteData {
+ const AddProfileRoute({this.url});
+
+ final String? url;
+
+ static const name = "Add Profile";
+
+ static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ return BottomSheetPage(
+ fixed: true,
+ name: name,
+ builder: (controller) => AddProfileModal(
+ url: url,
+ scrollController: controller,
+ ),
+ );
+ }
+}
+
+class ProfilesOverviewRoute extends GoRouteData {
+ const ProfilesOverviewRoute();
+ static const name = "Profiles";
+
+ static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ return BottomSheetPage(
+ name: name,
+ builder: (controller) =>
+ ProfilesOverviewModal(scrollController: controller),
+ );
+ }
+}
+
+class NewProfileRoute extends GoRouteData {
+ const NewProfileRoute();
+ static const name = "New Profile";
+
+ static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: ProfileDetailsPage("new"),
+ );
+ }
+}
+
+class ProfileDetailsRoute extends GoRouteData {
+ const ProfileDetailsRoute(this.id);
+ final String id;
+ static const name = "Profile Details";
+
+ static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ return MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: ProfileDetailsPage(id),
+ );
+ }
+}
+
+class LogsOverviewRoute extends GoRouteData {
+ const LogsOverviewRoute();
+ static const name = "Logs";
+
+ static final GlobalKey? $parentNavigatorKey = _dynamicRootKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ if (useMobileRouter) {
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: LogsOverviewPage(),
+ );
+ }
+ return const NoTransitionPage(name: name, child: LogsOverviewPage());
+ }
+}
+
+class SettingsRoute extends GoRouteData {
+ const SettingsRoute();
+ static const name = "Settings";
+
+ static final GlobalKey? $parentNavigatorKey = _dynamicRootKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ if (useMobileRouter) {
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: SettingsOverviewPage(),
+ );
+ }
+ return const NoTransitionPage(name: name, child: SettingsOverviewPage());
+ }
+}
+
+class ConfigOptionsRoute extends GoRouteData {
+ const ConfigOptionsRoute();
+ static const name = "Config Options";
+
+ static final GlobalKey? $parentNavigatorKey = _dynamicRootKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ if (useMobileRouter) {
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: ConfigOptionsPage(),
+ );
+ }
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: ConfigOptionsPage(),
+ );
+ }
+}
+
+class PerAppProxyRoute extends GoRouteData {
+ const PerAppProxyRoute();
+ static const name = "Per-app Proxy";
+
+ static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: PerAppProxyPage(),
+ );
+ }
+}
+
+class GeoAssetsRoute extends GoRouteData {
+ const GeoAssetsRoute();
+ static const name = "Routing Assets";
+
+ static final GlobalKey? $parentNavigatorKey = _dynamicRootKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ if (useMobileRouter) {
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: GeoAssetsOverviewPage(),
+ );
+ }
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: GeoAssetsOverviewPage(),
+ );
+ }
+}
+
+class AboutRoute extends GoRouteData {
+ const AboutRoute();
+ static const name = "About";
+
+ static final GlobalKey? $parentNavigatorKey = _dynamicRootKey;
+
+ @override
+ Page buildPage(BuildContext context, GoRouterState state) {
+ if (useMobileRouter) {
+ return const MaterialPage(
+ fullscreenDialog: true,
+ name: name,
+ child: AboutPage(),
+ );
+ }
+ return const NoTransitionPage(name: name, child: AboutPage());
+ }
+}
diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart
deleted file mode 100644
index 20c1f87d..00000000
--- a/lib/core/router/routes/desktop_routes.dart
+++ /dev/null
@@ -1,117 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:go_router/go_router.dart';
-import 'package:hiddify/core/router/routes/shared_routes.dart';
-import 'package:hiddify/features/about/view/view.dart';
-import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
-import 'package:hiddify/features/logs/view/view.dart';
-import 'package:hiddify/features/settings/view/view.dart';
-
-part 'desktop_routes.g.dart';
-
-@TypedShellRoute(
- routes: [
- TypedGoRoute(
- path: HomeRoute.path,
- name: HomeRoute.name,
- routes: [
- TypedGoRoute(
- path: AddProfileRoute.path,
- name: AddProfileRoute.name,
- ),
- TypedGoRoute(
- path: ProfilesRoute.path,
- name: ProfilesRoute.name,
- ),
- TypedGoRoute(
- path: NewProfileRoute.path,
- name: NewProfileRoute.name,
- ),
- TypedGoRoute(
- path: ProfileDetailsRoute.path,
- name: ProfileDetailsRoute.name,
- ),
- ],
- ),
- TypedGoRoute(
- path: ProxiesRoute.path,
- name: ProxiesRoute.name,
- ),
- TypedGoRoute(
- path: LogsRoute.path,
- name: LogsRoute.name,
- ),
- TypedGoRoute(
- path: SettingsRoute.path,
- name: SettingsRoute.name,
- routes: [
- TypedGoRoute(
- path: ConfigOptionsRoute.path,
- name: ConfigOptionsRoute.name,
- ),
- ],
- ),
- TypedGoRoute(
- path: AboutRoute.path,
- name: AboutRoute.name,
- ),
- ],
-)
-class DesktopWrapperRoute extends ShellRouteData {
- const DesktopWrapperRoute();
-
- @override
- Widget builder(BuildContext context, GoRouterState state, Widget navigator) {
- return AdaptiveRootScaffold(navigator);
- }
-}
-
-class LogsRoute extends GoRouteData {
- const LogsRoute();
- static const path = '/logs';
- static const name = 'Logs';
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const NoTransitionPage(name: name, child: LogsPage());
- }
-}
-
-class SettingsRoute extends GoRouteData {
- const SettingsRoute();
- static const path = '/settings';
- static const name = 'Settings';
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const NoTransitionPage(name: name, child: SettingsPage());
- }
-}
-
-class ConfigOptionsRoute extends GoRouteData {
- const ConfigOptionsRoute();
- static const path = 'config-options';
- static const name = 'Config Options';
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const MaterialPage(
- fullscreenDialog: true,
- name: name,
- child: ConfigOptionsPage(),
- );
- }
-}
-
-class AboutRoute extends GoRouteData {
- const AboutRoute();
- static const path = '/about';
- static const name = 'About';
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const NoTransitionPage(
- name: name,
- child: AboutPage(),
- );
- }
-}
diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart
deleted file mode 100644
index 8cfd3ca3..00000000
--- a/lib/core/router/routes/mobile_routes.dart
+++ /dev/null
@@ -1,156 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:go_router/go_router.dart';
-import 'package:hiddify/core/router/app_router.dart';
-import 'package:hiddify/core/router/routes/shared_routes.dart';
-import 'package:hiddify/features/about/view/view.dart';
-import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
-import 'package:hiddify/features/logs/view/view.dart';
-import 'package:hiddify/features/settings/view/view.dart';
-
-part 'mobile_routes.g.dart';
-
-@TypedShellRoute(
- routes: [
- TypedGoRoute(
- path: HomeRoute.path,
- name: HomeRoute.name,
- routes: [
- TypedGoRoute(
- path: AddProfileRoute.path,
- name: AddProfileRoute.name,
- ),
- TypedGoRoute(
- path: ProfilesRoute.path,
- name: ProfilesRoute.name,
- ),
- TypedGoRoute(
- path: NewProfileRoute.path,
- name: NewProfileRoute.name,
- ),
- TypedGoRoute(
- path: ProfileDetailsRoute.path,
- name: ProfileDetailsRoute.name,
- ),
- TypedGoRoute(
- path: LogsRoute.path,
- name: LogsRoute.name,
- ),
- TypedGoRoute(
- path: SettingsRoute.path,
- name: SettingsRoute.name,
- routes: [
- TypedGoRoute(
- path: ConfigOptionsRoute.path,
- name: ConfigOptionsRoute.name,
- ),
- TypedGoRoute(
- path: PerAppProxyRoute.path,
- name: PerAppProxyRoute.name,
- ),
- ],
- ),
- TypedGoRoute(
- path: AboutRoute.path,
- name: AboutRoute.name,
- ),
- ],
- ),
- TypedGoRoute(
- path: ProxiesRoute.path,
- name: ProxiesRoute.name,
- ),
- ],
-)
-class MobileWrapperRoute extends ShellRouteData {
- const MobileWrapperRoute();
-
- @override
- Widget builder(BuildContext context, GoRouterState state, Widget navigator) {
- return AdaptiveRootScaffold(navigator);
- }
-}
-
-class LogsRoute extends GoRouteData {
- const LogsRoute();
- static const path = 'logs';
- static const name = 'Logs';
-
- static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const MaterialPage(
- fullscreenDialog: true,
- name: name,
- child: LogsPage(),
- );
- }
-}
-
-class SettingsRoute extends GoRouteData {
- const SettingsRoute();
- static const path = 'settings';
- static const name = 'Settings';
-
- static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const MaterialPage(
- fullscreenDialog: true,
- name: name,
- child: SettingsPage(),
- );
- }
-}
-
-class ConfigOptionsRoute extends GoRouteData {
- const ConfigOptionsRoute();
- static const path = 'config-options';
- static const name = 'Config Options';
-
- static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const MaterialPage(
- fullscreenDialog: true,
- name: name,
- child: ConfigOptionsPage(),
- );
- }
-}
-
-class PerAppProxyRoute extends GoRouteData {
- const PerAppProxyRoute();
- static const path = 'per-app-proxy';
- static const name = 'Per-app Proxy';
-
- static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const MaterialPage(
- fullscreenDialog: true,
- name: name,
- child: PerAppProxyPage(),
- );
- }
-}
-
-class AboutRoute extends GoRouteData {
- const AboutRoute();
- static const path = 'about';
- static const name = 'About';
-
- static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const MaterialPage(
- fullscreenDialog: true,
- name: name,
- child: AboutPage(),
- );
- }
-}
diff --git a/lib/core/router/routes/shared_routes.dart b/lib/core/router/routes/shared_routes.dart
deleted file mode 100644
index 76410f5f..00000000
--- a/lib/core/router/routes/shared_routes.dart
+++ /dev/null
@@ -1,127 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:go_router/go_router.dart';
-import 'package:hiddify/core/router/app_router.dart';
-import 'package:hiddify/features/home/view/view.dart';
-import 'package:hiddify/features/intro/intro_page.dart';
-import 'package:hiddify/features/profile_detail/view/view.dart';
-import 'package:hiddify/features/profiles/view/view.dart';
-import 'package:hiddify/features/proxies/view/view.dart';
-import 'package:hiddify/utils/utils.dart';
-
-part 'shared_routes.g.dart';
-
-class HomeRoute extends GoRouteData {
- const HomeRoute();
- static const path = '/';
- static const name = 'Home';
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const NoTransitionPage(
- name: name,
- child: HomePage(),
- );
- }
-}
-
-class ProxiesRoute extends GoRouteData {
- const ProxiesRoute();
- static const path = '/proxies';
- static const name = 'Proxies';
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const NoTransitionPage(
- name: name,
- child: ProxiesPage(),
- );
- }
-}
-
-class AddProfileRoute extends GoRouteData {
- const AddProfileRoute({this.url});
- static const path = 'add';
- static const name = 'Add Profile';
- final String? url;
-
- static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return BottomSheetPage(
- fixed: true,
- name: name,
- builder: (controller) => AddProfileModal(
- url: url,
- scrollController: controller,
- ),
- );
- }
-}
-
-@TypedGoRoute(path: IntroRoute.path, name: IntroRoute.name)
-class IntroRoute extends GoRouteData {
- const IntroRoute();
- static const path = '/intro';
- static const name = 'Intro';
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const MaterialPage(
- fullscreenDialog: true,
- name: name,
- child: IntroPage(),
- );
- }
-}
-
-class ProfilesRoute extends GoRouteData {
- const ProfilesRoute();
- static const path = 'profiles';
- static const name = 'Profiles';
-
- static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return BottomSheetPage(
- name: name,
- builder: (controller) => ProfilesModal(scrollController: controller),
- );
- }
-}
-
-class NewProfileRoute extends GoRouteData {
- const NewProfileRoute();
- static const path = 'profiles/new';
- static const name = 'New Profile';
-
- static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return const MaterialPage(
- fullscreenDialog: true,
- name: name,
- child: ProfileDetailPage("new"),
- );
- }
-}
-
-class ProfileDetailsRoute extends GoRouteData {
- const ProfileDetailsRoute(this.id);
- final String id;
- static const path = 'profiles/:id';
- static const name = 'Profile Details';
-
- static final GlobalKey $parentNavigatorKey = rootNavigatorKey;
-
- @override
- Page buildPage(BuildContext context, GoRouterState state) {
- return MaterialPage(
- fullscreenDialog: true,
- name: name,
- child: ProfileDetailPage(id),
- );
- }
-}
diff --git a/lib/core/prefs/app_theme.dart b/lib/core/theme/app_theme.dart
similarity index 77%
rename from lib/core/prefs/app_theme.dart
rename to lib/core/theme/app_theme.dart
index 786be4f5..60bce95b 100644
--- a/lib/core/prefs/app_theme.dart
+++ b/lib/core/theme/app_theme.dart
@@ -1,31 +1,9 @@
+// mostly exact copy of flex color scheme 7.1's fabulous 12 theme
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
-import 'package:hiddify/core/prefs/locale_prefs.dart';
+import 'package:hiddify/core/theme/app_theme_mode.dart';
+import 'package:hiddify/core/theme/theme_extensions.dart';
-enum AppThemeMode {
- system,
- light,
- dark,
- black;
-
- String present(TranslationsEn t) => switch (this) {
- system => t.settings.general.themeModes.system,
- light => t.settings.general.themeModes.light,
- dark => t.settings.general.themeModes.dark,
- black => t.settings.general.themeModes.black,
- };
-
- ThemeMode get flutterThemeMode => switch (this) {
- system => ThemeMode.system,
- light => ThemeMode.light,
- dark => ThemeMode.dark,
- black => ThemeMode.dark,
- };
-
- bool get trueBlack => this == black;
-}
-
-// mostly exact copy of flex color scheme 7.1's fabulous 12 theme
class AppTheme {
AppTheme(
this.mode,
@@ -160,42 +138,3 @@ class AppTheme {
);
}
}
-
-class ConnectionButtonTheme extends ThemeExtension {
- const ConnectionButtonTheme({
- this.idleColor,
- this.connectedColor,
- });
-
- final Color? idleColor;
- final Color? connectedColor;
-
- static const ConnectionButtonTheme light = ConnectionButtonTheme(
- idleColor: Color(0xFF4a4d8b),
- connectedColor: Color(0xFF44a334),
- );
-
- @override
- ThemeExtension copyWith({
- Color? idleColor,
- Color? connectedColor,
- }) =>
- ConnectionButtonTheme(
- idleColor: idleColor ?? this.idleColor,
- connectedColor: connectedColor ?? this.connectedColor,
- );
-
- @override
- ThemeExtension lerp(
- covariant ThemeExtension? other,
- double t,
- ) {
- if (other is! ConnectionButtonTheme) {
- return this;
- }
- return ConnectionButtonTheme(
- idleColor: Color.lerp(idleColor, other.idleColor, t),
- connectedColor: Color.lerp(connectedColor, other.connectedColor, t),
- );
- }
-}
diff --git a/lib/core/theme/app_theme_mode.dart b/lib/core/theme/app_theme_mode.dart
new file mode 100644
index 00000000..5696f70c
--- /dev/null
+++ b/lib/core/theme/app_theme_mode.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:hiddify/core/localization/translations.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;
+}
diff --git a/lib/core/theme/theme_extensions.dart b/lib/core/theme/theme_extensions.dart
new file mode 100644
index 00000000..7a7895c2
--- /dev/null
+++ b/lib/core/theme/theme_extensions.dart
@@ -0,0 +1,40 @@
+import 'package:flutter/material.dart';
+
+class ConnectionButtonTheme extends ThemeExtension {
+ const ConnectionButtonTheme({
+ this.idleColor,
+ this.connectedColor,
+ });
+
+ final Color? idleColor;
+ final Color? connectedColor;
+
+ static const ConnectionButtonTheme light = ConnectionButtonTheme(
+ idleColor: Color(0xFF4a4d8b),
+ connectedColor: Color(0xFF44a334),
+ );
+
+ @override
+ ThemeExtension copyWith({
+ Color? idleColor,
+ Color? connectedColor,
+ }) =>
+ ConnectionButtonTheme(
+ idleColor: idleColor ?? this.idleColor,
+ connectedColor: connectedColor ?? this.connectedColor,
+ );
+
+ @override
+ ThemeExtension lerp(
+ covariant ThemeExtension? other,
+ double t,
+ ) {
+ if (other is! ConnectionButtonTheme) {
+ return this;
+ }
+ return ConnectionButtonTheme(
+ idleColor: Color.lerp(idleColor, other.idleColor, t),
+ connectedColor: Color.lerp(connectedColor, other.connectedColor, t),
+ );
+ }
+}
diff --git a/lib/core/theme/theme_preferences.dart b/lib/core/theme/theme_preferences.dart
new file mode 100644
index 00000000..29fd4d61
--- /dev/null
+++ b/lib/core/theme/theme_preferences.dart
@@ -0,0 +1,26 @@
+import 'package:hiddify/core/preferences/preferences_provider.dart';
+import 'package:hiddify/core/theme/app_theme_mode.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'theme_preferences.g.dart';
+
+@Riverpod(keepAlive: true)
+class ThemePreferences extends _$ThemePreferences {
+ @override
+ AppThemeMode build() {
+ final persisted = ref
+ .watch(sharedPreferencesProvider)
+ .requireValue
+ .getString("theme_mode");
+ if (persisted == null) return AppThemeMode.system;
+ return AppThemeMode.values.byName(persisted);
+ }
+
+ Future changeThemeMode(AppThemeMode value) async {
+ state = value;
+ await ref
+ .read(sharedPreferencesProvider)
+ .requireValue
+ .setString("theme_mode", value.name);
+ }
+}
diff --git a/lib/data/repository/exception_handlers.dart b/lib/core/utils/exception_handler.dart
similarity index 69%
rename from lib/data/repository/exception_handlers.dart
rename to lib/core/utils/exception_handler.dart
index 3805b584..34845072 100644
--- a/lib/data/repository/exception_handlers.dart
+++ b/lib/core/utils/exception_handler.dart
@@ -30,3 +30,19 @@ extension StreamExceptionHandler on Stream {
);
}
}
+
+extension TaskEitherExceptionHandler on TaskEither {
+ TaskEither handleExceptions(
+ F Function(Object error, StackTrace stackTrace) onError,
+ ) {
+ return TaskEither(
+ () async {
+ try {
+ return await run();
+ } catch (error, stackTrace) {
+ return Left(onError(error, stackTrace));
+ }
+ },
+ );
+ }
+}
diff --git a/lib/utils/ffi_utils.dart b/lib/core/utils/ffi_utils.dart
similarity index 100%
rename from lib/utils/ffi_utils.dart
rename to lib/core/utils/ffi_utils.dart
diff --git a/lib/core/utils/json_converters.dart b/lib/core/utils/json_converters.dart
new file mode 100644
index 00000000..290f6da8
--- /dev/null
+++ b/lib/core/utils/json_converters.dart
@@ -0,0 +1,11 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+class IntervalInSecondsConverter implements JsonConverter {
+ const IntervalInSecondsConverter();
+
+ @override
+ Duration fromJson(int json) => Duration(seconds: json);
+
+ @override
+ int toJson(Duration object) => object.inSeconds;
+}
diff --git a/lib/core/widget/custom_alert_dialog.dart b/lib/core/widget/custom_alert_dialog.dart
new file mode 100644
index 00000000..c3ca3f27
--- /dev/null
+++ b/lib/core/widget/custom_alert_dialog.dart
@@ -0,0 +1,50 @@
+import 'package:flutter/material.dart';
+import 'package:hiddify/core/model/failures.dart';
+
+class CustomAlertDialog extends StatelessWidget {
+ const CustomAlertDialog({
+ super.key,
+ this.title,
+ required this.message,
+ });
+
+ final String? title;
+ final String message;
+
+ factory CustomAlertDialog.fromError(PresentableError error) =>
+ CustomAlertDialog(
+ title: error.message == null ? null : error.type,
+ message: error.message ?? error.type,
+ );
+
+ Future show(BuildContext context) async {
+ await showDialog(
+ context: context,
+ useRootNavigator: true,
+ builder: (context) => this,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final localizations = MaterialLocalizations.of(context);
+
+ return AlertDialog(
+ title: title != null ? Text(title!) : null,
+ content: SingleChildScrollView(
+ child: SizedBox(
+ width: 468,
+ child: Text(message),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: Text(localizations.okButtonLabel),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/data/api/clash_api.dart b/lib/data/api/clash_api.dart
deleted file mode 100644
index c73397c9..00000000
--- a/lib/data/api/clash_api.dart
+++ /dev/null
@@ -1,145 +0,0 @@
-import 'dart:convert';
-import 'dart:io';
-
-import 'package:dio/dio.dart';
-import 'package:fpdart/fpdart.dart';
-import 'package:hiddify/domain/clash/clash.dart';
-import 'package:hiddify/domain/constants.dart';
-import 'package:hiddify/utils/utils.dart';
-import 'package:web_socket_channel/web_socket_channel.dart';
-
-class ClashApi with InfraLogger {
- ClashApi(int port) : address = "${Constants.localHost}:$port";
-
- final String address;
-
- late final _clashDio = Dio(
- BaseOptions(
- baseUrl: "http://$address",
- connectTimeout: const Duration(seconds: 3),
- receiveTimeout: const Duration(seconds: 10),
- sendTimeout: const Duration(seconds: 3),
- ),
- );
-
- TaskEither> getProxies() {
- return TaskEither(
- () async {
- final response = await _clashDio.get("/proxies");
- if (response.statusCode != 200 || response.data == null) {
- return left(response.statusMessage ?? "");
- }
- final proxies = ((jsonDecode(response.data! as String)
- as Map)["proxies"] as Map)
- .entries
- .map(
- (e) {
- final proxyMap = (e.value as Map)
- ..putIfAbsent('name', () => e.key);
- return ClashProxy.fromJson(proxyMap);
- },
- );
- return right(proxies.toList());
- },
- );
- }
-
- TaskEither changeProxy(String selectorName, String proxyName) {
- return TaskEither(
- () async {
- final response = await _clashDio.put(
- "/proxies/$selectorName",
- data: {"name": proxyName},
- );
- if (response.statusCode != HttpStatus.noContent) {
- return left(response.statusMessage ?? "");
- }
- return right(unit);
- },
- );
- }
-
- TaskEither getProxyDelay(
- String name,
- String url, {
- Duration timeout = const Duration(seconds: 10),
- }) {
- return TaskEither(
- () async {
- final response = await _clashDio.get