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 @@ [**![Lang_farsi](https://user-images.githubusercontent.com/125398461/234186932-52f1fa82-52c6-417f-8b37-08fe9250a55f.png) فارسی**](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.

English Demo @@ -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 @@ [**![Lang_farsi](https://user-images.githubusercontent.com/125398461/234186932-52f1fa82-52c6-417f-8b37-08fe9250a55f.png) فارسی**](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 无广告,并且代码开源。它为大家自由访问互联网提供了一个支持多种协议的、安全且私密的工具。

English Demo @@ -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 @@ [**![Lang_farsi](https://user-images.githubusercontent.com/125398461/234186932-52f1fa82-52c6-417f-8b37-08fe9250a55f.png) فارسی**](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 は無料でオープンソースです。幅広いプロトコルをサポートし、無料インターネットにアクセスするための安全でプライベートな方法を提供します。

English Demo @@ -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>( + source.key, + cancelToken: cancelToken, + ); + if (response.statusCode == 200 && response.data != null) { + return source.value(response.data!); + } + } catch (e) { + loggy.debug("failed getting ip info using [${source.key}]", e); + continue; + } + } + throw const ProxyFailure.unexpected(); + }, + ProxyUnexpectedFailure.new, + ); + } } diff --git a/lib/features/proxy/model/ip_info_entity.dart b/lib/features/proxy/model/ip_info_entity.dart new file mode 100644 index 00000000..6a5536d5 --- /dev/null +++ b/lib/features/proxy/model/ip_info_entity.dart @@ -0,0 +1,70 @@ +import 'package:dart_mappable/dart_mappable.dart'; + +part 'ip_info_entity.mapper.dart'; + +@MappableClass() +class IpInfo with IpInfoMappable { + const IpInfo({ + required this.ip, + required this.countryCode, + required this.region, + required this.city, + this.timezone, + this.asn, + this.org, + }); + + final String ip; + final String countryCode; + final String region; + final String city; + final String? timezone; + final String? asn; + final String? org; + + static IpInfo fromIpInfoIoJson(Map json) { + return switch (json) { + { + "ip": final String ip, + "country": final String country, + "region": final String region, + "city": final String city, + "timezone": final String timezone, + "org": final String org, + } => + IpInfo( + ip: ip, + countryCode: country, + region: region, + city: city, + timezone: timezone, + org: org, + ), + _ => throw const FormatException("invalid json"), + }; + } + + static IpInfo fromIpApiCoJson(Map json) { + return switch (json) { + { + "ip": final String ip, + "country_code": final String countryCode, + "region": final String region, + "city": final String city, + "timezone": final String timezone, + "asn": final String asn, + "org": final String org, + } => + IpInfo( + ip: ip, + countryCode: countryCode, + region: region, + city: city, + timezone: timezone, + asn: asn, + org: org, + ), + _ => throw const FormatException("invalid json"), + }; + } +} diff --git a/lib/features/proxy/model/proxy_entity.dart b/lib/features/proxy/model/proxy_entity.dart index 2dbf96d0..8b6c8472 100644 --- a/lib/features/proxy/model/proxy_entity.dart +++ b/lib/features/proxy/model/proxy_entity.dart @@ -31,6 +31,7 @@ class ProxyItemEntity with _$ProxyItemEntity { String get name => _sanitizedTag(tag); String? get selectedName => selectedTag == null ? null : _sanitizedTag(selectedTag!); + bool get isVisible => !tag.contains("§hide§"); } String _sanitizedTag(String tag) => diff --git a/lib/features/proxy/overview/proxies_overview_notifier.dart b/lib/features/proxy/overview/proxies_overview_notifier.dart index 292094a4..029631c4 100644 --- a/lib/features/proxy/overview/proxies_overview_notifier.dart +++ b/lib/features/proxy/overview/proxies_overview_notifier.dart @@ -8,7 +8,6 @@ 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/proxy_entity.dart'; import 'package:hiddify/features/proxy/model/proxy_failure.dart'; -import 'package:hiddify/singbox/model/singbox_proxy_type.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:hiddify/utils/riverpod_utils.dart'; import 'package:hiddify/utils/utils.dart'; @@ -94,14 +93,14 @@ class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger { for (final group in proxies) { final sortedItems = switch (sortBy) { ProxiesSort.name => group.items.sortedWith((a, b) { - if(a.type.isGroup && !b.type.isGroup) return -1; - if(!a.type.isGroup && b.type.isGroup) return 1; + if (a.type.isGroup && !b.type.isGroup) return -1; + if (!a.type.isGroup && b.type.isGroup) return 1; return a.tag.compareTo(b.tag); - }), + }), ProxiesSort.delay => group.items.sortedWith((a, b) { - if(a.type.isGroup && !b.type.isGroup) return -1; - if(!a.type.isGroup && b.type.isGroup) return 1; - + if (a.type.isGroup && !b.type.isGroup) return -1; + if (!a.type.isGroup && b.type.isGroup) return 1; + final ai = a.urlTestDelay; final bi = b.urlTestDelay; if (ai == 0 && bi == 0) return -1; diff --git a/lib/features/proxy/overview/proxies_overview_page.dart b/lib/features/proxy/overview/proxies_overview_page.dart index 389946e9..dd2a20d6 100644 --- a/lib/features/proxy/overview/proxies_overview_page.dart +++ b/lib/features/proxy/overview/proxies_overview_page.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'; import 'package:hiddify/core/model/failures.dart'; @@ -29,7 +30,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger { PopupMenuButton( initialValue: sortBy, onSelected: ref.read(proxiesSortNotifierProvider.notifier).update, - icon: const Icon(Icons.sort), + icon: const Icon(FluentIcons.arrow_sort_24_regular), tooltip: t.proxies.sortTooltip, itemBuilder: (context) { return [ @@ -131,7 +132,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger { floatingActionButton: FloatingActionButton( onPressed: () async => notifier.urlTest(group.tag), tooltip: t.proxies.delayTestTooltip, - child: const Icon(Icons.bolt), + child: const Icon(FluentIcons.flash_24_filled), ), ); diff --git a/lib/features/proxy/widget/proxy_tile.dart b/lib/features/proxy/widget/proxy_tile.dart index e23630e0..7e305d61 100644 --- a/lib/features/proxy/widget/proxy_tile.dart +++ b/lib/features/proxy/widget/proxy_tile.dart @@ -51,7 +51,7 @@ class ProxyTile extends HookConsumerWidget with PresLogger { ), trailing: proxy.urlTestDelay != 0 ? Text( - proxy.urlTestDelay.toString(), + proxy.urlTestDelay > 65000 ? "×" : proxy.urlTestDelay.toString(), style: TextStyle(color: delayColor(context, proxy.urlTestDelay)), ) : null, diff --git a/lib/features/settings/about/about_page.dart b/lib/features/settings/about/about_page.dart index c437a86a..144896a4 100644 --- a/lib/features/settings/about/about_page.dart +++ b/lib/features/settings/about/about_page.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:gap/gap.dart'; @@ -6,6 +7,7 @@ import 'package:hiddify/core/directories/directories_provider.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/model/failures.dart'; +import 'package:hiddify/core/widget/adaptive_icon.dart'; import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart'; import 'package:hiddify/features/app_update/notifier/app_update_state.dart'; import 'package:hiddify/features/app_update/widget/new_version_dialog.dart'; @@ -54,7 +56,7 @@ class AboutPage extends HookConsumerWidget { height: 24, child: CircularProgressIndicator(), ), - _ => const Icon(Icons.update), + _ => const Icon(FluentIcons.arrow_sync_24_regular), }, onTap: () async { await ref.read(appUpdateNotifierProvider.notifier).check(); @@ -63,7 +65,7 @@ class AboutPage extends HookConsumerWidget { if (PlatformUtils.isDesktop) ListTile( title: Text(t.settings.general.openWorkingDir), - trailing: const Icon(Icons.arrow_outward_outlined), + trailing: const Icon(FluentIcons.open_folder_24_regular), onTap: () async { final path = ref.watch(appDirectoriesProvider).requireValue.workingDir.uri; @@ -79,6 +81,7 @@ class AboutPage extends HookConsumerWidget { title: Text(t.about.pageTitle), actions: [ PopupMenuButton( + icon: Icon(AdaptiveIcon(context).more), itemBuilder: (context) { return [ PopupMenuItem( @@ -126,7 +129,7 @@ class AboutPage extends HookConsumerWidget { if (conditionalTiles.isNotEmpty) const Divider(), ListTile( title: Text(t.about.sourceCode), - trailing: const Icon(Icons.open_in_new), + trailing: const Icon(FluentIcons.open_24_regular), onTap: () async { await UriUtils.tryLaunch( Uri.parse(Constants.githubUrl), @@ -135,7 +138,7 @@ class AboutPage extends HookConsumerWidget { ), ListTile( title: Text(t.about.telegramChannel), - trailing: const Icon(Icons.open_in_new), + trailing: const Icon(FluentIcons.open_24_regular), onTap: () async { await UriUtils.tryLaunch( Uri.parse(Constants.telegramChannelUrl), @@ -144,7 +147,7 @@ class AboutPage extends HookConsumerWidget { ), ListTile( title: Text(t.about.termsAndConditions), - trailing: const Icon(Icons.open_in_new), + trailing: const Icon(FluentIcons.open_24_regular), onTap: () async { await UriUtils.tryLaunch( Uri.parse(Constants.termsAndConditionsUrl), @@ -153,7 +156,7 @@ class AboutPage extends HookConsumerWidget { ), ListTile( title: Text(t.about.privacyPolicy), - trailing: const Icon(Icons.open_in_new), + trailing: const Icon(FluentIcons.open_24_regular), onTap: () async { await UriUtils.tryLaunch( Uri.parse(Constants.privacyPolicyUrl), diff --git a/lib/features/settings/widgets/advanced_setting_tiles.dart b/lib/features/settings/widgets/advanced_setting_tiles.dart index 097aabcd..26b980e2 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; @@ -23,16 +24,11 @@ class AdvancedSettingTiles extends HookConsumerWidget { return Column( children: [ const RegionPrefTile(), - ListTile( - title: Text(t.settings.config.pageTitle), - leading: const Icon(Icons.edit_document), - onTap: () async { - await const ConfigOptionsRoute().push(context); - }, - ), ListTile( title: Text(t.settings.geoAssets.pageTitle), - leading: const Icon(Icons.folder), + leading: const Icon( + FluentIcons.arrow_routing_rectangle_multiple_24_regular, + ), onTap: () async { await const GeoAssetsRoute().push(context); }, @@ -40,7 +36,7 @@ class AdvancedSettingTiles extends HookConsumerWidget { if (Platform.isAndroid) ...[ ListTile( title: Text(t.settings.network.perAppProxyPageTitle), - leading: const Icon(Icons.apps), + leading: const Icon(FluentIcons.apps_list_detail_24_regular), trailing: Switch( value: perAppProxy, onChanged: (value) async { @@ -68,7 +64,7 @@ class AdvancedSettingTiles extends HookConsumerWidget { title: Text(t.settings.advanced.memoryLimit), subtitle: Text(t.settings.advanced.memoryLimitMsg), value: !disableMemoryLimit, - secondary: const Icon(Icons.memory), + secondary: const Icon(FluentIcons.developer_board_24_regular), onChanged: (value) async { await ref.read(disableMemoryLimitProvider.notifier).update(!value); }, @@ -76,7 +72,7 @@ class AdvancedSettingTiles extends HookConsumerWidget { if (Platform.isIOS) ListTile( title: Text(t.settings.advanced.resetTunnel), - leading: const Icon(Icons.restart_alt), + leading: const Icon(FluentIcons.arrow_reset_24_regular), onTap: () async { await ref.read(resetTunnelProvider.notifier).run(); }, @@ -84,7 +80,7 @@ class AdvancedSettingTiles extends HookConsumerWidget { SwitchListTile( title: Text(t.settings.advanced.debugMode), value: debug, - secondary: const Icon(Icons.bug_report), + secondary: const Icon(FluentIcons.window_dev_tools_24_regular), onChanged: (value) async { if (value) { await showDialog( diff --git a/lib/features/settings/widgets/general_setting_tiles.dart b/lib/features/settings/widgets/general_setting_tiles.dart index 29fd1ab6..8cd2fd79 100644 --- a/lib/features/settings/widgets/general_setting_tiles.dart +++ b/lib/features/settings/widgets/general_setting_tiles.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; @@ -25,7 +26,7 @@ class GeneralSettingTiles extends HookConsumerWidget { ListTile( title: Text(t.settings.general.themeMode), subtitle: Text(themeMode.present(t)), - leading: const Icon(Icons.light_mode), + leading: const Icon(FluentIcons.weather_moon_20_regular), onTap: () async { final selectedThemeMode = await showDialog( context: context, @@ -56,7 +57,7 @@ class GeneralSettingTiles extends HookConsumerWidget { if (Platform.isAndroid) SwitchListTile( title: Text(t.settings.general.dynamicNotification), - secondary: const Icon(Icons.speed), + secondary: const Icon(FluentIcons.top_speed_24_regular), value: ref.watch(dynamicNotificationProvider), onChanged: (value) async { await ref diff --git a/lib/features/settings/widgets/platform_settings_tiles.dart b/lib/features/settings/widgets/platform_settings_tiles.dart index cdeb18e4..7dbbca36 100644 --- a/lib/features/settings/widgets/platform_settings_tiles.dart +++ b/lib/features/settings/widgets/platform_settings_tiles.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/features/settings/notifier/platform_settings_notifier.dart'; @@ -18,7 +19,7 @@ class PlatformSettingsTiles extends HookConsumerWidget { ListTile buildIgnoreTile(bool enabled) => ListTile( title: Text(t.settings.general.ignoreBatteryOptimizations), subtitle: Text(t.settings.general.ignoreBatteryOptimizationsMsg), - leading: const Icon(Icons.running_with_errors), + leading: const Icon(FluentIcons.battery_saver_24_regular), enabled: enabled, onTap: () async { await ref diff --git a/lib/features/stats/notifier/stats_notifier.dart b/lib/features/stats/notifier/stats_notifier.dart index d04adbf8..fcce03d3 100644 --- a/lib/features/stats/notifier/stats_notifier.dart +++ b/lib/features/stats/notifier/stats_notifier.dart @@ -2,6 +2,7 @@ import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/stats/data/stats_data_providers.dart'; import 'package:hiddify/features/stats/model/stats_entity.dart'; import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'stats_notifier.g.dart'; @@ -10,6 +11,7 @@ part 'stats_notifier.g.dart'; class StatsNotifier extends _$StatsNotifier with AppLogger { @override Stream build() async* { + ref.disposeDelay(const Duration(seconds: 10)); final serviceRunning = await ref.watch(serviceRunningProvider.future); if (serviceRunning) { yield* ref diff --git a/lib/features/stats/widget/side_bar_stats_overview.dart b/lib/features/stats/widget/side_bar_stats_overview.dart index a1b6ff6b..d0783fe4 100644 --- a/lib/features/stats/widget/side_bar_stats_overview.dart +++ b/lib/features/stats/widget/side_bar_stats_overview.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/proxy/active/active_proxy_sidebar_card.dart'; import 'package:hiddify/features/stats/model/stats_entity.dart'; import 'package:hiddify/features/stats/notifier/stats_notifier.dart'; import 'package:hiddify/utils/number_formatters.dart'; @@ -21,6 +22,8 @@ class SideBarStatsOverview extends HookConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ + const ActiveProxySideBarCard(), + const Gap(8), _StatCard( title: t.home.stats.traffic, firstStat: ( diff --git a/lib/features/system_tray/notifier/system_tray_notifier.dart b/lib/features/system_tray/notifier/system_tray_notifier.dart index 37a9cbdb..8ed0f646 100644 --- a/lib/features/system_tray/notifier/system_tray_notifier.dart +++ b/lib/features/system_tray/notifier/system_tray_notifier.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/features/config_option/model/config_option_entity.dart'; import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; diff --git a/lib/features/window/notifier/window_notifier.dart b/lib/features/window/notifier/window_notifier.dart index e7fbbda7..05996432 100644 --- a/lib/features/window/notifier/window_notifier.dart +++ b/lib/features/window/notifier/window_notifier.dart @@ -19,7 +19,7 @@ class WindowNotifier extends _$WindowNotifier with AppLogger { // if (Platform.isWindows) { // loggy.debug("ensuring single instance"); - // await WindowsSingleInstance.ensureSingleInstance([], "HiddifyNext"); + // await WindowsSingleInstance.ensureSingleInstance([], "Hiddify"); // } await windowManager.ensureInitialized(); diff --git a/lib/singbox/model/singbox_config_enum.dart b/lib/singbox/model/singbox_config_enum.dart index 30aaac78..10d616a3 100644 --- a/lib/singbox/model/singbox_config_enum.dart +++ b/lib/singbox/model/singbox_config_enum.dart @@ -1,24 +1,36 @@ +import 'dart:io'; + +import 'package:dart_mappable/dart_mappable.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/utils/platform_utils.dart'; -import 'package:json_annotation/json_annotation.dart'; -@JsonEnum(valueField: 'key') +part 'singbox_config_enum.mapper.dart'; + +@MappableEnum() enum ServiceMode { - proxy("proxy"), - systemProxy("system-proxy"), - tun("vpn"); + @MappableValue("proxy") + proxy, - const ServiceMode(this.key); + @MappableValue("system-proxy") + systemProxy, - final String key; + @MappableValue("vpn") + tun, + + @MappableValue("vpn-service") + tunService; static ServiceMode get defaultMode => PlatformUtils.isDesktop ? systemProxy : tun; + /// supported service mode based on platform, use this instead of [values] in UI static List get choices { - if (PlatformUtils.isDesktop) { + if (Platform.isWindows || Platform.isLinux) { return values; + } else if (Platform.isMacOS) { + return [proxy, systemProxy, tun]; } + // mobile return [proxy, tun]; } @@ -27,19 +39,24 @@ enum ServiceMode { systemProxy => t.settings.config.serviceModes.systemProxy, tun => "${t.settings.config.serviceModes.tun}${PlatformUtils.isDesktop ? " (${t.settings.experimental})" : ""}", + tunService => + "${t.settings.config.serviceModes.tunService}${PlatformUtils.isDesktop ? " (${t.settings.experimental})" : ""}", }; } -@JsonEnum(valueField: 'key') +@MappableEnum() enum IPv6Mode { - disable("ipv4_only"), - enable("prefer_ipv4"), - prefer("prefer_ipv6"), - only("ipv6_only"); + @MappableValue("ipv4_only") + disable, - const IPv6Mode(this.key); + @MappableValue("prefer_ipv4") + enable, - final String key; + @MappableValue("prefer_ipv6") + prefer, + + @MappableValue("ipv6_only") + only; String present(TranslationsEn t) => switch (this) { disable => t.settings.config.ipv6Modes.disable, @@ -49,12 +66,21 @@ enum IPv6Mode { }; } -@JsonEnum(valueField: 'key') +@MappableEnum() enum DomainStrategy { + @MappableValue("") auto(""), + + @MappableValue("prefer_ipv6") preferIpv6("prefer_ipv6"), + + @MappableValue("prefer_ipv4") preferIpv4("prefer_ipv4"), + + @MappableValue("ipv4_only") ipv4Only("ipv4_only"), + + @MappableValue("ipv6_only") ipv6Only("ipv6_only"); const DomainStrategy(this.key); @@ -67,18 +93,21 @@ enum DomainStrategy { }; } +@MappableEnum() enum TunImplementation { mixed, system, gVisor; } +@MappableEnum() enum MuxProtocol { h2mux, smux, yamux; } +@MappableEnum() enum WarpDetourMode { outbound, inbound; diff --git a/lib/singbox/model/singbox_config_option.dart b/lib/singbox/model/singbox_config_option.dart index 5cbf4614..511838fa 100644 --- a/lib/singbox/model/singbox_config_option.dart +++ b/lib/singbox/model/singbox_config_option.dart @@ -1,85 +1,127 @@ import 'dart:convert'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/model/range.dart'; +import 'package:dart_mappable/dart_mappable.dart'; +import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/features/log/model/log_level.dart'; import 'package:hiddify/singbox/model/singbox_config_enum.dart'; import 'package:hiddify/singbox/model/singbox_rule.dart'; -part 'singbox_config_option.freezed.dart'; -part 'singbox_config_option.g.dart'; +part 'singbox_config_option.mapper.dart'; -@freezed -class SingboxConfigOption with _$SingboxConfigOption { - const SingboxConfigOption._(); +@MappableClass( + caseStyle: CaseStyle.paramCase, + includeCustomMappers: [ + OptionalRangeJsonMapper(), + IntervalMapper(), + ], +) +class SingboxConfigOption with SingboxConfigOptionMappable { + const SingboxConfigOption({ + required this.executeConfigAsIs, + required this.logLevel, + required this.resolveDestination, + required this.ipv6Mode, + required this.remoteDnsAddress, + required this.remoteDnsDomainStrategy, + required this.directDnsAddress, + required this.directDnsDomainStrategy, + required this.mixedPort, + required this.localDnsPort, + required this.tunImplementation, + required this.mtu, + required this.strictRoute, + required this.connectionTestUrl, + required this.urlTestInterval, + required this.enableClashApi, + required this.clashApiPort, + required this.enableTun, + required this.enableTunService, + required this.setSystemProxy, + required this.bypassLan, + required this.allowConnectionFromLan, + required this.enableFakeDns, + required this.enableDnsRouting, + required this.independentDnsCache, + required this.enableTlsFragment, + required this.tlsFragmentSize, + required this.tlsFragmentSleep, + required this.enableTlsMixedSniCase, + required this.enableTlsPadding, + required this.tlsPaddingSize, + required this.enableMux, + required this.muxPadding, + required this.muxMaxStreams, + required this.muxProtocol, + required this.enableWarp, + required this.warpDetourMode, + required this.warpLicenseKey, + required this.warpCleanIp, + required this.warpPort, + required this.warpNoise, + required this.geoipPath, + required this.geositePath, + required this.rules, + }); - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory SingboxConfigOption({ - required bool executeConfigAsIs, - required LogLevel logLevel, - required bool resolveDestination, - required IPv6Mode ipv6Mode, - required String remoteDnsAddress, - required DomainStrategy remoteDnsDomainStrategy, - required String directDnsAddress, - required DomainStrategy directDnsDomainStrategy, - required int mixedPort, - required int localDnsPort, - required TunImplementation tunImplementation, - required int mtu, - required bool strictRoute, - required String connectionTestUrl, - @IntervalConverter() required Duration urlTestInterval, - required bool enableClashApi, - required int clashApiPort, - required bool enableTun, - required bool setSystemProxy, - required bool bypassLan, - required bool allowConnectionFromLan, - required bool enableFakeDns, - required bool enableDnsRouting, - required bool independentDnsCache, - required bool enableTlsFragment, - @RangeWithOptionalCeilJsonConverter() - required RangeWithOptionalCeil tlsFragmentSize, - @RangeWithOptionalCeilJsonConverter() - required RangeWithOptionalCeil tlsFragmentSleep, - required bool enableTlsMixedSniCase, - required bool enableTlsPadding, - @RangeWithOptionalCeilJsonConverter() - required RangeWithOptionalCeil tlsPaddingSize, - required bool enableMux, - required bool muxPadding, - required int muxMaxStreams, - required MuxProtocol muxProtocol, - required bool enableWarp, - required WarpDetourMode warpDetourMode, - required String warpLicenseKey, - required String warpCleanIp, - required int warpPort, - @RangeWithOptionalCeilJsonConverter() - required RangeWithOptionalCeil warpNoise, - required String geoipPath, - required String geositePath, - required List rules, - }) = _SingboxConfigOption; + final bool executeConfigAsIs; + final LogLevel logLevel; + final bool resolveDestination; + @MappableField(key: "ipv6-mode") + final IPv6Mode ipv6Mode; + final String remoteDnsAddress; + final DomainStrategy remoteDnsDomainStrategy; + final String directDnsAddress; + final DomainStrategy directDnsDomainStrategy; + final int mixedPort; + final int localDnsPort; + final TunImplementation tunImplementation; + final int mtu; + final bool strictRoute; + final String connectionTestUrl; + final Duration urlTestInterval; + final bool enableClashApi; + final int clashApiPort; + final bool enableTun; + final bool enableTunService; + final bool setSystemProxy; + final bool bypassLan; + final bool allowConnectionFromLan; + final bool enableFakeDns; + final bool enableDnsRouting; + final bool independentDnsCache; + final bool enableTlsFragment; + final OptionalRange tlsFragmentSize; + final OptionalRange tlsFragmentSleep; + final bool enableTlsMixedSniCase; + final bool enableTlsPadding; + final OptionalRange tlsPaddingSize; + final bool enableMux; + final bool muxPadding; + final int muxMaxStreams; + final MuxProtocol muxProtocol; + final bool enableWarp; + final WarpDetourMode warpDetourMode; + final String warpLicenseKey; + final String warpCleanIp; + final int warpPort; + final OptionalRange warpNoise; + final String geoipPath; + final String geositePath; + final List rules; String format() { const encoder = JsonEncoder.withIndent(' '); - return encoder.convert(toJson()); + return encoder.convert(toMap()); } - - factory SingboxConfigOption.fromJson(Map json) => - _$SingboxConfigOptionFromJson(json); } -class IntervalConverter implements JsonConverter { - const IntervalConverter(); +class IntervalMapper extends SimpleMapper { + const IntervalMapper(); @override - Duration fromJson(String json) => - Duration(minutes: int.parse(json.replaceAll("m", ""))); + Duration decode(dynamic value) => + Duration(minutes: int.parse((value as String).replaceAll("m", ""))); @override - String toJson(Duration object) => "${object.inMinutes}m"; + String encode(Duration self) => "${self.inMinutes}m"; } diff --git a/lib/singbox/model/singbox_rule.dart b/lib/singbox/model/singbox_rule.dart index b927ffee..d93b2562 100644 --- a/lib/singbox/model/singbox_rule.dart +++ b/lib/singbox/model/singbox_rule.dart @@ -1,35 +1,37 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dart_mappable/dart_mappable.dart'; -part 'singbox_rule.freezed.dart'; -part 'singbox_rule.g.dart'; +part 'singbox_rule.mapper.dart'; -@freezed -class SingboxRule with _$SingboxRule { - const SingboxRule._(); +@MappableClass() +class SingboxRule with SingboxRuleMappable { + const SingboxRule({ + this.domains, + this.ip, + this.port, + this.protocol, + this.network = RuleNetwork.tcpAndUdp, + this.outbound = RuleOutbound.proxy, + }); - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory SingboxRule({ - String? domains, - String? ip, - String? port, - String? protocol, - @Default(RuleNetwork.tcpAndUdp) RuleNetwork network, - @Default(RuleOutbound.proxy) RuleOutbound outbound, - }) = _SingboxRule; - - factory SingboxRule.fromJson(Map json) => - _$SingboxRuleFromJson(json); + final String? domains; + final String? ip; + final String? port; + final String? protocol; + final RuleNetwork network; + final RuleOutbound outbound; } +@MappableEnum() enum RuleOutbound { proxy, bypass, block } -@JsonEnum(valueField: 'key') +@MappableEnum() enum RuleNetwork { - tcpAndUdp(""), - tcp("tcp"), - udp("udp"); + @MappableValue("") + tcpAndUdp, - const RuleNetwork(this.key); + @MappableValue("tcp") + tcp, - final String? key; + @MappableValue("udp") + udp; } diff --git a/lib/singbox/service/ffi_singbox_service.dart b/lib/singbox/service/ffi_singbox_service.dart index a026f2d4..ee60a00c 100644 --- a/lib/singbox/service/ffi_singbox_service.dart +++ b/lib/singbox/service/ffi_singbox_service.dart @@ -121,7 +121,7 @@ class FFISingboxService with InfraLogger implements SingboxService { return TaskEither( () => CombineWorker().execute( () { - final json = jsonEncode(options.toJson()); + final json = options.toJson(); final err = _box .changeConfigOptions(json.toNativeUtf8().cast()) .cast() @@ -237,7 +237,7 @@ class FFISingboxService with InfraLogger implements SingboxService { @override Stream watchStats() { if (_serviceStatsStream != null) return _serviceStatsStream!; - final receiver = ReceivePort('service stats receiver'); + final receiver = ReceivePort('stats'); final statusStream = receiver.asBroadcastStream( onCancel: (_) { _logger.debug("stopping stats command client"); @@ -277,47 +277,101 @@ class FFISingboxService with InfraLogger implements SingboxService { } @override - Stream> watchOutbounds() { + Stream> watchGroups() { + final logger = newLoggy("watchGroups"); if (_outboundsStream != null) return _outboundsStream!; - final receiver = ReceivePort('outbounds receiver'); + final receiver = ReceivePort('groups'); final outboundsStream = receiver.asBroadcastStream( onCancel: (_) { - _logger.debug("stopping group command client"); + logger.debug("stopping"); + receiver.close(); + _outboundsStream = null; final err = _box.stopCommandClient(4).cast().toDartString(); if (err.isNotEmpty) { _logger.error("error stopping group client"); } - receiver.close(); - _outboundsStream = null; }, ).map( (event) { if (event case String _) { if (event.startsWith('error:')) { - loggy.error("[group client] error received: $event"); + logger.error("error received: $event"); throw event.replaceFirst('error:', ""); } + return (jsonDecode(event) as List).map((e) { return SingboxOutboundGroup.fromJson(e as Map); }).toList(); } - loggy.error("[group client] unexpected type, msg: $event"); + logger.error("unexpected type, msg: $event"); throw "invalid type"; }, ); - final err = _box - .startCommandClient(4, receiver.sendPort.nativePort) - .cast() - .toDartString(); - if (err.isNotEmpty) { - loggy.error("error starting group command: $err"); - throw err; + try { + final err = _box + .startCommandClient(4, receiver.sendPort.nativePort) + .cast() + .toDartString(); + if (err.isNotEmpty) { + logger.error("error starting group command: $err"); + throw err; + } + } catch (e) { + receiver.close(); + rethrow; } return _outboundsStream = outboundsStream; } + @override + Stream> watchActiveGroups() { + final logger = newLoggy("[ActiveGroupsClient]"); + final receiver = ReceivePort('active groups'); + final outboundsStream = receiver.asBroadcastStream( + onCancel: (_) { + logger.debug("stopping"); + receiver.close(); + final err = _box.stopCommandClient(12).cast().toDartString(); + if (err.isNotEmpty) { + logger.error("failed stopping: $err"); + } + }, + ).map( + (event) { + if (event case String _) { + if (event.startsWith('error:')) { + logger.error(event); + throw event.replaceFirst('error:', ""); + } + + return (jsonDecode(event) as List).map((e) { + return SingboxOutboundGroup.fromJson(e as Map); + }).toList(); + } + logger.error("unexpected type, msg: $event"); + throw "invalid type"; + }, + ); + + try { + final err = _box + .startCommandClient(12, receiver.sendPort.nativePort) + .cast() + .toDartString(); + if (err.isNotEmpty) { + logger.error("error starting: $err"); + throw err; + } + } catch (e) { + receiver.close(); + rethrow; + } + + return outboundsStream; + } + @override TaskEither selectOutbound(String groupTag, String outboundTag) { return TaskEither( diff --git a/lib/singbox/service/platform_singbox_service.dart b/lib/singbox/service/platform_singbox_service.dart index 12cc5f70..bdf96804 100644 --- a/lib/singbox/service/platform_singbox_service.dart +++ b/lib/singbox/service/platform_singbox_service.dart @@ -13,12 +13,19 @@ import 'package:hiddify/utils/custom_loggers.dart'; import 'package:rxdart/rxdart.dart'; class PlatformSingboxService with InfraLogger implements SingboxService { - late final _methodChannel = const MethodChannel("com.hiddify.app/method"); - late final _statusChannel = - const EventChannel("com.hiddify.app/service.status", JSONMethodCodec()); - late final _alertsChannel = - const EventChannel("com.hiddify.app/service.alerts", JSONMethodCodec()); - late final _logsChannel = const EventChannel("com.hiddify.app/service.logs"); + static const channelPrefix = "com.hiddify.app"; + + static const methodChannel = MethodChannel("$channelPrefix/method"); + static const statusChannel = + EventChannel("$channelPrefix/service.status", JSONMethodCodec()); + static const alertsChannel = + EventChannel("$channelPrefix/service.alerts", JSONMethodCodec()); + static const statsChannel = + EventChannel("$channelPrefix/stats", JSONMethodCodec()); + static const groupsChannel = EventChannel("$channelPrefix/groups"); + static const activeGroupsChannel = + EventChannel("$channelPrefix/active-groups"); + static const logsChannel = EventChannel("$channelPrefix/service.logs"); late final ValueStream _status; @@ -26,26 +33,23 @@ class PlatformSingboxService with InfraLogger implements SingboxService { Future init() async { loggy.debug("initializing"); final status = - _statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); + statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); final alerts = - _alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); + alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); _status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); await _status.first; } @override - TaskEither setup( - Directories directories, - bool debug, - ) { + TaskEither setup(Directories directories, bool debug) { return TaskEither( () async { if (!Platform.isIOS) { return right(unit); } - await _methodChannel.invokeMethod("setup"); + await methodChannel.invokeMethod("setup"); return right(unit); }, ); @@ -59,7 +63,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService { ) { return TaskEither( () async { - final message = await _methodChannel.invokeMethod( + final message = await methodChannel.invokeMethod( "parse_config", {"path": path, "tempPath": tempPath, "debug": debug}, ); @@ -73,9 +77,10 @@ class PlatformSingboxService with InfraLogger implements SingboxService { TaskEither changeOptions(SingboxConfigOption options) { return TaskEither( () async { - await _methodChannel.invokeMethod( + loggy.debug("changing options"); + await methodChannel.invokeMethod( "change_config_options", - jsonEncode(options.toJson()), + options.toJson(), ); return right(unit); }, @@ -83,12 +88,11 @@ class PlatformSingboxService with InfraLogger implements SingboxService { } @override - TaskEither generateFullConfigByPath( - String path, - ) { + TaskEither generateFullConfigByPath(String path) { return TaskEither( () async { - final configJson = await _methodChannel.invokeMethod( + loggy.debug("generating full config by path"); + final configJson = await methodChannel.invokeMethod( "generate_config", {"path": path}, ); @@ -109,7 +113,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService { return TaskEither( () async { loggy.debug("starting"); - await _methodChannel.invokeMethod( + await methodChannel.invokeMethod( "start", {"path": path, "name": name}, ); @@ -123,7 +127,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService { return TaskEither( () async { loggy.debug("stopping"); - await _methodChannel.invokeMethod("stop"); + await methodChannel.invokeMethod("stop"); return right(unit); }, ); @@ -138,7 +142,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService { return TaskEither( () async { loggy.debug("restarting"); - await _methodChannel.invokeMethod( + await methodChannel.invokeMethod( "restart", {"path": path, "name": name}, ); @@ -159,17 +163,16 @@ class PlatformSingboxService with InfraLogger implements SingboxService { } loggy.debug("resetting tunnel"); - await _methodChannel.invokeMethod("reset"); + await methodChannel.invokeMethod("reset"); return right(unit); }, ); } @override - Stream> watchOutbounds() { - const channel = EventChannel("com.hiddify.app/groups"); - loggy.debug("watching outbounds"); - return channel.receiveBroadcastStream().map( + Stream> watchGroups() { + loggy.debug("watching groups"); + return groupsChannel.receiveBroadcastStream().map( (event) { if (event case String _) { return (jsonDecode(event) as List).map((e) { @@ -182,14 +185,29 @@ class PlatformSingboxService with InfraLogger implements SingboxService { ); } + @override + Stream> watchActiveGroups() { + loggy.debug("watching active groups"); + return activeGroupsChannel.receiveBroadcastStream().map( + (event) { + if (event case String _) { + return (jsonDecode(event) as List).map((e) { + return SingboxOutboundGroup.fromJson(e as Map); + }).toList(); + } + loggy.error("[active group client] unexpected type, msg: $event"); + throw "invalid type"; + }, + ); + } + @override Stream watchStatus() => _status; @override Stream watchStats() { - const channel = EventChannel("com.hiddify.app/stats", JSONMethodCodec()); loggy.debug("watching stats"); - return channel.receiveBroadcastStream().map( + return statsChannel.receiveBroadcastStream().map( (event) { if (event case Map _) { return SingboxStats.fromJson(event); @@ -207,7 +225,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService { return TaskEither( () async { loggy.debug("selecting outbound"); - await _methodChannel.invokeMethod( + await methodChannel.invokeMethod( "select_outbound", {"groupTag": groupTag, "outboundTag": outboundTag}, ); @@ -220,7 +238,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService { TaskEither urlTest(String groupTag) { return TaskEither( () async { - await _methodChannel.invokeMethod( + await methodChannel.invokeMethod( "url_test", {"groupTag": groupTag}, ); @@ -231,7 +249,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService { @override Stream> watchLogs(String path) async* { - yield* _logsChannel + yield* logsChannel .receiveBroadcastStream() .map((event) => (event as List).map((e) => e as String).toList()); } @@ -240,7 +258,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService { TaskEither clearLogs() { return TaskEither( () async { - await _methodChannel.invokeMethod("clear_logs"); + await methodChannel.invokeMethod("clear_logs"); return right(unit); }, ); diff --git a/lib/singbox/service/singbox_service.dart b/lib/singbox/service/singbox_service.dart index 7535a50e..895b74f7 100644 --- a/lib/singbox/service/singbox_service.dart +++ b/lib/singbox/service/singbox_service.dart @@ -21,11 +21,17 @@ abstract interface class SingboxService { Future init(); + /// setup directories and other initial platform services TaskEither setup( Directories directories, bool debug, ); + /// validates config by path and save it + /// + /// [path] is used to save validated config + /// [tempPath] includes base config, possibly invalid + /// [debug] indicates if debug mode (avoid in prod) TaskEither validateConfigByPath( String path, String tempPath, @@ -34,10 +40,17 @@ abstract interface class SingboxService { TaskEither changeOptions(SingboxConfigOption options); - TaskEither generateFullConfigByPath( - String path, - ); + /// generates full sing-box configuration + /// + /// [path] is the path to the base config file + /// returns full patched json config file as string + TaskEither generateFullConfigByPath(String path); + /// start sing-box service + /// + /// [path] is the path to the base config file (to be patched by previously set [SingboxConfigOption]) + /// [name] is the name of the active profile (not unique, used for presentation in platform specific ui) + /// [disableMemoryLimit] is used to disable service memory limit (mostly used in mobile platforms i.e. iOS) TaskEither start( String path, String name, @@ -46,6 +59,7 @@ abstract interface class SingboxService { TaskEither stop(); + /// similar to [start], but uses platform dependent behavior to restart the service TaskEither restart( String path, String name, @@ -54,14 +68,18 @@ abstract interface class SingboxService { TaskEither resetTunnel(); - Stream> watchOutbounds(); + Stream> watchGroups(); + + Stream> watchActiveGroups(); TaskEither selectOutbound(String groupTag, String outboundTag); TaskEither urlTest(String groupTag); + /// watch status of sing-box service (started, starting, etc.) Stream watchStatus(); + /// watch stats of sing-box service (uplink, downlink, etc.) Stream watchStats(); Stream> watchLogs(String path); diff --git a/lib/utils/alerts.dart b/lib/utils/alerts.dart index 71cc073f..9c47a844 100644 --- a/lib/utils/alerts.dart +++ b/lib/utils/alerts.dart @@ -1,3 +1,4 @@ +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:toastification/toastification.dart'; @@ -73,13 +74,13 @@ class CustomToast extends StatelessWidget { this.message, { this.duration = const Duration(seconds: 5), }) : type = AlertType.error, - icon = Icons.error; + icon = FluentIcons.error_circle_24_regular; const CustomToast.success( this.message, { this.duration = const Duration(seconds: 3), }) : type = AlertType.success, - icon = Icons.check; + icon = FluentIcons.checkmark_24_regular; final String message; final AlertType type; diff --git a/lib/utils/link_parsers.dart b/lib/utils/link_parsers.dart index 23dee9c1..74b1463f 100644 --- a/lib/utils/link_parsers.dart +++ b/lib/utils/link_parsers.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:dartx/dartx.dart'; -import 'package:fpdart/fpdart.dart'; import 'package:hiddify/features/profile/data/profile_parser.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart'; import 'package:hiddify/singbox/model/singbox_proxy_type.dart'; @@ -71,7 +70,7 @@ abstract class LinkParser { if (subinfo.name.isNotNullOrEmpty && subinfo.name != "Remote Profile") { name = subinfo.name; } - + return (content: normalContent, name: name ?? ProxyType.unknown.label); } diff --git a/lib/utils/placeholders.dart b/lib/utils/placeholders.dart index f56183b6..b3b59981 100644 --- a/lib/utils/placeholders.dart +++ b/lib/utils/placeholders.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:hooks_riverpod/hooks_riverpod.dart'; @@ -39,7 +40,7 @@ class SliverErrorBodyPlaceholder extends HookConsumerWidget { const SliverErrorBodyPlaceholder( this.msg, { super.key, - this.icon = Icons.error, + this.icon = FluentIcons.error_circle_24_regular, }); final String msg; diff --git a/libcore b/libcore index ad764a86..a006c94c 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit ad764a86b11bd3aca37e0de2d22e21659f6d61f0 +Subproject commit a006c94cdf5c6db5a569303357818143671c229c diff --git a/linux/.stignore b/linux/.stignore new file mode 100644 index 00000000..7d115384 --- /dev/null +++ b/linux/.stignore @@ -0,0 +1 @@ +/flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index ba38a4ab..8abb9b5b 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -108,6 +108,16 @@ install(CODE " set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") +install(FILES "../libcore/bin/libcore.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +install( + FILES "../libcore/bin/HiddifyService" + DESTINATION "${CMAKE_INSTALL_PREFIX}" + PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ + COMPONENT Runtime +) + install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) @@ -117,14 +127,7 @@ install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR} install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) -install(FILES "../libcore/bin/libcore.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -install( - FILES "../libcore/bin/HiddifyService" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime -) foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) install(FILES "${bundled_library}" diff --git a/linux/my_application.cc b/linux/my_application.cc index a4d15de0..65326859 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "Hiddify Next"); + gtk_header_bar_set_title(header_bar, "Hiddify"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "Hiddify Next"); + gtk_window_set_title(window, "Hiddify"); } gtk_window_set_default_size(window, 1280, 720); diff --git a/linux/packaging/appimage/AppRun b/linux/packaging/appimage/AppRun new file mode 100644 index 00000000..51d29413 --- /dev/null +++ b/linux/packaging/appimage/AppRun @@ -0,0 +1,9 @@ +#!/bin/bash + +cd "\$(dirname "\$0")" +export LD_LIBRARY_PATH=usr/lib +if [ "$1" == "HiddifyService" ];then + exec ./$@ +else + exec ./$appName $@ +fi diff --git a/linux/packaging/appimage/make_config.yaml b/linux/packaging/appimage/make_config.yaml index 73a36cac..949a5230 100644 --- a/linux/packaging/appimage/make_config.yaml +++ b/linux/packaging/appimage/make_config.yaml @@ -1,4 +1,4 @@ -display_name: HiddifyNext +display_name: Hiddify icon: ./assets/images/source/ic_launcher_border.png @@ -29,6 +29,8 @@ categories: startup_notify: true +app_run_file: AppRun + # You can specify the shared libraries that you want to bundle with your app # # flutter_distributor automatically detects the shared libraries that your app diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml index f20201df..a7f6a47a 100644 --- a/linux/packaging/deb/make_config.yaml +++ b/linux/packaging/deb/make_config.yaml @@ -1,5 +1,5 @@ -display_name: HiddifyNext -package_name: hiddify-next +display_name: Hiddify +package_name: hiddify maintainer: name: hiddify email: linux@hiddify.com @@ -11,11 +11,10 @@ essential: false icon: ./assets/images/source/ic_launcher_border.png postinstall_scripts: - - echo "Installed Hiddify Next" + - echo "Installed Hiddify" postuninstall_scripts: - echo "Surprised Why?" - keywords: - Hiddify - Proxy @@ -26,10 +25,9 @@ keywords: - Psiphon - OpenVPN - generic_name: Hiddify categories: - Network -startup_notify: true \ No newline at end of file +startup_notify: true diff --git a/linux/packaging/rpm/make_config.yaml b/linux/packaging/rpm/make_config.yaml index 1e474649..376386bc 100644 --- a/linux/packaging/rpm/make_config.yaml +++ b/linux/packaging/rpm/make_config.yaml @@ -1,8 +1,7 @@ -display_name: HiddifyNext +display_name: Hiddify url: https://github.com/hiddify/hiddify-next/ license: Other - packager: hiddify packagerEmail: linux@hiddify.com @@ -22,9 +21,8 @@ keywords: - Psiphon - OpenVPN - generic_name: Hiddify group: Applications/Internet -startup_notify: true \ No newline at end of file +startup_notify: true diff --git a/macos/.stignore b/macos/.stignore new file mode 100644 index 00000000..bf9897d0 --- /dev/null +++ b/macos/.stignore @@ -0,0 +1,5 @@ +**/Flutter/ephemeral/ +**/Pods/ + +**/dgph +**/xcuserdata/ diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 853a59de..b5bef7a0 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -71,7 +71,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* Hiddify Next.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Hiddify Next.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* Hiddify.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Hiddify.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -161,7 +161,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* Hiddify Next.app */, + 33CC10ED2044A3C60003C045 /* Hiddify.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -252,7 +252,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* Hiddify Next.app */; + productReference = 33CC10ED2044A3C60003C045 /* Hiddify.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -574,7 +574,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Hiddify Next"; + INFOPLIST_KEY_CFBundleDisplayName = "Hiddify"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -702,7 +702,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Hiddify Next"; + INFOPLIST_KEY_CFBundleDisplayName = "Hiddify"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -724,7 +724,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Hiddify Next"; + INFOPLIST_KEY_CFBundleDisplayName = "Hiddify"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 4a5f8690..b322df55 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -65,7 +65,7 @@ @@ -82,7 +82,7 @@ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png new file mode 100644 index 00000000..21c1c5ad Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-128.png new file mode 100644 index 00000000..40c6cb16 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-128@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-128@2x.png new file mode 100644 index 00000000..c8439b24 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-128@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-16.png new file mode 100644 index 00000000..d7e18e85 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-16@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-16@2x.png new file mode 100644 index 00000000..cda1f470 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-16@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-256.png new file mode 100644 index 00000000..c8439b24 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-256@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-256@2x.png new file mode 100644 index 00000000..d79199e9 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-256@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-32.png new file mode 100644 index 00000000..cda1f470 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-32@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-32@2x.png new file mode 100644 index 00000000..620bdaa0 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-32@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-512.png new file mode 100644 index 00000000..d79199e9 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-512@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-512@2x.png new file mode 100644 index 00000000..453bdad7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app-icon-512@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 2d85b159..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 5e2f25c7..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index d8ebe189..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index 631816ad..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 7aabdcd6..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 20826d4f..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 12a8c462..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 92a1cc71..7860ed54 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = Hiddify Next +PRODUCT_NAME = Hiddify // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = app.hiddify.com diff --git a/macos/packaging/dmg/make_config.yaml b/macos/packaging/dmg/make_config.yaml index 09fa419b..4c545c51 100644 --- a/macos/packaging/dmg/make_config.yaml +++ b/macos/packaging/dmg/make_config.yaml @@ -7,4 +7,4 @@ contents: - x: 192 y: 344 type: file - path: Hiddify Next.app + path: Hiddify.app diff --git a/macos/packaging/pkg/make_config.yaml b/macos/packaging/pkg/make_config.yaml new file mode 100644 index 00000000..0c2b8eaa --- /dev/null +++ b/macos/packaging/pkg/make_config.yaml @@ -0,0 +1,2 @@ +install-path: /Applications +#sign-identity: diff --git a/pubspec.lock b/pubspec.lock index 36ca7197..0b5b4908 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0" + circle_flags: + dependency: "direct main" + description: + name: circle_flags + sha256: "028d5ca1bb12b9a4b29dc1a25f32a428ffd69e91343274375d9e574855af2daa" + url: "https://pub.dev" + source: hosted + version: "4.0.2" cli_util: dependency: transitive description: @@ -289,6 +297,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.8" + dart_mappable: + dependency: "direct main" + description: + name: dart_mappable + sha256: "7b6d38ae95f1ae8ffa65df9a5464f14b56c2de94699a035202ca4cd3a0ba249e" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + dart_mappable_builder: + dependency: "direct dev" + description: + name: dart_mappable_builder + sha256: "98c058f7e80a98ea42d357d888ed1648d96bedac8b16872b58fc7024faefcdfe" + url: "https://pub.dev" + source: hosted + version: "4.2.0" dart_style: dependency: transitive description: @@ -433,6 +457,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + fluentui_system_icons: + dependency: "direct main" + description: + name: fluentui_system_icons + sha256: "1c02e6a4898dfc45e470ddcd62fb9c8fe59a7f8bb380e7f3edcb0d127c23bfd3" + url: "https://pub.dev" + source: hosted + version: "1.1.225" flutter: dependency: "direct main" description: flutter @@ -1538,6 +1570,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + type_plus: + dependency: transitive + description: + name: type_plus + sha256: "2e33cfac2e129297d5874567bdf7587502ec359881e9318551e014d91b02f84a" + url: "https://pub.dev" + source: hosted + version: "2.1.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 500a0f4e..c4e80b5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hiddify description: Cross Platform Multi Protocol Proxy Frontend. publish_to: "none" -version: 0.15.4+1504 +version: 0.15.11+1511 environment: sdk: ">=3.2.0 <4.0.0" @@ -71,6 +71,9 @@ dependencies: dio_smart_retry: ^6.0.0 cupertino_http: ^1.3.0 wolt_modal_sheet: ^0.4.0 + dart_mappable: ^4.2.0 + fluentui_system_icons: ^1.1.225 + circle_flags: ^4.0.2 dev_dependencies: flutter_test: @@ -86,6 +89,7 @@ dev_dependencies: flutter_gen_runner: ^5.4.0 go_router_builder: ^2.4.1 dependency_validator: ^3.2.3 + dart_mappable_builder: ^4.2.0 flutter: uses-material-design: true diff --git a/scripts/package_windows.ps1 b/scripts/package_windows.ps1 index fec75749..f4e67308 100644 --- a/scripts/package_windows.ps1 +++ b/scripts/package_windows.ps1 @@ -2,10 +2,13 @@ New-Item -ItemType Directory -Force -Name "dist\tmp" New-Item -ItemType Directory -Force -Name "out" # windows setup -Get-ChildItem -Recurse -File -Path "dist" -Filter "*windows-setup.exe" | Copy-Item -Destination "dist\tmp\hiddify-next-setup.exe" -ErrorAction SilentlyContinue -Compress-Archive -Force -Path "dist\tmp\hiddify-next-setup.exe",".github\help\mac-windows\*.url" -DestinationPath "out\hiddify-windows-x64-setup.zip" +# Get-ChildItem -Recurse -File -Path "dist" -Filter "*windows-setup.exe" | Copy-Item -Destination "dist\tmp\hiddify-next-setup.exe" -ErrorAction SilentlyContinue +# Compress-Archive -Force -Path "dist\tmp\hiddify-next-setup.exe",".github\help\mac-windows\*.url" -DestinationPath "out\hiddify-windows-x64-setup.zip" +Get-ChildItem -Recurse -File -Path "dist" -Filter "*windows-setup.exe" | Copy-Item -Destination "out\Hiddify-Windows-Setup-x64.exe" -ErrorAction SilentlyContinue + # windows portable xcopy "build\windows\x64\runner\Release" "dist\tmp\hiddify-next" /E/H/C/I/Y xcopy ".github\help\mac-windows\*.url" "dist\tmp\hiddify-next" /E/H/C/I/Y -Compress-Archive -Force -Path "dist\tmp\hiddify-next" -DestinationPath "out\hiddify-windows-x64-portable.zip" -ErrorAction SilentlyContinue \ No newline at end of file +Compress-Archive -Force -Path "dist\tmp\hiddify-next" -DestinationPath "out\Hiddify-Windows-Portable-x64.zip" -ErrorAction SilentlyContinue + diff --git a/test.configs/warp b/test.configs/warp new file mode 100644 index 00000000..9be0ea6a --- /dev/null +++ b/test.configs/warp @@ -0,0 +1,16 @@ +//profile-title: base64:8J+UpSBXQVJQIPCflKU= +//profile-update-interval: 1 +//subscription-userinfo: upload=0; download=0; total=10737418240000000; expire=2546249531 +//support-url: https://t.me/hiddify +//profile-web-page-url: https://hiddify.com + +warp://auto?ifp=10-20&ifps=40-100&ifpd=10-20#Warp_10-20_40-100_10-20 +warp://auto?ifp=10-20&ifps=40-100&ifpd=30-50#Warp_10-20_40-100_30-50 +warp://auto?ifp=10-20&ifps=40-100&ifpd=300-500#Warp_10-20_40-100_300-500 + + +warp://auto?ifp=5-10&ifps=40-100&ifpd=10-20#Warp_5-10_40-100_10-20 +warp://auto?ifp=5-10&ifps=40-100&ifpd=30-50#Warp_5-10_40-100_30-50 +warp://auto?ifp=5-10&ifps=40-100&ifpd=300-500#Warp_5-10_40-100_300-500 + +warp://auto#WarpInWarp&&detour=warp://auto?ifp=10-20&ifps=40-100&ifpd=10-200#Warp_10-20_40-100_10-200 \ No newline at end of file diff --git a/test/core/utils/ip_utils_test.dart b/test/core/utils/ip_utils_test.dart new file mode 100644 index 00000000..d0211d5f --- /dev/null +++ b/test/core/utils/ip_utils_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hiddify/core/utils/ip_utils.dart'; + +void main() { + group( + "obscureIp", + () { + test( + "Should pass given valid IPV4", + () { + const ipv4 = "1.1.1.1"; + final obscured = obscureIp(ipv4); + expect(obscured, "1.*.*.1"); + }, + ); + + test( + "Should pass given valid full IPV6", + () { + const ipv6 = "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210"; + final obscured = obscureIp(ipv6); + expect(obscured, "FEDC:****:****:****:****:****:****:****"); + }, + ); + + test( + "Should pass given valid IPV6", + () { + const ipv6 = "::1"; + final obscured = obscureIp(ipv6); + expect(obscured, "::*"); + }, + ); + }, + ); +} diff --git a/windows/.stignore b/windows/.stignore new file mode 100644 index 00000000..8c17aca5 --- /dev/null +++ b/windows/.stignore @@ -0,0 +1,9 @@ +/flutter/ephemeral/ + +*.suo +*.user +*.userosscache +*.sln.docstates + +/x64/ +/x86/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 7bca9452..f2e97534 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -4,7 +4,7 @@ project(hiddify LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "HiddifyNext") +set(BINARY_NAME "Hiddify") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. @@ -83,12 +83,11 @@ install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" # install(FILES "../libcore/bin/libcore.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" # COMPONENT Runtime) -set(HIDDIFY_NEXT_LIB "../libcore/bin/libcore.dll") -install(FILES "${HIDDIFY_NEXT_LIB}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + +install(FILES "../libcore/bin/libcore.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime RENAME libcore.dll) -set(HIDDIFY_NEXT_LIB "../libcore/bin/HiddifyService.exe") -install(FILES "${HIDDIFY_NEXT_LIB}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" +install(FILES "../libcore/bin/HiddifyService.exe" DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime RENAME HiddifyService.exe) diff --git a/windows/packaging/exe/make_config.yaml b/windows/packaging/exe/make_config.yaml index 78f61e98..7f20b076 100644 --- a/windows/packaging/exe/make_config.yaml +++ b/windows/packaging/exe/make_config.yaml @@ -1,17 +1,16 @@ app_id: 6L903538-42B1-4596-G479-BJ779F21A65D publisher: Hiddify publisher_url: https://github.com/hiddify/hiddify-next -display_name: Hiddify Next -executable_name: HiddifyNext.exe -output_base_file_name: HiddifyNext.exe +display_name: Hiddify +executable_name: Hiddify.exe +output_base_file_name: Hiddify.exe create_desktop_icon: true install_dir_name: "{autopf64}\\hiddify" setup_icon_file: ..\..\windows\runner\resources\app_icon.ico locales: - en - fa - - zh - ru - pt - tr -#script_template: inno_setup.sas \ No newline at end of file +#script_template: inno_setup.sas diff --git a/windows/packaging/msix/make_config.yaml b/windows/packaging/msix/make_config.yaml index 05d75687..e24f7556 100644 --- a/windows/packaging/msix/make_config.yaml +++ b/windows/packaging/msix/make_config.yaml @@ -1,6 +1,6 @@ -display_name: Hiddify Next +display_name: Hiddify publisher_display_name: Hiddify identity_name: app.hiddify.com msix_version: 1.0.0.0 -logo_path: windows\runner\resources\app_icon.ico -capabilities: internetClient, privateNetworkClientServer \ No newline at end of file +logo_path: windows\runner\resources\app_icon.ico +capabilities: internetClient, privateNetworkClientServer diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 511b6b8f..30ae4ef8 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -94,7 +94,7 @@ BEGIN VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "hiddify" "\0" VALUE "LegalCopyright", "Copyright (C) 2023 Hiddify.com. All rights reserved." "\0" - VALUE "OriginalFilename", "HiddifyNext.exe" "\0" + VALUE "OriginalFilename", "Hiddify.exe" "\0" VALUE "ProductName", "hiddify" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 9681dfea..63ed8bfe 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -10,14 +10,14 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L"HiddifyMutex"); - HWND handle = FindWindowA(NULL, "Hiddify Next"); + HWND handle = FindWindowA(NULL, "Hiddify"); if (GetLastError() == ERROR_ALREADY_EXISTS) { flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); - if (window.SendAppLinkToInstance(L"Hiddify Next")) { + if (window.SendAppLinkToInstance(L"Hiddify")) { return false; } @@ -47,7 +47,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"Hiddify Next", origin, size)) { + if (!window.Create(L"Hiddify", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index 99af9fd1..b9f7cdaf 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ