diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index f6acd8af..a6454a48 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -1,6 +1,6 @@
name: Bug Report
-description: Report a bug encountered while using Hiddify Next
-labels: ['bug']
+description: Report a bug encountered while using Hiddify
+labels: ["bug"]
body:
- type: markdown
attributes:
@@ -52,7 +52,7 @@ body:
id: version
attributes:
label: Version
- description: What version of Hiddify Next are you using?
+ description: What version of Hiddify are you using?
placeholder: v1.3.8 etc
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 51356b73..2fe745c7 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -2,4 +2,4 @@ blank_issues_enabled: false
contact_links:
- name: Questions & Help
url: https://t.me/hiddify_board
- about: Ask a question about Hiddify Next
+ about: Ask a question about Hiddify
diff --git a/.github/release_message.md b/.github/release_message.md
index 6aa515a6..83f4ef86 100644
--- a/.github/release_message.md
+++ b/.github/release_message.md
@@ -33,27 +33,27 @@
| Android |
- 
- 
- 
-
+ 
+ 
+ 
+
|
| Windows |
- 
-
+ | 
+
|
| macOS (v10.15+) |
-  |
+  |
| Linux |
-
-
- |
+
+
+ |
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 944bc00b..2034f049 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -13,11 +13,20 @@ on:
default: "dev"
env:
+ IS_GITHUB_ACTIONS: 1
CHANNEL: "${{ inputs.channel }}"
NDK_VERSION: r26b
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
TAG_NAME: "${{ inputs.tag-name }}"
-
+ TARGET_NAME_AppImage: "Hiddify-Linux-x64"
+ TARGET_NAME_deb: "Hiddify-Debian-x64"
+ TARGET_NAME_rpm: "Hiddify-rpm-x64"
+ TARGET_NAME_apk: "Hiddify-Android"
+ TARGET_NAME_aab: "Hiddify-Android"
+ #TARGET_NAME_exe: "Hiddify-Windows-x64"
+ TARGET_NAME_dmg: "Hiddify-MacOS"
+ TARGET_NAME_pkg: "Hiddify-MacOS-Installer"
+ TARGET_NAME_ipa: "Hiddify-iOS"
jobs:
build:
permissions: write-all
@@ -34,60 +43,36 @@ jobs:
targets: aab
- platform: windows
- os: windows-latest
+ os: windows-2019
aarch: amd64
targets: exe
- filename: hiddify-windows-x64
- # - platform: linux-appimage
- # os: ubuntu-20.04
- # aarch: amd64
- # targets: AppImage
- # filename: hiddify-linux-x64
-
- - platform: linux-deb
- os: ubuntu-20.04
+ - platform: linux
+ os: ubuntu-22.04
aarch: amd64
- targets: deb
- filename: hiddify-debian-x64
-
- - platform: linux-rpm
- os: ubuntu-20.04
- aarch: amd64
- targets: rpm
- filename: hiddify-rpm-x64
+ targets: AppImage,deb,rpm
- platform: macos
os: macos-13
aarch: universal
- targets: dmg
- filename: hiddify-macos-universal
+ targets: dmg,pkg
runs-on: ${{ matrix.os }}
steps:
- name: checkout
uses: actions/checkout@v3
- - name: Setup Apple dependencies
- if: matrix.platform == 'macos' || matrix.platform == 'ios'
- run: |
- brew install create-dmg tree
- echo "installed create-dmg tree "
- npm install -g appdmg
-
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
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'
java-version: 17
-
- name: Setup NDK
if: startsWith(matrix.platform,'android')
uses: nttld/setup-ndk@v1.4.1
@@ -97,36 +82,15 @@ jobs:
add-to-path: true
link-to-sdk: true
- - name: Setup Flutter Distributor
- if: ${{ !startsWith(matrix.platform,'android') }}
- run: |
- dart pub global activate flutter_distributor
- - name: Setup Linux dependencies
- if: ${{ startsWith(matrix.platform,'linux') }}
+ - name: Setup dependencies
run: |
- sudo apt install -y locate ninja-build pkg-config libgtk-3-dev libglib2.0-dev libgio2.0-cil-dev libayatana-appindicator3-dev fuse rpm patchelf
- sudo modprobe fuse
- wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
- chmod +x appimagetool
- mv appimagetool /usr/local/bin/
+ make ${{ matrix.platform }}-install-dependencies
- - name: Get Geo Assets
+ - name: Prepare for ${{ matrix.platform }}
run: |
- make get-geo-assets
-
- - name: Get Dependencies
- run: |
- make get
-
- - name: Generate
- run: |
- make translate
- make gen
-
- - name: Get Libs & Bindings ${{ matrix.platform }}
- run: |
- make ${{ matrix.platform }}-libs
+ make ${{ matrix.platform }}-prepare
+ tree
- name: Setup Android Signing Properties
if: startsWith(matrix.platform,'android')
@@ -198,10 +162,10 @@ jobs:
run: |
mkdir out
ls -R ./build/app/outputs
- cp ./build/app/outputs/flutter-apk/*arm64-v8a*.apk out/hiddify-android-arm64.apk || echo "no arm64 apk"
- cp ./build/app/outputs/flutter-apk/*armeabi-v7a*.apk out/hiddify-android-arm7.apk || echo "no arm7 apk"
- cp ./build/app/outputs/flutter-apk/*x86_64*.apk out/hiddify-android-x86_64.apk || echo "no x64 apk"
- cp ./build/app/outputs/flutter-apk/app-release.apk out/hiddify-android-universal.apk || echo "no universal apk"
+ cp ./build/app/outputs/flutter-apk/*arm64-v8a*.apk out/${TARGET_NAME_apk}-arm64.apk || echo "no arm64 apk"
+ cp ./build/app/outputs/flutter-apk/*armeabi-v7a*.apk out/${TARGET_NAME_apk}-arm7.apk || echo "no arm7 apk"
+ cp ./build/app/outputs/flutter-apk/*x86_64*.apk out/${TARGET_NAME_apk}-x86_64.apk || echo "no x64 apk"
+ cp ./build/app/outputs/flutter-apk/app-release.apk out/${TARGET_NAME_apk}-universal.apk || echo "no universal apk"
- name: Copy to out Android AAB
if: matrix.platform == 'android-aab'
@@ -216,21 +180,29 @@ jobs:
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
+
+ for EXT in $(echo ${{ matrix.targets }} | tr ',' '\n'); do
+ KEY=TARGET_NAME_${EXT}
+ FILENAME=${!KEY}
+ echo "For $EXT ($KEY) filename is ${FILENAME}"
+ mv dist/*/*.$EXT tmp_out/${FILENAME}.$EXT
+ chmod +x tmp_out/${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/${FILENAME}.$EXT bin/${FILENAME}.$EXT
+ else
+ cd tmp_out
+ # 7z a ${FILENAME}.zip ./
+ # mv ${FILENAME}.zip ../out/
+ # [[ $EXT == 'AppImage' ]]&& mv ${FILENAME}.$EXT ../out/ # added for appimage link
+ mv ${FILENAME}.$EXT ../out/
+ cd ..
+ fi
+ done
- name: Clean up keychain and provisioning profile
if: ${{ always() && startsWith(matrix.os,'macos')}}
@@ -341,5 +313,4 @@ jobs:
packageName: app.hiddify.com
releaseName: ${{ env.TAG_NAME }}
releaseFiles: ./hiddify-android-market.aab
- track: 'beta'
-
+ track: 'beta'
\ No newline at end of file
diff --git a/.github/workflows/dev-i copy.yml b/.github/workflows/dev-i copy.yml
new file mode 100644
index 00000000..db8d6551
--- /dev/null
+++ b/.github/workflows/dev-i copy.yml
@@ -0,0 +1,206 @@
+name: dev i new
+on:
+ push:
+ branches:
+ - ios
+ paths-ignore:
+ - '**.md'
+ - 'docs/**'
+ - '.github/**'
+ - '!.github/workflows/*'
+ - 'appcast.xml'
+
+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
+ brew install graphicsmagick imagemagick
+ gem install fastlane
+ bundle install
+ pip install setuptools
+ npm install --global create-dmg
+
+ - name: Fastlane Release
+ shell: bash
+ run: fastlane release
+ env:
+ MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
+ MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
+ FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120 # x86 is slooooooow
+
+ - 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/.github/workflows/dev-i.yml b/.github/workflows/dev-i.yml
index 95f01b16..4643c4ec 100644
--- a/.github/workflows/dev-i.yml
+++ b/.github/workflows/dev-i.yml
@@ -2,7 +2,7 @@ name: dev i
on:
push:
branches:
- - main
+ - ios
paths-ignore:
- '**.md'
- 'docs/**'
diff --git a/.gitignore b/.gitignore
index b21277a5..758f1ce0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,9 +35,8 @@ migrate_working_dir/
# generated files
**/*.g.dart
**/*.freezed.dart
+**/*.mapper.dart
**/*.gen.dart
-**/libclash.so
-**/libclash.h
**/*.dll
**/*.dylib
**/*.xcframework
diff --git a/.stignore b/.stignore
new file mode 100644
index 00000000..7d6411b7
--- /dev/null
+++ b/.stignore
@@ -0,0 +1,31 @@
+#include /.stignore
+#include /android/.stignore
+#include /ios/.stignore
+#include /linux/.stignore
+#include /windows/.stignore
+#include /macos/.stignore
+
+.git
+
+.DS_Store
+.idea
+.dart_tool
+
+.flutter-plugins
+.flutter-plugins-dependencies
+
+.packages
+.pub-cache
+.pub
+build
+
+*.log
+*.iml
+*.ipr
+*.iws
+
+**/ios/Flutter/.last_build_id
+
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..890a31e1
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+ "recommendations": [
+ "dart-code.dart-code",
+ "dart-code.flutter",
+ "github.vscode-github-actions",
+ "golang.go",
+ "redhat.vscode-yaml",
+ "codeium.codeium"
+ ]
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d1d9f5e..f2398752 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -203,7 +203,7 @@
- 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/).
+ - Now you're able to install and update Hiddify 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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e5b5d061..2627c143 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing
-Every contribution to Hiddify Next is welcome, whether it is reporting a bug, submitting a fix, proposing new features, or just asking a question. To make contributing to Hiddify Next as easy as possible, you will find more details for the development flow in this documentation.
+Every contribution to Hiddify is welcome, whether it is reporting a bug, submitting a fix, proposing new features, or just asking a question. To make contributing to Hiddify as easy as possible, you will find more details for the development flow in this documentation.
Please note, we have a [Code of Conduct](https://github.com/hiddify/hiddify-next/blob/main/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
@@ -23,11 +23,16 @@ If you encounter any issue, or you have an idea to improve, please:
## Adding new Features
-When contributing a complex change to the Hiddify Next repository, please discuss the change you wish to make within a GitHub issue with the owners of this repository before making the change.
+When contributing a complex change to the Hiddify repository, please discuss the change you wish to make within a GitHub issue with the owners of this repository before making the change.
+
## Development
-Hiddify Next uses [Flutter](https://flutter.dev) and [Go](https://go.dev), make sure that you have the correct version installed before starting development. You can use the following commands to check your installed version:
+### Adding Feature / Fix bug in Core:
+Please follow our [Go Core Development repository](https://github.com/hiddify/hiddify-next-core/main/CONTRIBUTING.m).
+
+### Working with the Flutter Code
+Hiddify uses [Flutter](https://flutter.dev), make sure that you have the correct version installed before starting development. You can use the following commands to check your installed version:
```shell
$ flutter --version
@@ -37,62 +42,31 @@ Flutter 3.13.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 367f9ea16b (4 weeks ago) • 2023-09-12 23:27:53 -0500
Engine • revision 9064459a8b
Tools • Dart 3.1.2 • DevTools 2.25.0
-
-
-$ go version
-
-# example response
-go version go1.21.1 darwin/arm64
```
-### Working with the Go Code
-
-> if you're not interested in building/contributing to the Go code, you can skip this section
-
-The Go code for Hiddify Next can be found in the `libcore` folder, as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) and in [core repository](https://github.com/hiddify/hiddify-next-core). The entrypoints for the desktop version are available in the [`libcore/custom`](https://github.com/hiddify/hiddify-next-core/tree/main/custom) folder and for the mobile version they can be found in the [`libcore/mobile`](https://github.com/hiddify/hiddify-next-core/tree/main/mobile) folder.
-
-For the desktop version, we have to compile the Go code into a C shared library. We are providing a Makefile to generate the C shared libraries for all operating systems. The following Make commands will build libcore and copy the resulting output in [`libcore/bin`](https://github.com/hiddify/hiddify-next-core/tree/main/bin):
-
-- `make windows-amd64`
-- `make linux-amd64`
-- `make macos-universal`
-
-For the mobile version, we are using the [`gomobile`](https://github.com/golang/go/wiki/Mobile) tools. The following Make commands will build libcore for Android and iOS and copy the resulting output in [`libcore/bin`](https://github.com/hiddify/hiddify-next-core/tree/main/bin):
-
-- `make android`
-- `make ios`
-
-### Working with the Flutter Code
We recommend using [Visual Studio Code](https://docs.flutter.dev/development/tools/vs-code) extensions for development.
#### Setting up the Environment
We have extensive use of code generation in the form of [freezed](https://github.com/rrousselGit/freezed), [riverpod](https://github.com/rrousselGit/riverpod), etc. So it's generate these before running the code. Execute the following make commands in order:
-
-```shell
-# fetch dependencies
-$ make get
-
-# generate translations
-$ make translate
-
-# fetch geo assets
-$ make get-geo-assets
-
-# generate dart code using build_runner
-$ make gen
-```
-
Assuming you have not built the `libcore` and want to use [existing releases](https://github.com/hiddify/hiddify-next-core/releases), you should run the following command (based on your target platform):
-- `make windows-libs`
-- `make linux-libs`
-- `make macos-libs`
-- `make android-libs`
-- `make ios-libs`
-If you want to build the `libcore` from source, prefix the above command with `build-` like `make build-windows-libs`.
+- `make windows-prepare`
+- `make linux-prepare`
+- `make macos-prepare`
+- `make ios-prepare`
+- `make android-prepare`
+
+
+##### build the `libcore` from source (Optional)
+If you want to build the `libcore` from source after `make prepare`, use:
+- `make build-windows-libs`
+- `make build-linux-libs`
+- `make build-macos-libs`
+- `make build-ios-libs`
+- `make build-android-libs`
#### Run Release Build on a Device
@@ -112,7 +86,9 @@ Chrome (web) • chrome • web-javascript • Google Chrome 117.0.5938.1
Then we can use one of the listed devices and execute the following command to build and run the app on this device:
```shell
-flutter run --release --target lib/main_dev.dart --device-id=35492ae2
+flutter run
+# or
+flutter run --device-id=35492ae2
```
## Release
@@ -132,7 +108,6 @@ We need your collaboration in order to develop this project. If you have experie
- Flutter Developing
- Swift Developing
-- Kotlin Developing
- Go Developing
diff --git a/Makefile b/Makefile
index 1cf36ddc..eacce6f7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,14 @@
include dependencies.properties
+MKDIR := mkdir -p
+RM := rm -rf
+SEP :=/
+
ifeq ($(OS),Windows_NT)
- MKDIR := -mkdir
- RM := rmdir /s /q
- SEP:=\\
- PLATFORM_REQ:= @set /p platform="Run 'make prepare platform=ios' or enter platform name:";
-else
- MKDIR := mkdir -p
- RM := rm -rf
- SEP :=/
- PLATFORM_REQ:= @read -p "Run make prepare platform=ios or enter platform name: " platform;
+ ifeq ($(IS_GITHUB_ACTIONS),)
+ MKDIR := -mkdir
+ RM := rmdir /s /q
+ SEP:=\\
+ endif
endif
BINDIR=libcore$(SEP)bin
@@ -19,17 +19,18 @@ GEO_ASSETS_DIR=assets$(SEP)core
CORE_PRODUCT_NAME=hiddify-core
CORE_NAME=$(CORE_PRODUCT_NAME)
+LIB_NAME=libcore
SRV_NAME=HiddifyService
ifeq ($(CHANNEL),prod)
-CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version)
+ CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version)
else
-CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft
+ CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft
endif
ifeq ($(CHANNEL),prod)
-TARGET=lib/main_prod.dart
+ TARGET=lib/main_prod.dart
else
-TARGET=lib/main.dart
+ TARGET=lib/main.dart
endif
BUILD_ARGS=--dart-define sentry_dsn=$(SENTRY_DSN)
@@ -37,8 +38,6 @@ DISTRIBUTOR_ARGS=--skip-clean --build-target $(TARGET) --build-dart-define sentr
-
-
get:
flutter pub get
@@ -50,7 +49,7 @@ translate:
-prepare: #get-geo-assets get gen translate
+prepare:
@echo use the following commands to prepare the library for each platform:
@echo make android-prepare
@echo make windows-prepare
@@ -62,11 +61,54 @@ windows-prepare: get-geo-assets get gen translate windows-libs
ios-prepare: get-geo-assets get gen translate ios-libs
macos-prepare: get-geo-assets get gen translate macos-libs
linux-prepare: get-geo-assets get gen translate linux-libs
+linux-appimage-prepare:linux-prepare
+linux-rpm-prepare:linux-prepare
+linux-deb-prepare:linux-prepare
+
android-prepare: get-geo-assets get gen translate android-libs
+android-apk-prepare:android-prepare
+android-aab-prepare:android-prepare
+
+macos-install-dependencies:
+ brew install create-dmg tree
+ npm install -g appdmg
+ dart pub global activate flutter_distributor
-sync_translate:
+ios-install-dependencies:
+ echo "not yet implemented"
+
+android-install-dependencies:
+ echo "nothing yet"
+android-apk-install-dependencies: android-install-dependencies
+android-aab-install-dependencies: android-install-dependencies
+
+linux-install-dependencies:
+ if [ "$(flutter)" = "true" ]; then \
+ wget -O ~/Downloads/flutter_linux_3.16.9-stable.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.16.9-stable.tar.xz; \
+ mkdir -p ~/develop; \
+ cd ~/develop; \
+ tar xf ~/Downloads/flutter_linux_3.16.9-stable.tar.xz; \
+ export PATH="$$PATH:$$HOME/develop/flutter/bin"; \
+ echo 'export PATH="$$PATH:$$HOME/develop/flutter/bin"' >> ~/.bashrc; \
+ fi
+ PATH="$$PATH":"$$HOME/.pub-cache/bin"
+ echo 'export PATH="$$PATH:$$HOME/.pub-cache/bin"' >>~/.bashrc
+ sudo apt install -y clang ninja-build pkg-config cmake libgtk-3-dev locate ninja-build pkg-config libgtk-3-dev libglib2.0-dev libgio2.0-cil-dev libayatana-appindicator3-dev fuse rpm patchelf file appstream
+
+
+ sudo modprobe fuse
+ wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
+ chmod +x appimagetool
+ sudo mv appimagetool /usr/local/bin/
+
+ dart pub global activate --source git https://github.com/hiddify/flutter_distributor --git-path packages/flutter_distributor
+
+windows-install-dependencies:
+ dart pub global activate flutter_distributor
+
+gen_translations: #generating missing translations using google translate
cd .github && bash sync_translate.sh
make translate
@@ -81,20 +123,13 @@ android-aab-release:
ls -R build/app/outputs
windows-release:
- flutter_distributor package --platform windows --targets exe $(DISTRIBUTOR_ARGS)
-
-linux-release: linux-appimage-release linux-deb-release linux-rpm-release
-
-linux-appimage-release:
- flutter_distributor package --platform linux --targets appimage $(DISTRIBUTOR_ARGS)
-linux-deb-release:
- flutter_distributor package --platform linux --targets deb $(DISTRIBUTOR_ARGS)
-linux-rpm-release:
- flutter_distributor package --platform linux --targets rpm $(DISTRIBUTOR_ARGS)
+ flutter_distributor package --flutter-build-args=verbose --platform windows --targets exe $(DISTRIBUTOR_ARGS)
+linux-release:
+ flutter_distributor package --platform linux --targets deb,rpm,appimage $(DISTRIBUTOR_ARGS)
macos-release:
- flutter_distributor package --platform macos --targets dmg $(DISTRIBUTOR_ARGS)
+ flutter_distributor package --platform macos --targets dmg,pkg $(DISTRIBUTOR_ARGS)
ios-release: #not tested
flutter_distributor package --platform ios --targets ipa --build-export-options-plist ios/exportOptions.plist $(DISTRIBUTOR_ARGS)
@@ -107,25 +142,22 @@ android-apk-libs: android-libs
android-aab-libs: android-libs
windows-libs:
- @$(MKDIR) $(DESKTOP_OUT) || echo Folder already exists. Skipping...
- curl -L $(CORE_URL)/$(CORE_NAME)-windows-amd64.tar.gz | tar xz -C $(DESKTOP_OUT)/
+ $(MKDIR) $(DESKTOP_OUT) || echo Folder already exists. Skipping...
+ curl -L $(CORE_URL)/$(CORE_NAME)-windows-amd64.tar.gz | tar xz -C $(DESKTOP_OUT)$(SEP)
+ ls $(DESKTOP_OUT) || dir $(DESKTOP_OUT)$(SEP)
linux-libs:
- @$(MKDIR) $(DESKTOP_OUT) || echo Folder already exists. Skipping...
+ mkdir -p $(DESKTOP_OUT)
curl -L $(CORE_URL)/$(CORE_NAME)-linux-amd64.tar.gz | tar xz -C $(DESKTOP_OUT)/
-linux-deb-libs:linux-libs
-linux-rpm-libs:linux-libs
-linux-appimage-libs:linux-libs
-
macos-libs:
- @$(MKDIR) $(DESKTOP_OUT) || echo Folder already exists. Skipping...
+ mkdir -p $(DESKTOP_OUT)
curl -L $(CORE_URL)/$(CORE_NAME)-macos-universal.tar.gz | tar xz -C $(DESKTOP_OUT)
ios-libs: #not tested
- @$(MKDIR) $(IOS_OUT) || echo Folder already exists. Skipping...
- @$(RM) $(IOS_OUT)/Libcore.xcframework
+ mkdir -p $(IOS_OUT)
+ rm -rf $(IOS_OUT)/Libcore.xcframework
curl -L $(CORE_URL)/$(CORE_NAME)-ios.tar.gz | tar xz -C "$(IOS_OUT)"
get-geo-assets:
@@ -137,34 +169,29 @@ build-headers:
build-android-libs:
make -C libcore -f Makefile android
- mv $(BINDIR)/$(CORE_NAME).aar $(ANDROID_OUT)/
+ mv $(BINDIR)/$(LIB_NAME).aar $(ANDROID_OUT)/
build-windows-libs:
make -C libcore -f Makefile windows-amd64
- mv $(BINDIR)/$(CORE_NAME).dll $(DESKTOP_OUT)/
- mv $(BINDIR)/$(SRV_NAME) $(DESKTOP_OUT)/
build-linux-libs:
make -C libcore -f Makefile linux-amd64
- mv $(BINDIR)/$(CORE_NAME).so $(DESKTOP_OUT)/
- mv $(BINDIR)/$(SRV_NAME) $(DESKTOP_OUT)/
build-macos-libs:
make -C libcore -f Makefile macos-universal
- mv $(BINDIR)/$(CORE_NAME).dylib $(DESKTOP_OUT)/
mv $(BINDIR)/$(SRV_NAME) $(DESKTOP_OUT)/
build-ios-libs:
- @$(RM) $(IOS_OUT)/Libcore.xcframework && \
- make -C libcore -f Makefile ios && \
- mv $(BINDIR)/$(CORE_NAME)-ios.xcframework $(IOS_OUT)/Libcore.xcframework
+ rf -rf $(IOS_OUT)/Libcore.xcframework
+ make -C libcore -f Makefile ios
+ mv $(BINDIR)/Libcore.xcframework $(IOS_OUT)/Libcore.xcframework
release: # Create a new tag for release.
@echo "previous version was $$(git describe --tags $$(git rev-list --tags --max-count=1))"
@echo "WARNING: This operation will creates version tag and push to github"
@bash -c '\
- [ "404" == $$(curl -I -s -w "%{http_code}" https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version)/hiddify-libcore-windows-amd64.h.gz -o /dev/null) ]&&{ echo "Core Not Found"; exit 1 ; };\
+ [ "404" == $$(curl -I -s -w "%{http_code}" https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version)/hiddify-core-linux-amd64.tar.gz -o /dev/null) ]&&{ echo "Core Not Found"; exit 1 ; };\
cversion_string=`grep -e "^version:" pubspec.yaml | cut -d: -f2-`; \
cstr_version=`echo "$${cversion_string}" | sed -n "s/[ ]*\\([0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\)+.*/\\1/p"`; \
cbuild_number=`echo "$${cversion_string}" | sed -n "s/.*+\\([0-9]\\+\\)/\\1/p"`; \
@@ -190,7 +217,7 @@ release: # Create a new tag for release.
ios-temp-prepare:
- make prepare platform=ios
+ make ios-prepare
flutter build ios-framework
cd ios
pod install
diff --git a/README.md b/README.md
index 030a3ace..1c505dcc 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,10 @@
[** فارسی**](README_fa.md) [**Русский 🇷🇺**](README_ru.md) [**简体中文 🇨🇳**](README_cn.md) [**日本語 🇯🇵**](README_ja.md)
+
-
-
+
+
@@ -23,7 +24,7 @@
## What is Hiddify-Next?
-
A multi-platform proxy client based on Sing-box universal proxy tool-chain. Hiddify Next offers a wide range of capabilities, like automatic node selection, TUN mode, remote profiles etc. Hiddify Next is ad-free and open-source. With support for a wide range of protocols, it provides a secure and private way for accessing free internet.
+
A multi-platform proxy client based on Sing-box universal proxy tool-chain. Hiddify offers a wide range of capabilities, like automatic node selection, TUN mode, remote profiles etc. Hiddify is ad-free and open-source. With support for a wide range of protocols, it provides a secure and private way for accessing free internet.

@@ -144,7 +145,7 @@ We also need financial support for our services. All of our activities are done
## 👩🏫 Collaboration and Contact Information
-Hiddify Next is a community driven project. If you're interested in contributing, please read the [contribution guidelines](./CONTRIBUTING.md). We would specially appreciate any help we can get in these areas: **Flutter, Go, iOS development (Swift), Android development (Kotlin).**
+Hiddify is a community driven project. If you're interested in contributing, please read the [contribution guidelines](./CONTRIBUTING.md). We would specially appreciate any help we can get in these areas: **Flutter, Go, iOS development (Swift), Android development (Kotlin).**
diff --git a/README_cn.md b/README_cn.md
index 9826a3cc..d238df87 100644
--- a/README_cn.md
+++ b/README_cn.md
@@ -3,8 +3,10 @@
[** فارسی**](README_fa.md) [**Русский 🇷🇺**](README_ru.md) [**English 🇺🇸**](README.md) [**日本語 🇯🇵**](README_ja.md)
+
-

+

+
@@ -18,7 +20,7 @@
## Hiddify-Next 是什么?
-
一款基于 Sing-box 通用代理工具的跨平台代理客户端。Hiddify Next 提供了较全面的代理功能,例如自动选择节点、TUN 模式、使用远程配置文件等。Hiddify Next 无广告,并且代码开源。它为大家自由访问互联网提供了一个支持多种协议的、安全且私密的工具。
+
一款基于 Sing-box 通用代理工具的跨平台代理客户端。Hiddify 提供了较全面的代理功能,例如自动选择节点、TUN 模式、使用远程配置文件等。Hiddify 无广告,并且代码开源。它为大家自由访问互联网提供了一个支持多种协议的、安全且私密的工具。

@@ -138,7 +140,7 @@
## 👩🏫 合作及联系信息
-Hiddify Next 是一个由社区驱动的项目。如果您有兴趣为本项目做出贡献,请阅读 [贡献指南](./CONTRIBUTING.md)。我们将非常感谢您,如果您能够在以下领域提供任何帮助:Flutter、Go、iOS 开发 (Swift)、Android 开发 (Kotlin)。
+Hiddify 是一个由社区驱动的项目。如果您有兴趣为本项目做出贡献,请阅读 [贡献指南](./CONTRIBUTING.md)。我们将非常感谢您,如果您能够在以下领域提供任何帮助:Flutter、Go、iOS 开发 (Swift)、Android 开发 (Kotlin)。
diff --git a/README_fa.md b/README_fa.md
index 2d752ee5..f0634ee2 100644
--- a/README_fa.md
+++ b/README_fa.md
@@ -1,10 +1,12 @@
-[**🇺🇸 English**](README.md) [**🇨🇳 简体中文**](README_cn.md) [**🇷🇺 Русский**](README_ru.md) [**日本語 🇯🇵**](README_ja.md)
+[**🇺🇸 English**](README.md) [**🇨🇳 简体中文**](README_cn.md) [**🇷🇺 Русский**](README_ru.md) [**🇯🇵 日本語**](README_ja.md)
+
-

+

+
diff --git a/README_ja.md b/README_ja.md
index f85aa688..23bb4fc4 100644
--- a/README_ja.md
+++ b/README_ja.md
@@ -3,9 +3,10 @@
[** فارسی**](README_fa.md) [**Русский 🇷🇺**](README_ru.md) [**简体中文 🇨🇳**](README_cn.md) [**English 🇺🇸**](README.md)
+
-

-
+

+
@@ -23,7 +24,7 @@
## Hiddify-Next とは?
-
Sing-box ユニバーサルプロキシツールチェーンに基づくマルチプラットフォームプロキシクライアントです。Hiddify Next は、自動ノード選択、TUN モード、リモートプロファイルなど、幅広い機能を提供します。Hiddify Next は無料でオープンソースです。幅広いプロトコルをサポートし、無料インターネットにアクセスするための安全でプライベートな方法を提供します。
+
Sing-box ユニバーサルプロキシツールチェーンに基づくマルチプラットフォームプロキシクライアントです。Hiddify は、自動ノード選択、TUN モード、リモートプロファイルなど、幅広い機能を提供します。Hiddify は無料でオープンソースです。幅広いプロトコルをサポートし、無料インターネットにアクセスするための安全でプライベートな方法を提供します。

@@ -144,7 +145,7 @@ Vless、Vmess、Reality、TUIC、Hysteria、SSHなど。
## 👩🏫 コラボレーションおよび連絡先
-Hiddify Next はコミュニティドリブンのプロジェクトです。コントリビュートすることに興味がある方は、[コントリビューションガイドライン](./CONTRIBUTING.md)をお読みください。私たちは特に以下の分野において、どのような協力でもいただけるとありがたいです: **Flutter、Go、iOS開発(Swift)、Androi d開発(Kotlin)。**
+Hiddify はコミュニティドリブンのプロジェクトです。コントリビュートすることに興味がある方は、[コントリビューションガイドライン](./CONTRIBUTING.md)をお読みください。私たちは特に以下の分野において、どのような協力でもいただけるとありがたいです: **Flutter、Go、iOS開発(Swift)、Androi d開発(Kotlin)。**
diff --git a/README_ru.md b/README_ru.md
index 83ae0079..a7d4a870 100644
--- a/README_ru.md
+++ b/README_ru.md
@@ -5,8 +5,10 @@
[**日本語 🇯🇵**](README_ja.md)
+
-

+

+
diff --git a/android/.stignore b/android/.stignore
new file mode 100644
index 00000000..03bc1f9a
--- /dev/null
+++ b/android/.stignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+.gradle
+captures/
+gradlew
+gradlew.bat
+local.properties
+GeneratedPluginRegistrant.java
+
+key.properties
+**.keystore
+**.jks
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5e87f957..03b2c845 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -22,9 +22,9 @@
diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png
index cfad25ec..18dadf71 100644
Binary files a/android/app/src/main/ic_launcher-playstore.png and b/android/app/src/main/ic_launcher-playstore.png differ
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/ActiveGroupsChannel.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/ActiveGroupsChannel.kt
new file mode 100644
index 00000000..34c5d547
--- /dev/null
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/ActiveGroupsChannel.kt
@@ -0,0 +1,60 @@
+package com.hiddify.hiddify
+
+import android.util.Log
+import com.google.gson.Gson
+import com.hiddify.hiddify.utils.CommandClient
+import com.hiddify.hiddify.utils.ParsedOutboundGroup
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.plugin.common.EventChannel
+import io.nekohasekai.libbox.OutboundGroup
+import kotlinx.coroutines.CoroutineScope
+
+
+class ActiveGroupsChannel(private val scope: CoroutineScope) : FlutterPlugin,
+ CommandClient.Handler {
+ companion object {
+ const val TAG = "A/ActiveGroupsChannel"
+ const val CHANNEL = "com.hiddify.app/active-groups"
+ val gson = Gson()
+ }
+
+ private val client =
+ CommandClient(scope, CommandClient.ConnectionType.GroupOnly, this)
+
+ private var channel: EventChannel? = null
+ private var event: EventChannel.EventSink? = null
+
+ override fun updateGroups(groups: List) {
+ MainActivity.instance.runOnUiThread {
+ val parsedGroups = groups.map { group -> ParsedOutboundGroup.fromOutbound(group) }
+ event?.success(gson.toJson(parsedGroups))
+ }
+ }
+
+ override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ channel = EventChannel(
+ flutterPluginBinding.binaryMessenger,
+ CHANNEL
+ )
+
+ channel!!.setStreamHandler(object : EventChannel.StreamHandler {
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
+ event = events
+ Log.d(TAG, "connecting active groups command client")
+ client.connect()
+ }
+
+ override fun onCancel(arguments: Any?) {
+ event = null
+ Log.d(TAG, "disconnecting active groups command client")
+ client.disconnect()
+ }
+ })
+ }
+
+ override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+ event = null
+ client.disconnect()
+ channel?.setStreamHandler(null)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt
index bf492953..7f2b907b 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt
@@ -2,85 +2,57 @@ package com.hiddify.hiddify
import android.util.Log
import com.google.gson.Gson
-import com.google.gson.annotations.SerializedName
import com.hiddify.hiddify.utils.CommandClient
+import com.hiddify.hiddify.utils.ParsedOutboundGroup
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.nekohasekai.libbox.OutboundGroup
-import io.nekohasekai.libbox.OutboundGroupItem
import kotlinx.coroutines.CoroutineScope
class GroupsChannel(private val scope: CoroutineScope) : FlutterPlugin, CommandClient.Handler {
companion object {
const val TAG = "A/GroupsChannel"
- const val GROUP_CHANNEL = "com.hiddify.app/groups"
+ const val CHANNEL = "com.hiddify.app/groups"
val gson = Gson()
}
- private val commandClient =
+ private val client =
CommandClient(scope, CommandClient.ConnectionType.Groups, this)
- private var groupsChannel: EventChannel? = null
-
- private var groupsEvent: EventChannel.EventSink? = null
+ private var channel: EventChannel? = null
+ private var event: EventChannel.EventSink? = null
override fun updateGroups(groups: List) {
MainActivity.instance.runOnUiThread {
- val kGroups = groups.map { group -> KOutboundGroup.fromOutbound(group) }
- groupsEvent?.success(gson.toJson(kGroups))
+ val parsedGroups = groups.map { group -> ParsedOutboundGroup.fromOutbound(group) }
+ event?.success(gson.toJson(parsedGroups))
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
- groupsChannel = EventChannel(
+ channel = EventChannel(
flutterPluginBinding.binaryMessenger,
- GROUP_CHANNEL
+ CHANNEL
)
- groupsChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
+ channel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
- groupsEvent = events
+ event = events
Log.d(TAG, "connecting groups command client")
- commandClient.connect()
+ client.connect()
}
override fun onCancel(arguments: Any?) {
- groupsEvent = null
+ event = null
Log.d(TAG, "disconnecting groups command client")
- commandClient.disconnect()
+ client.disconnect()
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
- groupsEvent = null
- commandClient.disconnect()
- groupsChannel?.setStreamHandler(null)
- }
-
- data class KOutboundGroup(
- @SerializedName("tag") val tag: String,
- @SerializedName("type") val type: String,
- @SerializedName("selected") val selected: String,
- @SerializedName("items") val items: List
- ) {
- companion object {
- fun fromOutbound(group: OutboundGroup): KOutboundGroup {
- val outboundItems = group.items
- val items = mutableListOf()
- while (outboundItems.hasNext()) {
- items.add(KOutboundGroupItem(outboundItems.next()))
- }
- return KOutboundGroup(group.tag, group.type, group.selected, items)
- }
- }
- }
-
- data class KOutboundGroupItem(
- @SerializedName("tag") val tag: String,
- @SerializedName("type") val type: String,
- @SerializedName("url-test-delay") val urlTestDelay: Int,
- ) {
- constructor(item: OutboundGroupItem) : this(item.tag, item.type, item.urlTestDelay)
+ event = null
+ client.disconnect()
+ channel?.setStreamHandler(null)
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt
index 47a452d6..1b48d640 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt
@@ -49,6 +49,7 @@ class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
flutterEngine.plugins.add(EventHandler())
flutterEngine.plugins.add(LogHandler())
flutterEngine.plugins.add(GroupsChannel(lifecycleScope))
+ flutterEngine.plugins.add(ActiveGroupsChannel(lifecycleScope))
flutterEngine.plugins.add(StatsChannel(lifecycleScope))
}
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt
index 49b1feab..580e09c8 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt
@@ -92,7 +92,7 @@ object Settings {
}
}
- private var currentServiceMode = ServiceMode.NORMAL;
+ private var currentServiceMode : String? = null
suspend fun rebuildServiceMode(): Boolean {
var newMode = ServiceMode.NORMAL
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 115280d4..3b370159 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
@@ -14,6 +14,7 @@ import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.Application
+import com.hiddify.hiddify.R
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Alert
@@ -140,6 +141,9 @@ class BoxService(
private suspend fun startService(delayStart: Boolean = false) {
try {
Log.d(TAG, "starting service")
+ withContext(Dispatchers.Main) {
+ notification.show(activeProfileName, R.string.status_starting)
+ }
val selectedConfigPath = Settings.activeConfigPath
if (selectedConfigPath.isBlank()) {
@@ -168,6 +172,7 @@ class BoxService(
}
withContext(Dispatchers.Main) {
+ notification.show(activeProfileName, R.string.status_starting)
binder.broadcast {
it.onServiceResetLogs(listOf())
}
@@ -194,8 +199,9 @@ class BoxService(
status.postValue(Status.Started)
withContext(Dispatchers.Main) {
- notification.show(activeProfileName)
+ notification.show(activeProfileName, R.string.status_started)
}
+ notification.start()
} catch (e: Exception) {
stopAndAlert(Alert.StartService, e.message)
return
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceNotification.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceNotification.kt
index 4dcdd67e..3be3178a 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceNotification.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceNotification.kt
@@ -9,6 +9,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
+import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.MutableLiveData
@@ -50,7 +51,7 @@ class ServiceNotification(private val status: MutableLiveData, private v
NotificationCompat.Builder(service, notificationChannel)
.setShowWhen(false)
.setOngoing(true)
- .setContentTitle("Hiddify Next")
+ .setContentTitle("Hiddify")
.setOnlyAlertOnce(true)
.setSmallIcon(R.drawable.ic_stat_logo)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
@@ -79,25 +80,27 @@ class ServiceNotification(private val status: MutableLiveData, private v
}
}
- suspend fun show(profileName: String) {
+ fun show(profileName: String, @StringRes contentTextId: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Application.notification.createNotificationChannel(
- NotificationChannel(
- notificationChannel, "hiddify service", NotificationManager.IMPORTANCE_LOW
- )
+ NotificationChannel(
+ notificationChannel, "hiddify service", NotificationManager.IMPORTANCE_LOW
+ )
)
}
service.startForeground(
- notificationId, notificationBuilder
- .setContentTitle(profileName.takeIf { it.isNotBlank() } ?: "Hiddify Next")
- .setContentText("service started").build()
+ notificationId, notificationBuilder
+ .setContentTitle(profileName.takeIf { it.isNotBlank() } ?: "Hiddify")
+ .setContentText(service.getString(contentTextId)).build()
)
- withContext(Dispatchers.IO) {
- if (Settings.dynamicNotification) {
- commandClient.connect()
- withContext(Dispatchers.Main) {
- registerReceiver()
- }
+ }
+
+
+ suspend fun start() {
+ if (Settings.dynamicNotification) {
+ commandClient.connect()
+ withContext(Dispatchers.Main) {
+ registerReceiver()
}
}
}
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt
index da38b11a..7830c1e2 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt
@@ -165,19 +165,14 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
}
}
- if (options.isHTTPProxyEnabled) {
+ if (options.isHTTPProxyEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
systemProxyAvailable = true
systemProxyEnabled = Settings.systemProxyEnabled
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- if (systemProxyEnabled) builder.setHttpProxy(
- ProxyInfo.buildDirectProxy(
- options.httpProxyServer,
- options.httpProxyServerPort
- )
+ if (systemProxyEnabled) builder.setHttpProxy(
+ ProxyInfo.buildDirectProxy(
+ options.httpProxyServer, options.httpProxyServerPort
)
- } else {
- error("android: tun.platform.http_proxy requires android 10 or higher")
- }
+ )
} else {
systemProxyAvailable = false
systemProxyEnabled = false
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt
index 8cf2f8bd..852a0439 100644
--- a/android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt
@@ -23,7 +23,7 @@ open class CommandClient(
) {
enum class ConnectionType {
- Status, Groups, Log, ClashMode
+ Status, Groups, Log, ClashMode, GroupOnly
}
interface Handler {
@@ -50,6 +50,7 @@ open class CommandClient(
ConnectionType.Groups -> Libbox.CommandGroup
ConnectionType.Log -> Libbox.CommandLog
ConnectionType.ClashMode -> Libbox.CommandClashMode
+ ConnectionType.GroupOnly -> Libbox.CommandGroupInfoOnly
}
options.statusInterval = 2 * 1000 * 1000 * 1000
val commandClient = CommandClient(clientHandler, options)
diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/utils/OutboundMapper.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/utils/OutboundMapper.kt
new file mode 100644
index 00000000..27937e83
--- /dev/null
+++ b/android/app/src/main/kotlin/com/hiddify/hiddify/utils/OutboundMapper.kt
@@ -0,0 +1,31 @@
+package com.hiddify.hiddify.utils
+
+import com.google.gson.annotations.SerializedName
+import io.nekohasekai.libbox.OutboundGroup
+import io.nekohasekai.libbox.OutboundGroupItem
+
+data class ParsedOutboundGroup(
+ @SerializedName("tag") val tag: String,
+ @SerializedName("type") val type: String,
+ @SerializedName("selected") val selected: String,
+ @SerializedName("items") val items: List
+) {
+ companion object {
+ fun fromOutbound(group: OutboundGroup): ParsedOutboundGroup {
+ val outboundItems = group.items
+ val items = mutableListOf()
+ while (outboundItems.hasNext()) {
+ items.add(ParsedOutboundGroupItem(outboundItems.next()))
+ }
+ return ParsedOutboundGroup(group.tag, group.type, group.selected, items)
+ }
+ }
+}
+
+data class ParsedOutboundGroupItem(
+ @SerializedName("tag") val tag: String,
+ @SerializedName("type") val type: String,
+ @SerializedName("url-test-delay") val urlTestDelay: Int,
+) {
+ constructor(item: OutboundGroupItem) : this(item.tag, item.type, item.urlTestDelay)
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable-hdpi/android12splash.png b/android/app/src/main/res/drawable-hdpi/android12splash.png
deleted file mode 100644
index 02dfcf50..00000000
Binary files a/android/app/src/main/res/drawable-hdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png
deleted file mode 100644
index 7cb46df4..00000000
Binary files a/android/app/src/main/res/drawable-hdpi/splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-mdpi/android12splash.png b/android/app/src/main/res/drawable-mdpi/android12splash.png
deleted file mode 100644
index d7c34860..00000000
Binary files a/android/app/src/main/res/drawable-mdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png
deleted file mode 100644
index 2867a2db..00000000
Binary files a/android/app/src/main/res/drawable-mdpi/splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/android/app/src/main/res/drawable-night-hdpi/android12splash.png
deleted file mode 100644
index 02dfcf50..00000000
Binary files a/android/app/src/main/res/drawable-night-hdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/android/app/src/main/res/drawable-night-mdpi/android12splash.png
deleted file mode 100644
index d7c34860..00000000
Binary files a/android/app/src/main/res/drawable-night-mdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png
deleted file mode 100644
index cda00421..00000000
Binary files a/android/app/src/main/res/drawable-night-xhdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-night-xhdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-night-xhdpi/ic_stat_logo.png
deleted file mode 100644
index 07b7c812..00000000
Binary files a/android/app/src/main/res/drawable-night-xhdpi/ic_stat_logo.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png
deleted file mode 100644
index a27a1e9f..00000000
Binary files a/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-night-xxhdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-night-xxhdpi/ic_stat_logo.png
deleted file mode 100644
index 31264850..00000000
Binary files a/android/app/src/main/res/drawable-night-xxhdpi/ic_stat_logo.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png
deleted file mode 100644
index fdf397f2..00000000
Binary files a/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-night-xxxhdpi/ic_stat_logo.png
deleted file mode 100644
index 6c9351d0..00000000
Binary files a/android/app/src/main/res/drawable-night-xxxhdpi/ic_stat_logo.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xhdpi/android12splash.png b/android/app/src/main/res/drawable-xhdpi/android12splash.png
deleted file mode 100644
index cda00421..00000000
Binary files a/android/app/src/main/res/drawable-xhdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png
deleted file mode 100644
index 36361365..00000000
Binary files a/android/app/src/main/res/drawable-xhdpi/splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxhdpi/android12splash.png
deleted file mode 100644
index a27a1e9f..00000000
Binary files a/android/app/src/main/res/drawable-xxhdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png
deleted file mode 100644
index 254a0e3a..00000000
Binary files a/android/app/src/main/res/drawable-xxhdpi/splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png
deleted file mode 100644
index fdf397f2..00000000
Binary files a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png
index feaf4001..f7d7bc57 100644
Binary files a/android/app/src/main/res/drawable-xxxhdpi/splash.png and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable/android12splash.xml b/android/app/src/main/res/drawable/android12splash.xml
new file mode 100644
index 00000000..50314d13
--- /dev/null
+++ b/android/app/src/main/res/drawable/android12splash.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_banner_foreground.xml b/android/app/src/main/res/drawable/ic_banner_foreground.xml
new file mode 100644
index 00000000..61267503
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_banner_foreground.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..9f79c323
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..50314d13
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml
new file mode 100644
index 00000000..a0a0dece
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index e7b046de..7353dbd1 100644
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,6 +1,5 @@
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index e7b046de..7353dbd1 100644
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,5 @@
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 7f088461..00000000
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..753e2691
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
deleted file mode 100644
index 767e978e..00000000
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
deleted file mode 100644
index d353ce33..00000000
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
deleted file mode 100644
index 358913b8..00000000
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index ebc6c93d..00000000
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..adc3d493
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index d58ea113..00000000
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..b30ec90c
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
deleted file mode 100644
index 2a0e45a3..00000000
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
deleted file mode 100644
index 1d35327c..00000000
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
deleted file mode 100644
index 70e9c253..00000000
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index 458efdab..00000000
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..3a4ac086
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_banner.png b/android/app/src/main/res/mipmap-xhdpi/ic_banner.png
new file mode 100644
index 00000000..f97689ac
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_banner.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 787d2dde..00000000
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..ebb8bb0a
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
deleted file mode 100644
index 94988fba..00000000
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
deleted file mode 100644
index f71d6eb6..00000000
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
deleted file mode 100644
index e5cb6a2b..00000000
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index b9c7dcb0..00000000
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..018dbbe2
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index edeebb97..00000000
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..bf57ad31
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
deleted file mode 100644
index 37ea66b7..00000000
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 48afe2e6..00000000
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
deleted file mode 100644
index 42cf3b85..00000000
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index 239e5e54..00000000
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9dce965d
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 05fec506..00000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..903d2b4d
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
deleted file mode 100644
index a140fc39..00000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_banner.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_banner.png
deleted file mode 100644
index f63fa5b9..00000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_banner.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index ca4fdde1..00000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
deleted file mode 100644
index 25f4b2ac..00000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index 7857add9..00000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..18346956
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml
index ba4b2aee..0e45e48d 100644
--- a/android/app/src/main/res/values-night-v31/styles.xml
+++ b/android/app/src/main/res/values-night-v31/styles.xml
@@ -6,7 +6,7 @@
- false
- false
- shortEdges
- - #ffffff
+ - #F0F3FA
- @drawable/android12splash
Running...');
- return await runTunnelService();
- }
- }
-
- @override
- Future deactivateTunnel() async {
- if (!Platform.isWindows && !Platform.isLinux && !Platform.isMacOS) {
- return true;
- }
- try {
- return await stopTunnelRequest();
- } catch (error) {
- loggy.error('Tunnel Service Stop Error: $error.');
- return false;
- }
- }
-
- Future startTunnelRequest() async {
- final params = {
- "Ipv6": false,
- "ServerPort": "2334",
- "StrictRoute": false,
- "EndpointIndependentNat": false,
- "Stack": "gvisor",
- };
-
- final query = mapToQueryString(params);
-
- try {
- final request =
- await HttpClient().get('localhost', 18020, "/start?$query");
- final response = await request.close();
- final body = await response.transform(utf8.decoder).join();
- loggy.debug(
- 'Status Code: ${response.statusCode} ${response.reasonPhrase}');
- loggy.debug('Response Body: ${body}');
- return true;
- } catch (error) {
- loggy.error('HTTP Request Error: $error');
- return false;
- }
- }
-
- Future stopTunnelRequest() async {
- try {
- final request = await HttpClient().get('localhost', 18020, "/stop");
- final response = await request.close();
- final body = await response.transform(utf8.decoder).join();
- loggy.debug(
- 'Status Code: ${response.statusCode} ${response.reasonPhrase}');
- loggy.debug('Response Body: ${body}');
- return true;
- } catch (error) {
- loggy.error('HTTP Request Error: $error');
- return false;
- }
- }
-
- String mapToQueryString(Map params) {
- return params.entries.map((entry) {
- final key = Uri.encodeQueryComponent(entry.key);
- final value = Uri.encodeQueryComponent(entry.value.toString());
- return '$key=$value';
- }).join('&');
- }
-
- Future runTunnelService() async {
- final executablePath = getTunnelServicePath();
-
- var command = [executablePath, "install"];
- if (Platform.isLinux) {
- command.insert(0, 'pkexec');
- }
-
- try {
- final result =
- await Process.run(command[0], command.sublist(1), runInShell: true);
- loggy.debug('Shell command executed: ${result.stdout} ${result.stderr}');
- return await startTunnelRequest();
- } catch (error) {
- loggy.error('Error executing shell command: $error');
- return false;
- }
- }
-
- static String getTunnelServicePath() {
- String fullPath = "";
- final binFolder =
- Directory(Platform.resolvedExecutable).parent.absolute.path;
- if (Platform.environment.containsKey('FLUTTER_TEST')) {
- fullPath = "libcore";
- }
- if (Platform.isWindows) {
- fullPath = p.join(fullPath, "HiddifyService.exe");
- } else if (Platform.isMacOS) {
- fullPath = p.join(fullPath, "HiddifyService");
- } else {
- fullPath = p.join(fullPath, "HiddifyService");
- }
-
- return "$binFolder/$fullPath";
- }
}
sealed class _TokenElevation extends Struct {
diff --git a/lib/features/connection/data/connection_repository.dart b/lib/features/connection/data/connection_repository.dart
index 57eb892e..9d3aeb5b 100644
--- a/lib/features/connection/data/connection_repository.dart
+++ b/lib/features/connection/data/connection_repository.dart
@@ -38,7 +38,7 @@ class ConnectionRepositoryImpl
required this.directories,
required this.singbox,
required this.platformSource,
- required this.configOptionRepository,
+ required this.singBoxConfigOptionRepository,
required this.profilePathResolver,
required this.geoAssetPathResolver,
});
@@ -46,7 +46,7 @@ class ConnectionRepositoryImpl
final Directories directories;
final SingboxService singbox;
final ConnectionPlatformSource platformSource;
- final ConfigOptionRepository configOptionRepository;
+ final SingBoxConfigOptionRepository singBoxConfigOptionRepository;
final ProfilePathResolver profilePathResolver;
final GeoAssetPathResolver geoAssetPathResolver;
@@ -83,7 +83,7 @@ class ConnectionRepositoryImpl
return TaskEither.Do(
($) async {
final options = await $(
- configOptionRepository
+ singBoxConfigOptionRepository
.getFullSingboxConfigOption()
.mapLeft((l) => const InvalidConfigOption()),
);
@@ -159,16 +159,11 @@ class ConnectionRepositoryImpl
await $(
TaskEither(() async {
if (options.enableTun) {
- final active = await platformSource.activateTunnel();
- if (!active) {
- loggy.warning("Possiblity missing privileges for tun mode");
+ final hasPrivilege = await platformSource.checkPrivilege();
+ if (!hasPrivilege) {
+ loggy.warning("missing privileges for tun mode");
return left(const MissingPrivilege());
}
- // final hasPrivilege = await platformSource.checkPrivilege();
- // if (!hasPrivilege) {
- // loggy.warning("missing privileges for tun mode");
- // return left(const MissingPrivilege());
- // }
}
return right(unit);
}),
@@ -194,31 +189,23 @@ class ConnectionRepositoryImpl
($) async {
final options = await $(getConfigOption());
- await $(
+ await $(
TaskEither(() async {
if (options.enableTun) {
- final active = await platformSource.deactivateTunnel();
- if (!active) {
- loggy.warning("Possiblity missing privileges for tun mode");
+ final hasPrivilege = await platformSource.checkPrivilege();
+ if (!hasPrivilege) {
+ loggy.warning("missing privileges for tun mode");
return left(const MissingPrivilege());
}
- // final hasPrivilege = await platformSource.checkPrivilege();
- // if (!hasPrivilege) {
- // loggy.warning("missing privileges for tun mode");
- // return left(const MissingPrivilege());
- // }
}
return right(unit);
}),
);
return await $(
- singbox.stop()
- .mapLeft(UnexpectedConnectionFailure.new),
+ singbox.stop().mapLeft(UnexpectedConnectionFailure.new),
);
},
).handleExceptions(UnexpectedConnectionFailure.new);
-
-
}
@override
diff --git a/lib/features/geo_asset/overview/geo_assets_overview_page.dart b/lib/features/geo_asset/overview/geo_assets_overview_page.dart
index 57f9667a..2af65703 100644
--- a/lib/features/geo_asset/overview/geo_assets_overview_page.dart
+++ b/lib/features/geo_asset/overview/geo_assets_overview_page.dart
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
+import 'package:hiddify/core/widget/adaptive_icon.dart';
+import 'package:hiddify/core/widget/animated_visibility.dart';
+import 'package:hiddify/core/widget/tip_card.dart';
import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_notifier.dart';
import 'package:hiddify/features/geo_asset/widget/geo_asset_tile.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -22,6 +25,7 @@ class GeoAssetsOverviewPage extends HookConsumerWidget {
pinned: true,
actions: [
PopupMenuButton(
+ icon: Icon(AdaptiveIcon(context).more),
itemBuilder: (context) {
return [
PopupMenuItem(
@@ -39,13 +43,13 @@ class GeoAssetsOverviewPage extends HookConsumerWidget {
),
if (state case AsyncData(value: (:final geoip, :final geosite)))
SliverPinnedHeader(
- child: AnimatedSwitcher(
- duration: const Duration(milliseconds: 200),
- child: (geoip + geosite)
- .where((e) => e.$1.active && e.$2 == null)
- .isNotEmpty
- ? const MissingRoutingAssetsCard()
- : const SizedBox(),
+ child: AnimatedVisibility(
+ visible: (geoip + geosite)
+ .where((e) => e.$1.active && e.$2 == null)
+ .isNotEmpty,
+ axis: Axis.vertical,
+ child:
+ TipCard(message: t.settings.geoAssets.missingGeoAssetsMsg),
),
),
switch (state) {
@@ -96,39 +100,3 @@ class GeoAssetsOverviewPage extends HookConsumerWidget {
);
}
}
-
-class MissingRoutingAssetsCard extends HookConsumerWidget {
- const MissingRoutingAssetsCard({super.key});
-
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final t = ref.watch(translationsProvider);
-
- return Card(
- margin: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 4,
- ),
- child: Row(
- children: [
- const Padding(
- padding: EdgeInsets.all(8.0),
- child: Icon(Icons.lightbulb),
- ),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
- child: Text(t.settings.geoAssets.missingGeoAssetsMsg),
- ),
- ],
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/features/geo_asset/widget/geo_asset_tile.dart b/lib/features/geo_asset/widget/geo_asset_tile.dart
index 61fbbee6..9180a6ff 100644
--- a/lib/features/geo_asset/widget/geo_asset_tile.dart
+++ b/lib/features/geo_asset/widget/geo_asset_tile.dart
@@ -2,6 +2,7 @@ import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart';
+import 'package:hiddify/core/widget/adaptive_icon.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart';
import 'package:hiddify/features/geo_asset/notifier/geo_asset_notifier.dart';
@@ -95,6 +96,7 @@ class GeoAssetTile extends HookConsumerWidget {
selected: geoAsset.active,
onTap: onMarkAsActive,
trailing: PopupMenuButton(
+ icon: Icon(AdaptiveIcon(context).more),
itemBuilder: (context) {
return [
PopupMenuItem(
diff --git a/lib/features/home/widget/connection_button.dart b/lib/features/home/widget/connection_button.dart
index 3fcbad18..eb1c3efe 100644
--- a/lib/features/home/widget/connection_button.dart
+++ b/lib/features/home/widget/connection_button.dart
@@ -149,9 +149,11 @@ class _ConnectionButton extends StatelessWidget {
.scaleXY(end: .88, curve: Curves.easeIn),
),
const Gap(16),
- Text(
- label,
- style: Theme.of(context).textTheme.bodyLarge,
+ ExcludeSemantics(
+ child: Text(
+ label,
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
),
],
);
diff --git a/lib/features/home/widget/empty_profiles_home_body.dart b/lib/features/home/widget/empty_profiles_home_body.dart
index eed2b370..b6b9506f 100644
--- a/lib/features/home/widget/empty_profiles_home_body.dart
+++ b/lib/features/home/widget/empty_profiles_home_body.dart
@@ -1,3 +1,4 @@
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
@@ -20,7 +21,7 @@ class EmptyProfilesHomeBody extends HookConsumerWidget {
const Gap(16),
OutlinedButton.icon(
onPressed: () => const AddProfileRoute().push(context),
- icon: const Icon(Icons.add),
+ icon: const Icon(FluentIcons.add_24_regular),
label: Text(t.profile.add.buttonText),
),
],
diff --git a/lib/features/home/widget/home_page.dart b/lib/features/home/widget/home_page.dart
index 785b0e2b..13b20d30 100644
--- a/lib/features/home/widget/home_page.dart
+++ b/lib/features/home/widget/home_page.dart
@@ -1,4 +1,5 @@
import 'package:dartx/dartx.dart';
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:hiddify/core/app_info/app_info_provider.dart';
import 'package:hiddify/core/localization/translations.dart';
@@ -9,6 +10,8 @@ import 'package:hiddify/features/home/widget/connection_button.dart';
import 'package:hiddify/features/home/widget/empty_profiles_home_body.dart';
import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart';
import 'package:hiddify/features/profile/widget/profile_tile.dart';
+import 'package:hiddify/features/proxy/active/active_proxy_delay_indicator.dart';
+import 'package:hiddify/features/proxy/active/active_proxy_footer.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sliver_tools/sliver_tools.dart';
@@ -44,7 +47,7 @@ class HomePage extends HookConsumerWidget {
actions: [
IconButton(
onPressed: () => const AddProfileRoute().push(context),
- icon: const Icon(Icons.add_circle),
+ icon: const Icon(FluentIcons.add_circle_24_filled),
tooltip: t.profile.add.buttonText,
),
],
@@ -53,16 +56,24 @@ class HomePage extends HookConsumerWidget {
AsyncData(value: final profile?) => MultiSliver(
children: [
ProfileTile(profile: profile, isMain: true),
- const SliverFillRemaining(
+ SliverFillRemaining(
hasScrollBody: false,
- child: Padding(
- padding: EdgeInsets.only(
- left: 8,
- right: 8,
- top: 8,
- bottom: 86,
- ),
- child: ConnectionButton(),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ ConnectionButton(),
+ ActiveProxyDelayIndicator(),
+ ],
+ ),
+ ),
+ if (MediaQuery.sizeOf(context).width < 840)
+ const ActiveProxyFooter(),
+ ],
),
),
],
diff --git a/lib/features/intro/widget/intro_page.dart b/lib/features/intro/widget/intro_page.dart
index c458b3fe..0a8909c5 100644
--- a/lib/features/intro/widget/intro_page.dart
+++ b/lib/features/intro/widget/intro_page.dart
@@ -1,15 +1,20 @@
+import 'dart:convert';
import 'package:flutter/gestures.dart';
+
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/analytics/analytics_controller.dart';
+import 'package:hiddify/core/localization/locale_preferences.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/constants.dart';
+import 'package:hiddify/core/model/region.dart';
import 'package:hiddify/core/preferences/general_preferences.dart';
import 'package:hiddify/features/common/general_pref_tiles.dart';
import 'package:hiddify/gen/assets.gen.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:http/http.dart' as http;
import 'package:sliver_tools/sliver_tools.dart';
class IntroPage extends HookConsumerWidget with PresLogger {
@@ -20,7 +25,8 @@ class IntroPage extends HookConsumerWidget with PresLogger {
final t = ref.watch(translationsProvider);
final isStarting = useState(false);
-
+ autoSelectRegion(ref)
+ .then((value) => loggy.debug("Auto Region selection finished!"));
return Scaffold(
body: SafeArea(
child: CustomScrollView(
@@ -109,4 +115,47 @@ class IntroPage extends HookConsumerWidget with PresLogger {
),
);
}
+
+ Future autoSelectRegion(WidgetRef ref) async {
+ final response = await http.get(Uri.parse('https://ipapi.co/json/'));
+
+ if (response.statusCode == 200) {
+ final jsonData = jsonDecode(response.body);
+ final regionLocale =
+ _getRegionLocale(jsonData['country']?.toString() ?? "");
+
+ loggy.debug(
+ 'Region: ${regionLocale.region} Locale: ${regionLocale.locale}');
+ await ref
+ .read(regionNotifierProvider.notifier)
+ .update(regionLocale.region);
+ await ref
+ .read(localePreferencesProvider.notifier)
+ .changeLocale(regionLocale.locale);
+ } else {
+ loggy.warning('Request failed with status: ${response.statusCode}');
+ }
+ }
+
+ RegionLocale _getRegionLocale(String country) {
+ switch (country) {
+ case "IR":
+ return RegionLocale(Region.ir, AppLocale.fa);
+ case "CN":
+ return RegionLocale(Region.cn, AppLocale.zhCn);
+ case "RU":
+ return RegionLocale(Region.ru, AppLocale.ru);
+ case "AF":
+ return RegionLocale(Region.af, AppLocale.fa);
+ default:
+ return RegionLocale(Region.other, AppLocale.en);
+ }
+ }
+}
+
+class RegionLocale {
+ final Region region;
+ final AppLocale locale;
+
+ RegionLocale(this.region, this.locale);
}
diff --git a/lib/features/log/model/log_level.dart b/lib/features/log/model/log_level.dart
index a81cf47c..714e7d38 100644
--- a/lib/features/log/model/log_level.dart
+++ b/lib/features/log/model/log_level.dart
@@ -1,6 +1,10 @@
+import 'package:dart_mappable/dart_mappable.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
+part 'log_level.mapper.dart';
+
+@MappableEnum()
enum LogLevel {
trace,
debug,
diff --git a/lib/features/log/overview/logs_overview_page.dart b/lib/features/log/overview/logs_overview_page.dart
index 17b1b8c9..33d3c817 100644
--- a/lib/features/log/overview/logs_overview_page.dart
+++ b/lib/features/log/overview/logs_overview_page.dart
@@ -1,3 +1,4 @@
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fpdart/fpdart.dart';
@@ -5,6 +6,7 @@ import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart';
import 'package:hiddify/core/preferences/general_preferences.dart';
+import 'package:hiddify/core/widget/adaptive_icon.dart';
import 'package:hiddify/features/common/nested_app_bar.dart';
import 'package:hiddify/features/log/data/log_data_providers.dart';
import 'package:hiddify/features/log/model/log_level.dart';
@@ -65,22 +67,26 @@ class LogsOverviewPage extends HookConsumerWidget with PresLogger {
if (state.paused)
IconButton(
onPressed: notifier.resume,
- icon: const Icon(Icons.play_arrow),
+ icon: const Icon(FluentIcons.play_20_regular),
tooltip: t.logs.resumeTooltip,
+ iconSize: 20,
)
else
IconButton(
onPressed: notifier.pause,
- icon: const Icon(Icons.pause),
+ icon: const Icon(FluentIcons.pause_20_regular),
tooltip: t.logs.pauseTooltip,
+ iconSize: 20,
),
IconButton(
onPressed: notifier.clear,
- icon: const Icon(Icons.clear_all),
+ icon: const Icon(FluentIcons.delete_lines_20_regular),
tooltip: t.logs.clearTooltip,
+ iconSize: 20,
),
if (popupButtons.isNotEmpty)
PopupMenuButton(
+ icon: Icon(AdaptiveIcon(context).more),
itemBuilder: (context) {
return popupButtons;
},
diff --git a/lib/features/per_app_proxy/overview/per_app_proxy_page.dart b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart
index 651fa6ff..c500e15f 100644
--- a/lib/features/per_app_proxy/overview/per_app_proxy_page.dart
+++ b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart
@@ -1,9 +1,11 @@
import 'package:dartx/dartx.dart';
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/preferences/general_preferences.dart';
+import 'package:hiddify/core/widget/adaptive_icon.dart';
import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart';
import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_notifier.dart';
@@ -83,11 +85,12 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
title: Text(t.settings.network.perAppProxyPageTitle),
actions: [
IconButton(
- icon: const Icon(Icons.search),
+ icon: const Icon(FluentIcons.search_24_regular),
onPressed: () => isSearching.value = true,
tooltip: localizations.searchFieldLabel,
),
PopupMenuButton(
+ icon: Icon(AdaptiveIcon(context).more),
itemBuilder: (context) {
return [
PopupMenuItem(
@@ -179,7 +182,8 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
.watch(packageIconProvider(package.packageName))
.when(
data: (data) => Image(image: data),
- error: (error, _) => const Icon(Icons.error),
+ error: (error, _) =>
+ const Icon(FluentIcons.error_circle_24_regular),
loading: () => const Center(
child: CircularProgressIndicator(),
),
diff --git a/lib/features/profile/add/add_profile_modal.dart b/lib/features/profile/add/add_profile_modal.dart
index d68831f3..7fb3c365 100644
--- a/lib/features/profile/add/add_profile_modal.dart
+++ b/lib/features/profile/add/add_profile_modal.dart
@@ -1,3 +1,4 @@
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -99,7 +100,7 @@ class AddProfileModal extends HookConsumerWidget {
_Button(
key: const ValueKey("add_from_clipboard_button"),
label: t.profile.add.fromClipboard,
- icon: Icons.content_paste,
+ icon: FluentIcons.clipboard_paste_24_regular,
size: buttonWidth,
onTap: () async {
final captureResult =
@@ -116,7 +117,7 @@ class AddProfileModal extends HookConsumerWidget {
_Button(
key: const ValueKey("add_by_qr_code_button"),
label: t.profile.add.scanQr,
- icon: Icons.qr_code_scanner,
+ icon: FluentIcons.qr_code_24_regular,
size: buttonWidth,
onTap: () async {
final captureResult =
@@ -133,7 +134,7 @@ class AddProfileModal extends HookConsumerWidget {
_Button(
key: const ValueKey("add_manually_button"),
label: t.profile.add.manually,
- icon: Icons.add,
+ icon: FluentIcons.add_24_regular,
size: buttonWidth,
onTap: () async {
context.pop();
@@ -170,7 +171,7 @@ class AddProfileModal extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
- Icons.add,
+ FluentIcons.add_24_regular,
color: theme.colorScheme.primary,
),
const Gap(8),
diff --git a/lib/features/profile/details/profile_details_page.dart b/lib/features/profile/details/profile_details_page.dart
index 642b24cc..af940db5 100644
--- a/lib/features/profile/details/profile_details_page.dart
+++ b/lib/features/profile/details/profile_details_page.dart
@@ -1,8 +1,10 @@
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:fpdart/fpdart.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart';
+import 'package:hiddify/core/widget/adaptive_icon.dart';
import 'package:hiddify/features/common/confirmation_dialogs.dart';
import 'package:hiddify/features/profile/details/profile_details_notifier.dart';
import 'package:hiddify/features/profile/model/profile_entity.dart';
@@ -95,6 +97,7 @@ class ProfileDetailsPage extends HookConsumerWidget with PresLogger {
actions: [
if (state.isEditing)
PopupMenuButton(
+ icon: Icon(AdaptiveIcon(context).more),
itemBuilder: (context) {
return [
if (state.profile case RemoteProfileEntity())
@@ -175,7 +178,8 @@ class ProfileDetailsPage extends HookConsumerWidget with PresLogger {
) ??
t.general.toggle.disabled,
),
- leading: const Icon(Icons.update),
+ leading:
+ const Icon(FluentIcons.arrow_sync_24_regular),
onTap: () async {
final intervalInHours =
await SettingsInputDialog(
@@ -185,7 +189,8 @@ class ProfileDetailsPage extends HookConsumerWidget with PresLogger {
optionalAction: (
t.general.state.disable,
() => notifier.setField(
- updateInterval: none()),
+ updateInterval: none(),
+ ),
),
validator: isPort,
mapTo: int.tryParse,
diff --git a/lib/features/profile/model/profile_sort_enum.dart b/lib/features/profile/model/profile_sort_enum.dart
index 5852a515..78b65be5 100644
--- a/lib/features/profile/model/profile_sort_enum.dart
+++ b/lib/features/profile/model/profile_sort_enum.dart
@@ -1,3 +1,4 @@
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:hiddify/core/localization/translations.dart';
@@ -13,8 +14,8 @@ enum ProfilesSort {
}
IconData get icon => switch (this) {
- lastUpdate => Icons.update,
- name => Icons.sort_by_alpha,
+ lastUpdate => FluentIcons.history_24_regular,
+ name => FluentIcons.text_sort_ascending_24_regular,
};
}
diff --git a/lib/features/profile/overview/profiles_overview_page.dart b/lib/features/profile/overview/profiles_overview_page.dart
index 1d0dac0d..ef542626 100644
--- a/lib/features/profile/overview/profiles_overview_page.dart
+++ b/lib/features/profile/overview/profiles_overview_page.dart
@@ -1,3 +1,4 @@
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
@@ -79,7 +80,7 @@ class ProfilesOverviewModal extends HookConsumerWidget {
onPressed: () {
const AddProfileRoute().push(context);
},
- icon: const Icon(Icons.add),
+ icon: const Icon(FluentIcons.add_24_filled),
label: Text(t.profile.add.shortBtnTxt),
),
FilledButton.icon(
@@ -91,7 +92,7 @@ class ProfilesOverviewModal extends HookConsumerWidget {
},
);
},
- icon: const Icon(Icons.sort),
+ icon: const Icon(FluentIcons.arrow_sort_24_filled),
label: Text(t.general.sort),
),
FilledButton.icon(
@@ -102,7 +103,7 @@ class ProfilesOverviewModal extends HookConsumerWidget {
)
.trigger();
},
- icon: const Icon(Icons.update),
+ icon: const Icon(FluentIcons.arrow_sync_24_filled),
label: Text(t.profile.update.updateSubscriptions),
),
],
@@ -159,7 +160,7 @@ class ProfilesSortModal extends HookConsumerWidget {
turns: arrowTurn,
duration: const Duration(milliseconds: 100),
child: Icon(
- Icons.arrow_upward,
+ FluentIcons.arrow_sort_up_24_regular,
semanticLabel: sort.mode.name,
),
),
diff --git a/lib/features/profile/widget/profile_tile.dart b/lib/features/profile/widget/profile_tile.dart
index 50215660..e2cb7288 100644
--- a/lib/features/profile/widget/profile_tile.dart
+++ b/lib/features/profile/widget/profile_tile.dart
@@ -1,3 +1,4 @@
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
@@ -6,6 +7,7 @@ import 'package:go_router/go_router.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart';
import 'package:hiddify/core/router/router.dart';
+import 'package:hiddify/core/widget/adaptive_icon.dart';
import 'package:hiddify/core/widget/adaptive_menu.dart';
import 'package:hiddify/features/common/confirmation_dialogs.dart';
import 'package:hiddify/features/common/qr_code_dialog.dart';
@@ -132,7 +134,10 @@ class ProfileTile extends HookConsumerWidget {
),
),
),
- const Icon(Icons.arrow_drop_down),
+ const Icon(
+ FluentIcons.caret_down_16_filled,
+ size: 16,
+ ),
],
),
),
@@ -196,7 +201,7 @@ class ProfileActionButton extends HookConsumerWidget {
.read(updateProfileProvider(profile.id).notifier)
.updateProfile(profile as RemoteProfileEntity);
},
- child: const Icon(Icons.update),
+ child: const Icon(FluentIcons.arrow_sync_24_filled),
),
),
);
@@ -210,7 +215,7 @@ class ProfileActionButton extends HookConsumerWidget {
message: MaterialLocalizations.of(context).showMenuTooltip,
child: InkWell(
onTap: toggleVisibility,
- child: const Icon(Icons.more_vert),
+ child: Icon(AdaptiveIcon(context).more),
),
),
);
@@ -248,7 +253,7 @@ class ProfileActionsMenu extends HookConsumerWidget {
if (profile case RemoteProfileEntity())
AdaptiveMenuItem(
title: t.profile.update.buttonTxt,
- icon: Icons.update,
+ icon: FluentIcons.arrow_sync_24_regular,
onTap: () {
if (ref.read(updateProfileProvider(profile.id)).isLoading) {
return;
@@ -260,7 +265,7 @@ class ProfileActionsMenu extends HookConsumerWidget {
),
AdaptiveMenuItem(
title: t.profile.share.buttonText,
- icon: Icons.share,
+ icon: AdaptiveIcon(context).share,
subItems: [
if (profile case RemoteProfileEntity(:final url, :final name)) ...[
AdaptiveMenuItem(
@@ -305,14 +310,14 @@ class ProfileActionsMenu extends HookConsumerWidget {
],
),
AdaptiveMenuItem(
- icon: Icons.edit,
+ icon: FluentIcons.edit_24_regular,
title: t.profile.edit.buttonTxt,
onTap: () async {
await ProfileDetailsRoute(profile.id).push(context);
},
),
AdaptiveMenuItem(
- icon: Icons.delete,
+ icon: FluentIcons.delete_24_regular,
title: t.profile.delete.buttonTxt,
onTap: () async {
if (deleteProfileMutation.state.isInProgress) {
diff --git a/lib/features/proxy/active/active_proxy_delay_indicator.dart b/lib/features/proxy/active/active_proxy_delay_indicator.dart
new file mode 100644
index 00000000..ff8cde35
--- /dev/null
+++ b/lib/features/proxy/active/active_proxy_delay_indicator.dart
@@ -0,0 +1,85 @@
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:hiddify/core/localization/translations.dart';
+import 'package:hiddify/core/widget/animated_visibility.dart';
+import 'package:hiddify/core/widget/shimmer_skeleton.dart';
+import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+class ActiveProxyDelayIndicator extends HookConsumerWidget {
+ const ActiveProxyDelayIndicator({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final t = ref.watch(translationsProvider);
+ final theme = Theme.of(context);
+ final activeProxy = ref.watch(activeProxyNotifierProvider);
+
+ return AnimatedVisibility(
+ axis: Axis.vertical,
+ visible: activeProxy is AsyncData,
+ child: () {
+ switch (activeProxy) {
+ case AsyncData(value: final proxy):
+ final delay = proxy.urlTestDelay;
+ final timeout = delay > 65000;
+ return Center(
+ child: InkWell(
+ onTap: () async {
+ await ref
+ .read(activeProxyNotifierProvider.notifier)
+ .urlTest(proxy.tag);
+ },
+ borderRadius: BorderRadius.circular(24),
+ child: Padding(
+ padding:
+ const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(FluentIcons.wifi_1_24_regular),
+ const Gap(8),
+ if (delay > 0)
+ Text.rich(
+ semanticsLabel: timeout
+ ? t.proxies.delaySemantics.timeout
+ : t.proxies.delaySemantics.result(delay: delay),
+ TextSpan(
+ children: [
+ if (timeout)
+ TextSpan(
+ text: t.general.timeout,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ color: theme.colorScheme.error,
+ ),
+ )
+ else ...[
+ TextSpan(
+ text: delay.toString(),
+ style: theme.textTheme.titleMedium
+ ?.copyWith(fontWeight: FontWeight.bold),
+ ),
+ const TextSpan(text: " ms"),
+ ],
+ ],
+ ),
+ )
+ else
+ Semantics(
+ label: t.proxies.delaySemantics.testing,
+ child: const ShimmerSkeleton(width: 48, height: 18),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ default:
+ return const SizedBox();
+ }
+ }(),
+ );
+ }
+}
diff --git a/lib/features/proxy/active/active_proxy_footer.dart b/lib/features/proxy/active/active_proxy_footer.dart
new file mode 100644
index 00000000..2cd90270
--- /dev/null
+++ b/lib/features/proxy/active/active_proxy_footer.dart
@@ -0,0 +1,151 @@
+import 'package:dartx/dartx.dart';
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:hiddify/core/localization/translations.dart';
+import 'package:hiddify/core/widget/animated_visibility.dart';
+import 'package:hiddify/core/widget/shimmer_skeleton.dart';
+import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart';
+import 'package:hiddify/features/proxy/active/ip_widget.dart';
+import 'package:hiddify/features/stats/notifier/stats_notifier.dart';
+import 'package:hiddify/utils/utils.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+class ActiveProxyFooter extends HookConsumerWidget {
+ const ActiveProxyFooter({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final t = ref.watch(translationsProvider);
+ final activeProxy = ref.watch(activeProxyNotifierProvider);
+ final ipInfo = ref.watch(ipInfoNotifierProvider);
+
+ return AnimatedVisibility(
+ axis: Axis.vertical,
+ visible: activeProxy is AsyncData,
+ child: switch (activeProxy) {
+ AsyncData(value: final proxy) => Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Flexible(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _InfoProp(
+ icon: FluentIcons.arrow_routing_20_regular,
+ text: proxy.selectedName.isNotNullOrBlank
+ ? proxy.selectedName!
+ : proxy.name,
+ semanticLabel: t.proxies.activeProxySemanticLabel,
+ ),
+ const Gap(8),
+ switch (ipInfo) {
+ AsyncData(value: final info) => Row(
+ children: [
+ IPCountryFlag(countryCode: info.countryCode),
+ const Gap(8),
+ IPText(
+ ip: info.ip,
+ onLongPress: () async {
+ ref
+ .read(ipInfoNotifierProvider.notifier)
+ .refresh();
+ },
+ ),
+ ],
+ ),
+ AsyncError() => _InfoProp(
+ icon: FluentIcons.error_circle_20_regular,
+ text: t.general.unknown,
+ ),
+ _ => const Row(
+ children: [
+ Icon(FluentIcons.question_circle_20_regular),
+ Gap(8),
+ Flexible(
+ child: ShimmerSkeleton(
+ height: 16,
+ widthFactor: 1,
+ ),
+ ),
+ ],
+ ),
+ },
+ ],
+ ),
+ ),
+ const _StatsColumn(),
+ ],
+ ),
+ ),
+ _ => const SizedBox(),
+ },
+ );
+ }
+}
+
+class _StatsColumn extends HookConsumerWidget {
+ const _StatsColumn();
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final t = ref.watch(translationsProvider);
+ final stats = ref.watch(statsNotifierProvider).value;
+
+ return Directionality(
+ textDirection: TextDirection.values[
+ (Directionality.of(context).index + 1) % TextDirection.values.length],
+ child: Flexible(
+ child: Column(
+ children: [
+ _InfoProp(
+ icon: FluentIcons.arrow_bidirectional_up_down_20_regular,
+ text: (stats?.downlinkTotal ?? 0).size(),
+ semanticLabel: t.proxies.statsSemantics.totalTransferred,
+ ),
+ const Gap(8),
+ _InfoProp(
+ icon: FluentIcons.arrow_download_20_regular,
+ text: (stats?.downlink ?? 0).speed(),
+ semanticLabel: t.proxies.statsSemantics.speed,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _InfoProp extends StatelessWidget {
+ const _InfoProp({
+ required this.icon,
+ required this.text,
+ this.semanticLabel,
+ });
+
+ final IconData icon;
+ final String text;
+ final String? semanticLabel;
+
+ @override
+ Widget build(BuildContext context) {
+ return Semantics(
+ label: semanticLabel,
+ child: Row(
+ children: [
+ Icon(icon),
+ const Gap(8),
+ Flexible(
+ child: Text(
+ text,
+ style: Theme.of(context).textTheme.labelMedium,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/features/proxy/active/active_proxy_notifier.dart b/lib/features/proxy/active/active_proxy_notifier.dart
new file mode 100644
index 00000000..5d5351eb
--- /dev/null
+++ b/lib/features/proxy/active/active_proxy_notifier.dart
@@ -0,0 +1,83 @@
+import 'package:dio/dio.dart';
+import 'package:hiddify/core/utils/throttler.dart';
+import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
+import 'package:hiddify/features/proxy/data/proxy_data_providers.dart';
+import 'package:hiddify/features/proxy/model/ip_info_entity.dart';
+import 'package:hiddify/features/proxy/model/proxy_entity.dart';
+import 'package:hiddify/features/proxy/model/proxy_failure.dart';
+import 'package:hiddify/utils/riverpod_utils.dart';
+import 'package:hiddify/utils/utils.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'active_proxy_notifier.g.dart';
+
+@riverpod
+class IpInfoNotifier extends _$IpInfoNotifier with AppLogger {
+ @override
+ Future build() async {
+ ref.disposeDelay(const Duration(seconds: 20));
+ final cancelToken = CancelToken();
+ ref.onDispose(() {
+ loggy.debug("disposing");
+ cancelToken.cancel();
+ });
+
+ final serviceRunning = await ref.watch(serviceRunningProvider.future);
+ if (!serviceRunning) {
+ throw const ServiceNotRunning();
+ }
+
+ return ref
+ .watch(proxyRepositoryProvider)
+ .getCurrentIpInfo(cancelToken)
+ .getOrElse(
+ (err) {
+ loggy.error("error getting proxy ip info", err);
+ throw err;
+ },
+ ).run();
+ }
+
+ Future refresh() async {
+ loggy.debug("refreshing");
+ ref.invalidateSelf();
+ }
+}
+
+@riverpod
+class ActiveProxyNotifier extends _$ActiveProxyNotifier with AppLogger {
+ @override
+ Stream build() async* {
+ ref.disposeDelay(const Duration(seconds: 20));
+
+ final serviceRunning = await ref.watch(serviceRunningProvider.future);
+ if (!serviceRunning) {
+ throw const ServiceNotRunning();
+ }
+
+ yield* ref
+ .watch(proxyRepositoryProvider)
+ .watchActiveProxies()
+ .map((event) => event.getOrElse((l) => throw l))
+ .map((event) => event.firstOrNull!.items.first);
+ }
+
+ final _urlTestThrottler = Throttler(const Duration(seconds: 2));
+
+ Future urlTest(String groupTag) async {
+ _urlTestThrottler(
+ () async {
+ loggy.debug("testing group: [$groupTag]");
+ if (state case AsyncData()) {
+ await ref
+ .read(proxyRepositoryProvider)
+ .urlTest(groupTag)
+ .getOrElse((err) {
+ loggy.error("error testing group", err);
+ throw err;
+ }).run();
+ }
+ },
+ );
+ }
+}
diff --git a/lib/features/proxy/active/active_proxy_sidebar_card.dart b/lib/features/proxy/active/active_proxy_sidebar_card.dart
new file mode 100644
index 00000000..f0dc6bd3
--- /dev/null
+++ b/lib/features/proxy/active/active_proxy_sidebar_card.dart
@@ -0,0 +1,98 @@
+import 'package:dartx/dartx.dart';
+import 'package:fluentui_system_icons/fluentui_system_icons.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:hiddify/core/localization/translations.dart';
+import 'package:hiddify/core/widget/shimmer_skeleton.dart';
+import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart';
+import 'package:hiddify/features/proxy/active/ip_widget.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+class ActiveProxySideBarCard extends HookConsumerWidget {
+ const ActiveProxySideBarCard({super.key});
+
+ Widget buildProp(Widget icon, Widget child) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ icon,
+ const Gap(4),
+ Flexible(child: child),
+ ],
+ );
+ }
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final theme = Theme.of(context);
+ final t = ref.watch(translationsProvider);
+ final activeProxy = ref.watch(activeProxyNotifierProvider);
+ final ipInfo = ref.watch(ipInfoNotifierProvider);
+
+ Widget propText(String txt) {
+ return Text(
+ txt,
+ overflow: TextOverflow.ellipsis,
+ style: theme.textTheme.bodySmall,
+ );
+ }
+
+ return Theme(
+ data: theme.copyWith(
+ iconTheme: theme.iconTheme.copyWith(size: 14),
+ ),
+ child: Card(
+ margin: EdgeInsets.zero,
+ shadowColor: Colors.transparent,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(t.home.stats.connection),
+ const Gap(4),
+ switch (activeProxy) {
+ AsyncData(value: final proxy) => buildProp(
+ const Icon(FluentIcons.arrow_routing_20_regular),
+ propText(
+ proxy.selectedName.isNotNullOrBlank
+ ? proxy.selectedName!
+ : proxy.name,
+ ),
+ ),
+ _ => buildProp(
+ const Icon(FluentIcons.arrow_routing_20_regular),
+ propText("..."),
+ ),
+ },
+ const Gap(4),
+ switch (ipInfo) {
+ AsyncData(value: final info) => buildProp(
+ IPCountryFlag(
+ countryCode: info.countryCode,
+ size: 16,
+ ),
+ IPText(
+ ip: info.ip,
+ onLongPress: () async {
+ ref.read(ipInfoNotifierProvider.notifier).refresh();
+ },
+ constrained: true,
+ ),
+ ),
+ AsyncLoading() => buildProp(
+ const Icon(FluentIcons.question_circle_20_regular),
+ const ShimmerSkeleton(widthFactor: .85, height: 14),
+ ),
+ _ => buildProp(
+ const Icon(FluentIcons.error_circle_20_regular),
+ propText(t.general.unknown),
+ ),
+ },
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/proxy/active/ip_widget.dart b/lib/features/proxy/active/ip_widget.dart
new file mode 100644
index 00000000..eeb1fe70
--- /dev/null
+++ b/lib/features/proxy/active/ip_widget.dart
@@ -0,0 +1,92 @@
+import 'package:circle_flags/circle_flags.dart';
+import 'package:flutter/material.dart';
+import 'package:hiddify/core/localization/translations.dart';
+import 'package:hiddify/core/utils/ip_utils.dart';
+import 'package:hiddify/utils/riverpod_utils.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+final _showIp = StateProvider.autoDispose((ref) {
+ ref.disposeDelay(const Duration(seconds: 20));
+ return false;
+});
+
+class IPText extends HookConsumerWidget {
+ const IPText({
+ required this.ip,
+ required this.onLongPress,
+ this.constrained = false,
+ super.key,
+ });
+
+ final String ip;
+ final VoidCallback onLongPress;
+ final bool constrained;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final t = ref.watch(translationsProvider);
+ final isVisible = ref.watch(_showIp);
+ final textTheme = Theme.of(context).textTheme;
+ final ipStyle = constrained ? textTheme.labelMedium : textTheme.labelLarge;
+
+ return Semantics(
+ label: t.proxies.ipInfoSemantics.address,
+ child: InkWell(
+ onTap: () {
+ ref.read(_showIp.notifier).state = !isVisible;
+ },
+ onLongPress: onLongPress,
+ borderRadius: BorderRadius.circular(12),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 2),
+ child: AnimatedCrossFade(
+ firstChild: Text(
+ ip,
+ style: ipStyle,
+ textDirection: TextDirection.ltr,
+ overflow: TextOverflow.ellipsis,
+ ),
+ secondChild: Padding(
+ padding: constrained
+ ? EdgeInsets.zero
+ : const EdgeInsetsDirectional.only(end: 48),
+ child: Text(
+ obscureIp(ip),
+ semanticsLabel: t.general.hidden,
+ style: ipStyle,
+ textDirection: TextDirection.ltr,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ crossFadeState: isVisible
+ ? CrossFadeState.showFirst
+ : CrossFadeState.showSecond,
+ duration: const Duration(milliseconds: 200),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class IPCountryFlag extends HookConsumerWidget {
+ const IPCountryFlag({required this.countryCode, this.size = 24, super.key});
+
+ final String countryCode;
+ final double size;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final t = ref.watch(translationsProvider);
+
+ return Semantics(
+ label: t.proxies.ipInfoSemantics.country,
+ child: Container(
+ width: size,
+ height: size,
+ padding: const EdgeInsets.all(2),
+ child: Center(child: CircleFlag(countryCode)),
+ ),
+ );
+ }
+}
diff --git a/lib/features/proxy/data/proxy_data_providers.dart b/lib/features/proxy/data/proxy_data_providers.dart
index 1c0c0823..bfc29735 100644
--- a/lib/features/proxy/data/proxy_data_providers.dart
+++ b/lib/features/proxy/data/proxy_data_providers.dart
@@ -1,3 +1,4 @@
+import 'package:hiddify/core/http_client/http_client_provider.dart';
import 'package:hiddify/features/proxy/data/proxy_repository.dart';
import 'package:hiddify/singbox/service/singbox_service_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -8,5 +9,6 @@ part 'proxy_data_providers.g.dart';
ProxyRepository proxyRepository(ProxyRepositoryRef ref) {
return ProxyRepositoryImpl(
singbox: ref.watch(singboxServiceProvider),
+ client: ref.watch(httpClientProvider),
);
}
diff --git a/lib/features/proxy/data/proxy_repository.dart b/lib/features/proxy/data/proxy_repository.dart
index f318ecaf..8d931d50 100644
--- a/lib/features/proxy/data/proxy_repository.dart
+++ b/lib/features/proxy/data/proxy_repository.dart
@@ -1,5 +1,8 @@
+import 'package:dio/dio.dart';
import 'package:fpdart/fpdart.dart';
+import 'package:hiddify/core/http_client/dio_http_client.dart';
import 'package:hiddify/core/utils/exception_handler.dart';
+import 'package:hiddify/features/proxy/model/ip_info_entity.dart';
import 'package:hiddify/features/proxy/model/proxy_entity.dart';
import 'package:hiddify/features/proxy/model/proxy_failure.dart';
import 'package:hiddify/singbox/service/singbox_service.dart';
@@ -7,6 +10,8 @@ import 'package:hiddify/utils/custom_loggers.dart';
abstract interface class ProxyRepository {
Stream>> watchProxies();
+ Stream>> watchActiveProxies();
+ TaskEither getCurrentIpInfo(CancelToken cancelToken);
TaskEither selectProxy(
String groupTag,
String outboundTag,
@@ -17,13 +22,51 @@ abstract interface class ProxyRepository {
class ProxyRepositoryImpl
with ExceptionHandler, InfraLogger
implements ProxyRepository {
- ProxyRepositoryImpl({required this.singbox});
+ ProxyRepositoryImpl({
+ required this.singbox,
+ required this.client,
+ });
final SingboxService singbox;
+ final DioHttpClient client;
@override
Stream>> watchProxies() {
- return singbox.watchOutbounds().map((event) {
+ return singbox.watchGroups().map((event) {
+ final groupWithSelected = {
+ for (final group in event) group.tag: group.selected,
+ };
+ return event
+ .map(
+ (e) => ProxyGroupEntity(
+ tag: e.tag,
+ type: e.type,
+ selected: e.selected,
+ items: e.items
+ .map(
+ (e) => ProxyItemEntity(
+ tag: e.tag,
+ type: e.type,
+ urlTestDelay: e.urlTestDelay,
+ selectedTag: groupWithSelected[e.tag],
+ ),
+ )
+ .filter((t) => t.isVisible)
+ .toList(),
+ ),
+ )
+ .toList();
+ }).handleExceptions(
+ (error, stackTrace) {
+ loggy.error("error watching proxies", error, stackTrace);
+ return ProxyUnexpectedFailure(error, stackTrace);
+ },
+ );
+ }
+
+ @override
+ Stream>> watchActiveProxies() {
+ return singbox.watchActiveGroups().map((event) {
final groupWithSelected = {
for (final group in event) group.tag: group.selected,
};
@@ -48,7 +91,7 @@ class ProxyRepositoryImpl
.toList();
}).handleExceptions(
(error, stackTrace) {
- loggy.error("error watching proxies", error, stackTrace);
+ loggy.error("error watching active proxies", error, stackTrace);
return ProxyUnexpectedFailure(error, stackTrace);
},
);
@@ -75,4 +118,35 @@ class ProxyRepositoryImpl
ProxyUnexpectedFailure.new,
);
}
+
+ final Map response)>
+ _ipInfoSources = {
+ "https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
+ "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
+ };
+
+ @override
+ TaskEither getCurrentIpInfo(CancelToken cancelToken) {
+ return TaskEither.tryCatch(
+ () async {
+ for (final source in _ipInfoSources.entries) {
+ try {
+ loggy.debug("getting current ip info using [${source.key}]");
+ final response = await client.get