├── .github ├── scripts │ ├── check_frp.sh │ └── lzy_web.py └── workflows │ ├── build.yaml │ ├── release.yaml │ └── sync_frp.yaml ├── .gitignore ├── .idea ├── .gitignore ├── assetWizardSettings.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── CHANGELOG.md ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro ├── schemas │ └── com.github.jing332.frpandroid.data.AppDatabase │ │ ├── 1.json │ │ └── 2.json └── src │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── defaultData │ │ ├── LICENSE │ │ ├── frpc.ini │ │ ├── frpc_full.ini │ │ ├── frps.ini │ │ └── frps_full.ini │ └── docs │ │ └── zh │ │ ├── client-configures.md │ │ ├── proxy.md │ │ ├── server-configures.md │ │ └── visitor.md │ ├── ic_app_launcher-playstore.png │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── github │ │ └── jing332 │ │ └── frpandroid │ │ ├── App.kt │ │ ├── config │ │ ├── AppConfig.kt │ │ └── ServerConfig.kt │ │ ├── constant │ │ ├── AppConst.kt │ │ ├── FrpType.kt │ │ └── LogLevel.kt │ │ ├── data │ │ ├── AppDatabase.kt │ │ ├── dao │ │ │ └── FrpLogDao.kt │ │ └── entities │ │ │ └── FrpLog.kt │ │ ├── model │ │ ├── AppUpdateChecker.kt │ │ ├── Github.kt │ │ ├── ShortCuts.kt │ │ ├── UpdateResult.kt │ │ └── frp │ │ │ ├── DocumentTableManager.kt │ │ │ ├── Frp.kt │ │ │ ├── Frpc.kt │ │ │ ├── Frps.kt │ │ │ ├── LogUtils.kt │ │ │ └── config │ │ │ ├── IniConfigManager.kt │ │ │ └── IniConfigParser.kt │ │ ├── service │ │ ├── FrpNotification.kt │ │ ├── FrpService.kt │ │ ├── FrpServiceManager.kt │ │ ├── FrpcService.kt │ │ └── FrpsService.kt │ │ ├── ui │ │ ├── AboutDialog.kt │ │ ├── AppUpdateDialog.kt │ │ ├── BaseComposeActivity.kt │ │ ├── LibrariesActivity.kt │ │ ├── MainActivity.kt │ │ ├── MainViewModel.kt │ │ ├── MyTools.kt │ │ ├── SwitchFrpActivity.kt │ │ ├── nav │ │ │ ├── BasicFrpScreen.kt │ │ │ ├── BottomNavBar.kt │ │ │ ├── BottomNavRoute.kt │ │ │ ├── FrpTopAppBar.kt │ │ │ ├── NavigationGraph.kt │ │ │ ├── frpc │ │ │ │ ├── ConfigEditDialog.kt │ │ │ │ ├── ConfigListScreen.kt │ │ │ │ ├── ConfigListViewModel.kt │ │ │ │ ├── ConfigScreen.kt │ │ │ │ ├── ConfigSelectionDialog.kt │ │ │ │ ├── ConfigViewModel.kt │ │ │ │ ├── FrpcScreen.kt │ │ │ │ └── LogScreen.kt │ │ │ ├── frps │ │ │ │ └── FrpsScreen.kt │ │ │ └── settings │ │ │ │ ├── SettingsScreen.kt │ │ │ │ └── SettingsWidget.kt │ │ ├── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── widgets │ │ │ ├── AppDialog.kt │ │ │ ├── DenseOutlinedField.kt │ │ │ ├── ErrorDialog.kt │ │ │ ├── ExpandableText.kt │ │ │ ├── ExposedDropTextField.kt │ │ │ ├── LabelSlider.kt │ │ │ ├── LoadingDialog.kt │ │ │ ├── Markdown.kt │ │ │ ├── SelectableText.kt │ │ │ ├── TextFieldDialog.kt │ │ │ └── Widgets.kt │ │ └── util │ │ ├── AndroidUtils.kt │ │ ├── ClipBoardUtils.kt │ │ ├── FileUtil.java │ │ ├── FileUtils.kt │ │ ├── NetworkUtils.kt │ │ ├── StringUtils.kt │ │ ├── ThrottleUtil.kt │ │ ├── ToastUtils.kt │ │ └── ZipUtil.kt │ └── res │ ├── drawable-anydpi-v24 │ ├── ic_notification_frpc.xml │ └── ic_notification_frps.xml │ ├── drawable-hdpi │ ├── ic_notification_frpc.png │ └── ic_notification_frps.png │ ├── drawable-mdpi │ ├── ic_notification_frpc.png │ └── ic_notification_frps.png │ ├── drawable-xhdpi │ ├── ic_notification_frpc.png │ └── ic_notification_frps.png │ ├── drawable-xxhdpi │ ├── ic_notification_frpc.png │ └── ic_notification_frps.png │ ├── drawable │ ├── baseline_trending_up_24.xml │ ├── client.xml │ ├── edit.xml │ ├── ic_app_launcher_foreground.xml │ ├── ic_frpc.xml │ ├── ic_frps.xml │ └── server.xml │ ├── mipmap-anydpi-v26 │ ├── ic_app_launcher.xml │ └── ic_app_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_app_launcher.webp │ └── ic_app_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_app_launcher.webp │ └── ic_app_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_app_launcher.webp │ └── ic_app_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_app_launcher.webp │ └── ic_app_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_app_launcher.webp │ └── ic_app_launcher_round.webp │ ├── values │ ├── ic_app_launcher_background.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ ├── file_path_data.xml │ ├── file_paths.xml │ └── network_security_config.xml ├── build.gradle ├── frp ├── install_all_win.sh ├── install_frp.sh ├── install_src.sh └── update_docs_md.sh ├── frp_version ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── 1.jpg └── 2.jpg └── settings.gradle /.github/scripts/check_frp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function to_int() { 4 | echo $(echo "$1" | grep -oE '[0-9]+' | tr -d '\n') 5 | } 6 | 7 | LATEST_VER=$(curl -s https://api.github.com/repos/fatedier/frp/releases/latest | grep -o '"tag_name": ".*"' | cut -d'"' -f4) 8 | LATEST_VER_INT=$(to_int $LATEST_VER) 9 | echo "Latest AList version $LATEST_VER ${LATEST_VER_INT}" 10 | echo "alist_version=$LATEST_VER" >> $GITHUB_ENV 11 | 12 | # VERSION_FILE="$GITHUB_WORKSPACE/alist_version.txt" 13 | 14 | VER=$(cat $VERSION_FILE) 15 | if [ -z $VER ]; then 16 | VER="v0.51.3" 17 | fi 18 | 19 | VER_INT=$(to_int $VER) 20 | 21 | echo "Current version: $VER ${VER_INT}" 22 | 23 | 24 | if [ $VER_INT -ge $LATEST_VER_INT ]; then 25 | echo "Current >= Latest" 26 | echo "has_update=0" >> $GITHUB_ENV 27 | else 28 | echo "Current < Latest" 29 | echo "has_update=1" >> $GITHUB_ENV 30 | fi 31 | -------------------------------------------------------------------------------- /.github/scripts/lzy_web.py: -------------------------------------------------------------------------------- 1 | import requests, os, datetime, sys 2 | 3 | # Cookie 中 phpdisk_info 的值 4 | cookie_phpdisk_info = os.environ.get('phpdisk_info') 5 | # Cookie 中 ylogin 的值 6 | cookie_ylogin = os.environ.get('ylogin') 7 | 8 | # 请求头 9 | headers = { 10 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45', 11 | 'Accept-Language': 'zh-CN,zh;q=0.9', 12 | 'Referer': 'https://pc.woozooo.com/account.php?action=login' 13 | } 14 | 15 | # 小饼干 16 | cookie = { 17 | 'ylogin': cookie_ylogin, 18 | 'phpdisk_info': cookie_phpdisk_info 19 | } 20 | 21 | 22 | # 日志打印 23 | def log(msg): 24 | utc_time = datetime.datetime.utcnow() 25 | china_time = utc_time + datetime.timedelta(hours=8) 26 | print(f"[{china_time.strftime('%Y.%m.%d %H:%M:%S')}] {msg}") 27 | 28 | 29 | # 检查是否已登录 30 | def login_by_cookie(): 31 | url_account = "https://pc.woozooo.com/account.php" 32 | if cookie['phpdisk_info'] is None: 33 | log('ERROR: 请指定 Cookie 中 phpdisk_info 的值!') 34 | return False 35 | if cookie['ylogin'] is None: 36 | log('ERROR: 请指定 Cookie 中 ylogin 的值!') 37 | return False 38 | res = requests.get(url_account, headers=headers, cookies=cookie, verify=True) 39 | if '网盘用户登录' in res.text: 40 | log('ERROR: 登录失败,请更新Cookie') 41 | return False 42 | else: 43 | log('登录成功') 44 | return True 45 | 46 | 47 | # 上传文件 48 | def upload_file(file_dir, folder_id): 49 | file_name = os.path.basename(file_dir) 50 | url_upload = "https://up.woozooo.com/fileup.php" 51 | headers['Referer'] = f'https://up.woozooo.com/mydisk.php?item=files&action=index&u={cookie_ylogin}' 52 | post_data = { 53 | "task": "1", 54 | "folder_id": folder_id, 55 | "id": "WU_FILE_0", 56 | "name": file_name, 57 | } 58 | files = {'upload_file': (file_name, open(file_dir, "rb"), 'application/octet-stream')} 59 | res = requests.post(url_upload, data=post_data, files=files, headers=headers, cookies=cookie, timeout=120).json() 60 | log(f"{file_dir} -> {res['info']}") 61 | return res['zt'] == 1 62 | 63 | 64 | # 上传文件夹内的文件 65 | def upload_folder(folder_dir, folder_id): 66 | file_list = sorted(os.listdir(folder_dir), reverse=True) 67 | for file in file_list: 68 | path = os.path.join(folder_dir, file) 69 | if os.path.isfile(path): 70 | upload_file(path, folder_id) 71 | else: 72 | upload_folder(path, folder_id) 73 | 74 | 75 | # 上传 76 | def upload(dir, folder_id): 77 | if dir is None: 78 | log('ERROR: 请指定上传的文件路径') 79 | return 80 | if folder_id is None: 81 | log('ERROR: 请指定蓝奏云的文件夹id') 82 | return 83 | if os.path.isfile(dir): 84 | upload_file(dir, str(folder_id)) 85 | else: 86 | upload_folder(dir, str(folder_id)) 87 | 88 | 89 | if __name__ == '__main__': 90 | argv = sys.argv[1:] 91 | if len(argv) != 2: 92 | log('ERROR: 参数错误,请以这种格式重新尝试\npython lzy_web.py 需上传的路径 蓝奏云文件夹id') 93 | # 需上传的路径 94 | upload_path = argv[0] 95 | # 蓝奏云文件夹id 96 | lzy_folder_id = argv[1] 97 | if login_by_cookie(): 98 | upload(upload_path, lzy_folder_id) -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | paths-ignore: 8 | - "*.md" 9 | - "*.sh" 10 | - "release.yaml" 11 | - "sync_frp.yaml" 12 | 13 | workflow_dispatch: 14 | 15 | jobs: 16 | go: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | GO_ARCH: [ "386", amd64, arm, arm64 ] 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Download FRP Source Code 25 | run: | 26 | cd $GITHUB_WORKSPACE/frp 27 | chmod +x *.sh 28 | 29 | ./install_src.sh 30 | 31 | - uses: actions/setup-go@v4 32 | with: 33 | go-version: 1.20.3 34 | cache-dependency-path: ${{ github.workspace }}/frp/frp-*/go.sum 35 | 36 | - name: Build 37 | run: | 38 | cd $GITHUB_WORKSPACE/frp 39 | chmod +x *.sh 40 | GOARCH=${{ matrix.GO_ARCH }} 41 | 42 | declare -A goarch2cc=( ["arm64"]="aarch64-linux-android32-clang" ["arm"]="armv7a-linux-androideabi32-clang" ["amd64"]="x86_64-linux-android32-clang" ["386"]="i686-linux-android32-clang") 43 | export CC="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/${goarch2cc[$GOARCH]}" 44 | 45 | declare -A arch2lib=( ["arm64"]="arm64-v8a" ["arm"]="armeabi-v7a" ["amd64"]="x86_64" ["386"]="x86") 46 | export LIB="${arch2lib[$GOARCH]}" 47 | 48 | ./install_frp.sh frpc $GOARCH $LIB 49 | ./install_frp.sh frps $GOARCH $LIB 50 | 51 | - name: Upload to Artifact 52 | uses: actions/upload-artifact@v3 53 | with: 54 | name: "app_libs" 55 | path: "${{ github.workspace }}/app/libs" 56 | 57 | android: 58 | runs-on: ubuntu-latest 59 | needs: [ go ] 60 | env: 61 | output: "${{ github.workspace }}/app/build/outputs/apk/release" 62 | steps: 63 | - uses: actions/checkout@v3 64 | with: 65 | fetch-depth: 0 66 | 67 | - uses: actions/setup-java@v3 68 | with: 69 | distribution: temurin 70 | java-version: 17 71 | 72 | - name: Setup Gradle 73 | uses: gradle/gradle-build-action@v2 74 | 75 | - name: Download Artifact 76 | uses: actions/download-artifact@v3 77 | with: 78 | name: "app_libs" 79 | path: "${{ github.workspace }}/app/libs" 80 | 81 | - name: Init Signature 82 | run: | 83 | touch local.properties 84 | echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties 85 | echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties 86 | echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties 87 | echo KEY_PATH='./key.jks' >> local.properties 88 | # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks 89 | echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks 90 | 91 | - name: Grant execute permission for gradlew 92 | run: chmod +x gradlew 93 | - name: Build with Gradle 94 | id: gradle 95 | run: ./gradlew assembleRelease -build-cache --parallel --daemon --warning-mode all 96 | 97 | - name: Upload missing_rules.txt 98 | if: failure() && steps.gradle.outcome != 'success' 99 | uses: actions/upload-artifact@v3 100 | with: 101 | name: "missing_rules" 102 | path: "${{ github.workspace }}/app/build/outputs/mapping/release/missing_rules.txt" 103 | 104 | - name: Init APP Version Name 105 | run: | 106 | echo "ver_name=$(grep -m 1 'versionName' ${{ env.output }}/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV 107 | 108 | - name: Upload App To Artifact arm64-v8a 109 | if: success () || failure () 110 | uses: actions/upload-artifact@v3 111 | with: 112 | name: "FrpAndroid-v${{ env.ver_name }}_arm64-v8a" 113 | path: "${{ env.output }}/*-v8a.apk" 114 | 115 | - name: Upload App To Artifact arm-v7a 116 | if: success () || failure () 117 | uses: actions/upload-artifact@v3 118 | with: 119 | name: "FrpAndroid-v${{ env.ver_name }}_arm-v7a" 120 | path: "${{ env.output }}/*-v7a.apk" 121 | 122 | - name: Upload App To Artifact x86 123 | if: success () || failure () 124 | uses: actions/upload-artifact@v3 125 | with: 126 | name: "FrpAndroid-v${{ env.ver_name }}_x86" 127 | path: "${{ env.output }}/*_x86.apk" -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | paths: 8 | - "CHANGELOG.md" 9 | 10 | workflow_dispatch: 11 | 12 | jobs: 13 | go: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | GO_ARCH: [ "386", amd64, arm, arm64 ] 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Download FRP Source Code 22 | run: | 23 | cd $GITHUB_WORKSPACE/frp 24 | chmod +x *.sh 25 | 26 | ./install_src.sh 27 | 28 | - uses: actions/setup-go@v4 29 | with: 30 | go-version: 1.20.3 31 | cache-dependency-path: ${{ github.workspace }}/frp/frp-*/go.sum 32 | 33 | - name: Build 34 | run: | 35 | cd $GITHUB_WORKSPACE/frp 36 | chmod +x *.sh 37 | GOARCH=${{ matrix.GO_ARCH }} 38 | 39 | declare -A goarch2cc=( ["arm64"]="aarch64-linux-android32-clang" ["arm"]="armv7a-linux-androideabi32-clang" ["amd64"]="x86_64-linux-android32-clang" ["386"]="i686-linux-android32-clang") 40 | export CC="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/${goarch2cc[$GOARCH]}" 41 | 42 | declare -A arch2lib=( ["arm64"]="arm64-v8a" ["arm"]="armeabi-v7a" ["amd64"]="x86_64" ["386"]="x86") 43 | export LIB="${arch2lib[$GOARCH]}" 44 | 45 | ./install_frp.sh frpc $GOARCH $LIB 46 | ./install_frp.sh frps $GOARCH $LIB 47 | 48 | - name: Upload to Artifact 49 | uses: actions/upload-artifact@v3 50 | with: 51 | name: "app_libs" 52 | path: "${{ github.workspace }}/app/libs" 53 | 54 | android: 55 | runs-on: ubuntu-latest 56 | needs: [ go ] 57 | env: 58 | output: "${{ github.workspace }}/app/build/outputs/apk/release" 59 | steps: 60 | - uses: actions/checkout@v3 61 | with: 62 | fetch-depth: 0 63 | 64 | - uses: actions/setup-java@v3 65 | with: 66 | distribution: temurin 67 | java-version: 17 68 | 69 | - name: Setup Gradle 70 | uses: gradle/gradle-build-action@v2 71 | 72 | - name: Download Artifact 73 | uses: actions/download-artifact@v3 74 | with: 75 | name: "app_libs" 76 | path: "${{ github.workspace }}/app/libs" 77 | 78 | - name: Init Signature 79 | run: | 80 | touch local.properties 81 | echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties 82 | echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties 83 | echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties 84 | echo KEY_PATH='./key.jks' >> local.properties 85 | # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks 86 | echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks 87 | 88 | - name: Grant execute permission for gradlew 89 | run: chmod +x gradlew 90 | - name: Build with Gradle 91 | run: ./gradlew assembleRelease -build-cache --parallel --daemon --warning-mode all 92 | 93 | - name: Init APP Version Name 94 | run: | 95 | echo "ver_name=$(grep -m 1 'versionName' ${{ env.output }}/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV 96 | 97 | - name: Upload App To Artifact arm64-v8a 98 | if: success () || failure () 99 | uses: actions/upload-artifact@v3 100 | with: 101 | name: "FrpAndroid-v${{ env.ver_name }}_arm64-v8a" 102 | path: "${{ env.output }}/*-v8a.apk" 103 | 104 | - name: Upload App To Artifact arm-v7a 105 | if: success () || failure () 106 | uses: actions/upload-artifact@v3 107 | with: 108 | name: "FrpAndroid-v${{ env.ver_name }}_arm-v7a" 109 | path: "${{ env.output }}/*-v7a.apk" 110 | 111 | - name: Upload App To Artifact x86 112 | if: success () || failure () 113 | uses: actions/upload-artifact@v3 114 | with: 115 | name: "FrpAndroid-v${{ env.ver_name }}_x86" 116 | path: "${{ env.output }}/*_x86.apk" 117 | 118 | - name: Make "CHANGELOG.md" 119 | run: | 120 | file="${{ github.workspace }}/CHANGELOG.md" 121 | echo -e > "#### 更新内容\n$(cat $file)"> $file 122 | 123 | - uses: softprops/action-gh-release@v0.1.15 124 | with: 125 | name: ${{ env.ver_name }} 126 | tag_name: ${{ env.ver_name }} 127 | body_path: ${{ github.workspace }}/CHANGELOG.md 128 | draft: false 129 | prerelease: false 130 | files: ${{ env.output }}/*.apk 131 | env: 132 | GITHUB_TOKEN: ${{ secrets.TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/sync_frp.yaml: -------------------------------------------------------------------------------- 1 | name: CheckFrpVersion 2 | 3 | on: 4 | schedule: 5 | - cron: "0 * * * *" # 每小时执行一次 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - "master" 10 | paths: 11 | - "sync_frp.yaml" 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | env: 17 | VERSION_FILE: ${{ github.workspace }}/frp_version 18 | steps: 19 | - uses: actions/checkout@v3 20 | - run: | 21 | cd $GITHUB_WORKSPACE/.github/scripts 22 | chmod +x ./*.sh 23 | 24 | touch ${{ env.VERSION_FILE }} 25 | ./check_frp.sh 26 | 27 | - name: Shell 28 | run: | 29 | echo "frp_version=${{ env.frp_version }}" 30 | echo "has_update=${{ env.has_update }}" 31 | 32 | # 用于测试 33 | # echo "has_update=1" >> $GITHUB_ENV 34 | 35 | if [ ${{ env.has_update }} -eq 0 ] 36 | then 37 | echo "无更新" 38 | else 39 | echo -e "[同步FRP] ${{ env.frp_version }}" > $GITHUB_WORKSPACE/CHANGELOG.md 40 | echo -e "${{ env.frp_version }}" > ${{ env.VERSION_FILE }} 41 | 42 | git config user.name "github-actions" 43 | git config user.email "42014615+jing332@users.noreply.github.com" 44 | git add . 45 | git commit -m "[bot] Update frp to ${{ env.frp_version }}" 46 | git push 47 | fi 48 | 49 | - name: Run workflow release 50 | if: env.has_update == 1 && ( success() || failure() ) 51 | run: | 52 | gh workflow run release.yaml -R jing332/frpandroid 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | /app/build/ 4 | /build/ 5 | /app/release/ 6 | 7 | local.properties 8 | 9 | *.so 10 | *.exe 11 | *.tgz 12 | *.zip 13 | *.jks 14 | frp-* -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 42 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [同步FRP] 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Release](https://github.com/jing332/FrpAndroid/actions/workflows/release.yaml/badge.svg)](https://github.com/jing332/FrpAndroid/actions/workflows/release.yaml) 2 | [![Build](https://github.com/jing332/FrpAndroid/actions/workflows/build.yaml/badge.svg)](https://github.com/jing332/FrpAndroid/actions/workflows/build.yaml) 3 | [![CheckFrpVersion](https://github.com/jing332/FrpAndroid/actions/workflows/sync_frp.yaml/badge.svg)](https://github.com/jing332/FrpAndroid/actions/workflows/sync_frp.yaml) 4 | 5 | # FrpAndroid 6 | 7 | -------------------------------------------------------------------------------- /app/schemas/com.github.jing332.frpandroid.data.AppDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "44600360650a956f56a2eb3c40a34630", 6 | "entities": [ 7 | { 8 | "tableName": "frp_logs", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `level` INTEGER NOT NULL, `message` TEXT NOT NULL, `description` TEXT)", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "type", 19 | "columnName": "type", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "level", 25 | "columnName": "level", 26 | "affinity": "INTEGER", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "message", 31 | "columnName": "message", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "description", 37 | "columnName": "description", 38 | "affinity": "TEXT", 39 | "notNull": false 40 | } 41 | ], 42 | "primaryKey": { 43 | "autoGenerate": true, 44 | "columnNames": [ 45 | "id" 46 | ] 47 | }, 48 | "indices": [], 49 | "foreignKeys": [] 50 | } 51 | ], 52 | "views": [], 53 | "setupQueries": [ 54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '44600360650a956f56a2eb3c40a34630')" 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /app/schemas/com.github.jing332.frpandroid.data.AppDatabase/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 2, 5 | "identityHash": "13208f73136cf97c16ed23fb636e3e43", 6 | "entities": [ 7 | { 8 | "tableName": "server_logs", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `level` INTEGER NOT NULL, `message` TEXT NOT NULL, `description` TEXT)", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "level", 19 | "columnName": "level", 20 | "affinity": "INTEGER", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "message", 25 | "columnName": "message", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "description", 31 | "columnName": "description", 32 | "affinity": "TEXT", 33 | "notNull": false 34 | } 35 | ], 36 | "primaryKey": { 37 | "autoGenerate": true, 38 | "columnNames": [ 39 | "id" 40 | ] 41 | }, 42 | "indices": [], 43 | "foreignKeys": [] 44 | }, 45 | { 46 | "tableName": "provider_caches", 47 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `path` TEXT NOT NULL, `status` TEXT NOT NULL, `modifier` TEXT NOT NULL, `isDirection` INTEGER NOT NULL)", 48 | "fields": [ 49 | { 50 | "fieldPath": "id", 51 | "columnName": "id", 52 | "affinity": "INTEGER", 53 | "notNull": true 54 | }, 55 | { 56 | "fieldPath": "path", 57 | "columnName": "path", 58 | "affinity": "TEXT", 59 | "notNull": true 60 | }, 61 | { 62 | "fieldPath": "status", 63 | "columnName": "status", 64 | "affinity": "TEXT", 65 | "notNull": true 66 | }, 67 | { 68 | "fieldPath": "modifier", 69 | "columnName": "modifier", 70 | "affinity": "TEXT", 71 | "notNull": true 72 | }, 73 | { 74 | "fieldPath": "isDirection", 75 | "columnName": "isDirection", 76 | "affinity": "INTEGER", 77 | "notNull": true 78 | } 79 | ], 80 | "primaryKey": { 81 | "autoGenerate": true, 82 | "columnNames": [ 83 | "id" 84 | ] 85 | }, 86 | "indices": [], 87 | "foreignKeys": [] 88 | } 89 | ], 90 | "views": [], 91 | "setupQueries": [ 92 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 93 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '13208f73136cf97c16ed23fb636e3e43')" 94 | ] 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | D·FrpAndroid 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 48 | 49 | 54 | 55 | 60 | 61 | 66 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 90 | 91 | 92 | 93 | 94 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/assets/defaultData/frpc.ini: -------------------------------------------------------------------------------- 1 | [common] 2 | server_addr = 127.0.0.1 3 | server_port = 7000 4 | 5 | [ssh] 6 | type = tcp 7 | local_ip = 127.0.0.1 8 | local_port = 22 9 | remote_port = 6000 10 | -------------------------------------------------------------------------------- /app/src/main/assets/defaultData/frps.ini: -------------------------------------------------------------------------------- 1 | [common] 2 | bind_port = 7000 3 | -------------------------------------------------------------------------------- /app/src/main/assets/docs/zh/client-configures.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "客户端配置" 3 | weight: 2 4 | description: > 5 | frp 客户端的详细配置说明。 6 | --- 7 | 8 | ## 基础配置 9 | 10 | | 参数 | 类型 | 说明 | 默认值 | 可选值 | 备注 | 11 | | :--- | :--- | :--- | :--- | :--- | :---| 12 | | server_addr | string | 连接服务端的地址 | 0.0.0.0 | | | 13 | | server_port | int | 连接服务端的端口 | 7000 | | | 14 | | nat_hole_stun_server | string | xtcp 打洞所需的 stun 服务器地址 | stun.easyvoip.com:3478 | | | 15 | | connect_server_local_ip | string | 连接服务端时所绑定的本地 IP | | | | 16 | | dial_server_timeout | int | 连接服务端的超时时间 | 10 | | | 17 | | dial_server_keepalive | int | 和服务端底层 TCP 连接的 keepalive 间隔时间,单位秒 | 7200 | | 负数不启用 | 18 | | http_proxy | string | 连接服务端使用的代理地址 | | | 格式为 {protocol}://user:passwd@192.168.1.128:8080 protocol 目前支持 http、socks5、ntlm | 19 | | log_file | string | 日志文件地址 | ./frpc.log | | 如果设置为 console,会将日志打印在标准输出中 | 20 | | log_level | string | 日志等级 | info | trace, debug, info, warn, error | | 21 | | log_max_days | int | 日志文件保留天数 | 3 | | | 22 | | disable_log_color | bool | 禁用标准输出中的日志颜色 | false | | | 23 | | pool_count | int | 连接池大小 | 0 | | | 24 | | user | string | 用户名 | | | 设置此参数后,代理名称会被修改为 {user}.{proxyName},避免代理名称和其他用户冲突 | 25 | | dns_server | string | 使用 DNS 服务器地址 | | | 默认使用系统配置的 DNS 服务器,指定此参数可以强制替换为自定义的 DNS 服务器地址 | 26 | | login_fail_exit | bool | 第一次登陆失败后是否退出 | true | | | 27 | | protocol | string | 连接服务端的通信协议 | tcp | tcp, kcp, quic, websocket, wss | | 28 | | quic_keepalive_period | int | quic 协议 keepalive 间隔,单位: 秒 | 10 | | | 29 | | quic_max_idle_timeout | int | quic 协议的最大空闲超时时间,单位: 秒 | 30 | | | 30 | | quic_max_incoming_streams | int | quic 协议最大并发 stream 数 | 100000 | | | 31 | | tls_enable | bool | 启用 TLS 协议加密连接 | true | | | 32 | | tls_cert_file | string | TLS 客户端证书文件路径 | | | | 33 | | tls_key_file | string | TLS 客户端密钥文件路径 | | | | 34 | | tls_trusted_ca_file | string | TLS CA 证书路径 | | | | 35 | | tls_server_name | string | TLS Server 名称 | | | 为空则使用 server_addr | 36 | | disable_custom_tls_first_byte | bool | TLS 不发送 0x17 | true | | 当为 true 时,不能端口复用 | 37 | | tcp_mux_keepalive_interval | int | tcp_mux 的心跳检查间隔时间 | 60 | | 单位:秒 | 38 | | heartbeat_interval | int | 向服务端发送心跳包的间隔时间 | 30 | | 建议启用 tcp_mux_keepalive_interval,将此值设置为 -1 | 39 | | heartbeat_timeout | int | 和服务端心跳的超时时间 | 90 | | | 40 | | udp_packet_size | int | 代理 UDP 服务时支持的最大包长度 | 1500 | | 服务端和客户端的值需要一致 | 41 | | start | string | 指定启用部分代理 | | | 当配置了较多代理,但是只希望启用其中部分时可以通过此参数指定,默认为全部启用 | 42 | | meta_xxx | map | 附加元数据 | | | 会传递给服务端插件,提供附加能力 | 43 | 44 | ## 权限验证 45 | 46 | | 参数 | 类型 | 说明 | 默认值 | 可选值 | 备注 | 47 | | :--- | :--- | :--- | :--- | :--- | :---| 48 | | authentication_method | string | 鉴权方式 | token | token, oidc | 需要和服务端一致 | 49 | | authenticate_heartbeats | bool | 开启心跳消息鉴权 | false | | 需要和服务端一致 | 50 | | authenticate_new_work_conns | bool | 开启建立工作连接的鉴权 | false | | 需要和服务端一致 | 51 | | token | string | 鉴权使用的 token 值 | | | 需要和服务端设置一样的值才能鉴权通过 | 52 | | oidc_client_id | string | oidc_client_id | | | | 53 | | oidc_client_secret | string | oidc_client_secret | | | | 54 | | oidc_audience | string | oidc_audience | | | | 55 | | oidc_scope | string | oidc_scope | | | | 56 | | oidc_token_endpoint_url | string | oidc_token_endpoint_url | | | | 57 | | oidc_additional_xxx | map | OIDC 附加参数 | | | map 结构,key 需要以 `oidc_additional_` 开头 | 58 | 59 | ## UI 60 | 61 | | 参数 | 类型 | 说明 | 默认值 | 可选值 | 备注 | 62 | | :--- | :--- | :--- | :--- | :--- | :---| 63 | | admin_addr | string | 启用 AdminUI 监听的本地地址 | 0.0.0.0 | | | 64 | | admin_port | int | 启用 AdminUI 监听的本地端口 | 0 | | | 65 | | admin_user | string | HTTP BasicAuth 用户名 | | | | 66 | | admin_pwd | string | HTTP BasicAuth 密码 | | | | 67 | | asserts_dir | string | 静态资源目录 | | | AdminUI 使用的资源默认打包在二进制文件中,通过指定此参数使用自定义的静态资源 | 68 | | pprof_enable | bool | 启动 Go HTTP pprof | false | | 用于应用调试 | 69 | -------------------------------------------------------------------------------- /app/src/main/assets/docs/zh/server-configures.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "服务端配置" 3 | weight: 1 4 | description: > 5 | frp 服务端详细配置说明。 6 | --- 7 | 8 | ## 基础配置 9 | 10 | | 参数 | 类型 | 说明 | 默认值 | 可选值 | 备注 | 11 | | :--- | :--- | :--- | :--- | :--- | :---| 12 | | bind_addr | string | 服务端监听地址 | 0.0.0.0 | | | 13 | | bind_port | int | 服务端监听端口 | 7000 | | 接收 frpc 的连接 | 14 | | kcp_bind_port | int | 服务端监听 KCP 协议端口 | 0 | | 用于接收采用 KCP 连接的 frpc | 15 | | quic_bind_port | int | 服务端监听 QUIC 协议端口 | 0 | | 用于接收采用 QUIC 连接的 frpc | 16 | | quic_keepalive_period | int | quic 协议 keepalive 间隔,单位: 秒 | 10 | | | 17 | | quic_max_idle_timeout | int | quic 协议的最大空闲超时时间,单位: 秒 | 30 | | | 18 | | quic_max_incoming_streams | int | quic 协议最大并发 stream 数 | 100000 | | | 19 | | proxy_bind_addr | string | 代理监听地址 | 同 bind_addr | | 可以使代理监听在不同的网卡地址 | 20 | | log_file | string | 日志文件地址 | ./frps.log | | 如果设置为 console,会将日志打印在标准输出中 | 21 | | log_level | string | 日志等级 | info | trace, debug, info, warn, error | | 22 | | log_max_days | int | 日志文件保留天数 | 3 | | | 23 | | disable_log_color | bool | 禁用标准输出中的日志颜色 | false | | | 24 | | detailed_errors_to_client | bool | 服务端返回详细错误信息给客户端 | true | | | 25 | | tcp_mux_keepalive_interval | int | tcp_mux 的心跳检查间隔时间 | 60 | | 单位:秒 | 26 | | tcp_keepalive | int | 和客户端底层 TCP 连接的 keepalive 间隔时间,单位秒 | 7200 | | 负数不启用 | 27 | | heartbeat_timeout | int | 服务端和客户端心跳连接的超时时间 | 90 | | 单位:秒 | 28 | | user_conn_timeout | int | 用户建立连接后等待客户端响应的超时时间 | 10 | | 单位:秒 | 29 | | udp_packet_size | int | 代理 UDP 服务时支持的最大包长度 | 1500 | | 服务端和客户端的值需要一致 | 30 | | tls_cert_file | string | TLS 服务端证书文件路径 | | | | 31 | | tls_key_file | string | TLS 服务端密钥文件路径 | | | | 32 | | tls_trusted_ca_file | string | TLS CA 证书路径 | | | | 33 | | nat_hole_analysis_data_reserve_hours | int | 打洞策略数据的保留时间 | 168 | | | 34 | 35 | ## 权限验证 36 | 37 | | 参数 | 类型 | 说明 | 默认值 | 可选值 | 备注 | 38 | | :--- | :--- | :--- | :--- | :--- | :---| 39 | | authentication_method | string | 鉴权方式 | token | token, oidc | | 40 | | authenticate_heartbeats | bool | 开启心跳消息鉴权 | false | | | 41 | | authenticate_new_work_conns | bool | 开启建立工作连接的鉴权 | false | | | 42 | | token | string | 鉴权使用的 token 值 | | | 客户端需要设置一样的值才能鉴权通过 | 43 | | oidc_issuer | string | oidc_issuer | | | | 44 | | oidc_audience | string | oidc_audience | | | | 45 | | oidc_skip_expiry_check | bool | oidc_skip_expiry_check | | | | 46 | | oidc_skip_issuer_check | bool | oidc_skip_issuer_check | | | | 47 | 48 | ## 管理配置 49 | 50 | | 参数 | 类型 | 说明 | 默认值 | 可选值 | 备注 | 51 | | :--- | :--- | :--- | :--- | :--- | :---| 52 | | allow_ports | string | 允许代理绑定的服务端端口 | | | 格式为 1000-2000,2001,3000-4000 | 53 | | max_pool_count | int | 最大连接池大小 | 5 | | | 54 | | max_ports_per_client | int | 限制单个客户端最大同时存在的代理数 | 0 | | 0 表示没有限制 | 55 | | tls_only | bool | 只接受启用了 TLS 的客户端连接 | false | | | 56 | 57 | ## Dashboard, 监控 58 | 59 | | 参数 | 类型 | 说明 | 默认值 | 可选值 | 备注 | 60 | | :--- | :--- | :--- | :--- | :--- | :---| 61 | | dashboard_addr | string | 启用 Dashboard 监听的本地地址 | 0.0.0.0 | | | 62 | | dashboard_port | int | 启用 Dashboard 监听的本地端口 | 0 | | | 63 | | dashboard_user | string | HTTP BasicAuth 用户名 | | | | 64 | | dashboard_pwd | string | HTTP BasicAuth 密码 | | | | 65 | | dashboard_tls_mode | bool | 是否启用 TLS 模式 | false | | | 66 | | dashboard_tls_cert_file | string | TLS 证书文件路径 | | | | 67 | | dashboard_tls_key_file | string | TLS 密钥文件路径 | | | | 68 | | enable_prometheus | bool | 是否提供 Prometheus 监控接口 | false | | 需要同时启用了 Dashboard 才会生效 | 69 | | asserts_dir | string | 静态资源目录 | | | Dashboard 使用的资源默认打包在二进制文件中,通过指定此参数使用自定义的静态资源 | 70 | | pprof_enable | bool | 启动 Go HTTP pprof | false | | 用于应用调试 | 71 | 72 | ## HTTP & HTTPS 73 | 74 | | 参数 | 类型 | 说明 | 默认值 | 可选值 | 备注 | 75 | | :--- | :--- | :--- | :--- | :--- | :---| 76 | | vhost_http_port | int | 为 HTTP 类型代理监听的端口 | 0 | | 启用后才支持 HTTP 类型的代理,默认不启用 | 77 | | vhost_https_port | int | 为 HTTPS 类型代理监听的端口 | 0 | | 启用后才支持 HTTPS 类型的代理,默认不启用 | 78 | | vhost_http_timeout | int | HTTP 类型代理在服务端的 ResponseHeader 超时时间 | 60 | | | 79 | | subdomain_host | string | 二级域名后缀 | | | | 80 | | custom_404_page | string | 自定义 404 错误页面地址 | | | | 81 | 82 | ## TCPMUX 83 | 84 | | 参数 | 类型 | 说明 | 默认值 | 可选值 | 备注 | 85 | | :--- | :--- | :--- | :--- | :--- | :---| 86 | | tcpmux_httpconnect_port | int | 为 TCPMUX 类型代理监听的端口 | 0 | | 启用后才支持 TCPMUX 类型的代理,默认不启用 | 87 | | tcpmux_passthrough | bool | 是否透传 CONNECT 请求 | false | | 通常在本地服务是 HTTP Proxy 时使用 | 88 | -------------------------------------------------------------------------------- /app/src/main/assets/docs/zh/visitor.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "visitor 配置" 3 | weight: 4 4 | description: > 5 | frp visitor 的详细配置说明。 6 | --- 7 | 8 | ## 通用配置 9 | 10 | 通用配置是指不同类型的 visitor 共同使用的一些配置参数。 11 | 12 | ### 基础配置 13 | 14 | | 参数 | 类型 | 说明 | 是否必须 | 默认值 | 可选值 | 备注 | 15 | | :--- | :--- | :--- | :--- | :--- | :--- | :---| 16 | | role | string | 角色 | 是 | visitor | visitor | visitor 表示访问端 | 17 | | server_user | string | 要访问的 proxy 所属的用户名 | 否 | 当前用户 | | 如果为空,则默认为当前用户 | 18 | | server_name | string | 要访问的 proxy 名称 | 是 | | | | 19 | | type | string | visitor 类型 | 是 | | stcp, sudp, xtcp | | 20 | | sk | string | 密钥 | 是 | | | 服务端和访问端的密钥需要一致,访问端才能访问到服务端 | 21 | | use_encryption | bool | 是否启用加密功能 | 否 | false | | 启用后该代理和服务端之间的通信内容都会被加密传输 | 22 | | use_compression | bool | 是否启用压缩功能 | 否 | false | | 启用后该代理和服务端之间的通信内容都会被压缩传输 | 23 | | bind_addr | string | visitor 监听的本地地址 | 否 | 127.0.0.1 | | 通过访问监听的地址和端口,连接到远端代理的服务 | 24 | | bind_port | int | visitor 监听的本地端口 | 否 | | | 如果为 -1,表示不需要监听物理端口,通常可以用于作为其他 visitor 的 fallback | 25 | 26 | ## XTCP 27 | 28 | | 参数 | 类型 | 说明 | 是否必须 | 默认值 | 可选值 | 备注 | 29 | | :--- | :---: | :--- | :---: | :---: | :--- | :--- | 30 | | keep_tunnel_open | bool | 是否保持隧道打开 | 否 | false | | 如果开启,会定期检查隧道状态并尝试保持打开 | 31 | | max_retries_an_hour | int | 每小时尝试打开隧道的次数 | 否 | 8 | | 仅在 keep_tunnel_open 为 true 时有效 | 32 | | min_retry_interval | int | 重试打开隧道的最小间隔时间,单位: 秒 | 否 | 90 | | 仅在 keep_tunnel_open 为 true 时有效 | 33 | | protocol | string | 隧道底层通信协议 | 否 | quic | quic, kcp | | 34 | | fallback_to | string | 回退到的其他 visitor 名称 | 否 | | | | 35 | | fallback_timeout_ms | int | 连接建立超过多长时间后回退到其他 visitor | 否 | | | | 36 | -------------------------------------------------------------------------------- /app/src/main/ic_app_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/ic_app_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/App.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.os.Looper 6 | import com.github.jing332.frpandroid.model.frp.Frp.Companion.getFrpDir 7 | import com.github.jing332.frpandroid.util.ClipboardUtils 8 | import com.github.jing332.frpandroid.util.ToastUtils.longToast 9 | import xcrash.XCrash 10 | import java.io.File 11 | import java.net.InetAddress 12 | import java.net.UnknownHostException 13 | 14 | val app by lazy { App.application } 15 | 16 | class App : Application() { 17 | 18 | override fun onCreate() { 19 | super.onCreate() 20 | 21 | assets.list("defaultData")?.forEach { fileName -> 22 | assets.open("defaultData/${fileName}").use { ins -> 23 | ins.bufferedReader().use { reader -> 24 | val f = File(this.getFrpDir() + File.separator + fileName) 25 | if (f.exists()) return@forEach 26 | 27 | f.createNewFile() 28 | f.writeText(reader.readText()) 29 | } 30 | } 31 | } 32 | } 33 | 34 | override fun attachBaseContext(base: Context?) { 35 | super.attachBaseContext(base) 36 | application = this 37 | 38 | XCrash.init( 39 | this, 40 | XCrash.InitParameters() 41 | .setLogDir(this.getExternalFilesDir("xCrash")!!.absolutePath) 42 | .setAppVersion(BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")") 43 | .setJavaCallback { logPath, _ -> 44 | longToast("程序崩溃:${logPath}") 45 | copyLog(logPath) 46 | } 47 | .setNativeCallback { logPath, _ -> 48 | longToast("底层程序崩溃:${logPath}") 49 | copyLog(logPath) 50 | } 51 | .setJavaRethrow(true) 52 | .setNativeRethrow(true) 53 | ) 54 | } 55 | 56 | private fun copyLog(logPath: String) { 57 | val file = File(logPath) 58 | file.inputStream().use { 59 | it.bufferedReader().use { reader -> 60 | val sb = StringBuilder() 61 | for (i in 0..300) { 62 | sb.appendLine(reader.readLine() ?: break) 63 | } 64 | ClipboardUtils.copyText(sb.toString()) 65 | } 66 | } 67 | } 68 | 69 | companion object { 70 | lateinit var application: Application 71 | 72 | fun getByName(ip: String?): InetAddress? { 73 | return try { 74 | InetAddress.getByName(ip) 75 | } catch (unused: UnknownHostException) { 76 | null 77 | } 78 | } 79 | 80 | val isMainThread: Boolean 81 | get() = Looper.getMainLooper().thread.id == Thread.currentThread().id 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/config/AppConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.config 2 | 3 | import com.funny.data_saver.core.DataSaverConverter 4 | import com.funny.data_saver.core.DataSaverPreferences 5 | import com.funny.data_saver.core.mutableDataSaverStateOf 6 | import com.github.jing332.frpandroid.app 7 | import com.github.jing332.frpandroid.constant.FrpType 8 | import java.io.File 9 | 10 | object AppConfig { 11 | private val pref = 12 | DataSaverPreferences(app.getSharedPreferences("app", 0)) 13 | 14 | val isFirstRun: Boolean 15 | get() = File( 16 | (app.filesDir.parentFile?.absolutePath + File.separator + "shared_prefs") 17 | ).exists().not() 18 | 19 | init { 20 | DataSaverConverter.registerTypeConverters( 21 | { it.name }, 22 | { FrpType.valueOf(it) } 23 | ) 24 | } 25 | 26 | var isAutoCheckUpdate = mutableDataSaverStateOf( 27 | dataSaverInterface = pref, 28 | key = "isCheckUpdate", 29 | initialValue = true 30 | ) 31 | 32 | var frpPageType = mutableDataSaverStateOf( 33 | dataSaverInterface = pref, 34 | key = "frpType", 35 | initialValue = FrpType.FRPC 36 | ) 37 | 38 | var subPageIndex = mutableDataSaverStateOf( 39 | dataSaverInterface = pref, 40 | key = "pageIndex", 41 | initialValue = 0 42 | ) 43 | 44 | val enabledWakeLock = mutableDataSaverStateOf( 45 | dataSaverInterface = pref, 46 | key = "enabledWakeLock", 47 | initialValue = false 48 | ) 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/config/ServerConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.config 2 | 3 | import com.funny.data_saver.core.DataSaverPreferences 4 | import com.funny.data_saver.core.mutableDataSaverStateOf 5 | import com.github.jing332.frpandroid.app 6 | 7 | object ServerConfig { 8 | private val pref = 9 | DataSaverPreferences(app.getSharedPreferences("server", 0)) 10 | 11 | 12 | var port = mutableDataSaverStateOf( 13 | dataSaverInterface = pref, 14 | key = "port", 15 | initialValue = 2344 16 | ) 17 | 18 | /** 19 | * 单位 秒 20 | */ 21 | var timeout = mutableDataSaverStateOf( 22 | dataSaverInterface = pref, 23 | key = "timeout", 24 | initialValue = 10 25 | ) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/constant/AppConst.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.constant 2 | 3 | import androidx.localbroadcastmanager.content.LocalBroadcastManager 4 | import com.charleskorn.kaml.Yaml 5 | import com.charleskorn.kaml.YamlConfiguration 6 | import com.github.jing332.frpandroid.BuildConfig 7 | import com.github.jing332.frpandroid.app 8 | import kotlinx.serialization.ExperimentalSerializationApi 9 | import kotlinx.serialization.json.Json 10 | 11 | @Suppress("DEPRECATION") 12 | object AppConst { 13 | val yaml = Yaml(configuration = YamlConfiguration(strictMode = false)) 14 | 15 | @OptIn(ExperimentalSerializationApi::class) 16 | val json = Json { 17 | ignoreUnknownKeys = true 18 | allowStructuredMapKeys = true 19 | prettyPrint = true 20 | isLenient = true 21 | explicitNulls = false 22 | } 23 | 24 | val localBroadcast by lazy { 25 | LocalBroadcastManager.getInstance(app) 26 | } 27 | 28 | val fileProviderAuthor = BuildConfig.APPLICATION_ID + ".fileprovider" 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/constant/FrpType.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.constant 2 | 3 | enum class FrpType(val value: Int) { 4 | FRPC(0), 5 | FRPS(1), 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/constant/LogLevel.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.constant 2 | 3 | import androidx.annotation.IntDef 4 | 5 | @IntDef( 6 | LogLevel.PANIC, 7 | LogLevel.FATAL, 8 | LogLevel.ERROR, 9 | LogLevel.WARN, 10 | LogLevel.INFO, 11 | LogLevel.DEBUG, 12 | LogLevel.TRACE 13 | ) 14 | annotation class LogLevel { 15 | companion object { 16 | const val PANIC = 0 17 | const val FATAL = 1 18 | const val ERROR = 2 19 | const val WARN = 3 20 | const val INFO = 4 21 | const val DEBUG = 5 22 | const val TRACE = 6 23 | 24 | fun Int.toLevelString(): String { 25 | return when (this) { 26 | PANIC -> "PANIC" 27 | FATAL -> "FATAL" 28 | ERROR -> "ERROR" 29 | WARN -> "WARN" 30 | INFO -> "INFO" 31 | DEBUG -> "DEBUG" 32 | TRACE -> "TRACE" 33 | else -> "UNKNOWN" 34 | } 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/data/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.data 2 | 3 | import androidx.room.Database 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | import com.github.jing332.frpandroid.app 7 | import com.github.jing332.frpandroid.data.dao.FrpLogDao 8 | import com.github.jing332.frpandroid.data.entities.FrpLog 9 | 10 | val appDb by lazy { AppDatabase.create() } 11 | 12 | @Database( 13 | version = 1, 14 | entities = [FrpLog::class], 15 | autoMigrations = [ 16 | // AutoMigration(from = 1, to = 2) 17 | ] 18 | ) 19 | abstract class AppDatabase : RoomDatabase() { 20 | abstract val frpLogDao: FrpLogDao 21 | 22 | companion object { 23 | fun create() = Room.databaseBuilder( 24 | app, 25 | AppDatabase::class.java, 26 | "frpandroid.db" 27 | ) 28 | .allowMainThreadQueries() 29 | .build() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/data/dao/FrpLogDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.data.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.github.jing332.frpandroid.constant.FrpType 8 | import com.github.jing332.frpandroid.data.entities.FrpLog 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | @Dao 12 | interface FrpLogDao { 13 | @Query("SELECT * FROM frp_logs WHERE type = :type") 14 | fun flowAll(type: FrpType): Flow> 15 | 16 | @Query("SELECT * FROM frp_logs WHERE type = :type") 17 | fun all(type: FrpType): List 18 | 19 | @Query("DELETE FROM frp_logs WHERE type = :type") 20 | fun deleteAll(type: FrpType) 21 | 22 | @Insert(onConflict = OnConflictStrategy.REPLACE) 23 | fun insert(vararg log: FrpLog) 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/data/entities/FrpLog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.data.entities 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.github.jing332.frpandroid.constant.FrpType 6 | import com.github.jing332.frpandroid.constant.LogLevel 7 | 8 | @Entity("frp_logs") 9 | data class FrpLog( 10 | @PrimaryKey(autoGenerate = true) 11 | val id: Long = 0, 12 | 13 | val type: FrpType, 14 | @LogLevel val level: Int, 15 | val message: String, 16 | val description: String? = null, 17 | ) { 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/AppUpdateChecker.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model 2 | 3 | import android.util.Log 4 | import com.github.jing332.frpandroid.BuildConfig 5 | import com.github.jing332.frpandroid.util.AndroidUtils 6 | import com.github.jing332.frpandroid.util.StringUtils.toNumberInt 7 | 8 | object AppUpdateChecker { 9 | const val TAG = "AppUpdateChecker" 10 | 11 | fun checkUpdate(): UpdateResult { 12 | val rel = Github.getLatestRelease() 13 | 14 | val latest = rel.tagName.toNumberInt() 15 | val current = BuildConfig.VERSION_NAME.toNumberInt() 16 | Log.i(TAG, "checkUpdate: current=$current, latest=$latest") 17 | 18 | if (current < latest) { 19 | val ass = getApkDownloadUrl(rel.assets) 20 | return UpdateResult( 21 | version = rel.tagName, 22 | content = rel.body, 23 | downloadUrl = ass.browserDownloadUrl 24 | ).apply { 25 | Log.i(TAG, "checkUpdate: hasUpdate=${this.downloadUrl}") 26 | } 27 | } 28 | 29 | return UpdateResult() 30 | } 31 | 32 | private fun getApkDownloadUrl(assets: List): Github.Release.Asset { 33 | val abi = AndroidUtils.getABI() 34 | 35 | // 最大的为全量apk 36 | val apkUniversal = assets.sortedByDescending { it.size }[0] 37 | // 根据CPU ABI判断精简版apk 38 | val liteApk = 39 | assets.find { it.name.endsWith("${abi}.apk") } 40 | 41 | return liteApk ?: apkUniversal 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/ShortCuts.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.core.content.pm.ShortcutInfoCompat 6 | import androidx.core.content.pm.ShortcutManagerCompat 7 | import androidx.core.graphics.drawable.IconCompat 8 | import com.github.jing332.frpandroid.R 9 | import com.github.jing332.frpandroid.ui.SwitchFrpActivity 10 | 11 | 12 | object ShortCuts { 13 | private inline fun buildIntent(context: Context): Intent { 14 | val intent = Intent(context, T::class.java) 15 | intent.action = Intent.ACTION_VIEW 16 | return intent 17 | } 18 | 19 | 20 | private fun buildFrpcSwitchShortCutInfo(context: Context): ShortcutInfoCompat { 21 | val msSwitchIntent = buildIntent(context).apply { action = "frpc" } 22 | return ShortcutInfoCompat.Builder(context, "frpc_switch") 23 | .setShortLabel(context.getString(R.string.frpc_switch)) 24 | .setLongLabel(context.getString(R.string.frpc_switch)) 25 | .setIcon(IconCompat.createWithResource(context, R.drawable.ic_frpc)) 26 | .setIntent(msSwitchIntent) 27 | .build() 28 | } 29 | 30 | private fun buildFrpsSwitchShortCutInfo(context: Context): ShortcutInfoCompat { 31 | val msSwitchIntent = buildIntent(context).apply { action = "frps" } 32 | return ShortcutInfoCompat.Builder(context, "frps_switch") 33 | .setShortLabel(context.getString(R.string.frps_switch)) 34 | .setLongLabel(context.getString(R.string.frps_switch)) 35 | .setIcon(IconCompat.createWithResource(context, R.drawable.ic_frps)) 36 | .setIntent(msSwitchIntent) 37 | .build() 38 | } 39 | 40 | 41 | fun buildShortCuts(context: Context) { 42 | ShortcutManagerCompat.setDynamicShortcuts( 43 | context, listOf( 44 | buildFrpcSwitchShortCutInfo(context), 45 | buildFrpsSwitchShortCutInfo(context) 46 | ) 47 | ) 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/UpdateResult.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model 2 | 3 | data class UpdateResult( 4 | val version: String = "", 5 | val time: String = "", 6 | val content: String = "", 7 | val downloadUrl: String = "", 8 | val size: Long = 0, 9 | ) { 10 | fun hasUpdate() = version.isNotBlank() && downloadUrl.isNotBlank() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/frp/DocumentTableManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model.frp 2 | 3 | import android.content.Context 4 | import com.github.jing332.frpandroid.util.FileUtils.readAllText 5 | import kotlinx.coroutines.coroutineScope 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.launch 8 | 9 | object DocumentTableManager { 10 | val serverTables = mutableListOf() 11 | val clientTables = mutableListOf
() 12 | val proxyTables = mutableListOf
() 13 | val visitorTables = mutableListOf
() 14 | 15 | suspend fun load(context: Context) = coroutineScope { 16 | fun mdString(path: String) = 17 | context.assets.open(path).readAllText() 18 | 19 | fun loadTables(mutableList: MutableList
, path: String) { 20 | launch { 21 | mutableList.clear() 22 | mutableList.addAll(parseTables(mdString(path))) 23 | } 24 | } 25 | 26 | loadTables(serverTables, "docs/zh/server-configures.md") 27 | loadTables(clientTables, "docs/zh/client-configures.md") 28 | loadTables(proxyTables, "docs/zh/proxy.md") 29 | loadTables(visitorTables, "docs/zh/visitor.md") 30 | } 31 | 32 | suspend fun findDocumentToMarkdown(name: String): String { 33 | while (clientTables.isEmpty() || serverTables.isEmpty()) { 34 | delay(500) 35 | } 36 | 37 | fun kv(key: String, value: String) = "${key}:`${value}`".replace("``", "无") 38 | 39 | val sb = StringBuilder() 40 | fun addParameter(parameter: Parameter) { 41 | sb.appendLine("参数: `${parameter.name}`") 42 | sb.appendLine() 43 | sb.appendLine("类型:`" + parameter.type + "`") 44 | sb.appendLine() 45 | sb.appendLine(kv("默认", parameter.defaultValue)) 46 | sb.appendLine() 47 | sb.appendLine(kv("可选", parameter.optionalValues)) 48 | sb.appendLine() 49 | sb.appendLine("> " + parameter.description) 50 | sb.appendLine() 51 | sb.appendLine("> " + parameter.remark) 52 | sb.appendLine() 53 | sb.appendLine() 54 | } 55 | 56 | fun addTables(list: List
, type: String) { 57 | val ret = mutableListOf() 58 | list.forEach { table -> 59 | table.parameters.forEach { parameter -> 60 | if (parameter.name == name) { 61 | ret.add(parameter) 62 | } 63 | } 64 | } 65 | if (ret.isNotEmpty()) { 66 | sb.appendLine("## $type") 67 | sb.appendLine() 68 | for (v in ret.distinctBy { it.name }) { 69 | addParameter(v) 70 | } 71 | } 72 | } 73 | 74 | addTables(clientTables, "Client") 75 | addTables(serverTables, "Server") 76 | addTables(proxyTables, "Proxy") 77 | addTables(visitorTables, "Visitor") 78 | 79 | return sb.toString() 80 | } 81 | 82 | 83 | data class Table( 84 | val title: String, 85 | val parameters: List 86 | ) 87 | 88 | data class Parameter( 89 | val name: String, 90 | val type: String, 91 | val description: String, 92 | val defaultValue: String, 93 | val optionalValues: String, 94 | val remark: String 95 | ) 96 | 97 | fun parseTables(tableString: String): List
{ 98 | val lines = tableString.trim().lines() 99 | val tables = mutableListOf
() 100 | 101 | var currentTableTitle = "" 102 | var currentTableRows = mutableListOf() 103 | for (line in lines) { 104 | if (line.startsWith("##")) { 105 | if (currentTableTitle.isNotEmpty()) { 106 | tables.add(Table(currentTableTitle, currentTableRows)) 107 | currentTableRows = mutableListOf() 108 | } 109 | currentTableTitle = line.trimStart { it == '#' } 110 | } else if (line.startsWith("|")) { 111 | val values = line.trim('|').split("|").map { it.trim() } 112 | if (values.size >= 6) { 113 | val parameter = Parameter( 114 | name = values[0], 115 | type = values[1], 116 | description = values[2], 117 | defaultValue = values[3], 118 | optionalValues = values[4], 119 | remark = values[5] 120 | ) 121 | 122 | // Ignore row: 123 | // name=参数, type=类型, description=说明, defaultValue=默认值, optionalValues=可选值, remark=备注 124 | // name=:---, type=:---, description=:---, defaultValue=:---, optionalValues=:---, remark=:--- 125 | if (parameter.name.endsWith("---") && parameter.type.endsWith("---")) { 126 | currentTableRows.removeAt(currentTableRows.size - 1) 127 | continue 128 | } 129 | 130 | currentTableRows.add(parameter) 131 | } 132 | } 133 | } 134 | 135 | if (currentTableTitle.isNotEmpty()) 136 | tables.add(Table(currentTableTitle, currentTableRows)) 137 | 138 | return tables 139 | } 140 | 141 | 142 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/frp/Frp.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model.frp 2 | 3 | import android.content.Context 4 | import com.github.jing332.frpandroid.util.FileUtils.readAllText 5 | import java.io.IOException 6 | 7 | 8 | abstract class Frp(open val execPath: String) { 9 | companion object { 10 | const val TAG = "Frp" 11 | const val RET_OK = 0 12 | 13 | fun Context.getFrpDir(): String = getExternalFilesDir("frp")!!.absolutePath 14 | } 15 | 16 | 17 | abstract fun getConfigFilePath(): String 18 | 19 | fun start(configFilePath: String = getConfigFilePath()): Process { 20 | return execWithParams(params = arrayOf("-c", configFilePath)) 21 | } 22 | 23 | /** 24 | * @return 空字符串表示成功,否则返回错误信息 25 | */ 26 | open fun verify(configFilePath: String): String { 27 | val p = execWithParams(params = arrayOf("verify", "-c", configFilePath)) 28 | val ret = p.waitFor() 29 | 30 | return if (ret == RET_OK) 31 | "" 32 | else 33 | p.errorStream.readAllText() 34 | } 35 | 36 | fun version(): String { 37 | val p = execWithParams(params = arrayOf("-v")) 38 | p.waitFor() 39 | p.inputStream?.let { 40 | it.bufferedReader().use { buffered -> 41 | return buffered.readLine() 42 | } 43 | } 44 | 45 | return "" 46 | } 47 | 48 | @Throws(IOException::class) 49 | fun execWithParams( 50 | redirect: Boolean = false, 51 | vararg params: String 52 | ): Process { 53 | val cmdline = arrayOfNulls(params.size + 1) 54 | cmdline[0] = execPath 55 | System.arraycopy(params, 0, cmdline, 1, params.size) 56 | return ProcessBuilder(*cmdline).redirectErrorStream(redirect).start() 57 | ?: throw IOException("Process is null!") 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/frp/Frpc.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model.frp 2 | 3 | import android.content.Context 4 | import java.io.File 5 | 6 | class Frpc(val context: Context) : 7 | Frp(context.applicationInfo.nativeLibraryDir + File.separator + "libfrpc.so") { 8 | 9 | 10 | override fun getConfigFilePath(): String = 11 | "${context.getFrpDir()}/frpc.ini" 12 | 13 | private var mProcess: Process? = null 14 | val process: Process? 15 | get() = mProcess 16 | 17 | fun startup() { 18 | mProcess = start() 19 | } 20 | 21 | fun shutdown() { 22 | mProcess?.destroy() 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/frp/Frps.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model.frp 2 | 3 | import android.content.Context 4 | import java.io.File 5 | 6 | class Frps(val context: Context) : 7 | Frp(context.applicationInfo.nativeLibraryDir + File.separator + "libfrps.so") { 8 | override fun getConfigFilePath(): String = 9 | "${context.getExternalFilesDir("frp")!!.absolutePath}/frps.ini" 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/frp/LogUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model.frp 2 | 3 | import com.github.jing332.frpandroid.constant.LogLevel 4 | 5 | object LogUtils { 6 | @Suppress("RegExpRedundantEscape") 7 | fun String.evalLog(): Log? { 8 | val regexPattern = 9 | Regex("""(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] \[(\w+.\w+:\d+)\] (.*)""") 10 | 11 | val matchResult = regexPattern.find(this) 12 | return matchResult?.let { result -> 13 | val time = result.groupValues[1] 14 | val level = result.groupValues[2] 15 | val code = result.groupValues[3] 16 | val msg = result.groupValues[4] 17 | 18 | val l = when (level) { 19 | "D" -> LogLevel.DEBUG 20 | "I" -> LogLevel.INFO 21 | "W" -> LogLevel.WARN 22 | "E" -> LogLevel.ERROR 23 | else -> LogLevel.INFO 24 | } 25 | return Log(time, l, code, msg) 26 | } 27 | } 28 | 29 | data class Log(val time: String, @LogLevel val level: Int, val code: String, val msg: String) 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/frp/config/IniConfigManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model.frp.config 2 | 3 | import android.os.FileObserver 4 | import android.util.Log 5 | import kotlinx.coroutines.CancellationException 6 | import kotlinx.coroutines.awaitCancellation 7 | import kotlinx.coroutines.coroutineScope 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.channelFlow 10 | import kotlinx.coroutines.runBlocking 11 | import java.io.File 12 | 13 | class IniConfigManager { 14 | companion object { 15 | const val TAG = "IniConfigManager" 16 | } 17 | 18 | var iniFilePath: String = "" 19 | 20 | var cfg: IniConfig? = null 21 | 22 | private var mFileObserver: FileObserver? = null 23 | 24 | private fun checkPath() { 25 | if (iniFilePath.isBlank()) 26 | throw IllegalArgumentException("iniFilePath is empty") 27 | } 28 | 29 | @Suppress("DEPRECATION") 30 | suspend fun flowConfig(): Flow = coroutineScope { 31 | checkPath() 32 | return@coroutineScope channelFlow { 33 | fun sendConfig() { 34 | val str = File(iniFilePath).readText() 35 | cfg = IniConfigParser.load(str) 36 | runBlocking { send(cfg!!) } 37 | } 38 | 39 | mFileObserver = object : FileObserver(iniFilePath, ALL_EVENTS) { 40 | override fun onEvent(event: Int, path: String?) { 41 | when (event) { 42 | MODIFY -> { 43 | Log.i(TAG, "MODIFY") 44 | sendConfig() 45 | } 46 | } 47 | } 48 | } 49 | 50 | mFileObserver!!.startWatching() 51 | sendConfig() 52 | 53 | try { 54 | awaitCancellation() 55 | } catch (e: CancellationException) { 56 | Log.i(TAG, "flowConfig: stopWatching() ...") 57 | } 58 | mFileObserver?.stopWatching() 59 | } 60 | } 61 | 62 | 63 | fun edit(section: String, key: String, value: String) { 64 | Log.i(TAG, "edit: section=$section, key=$key, newValue=$value") 65 | checkPath() 66 | var iniString = File(iniFilePath).readText() 67 | iniString = IniConfigParser.edit(iniString, section, key, value) 68 | dumpToFile(iniString) 69 | } 70 | 71 | fun dumpToFile(str: String) { 72 | checkPath() 73 | 74 | backupIniFile() 75 | val file = File(iniFilePath) 76 | file.writeText(str) 77 | } 78 | 79 | private fun backupIniFile() { 80 | val file = File(iniFilePath) 81 | val backupFile = File("$iniFilePath.bak") 82 | file.copyTo(backupFile, true) 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/model/frp/config/IniConfigParser.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.model.frp.config 2 | 3 | typealias IniKV = LinkedHashMap 4 | typealias IniConfig = LinkedHashMap 5 | 6 | object IniConfigParser { 7 | private fun matchSection(line: String): String { 8 | val sectionMatch = Regex("\\[(.*)\\]").find(line) 9 | if (sectionMatch != null) { 10 | return sectionMatch.groupValues[1].trim() 11 | } 12 | return "" 13 | } 14 | 15 | private fun matchKeyValue(line: String): Pair { 16 | val keyValueMatch = Regex("(.*)=(.*)").find(line) 17 | if (keyValueMatch != null) { 18 | val key = keyValueMatch.groupValues[1] 19 | val value = keyValueMatch.groupValues[2] 20 | return Pair(key.trim(), value.trim()) 21 | } 22 | return Pair("", "") 23 | } 24 | 25 | fun loadFromString(str: String): IniConfig { 26 | val map = IniConfig() 27 | var currentSection = "" 28 | for (line in str.lines()) { 29 | if (line.startsWith("#")) continue 30 | 31 | val section = matchSection(line) 32 | if (section.isNotEmpty()) { 33 | currentSection = section 34 | map[section] = IniKV() 35 | } 36 | 37 | val kv = matchKeyValue(line) 38 | if (kv.first.isNotEmpty() && kv.second.isNotEmpty()) { 39 | map[currentSection]?.set(kv.first, kv.second) 40 | } 41 | 42 | } 43 | 44 | return map 45 | } 46 | 47 | fun load(str: String): LinkedHashMap> { 48 | return loadFromString(str) 49 | } 50 | 51 | fun edit(str: String, section: String, key: String, value: String): String { 52 | val sb = StringBuilder() 53 | 54 | var isEdited = false 55 | var currentSection = "" 56 | for (line in str.lines()) { 57 | if (line.startsWith("#")) { 58 | sb.appendLine(line) 59 | continue 60 | } 61 | 62 | val s = matchSection(line) 63 | if (s.isNotEmpty()) { 64 | if (currentSection.isNotEmpty() && !isEdited && currentSection != s) { 65 | sb.appendLine("$key = $value") 66 | } 67 | currentSection = s 68 | sb.appendLine(line) 69 | continue 70 | } 71 | 72 | val kv = matchKeyValue(line) 73 | if (kv.first.isNotEmpty() && kv.second.isNotEmpty()) { 74 | if (currentSection == section && kv.first == key) { 75 | isEdited = true 76 | sb.appendLine("$key = $value") 77 | continue 78 | } 79 | } 80 | 81 | sb.appendLine(line) 82 | } 83 | 84 | return sb.toString() 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/service/FrpNotification.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.service 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.PendingIntent 7 | import android.content.Context 8 | import android.os.Build 9 | import com.github.jing332.frpandroid.R 10 | import com.github.jing332.frpandroid.ui.theme.androidColor 11 | 12 | @Suppress("DEPRECATION") 13 | object FrpNotification { 14 | fun contentPaddingFlag(): Int { 15 | // Android 12(S)+ 必须指定PendingIntent.FLAG_ 16 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 17 | PendingIntent.FLAG_IMMUTABLE 18 | else 19 | 0 20 | } 21 | 22 | fun Context.createNotification( 23 | title: String, 24 | contentText: String, 25 | icon: Int, 26 | chanelId: String, 27 | 28 | contentAction: PendingIntent, 29 | shutdownAction: PendingIntent, 30 | ): Notification { 31 | 32 | val color = com.github.jing332.frpandroid.ui.theme.seed.androidColor 33 | val smallIconRes: Int = icon 34 | val builder = Notification.Builder(applicationContext) 35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {/*Android 8.0+ 要求必须设置通知信道*/ 36 | val chan = NotificationChannel( 37 | chanelId, 38 | getString(R.string.frp_server), 39 | NotificationManager.IMPORTANCE_NONE 40 | ) 41 | chan.lightColor = color 42 | chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE 43 | val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 44 | service.createNotificationChannel(chan) 45 | builder.setChannelId(chanelId) 46 | } else { 47 | } 48 | 49 | return builder 50 | .setColor(color) 51 | .setContentTitle(title) 52 | .setContentText(contentText) 53 | .setSmallIcon(smallIconRes) 54 | .setContentIntent(contentAction) 55 | .addAction(0, getString(R.string.shutdown), shutdownAction) 56 | .build() 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/service/FrpServiceManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.service 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | 6 | object FrpServiceManager { 7 | 8 | fun Context.frpcSwitch() { 9 | if (frpcRunning()) { 10 | shutdownFrpc() 11 | } else { 12 | startFrpc() 13 | } 14 | } 15 | 16 | fun Context.frpsSwitch() { 17 | if (frpsRunning()) { 18 | shutdownFrps() 19 | } else { 20 | startFrps() 21 | } 22 | } 23 | 24 | fun Context.startFrpc() { 25 | startService(Intent(this, FrpcService::class.java)) 26 | } 27 | 28 | fun Context.shutdownFrpc() { 29 | startService(Intent(this, FrpcService::class.java).apply { 30 | action = FrpcService.ACTION_SHUTDOWN 31 | }) 32 | } 33 | 34 | fun Context.startFrps() { 35 | startService(Intent(this, FrpsService::class.java)) 36 | } 37 | 38 | fun Context.shutdownFrps() { 39 | startService(Intent(this, FrpsService::class.java).apply { 40 | action = FrpsService.ACTION_SHUTDOWN 41 | }) 42 | } 43 | 44 | fun frpcRunning(): Boolean = FrpcService.frpcRunning 45 | fun frpsRunning(): Boolean = FrpsService.frpRunning 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/service/FrpcService.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.service 2 | 3 | import android.app.PendingIntent 4 | import android.content.Intent 5 | import android.os.IBinder 6 | import com.github.jing332.frpandroid.BuildConfig 7 | import com.github.jing332.frpandroid.R 8 | import com.github.jing332.frpandroid.constant.FrpType 9 | import com.github.jing332.frpandroid.model.frp.Frp 10 | import com.github.jing332.frpandroid.model.frp.Frpc 11 | import com.github.jing332.frpandroid.service.FrpNotification.createNotification 12 | import com.github.jing332.frpandroid.ui.MainActivity 13 | 14 | class FrpcService( 15 | ) : FrpService(FrpType.FRPC, ACTION_SHUTDOWN, ACTION_STATUS_CHANGED) { 16 | companion object { 17 | 18 | const val TAG = "FrpcService" 19 | 20 | const val FOREGROUND_ID = 7000 21 | const val ACTION_SHUTDOWN = 22 | "${BuildConfig.APPLICATION_ID}.service.FrpcService.SHUTDOWN" 23 | 24 | const val ACTION_STATUS_CHANGED = 25 | "${BuildConfig.APPLICATION_ID}.service.FrpcService.ACTION_STATUS_CHANGED" 26 | 27 | var frpcRunning: Boolean = false 28 | } 29 | 30 | override var isRunning: Boolean 31 | get() = super.isRunning 32 | set(value) { 33 | super.isRunning = value 34 | frpcRunning = value 35 | } 36 | 37 | private val mFrp by lazy { 38 | Frpc(this) 39 | } 40 | 41 | override val frp: Frp 42 | get() = mFrp 43 | 44 | override fun onBind(intent: Intent?): IBinder? { 45 | return null 46 | } 47 | 48 | 49 | override fun onCreate() { 50 | super.onCreate() 51 | 52 | createNotification( 53 | title = "Frpc", 54 | contentText = "FrpAndroid", 55 | icon = R.drawable.ic_notification_frpc, 56 | chanelId = "frpc", 57 | contentAction = PendingIntent.getActivity( 58 | this, 59 | 0, 60 | Intent(this, MainActivity::class.java), 61 | FrpNotification.contentPaddingFlag() 62 | ), 63 | shutdownAction = PendingIntent.getBroadcast( 64 | this, 65 | 0, 66 | Intent(ACTION_SHUTDOWN), 67 | FrpNotification.contentPaddingFlag() 68 | ) 69 | ).also { 70 | startForeground(FOREGROUND_ID, it) 71 | } 72 | } 73 | 74 | 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/service/FrpsService.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.service 2 | 3 | import android.app.PendingIntent 4 | import android.content.Intent 5 | import com.github.jing332.frpandroid.BuildConfig 6 | import com.github.jing332.frpandroid.R 7 | import com.github.jing332.frpandroid.constant.FrpType 8 | import com.github.jing332.frpandroid.data.entities.FrpLog 9 | import com.github.jing332.frpandroid.model.frp.Frp 10 | import com.github.jing332.frpandroid.model.frp.Frps 11 | import com.github.jing332.frpandroid.service.FrpNotification.createNotification 12 | import com.github.jing332.frpandroid.ui.MainActivity 13 | 14 | class FrpsService() : 15 | FrpService(FrpType.FRPS, ACTION_SHUTDOWN, ACTION_STATUS_CHANGED) { 16 | private val mFrp by lazy { 17 | Frps(this) 18 | } 19 | 20 | override val frp: Frp 21 | get() = mFrp 22 | 23 | companion object { 24 | const val TAG = "FrpsService" 25 | const val FOREGROUND_ID = 7001 26 | const val ACTION_SHUTDOWN = "${BuildConfig.APPLICATION_ID}.service.FrpcService.SHUTDOWN" 27 | const val ACTION_STATUS_CHANGED = 28 | "${BuildConfig.APPLICATION_ID}.service.FrpcService.ACTION_STATUS_CHANGED" 29 | 30 | var frpRunning: Boolean = false 31 | } 32 | 33 | override var isRunning: Boolean 34 | get() = super.isRunning 35 | set(value) { 36 | super.isRunning = value 37 | frpRunning = value 38 | } 39 | 40 | override fun onCreate() { 41 | super.onCreate() 42 | 43 | createNotification( 44 | title = "Frps", 45 | contentText = "FrpAndroid", 46 | icon = R.drawable.ic_notification_frps, 47 | chanelId = "frps", 48 | contentAction = PendingIntent.getActivity( 49 | this, 50 | 0, 51 | Intent(this, MainActivity::class.java), 52 | PendingIntent.FLAG_UPDATE_CURRENT 53 | ), 54 | shutdownAction = PendingIntent.getBroadcast( 55 | this, 56 | 0, 57 | Intent(ACTION_SHUTDOWN), 58 | PendingIntent.FLAG_UPDATE_CURRENT 59 | ) 60 | ).also { 61 | startForeground(FOREGROUND_ID, it) 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/AboutDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui 2 | 3 | import android.content.Intent 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.text.selection.SelectionContainer 13 | import androidx.compose.material3.HorizontalDivider 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.TextButton 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.res.painterResource 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.unit.dp 26 | import androidx.core.net.toUri 27 | import com.github.jing332.frpandroid.BuildConfig 28 | import com.github.jing332.frpandroid.R 29 | import com.github.jing332.frpandroid.model.frp.Frpc 30 | import com.github.jing332.frpandroid.ui.widgets.AppDialog 31 | 32 | @Composable 33 | fun AboutDialog(onDismissRequest: () -> Unit) { 34 | val context = LocalContext.current 35 | 36 | AppDialog( 37 | onDismissRequest = onDismissRequest, 38 | title = { 39 | Row { 40 | Image( 41 | painter = painterResource(id = R.drawable.ic_app_launcher_foreground), 42 | contentDescription = "Logo", 43 | modifier = Modifier.align(Alignment.CenterVertically) 44 | ) 45 | Text( 46 | stringResource(id = R.string.app_name), 47 | modifier = Modifier 48 | .align(Alignment.CenterVertically) 49 | .padding(start = 8.dp) 50 | ) 51 | } 52 | }, 53 | content = { 54 | fun openUrl(uri: String) { 55 | context.startActivity( 56 | Intent(Intent.ACTION_VIEW).apply { 57 | data = uri.toUri() 58 | } 59 | ) 60 | } 61 | 62 | Column { 63 | val frpVersion = remember { Frpc(context).version() } 64 | SelectionContainer { 65 | Column { 66 | Text("APP - ${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE})") 67 | Text("Frp - $frpVersion") 68 | } 69 | } 70 | HorizontalDivider(Modifier.padding(vertical = 8.dp)) 71 | Text( 72 | "Github - FrpAndroid", 73 | color = MaterialTheme.colorScheme.primary, 74 | fontWeight = FontWeight.Bold, 75 | modifier = Modifier 76 | .clickable { 77 | openUrl("https://github.com/jing332/FrpAndroid") 78 | } 79 | .padding(vertical = 8.dp) 80 | .fillMaxWidth() 81 | ) 82 | Spacer(modifier = Modifier.height(4.dp)) 83 | Text( 84 | "Github - Frp", 85 | color = MaterialTheme.colorScheme.primary, 86 | fontWeight = FontWeight.Bold, 87 | modifier = Modifier 88 | .clickable { 89 | openUrl("https://github.com/fatedier/frp/releases/${frpVersion}") 90 | } 91 | .padding(vertical = 8.dp) 92 | .fillMaxWidth() 93 | ) 94 | 95 | } 96 | }, 97 | buttons = { 98 | TextButton(onClick = { 99 | onDismissRequest() 100 | context.startActivity( 101 | Intent(context, LibrariesActivity::class.java).setAction( 102 | Intent.ACTION_VIEW 103 | ) 104 | ) 105 | }) { 106 | Text(text = stringResource(id = R.string.open_source_license)) 107 | } 108 | 109 | TextButton(onClick = { onDismissRequest() }) { 110 | Text(stringResource(id = R.string.ok)) 111 | } 112 | }) 113 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/AppUpdateDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.wrapContentHeight 12 | import androidx.compose.foundation.rememberScrollState 13 | import androidx.compose.foundation.verticalScroll 14 | import androidx.compose.material3.AlertDialog 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.HorizontalDivider 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.Surface 19 | import androidx.compose.material3.Text 20 | import androidx.compose.material3.TextButton 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.platform.LocalContext 28 | import androidx.compose.ui.tooling.preview.Preview 29 | import androidx.compose.ui.unit.dp 30 | import com.github.jing332.frpandroid.ui.widgets.AppDialog 31 | import com.github.jing332.frpandroid.ui.widgets.Markdown 32 | import com.github.jing332.frpandroid.util.ClipboardUtils 33 | 34 | @Preview 35 | @Composable 36 | private fun PreviewAppUpdateDialog() { 37 | var show by remember { androidx.compose.runtime.mutableStateOf(true) } 38 | if (show) 39 | AppUpdateDialog( 40 | onDismissRequest = { 41 | show = false 42 | }, version = "1.0.0", content = "## 更新内容\n\n- 123", downloadUrl = "url" 43 | ) 44 | 45 | } 46 | 47 | @OptIn(ExperimentalMaterial3Api::class) 48 | @Composable 49 | fun AppUpdateDialog( 50 | onDismissRequest: () -> Unit, 51 | version: String, 52 | content: String, 53 | downloadUrl: String 54 | ) { 55 | val context = LocalContext.current 56 | fun openDownloadUrl(url: String) { 57 | ClipboardUtils.copyText("FrpAndroid下载链接", url) 58 | context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) 59 | } 60 | 61 | AppDialog(onDismissRequest = onDismissRequest, 62 | title = { 63 | Text( 64 | "新版本", 65 | style = MaterialTheme.typography.titleLarge, 66 | ) 67 | }, 68 | content = { 69 | Column { 70 | Text( 71 | text = version, 72 | style = MaterialTheme.typography.titleSmall, 73 | modifier = Modifier.align(Alignment.CenterHorizontally) 74 | ) 75 | 76 | val scrollState = rememberScrollState() 77 | Column( 78 | Modifier 79 | .padding(8.dp) 80 | .verticalScroll(scrollState), 81 | verticalArrangement = Arrangement.Center 82 | ) { 83 | Markdown( 84 | content = content, 85 | modifier = Modifier 86 | .padding(4.dp), 87 | ) 88 | 89 | Spacer(modifier = Modifier.height(12.dp)) 90 | 91 | } 92 | } 93 | }, 94 | buttons = { 95 | Row { 96 | TextButton(onClick = { openDownloadUrl(downloadUrl) }) { 97 | Text("下载(Github)") 98 | } 99 | TextButton(onClick = { openDownloadUrl("https://ghproxy.com/${downloadUrl}") }) { 100 | Text("下载(ghproxy加速)") 101 | } 102 | } 103 | } 104 | ) 105 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/BaseComposeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.runtime.Composable 7 | import androidx.core.view.WindowCompat 8 | import com.github.jing332.frpandroid.ui.theme.AppTheme 9 | import com.github.jing332.frpandroid.ui.widgets.TransparentSystemBars 10 | 11 | abstract class BaseComposeActivity() : ComponentActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | WindowCompat.setDecorFitsSystemWindows(window, false) 15 | 16 | setContent { 17 | AppTheme { 18 | TransparentSystemBars() 19 | Content() 20 | } 21 | } 22 | } 23 | 24 | @Composable 25 | open fun Content() { 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/LibrariesActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.ArrowBack 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.IconButton 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Scaffold 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TopAppBar 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import com.github.jing332.frpandroid.R 18 | import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer 19 | import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults 20 | 21 | class LibrariesActivity : BaseComposeActivity() { 22 | @OptIn(ExperimentalMaterial3Api::class) 23 | @Composable 24 | override fun Content() { 25 | Scaffold( 26 | topBar = { 27 | TopAppBar( 28 | title = { Text(text = stringResource(id = R.string.open_source_license)) }, 29 | navigationIcon = { 30 | IconButton(onClick = { finish() }) { 31 | Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(id = R.string.back)) 32 | } 33 | } 34 | ) 35 | } 36 | ) { 37 | LibrariesContainer( 38 | Modifier 39 | .fillMaxSize() 40 | .padding(it), 41 | colors = LibraryDefaults.libraryColors( 42 | backgroundColor = MaterialTheme.colorScheme.background, 43 | contentColor = MaterialTheme.colorScheme.onBackground, 44 | badgeBackgroundColor = MaterialTheme.colorScheme.primaryContainer, 45 | badgeContentColor = MaterialTheme.colorScheme.onPrimaryContainer 46 | ), 47 | ) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import androidx.activity.viewModels 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Scaffold 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.CompositionLocalProvider 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.runtime.rememberCoroutineScope 12 | import androidx.compose.runtime.staticCompositionLocalOf 13 | import androidx.compose.ui.Modifier 14 | import androidx.lifecycle.lifecycleScope 15 | import androidx.navigation.compose.rememberNavController 16 | import com.github.jing332.frpandroid.config.AppConfig 17 | import com.github.jing332.frpandroid.model.ShortCuts 18 | import com.github.jing332.frpandroid.ui.MyTools.killBattery 19 | import com.github.jing332.frpandroid.ui.nav.BottomNavBar 20 | import com.github.jing332.frpandroid.ui.nav.NavigationGraph 21 | import kotlinx.coroutines.launch 22 | 23 | val LocalMainViewModel = staticCompositionLocalOf { 24 | error("No MainViewModel provided") 25 | } 26 | 27 | class MainActivity : BaseComposeActivity() { 28 | companion object { 29 | private const val TAG = "MainActivity" 30 | } 31 | 32 | private val vm: MainViewModel by viewModels() 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | 37 | lifecycleScope.launch { 38 | ShortCuts.buildShortCuts(this@MainActivity) 39 | } 40 | } 41 | 42 | 43 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") 44 | @Composable 45 | override fun Content() { 46 | val scope = rememberCoroutineScope() 47 | 48 | if (vm.showUpdateDialog != null) { 49 | val data = vm.showUpdateDialog ?: return 50 | AppUpdateDialog( 51 | onDismissRequest = { vm.showUpdateDialog = null }, 52 | content = data.content, 53 | version = data.version, 54 | downloadUrl = data.downloadUrl, 55 | ) 56 | } 57 | 58 | LaunchedEffect(vm.hashCode()) { 59 | vm.checkAppUpdate() 60 | } 61 | CompositionLocalProvider( 62 | LocalMainViewModel provides vm 63 | ) { 64 | 65 | val navController = rememberNavController() 66 | Scaffold( 67 | bottomBar = { 68 | BottomNavBar(navController) 69 | } 70 | ) { 71 | NavigationGraph( 72 | navController = navController, 73 | Modifier.padding(bottom = it.calculateBottomPadding()) 74 | ) 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.github.jing332.frpandroid.config.AppConfig 10 | import com.github.jing332.frpandroid.model.AppUpdateChecker 11 | import com.github.jing332.frpandroid.model.UpdateResult 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.launch 14 | 15 | class MainViewModel : ViewModel() { 16 | companion object { 17 | const val TAG = "MainViewModel" 18 | } 19 | 20 | var showUpdateDialog by mutableStateOf(null) 21 | 22 | fun checkAppUpdate() { 23 | if (AppConfig.isAutoCheckUpdate.value) 24 | viewModelScope.launch(Dispatchers.IO) { 25 | runCatching { 26 | val ret = AppUpdateChecker.checkUpdate() 27 | if (ret.hasUpdate()) showUpdateDialog = ret 28 | }.onFailure { 29 | Log.e(TAG, "checkAppUpdate: ", it) 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/MyTools.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.PendingIntent 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.ShortcutInfo 8 | import android.content.pm.ShortcutManager 9 | import android.graphics.drawable.Icon 10 | import android.net.Uri 11 | import android.os.Build 12 | import android.provider.Settings 13 | import com.github.jing332.frpandroid.util.ToastUtils.longToast 14 | import splitties.systemservices.powerManager 15 | 16 | object MyTools { 17 | 18 | @SuppressLint("BatteryLife") 19 | fun Context.killBattery(): Boolean { 20 | runCatching { 21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 22 | return if (!powerManager.isIgnoringBatteryOptimizations(packageName)) { 23 | startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { 24 | data = Uri.parse("package:$packageName") 25 | }) 26 | false 27 | } else { 28 | true 29 | } 30 | } 31 | }.onFailure { 32 | longToast("请求电池优化白名单失败") 33 | } 34 | 35 | return false 36 | } 37 | 38 | /* 添加快捷方式 */ 39 | @SuppressLint("UnspecifiedImmutableFlag") 40 | @Suppress("DEPRECATION") 41 | fun addShortcut( 42 | ctx: Context, 43 | name: String, 44 | id: String, 45 | iconResId: Int, 46 | launcherIntent: Intent 47 | ) { 48 | if (Build.VERSION.SDK_INT < 26) { /* Android8.0 */ 49 | ctx.longToast("如失败 请手动授予权限") 50 | 51 | val addShortcutIntent = Intent("com.android.launcher.action.INSTALL_SHORTCUT") 52 | // 不允许重复创建 53 | addShortcutIntent.putExtra("duplicate", false) // 经测试不是根据快捷方式的名字判断重复的 54 | addShortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name) 55 | addShortcutIntent.putExtra( 56 | Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 57 | Intent.ShortcutIconResource.fromContext( 58 | ctx, iconResId 59 | ) 60 | ) 61 | 62 | launcherIntent.action = Intent.ACTION_MAIN 63 | launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER) 64 | addShortcutIntent 65 | .putExtra(Intent.EXTRA_SHORTCUT_INTENT, launcherIntent) 66 | 67 | // 发送广播 68 | ctx.sendBroadcast(addShortcutIntent) 69 | } else { 70 | val shortcutManager: ShortcutManager = ctx.getSystemService(ShortcutManager::class.java) 71 | if (shortcutManager.isRequestPinShortcutSupported) { 72 | launcherIntent.action = Intent.ACTION_VIEW 73 | val pinShortcutInfo = ShortcutInfo.Builder(ctx, id) 74 | .setIcon( 75 | Icon.createWithResource(ctx, iconResId) 76 | ) 77 | .setIntent(launcherIntent) 78 | .setShortLabel(name) 79 | .build() 80 | val pinnedShortcutCallbackIntent = shortcutManager 81 | .createShortcutResultIntent(pinShortcutInfo) 82 | //Get notified when a shortcut is pinned successfully// 83 | val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 84 | PendingIntent.FLAG_IMMUTABLE 85 | } else { 86 | 0 87 | } 88 | val successCallback = PendingIntent.getBroadcast( 89 | ctx, 0, pinnedShortcutCallbackIntent, pendingIntentFlags 90 | ) 91 | shortcutManager.requestPinShortcut( 92 | pinShortcutInfo, successCallback.intentSender 93 | ) 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/SwitchFrpActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import com.github.jing332.frpandroid.service.FrpServiceManager.frpcSwitch 6 | import com.github.jing332.frpandroid.util.ToastUtils.longToast 7 | import com.github.jing332.frpandroid.util.ToastUtils.toast 8 | 9 | class SwitchFrpActivity : Activity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | switch((intent.type ?: "").ifBlank { intent.action ?: "" }) 14 | 15 | finish() 16 | } 17 | 18 | private fun switch(type: String) { 19 | toast("$type 启动中") 20 | when (type) { 21 | "frpc" -> { 22 | frpcSwitch() 23 | } 24 | 25 | "frps" -> { 26 | frpcSwitch() 27 | } 28 | 29 | else -> { 30 | longToast("未知action: ${intent.action}") 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/BasicFrpScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.pager.HorizontalPager 11 | import androidx.compose.foundation.pager.rememberPagerState 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.ElevatedCard 14 | import androidx.compose.material3.HorizontalDivider 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Tab 17 | import androidx.compose.material3.TabRow 18 | import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.LaunchedEffect 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.rememberCoroutineScope 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.draw.clip 27 | import androidx.compose.ui.res.stringResource 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.unit.dp 30 | import com.github.jing332.frpandroid.R 31 | import kotlinx.coroutines.launch 32 | 33 | @OptIn(ExperimentalFoundationApi::class) 34 | @Composable 35 | fun BasicFrpScreen( 36 | modifier: Modifier, 37 | configScreen: @Composable () -> Unit, 38 | logScreen: @Composable () -> Unit, 39 | pageIndex: Int, 40 | onPageIndexChanged: (Int) -> Unit 41 | ) { 42 | val scope = rememberCoroutineScope() 43 | 44 | Column(modifier) { 45 | val tabs = remember { listOf(R.string.config, R.string.log) } 46 | val pagerState = rememberPagerState(pageIndex) { tabs.size } 47 | 48 | LaunchedEffect(pageIndex) { 49 | pagerState.scrollToPage(pageIndex) 50 | } 51 | 52 | LaunchedEffect(pagerState.currentPage) { 53 | onPageIndexChanged(pagerState.currentPage) 54 | } 55 | 56 | ElevatedCard { 57 | TabRow( 58 | modifier = Modifier.align(Alignment.CenterHorizontally), 59 | selectedTabIndex = pagerState.currentPage, 60 | indicator = { tabPositions -> 61 | TabIndicator( 62 | color = MaterialTheme.colorScheme.primary, 63 | modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]) 64 | ) 65 | }, 66 | divider = { 67 | HorizontalDivider() 68 | } 69 | ) { 70 | tabs.forEachIndexed { index, strId -> 71 | val selected = index == pagerState.currentPage 72 | Tab( 73 | text = { 74 | Text( 75 | stringResource(id = strId), 76 | fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal 77 | ) 78 | }, 79 | selected = selected, 80 | onClick = { 81 | scope.launch { 82 | pagerState.animateScrollToPage(index) 83 | } 84 | }, 85 | selectedContentColor = MaterialTheme.colorScheme.primary, 86 | unselectedContentColor = MaterialTheme.colorScheme.tertiary 87 | ) 88 | } 89 | } 90 | 91 | HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { 92 | when (it) { 93 | 0 -> configScreen() 94 | 1 -> logScreen() 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | 102 | @Composable 103 | fun TabIndicator(color: androidx.compose.ui.graphics.Color, modifier: Modifier = Modifier) { 104 | Box( 105 | modifier 106 | .height(4.dp) 107 | .clip(RoundedCornerShape(8.dp)) 108 | .padding(horizontal = 28.dp) 109 | .background( 110 | color = MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(8.dp) 111 | ) 112 | ) 113 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/BottomNavBar.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav 2 | 3 | import androidx.compose.material3.NavigationBar 4 | import androidx.compose.material3.NavigationBarItem 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.res.stringResource 8 | import androidx.navigation.NavHostController 9 | import androidx.navigation.compose.currentBackStackEntryAsState 10 | 11 | @Composable 12 | fun BottomNavBar(navController: NavHostController) { 13 | val backStackEntry = navController.currentBackStackEntryAsState() 14 | NavigationBar { 15 | for (route in BottomNavRoute.routes) { 16 | val isSelected = backStackEntry.value?.destination?.route == route.id 17 | NavigationBarItem( 18 | icon = route.icon, 19 | label = { Text(stringResource(route.strId)) }, 20 | selected = isSelected, 21 | onClick = { 22 | navController.navigate(route.id) { 23 | popUpTo(navController.graph.startDestinationId) 24 | launchSingleTop = true 25 | } 26 | }, 27 | alwaysShowLabel = false 28 | ) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/BottomNavRoute.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.Settings 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.res.painterResource 9 | import com.github.jing332.frpandroid.R 10 | 11 | sealed class BottomNavRoute( 12 | @StringRes val strId: Int, 13 | val id: String, 14 | val icon: @Composable (() -> Unit), 15 | ) { 16 | companion object { 17 | val routes = listOf( 18 | Frps, 19 | Frpc, 20 | Settings, 21 | ) 22 | } 23 | 24 | data object Frps : BottomNavRoute(R.string.frps, "frps", { 25 | Icon( 26 | painterResource(id = R.drawable.server), null, 27 | ) 28 | }) 29 | 30 | data object Frpc : BottomNavRoute(R.string.frpc, "frpc", { 31 | Icon( 32 | painterResource(id = R.drawable.client), null, 33 | ) 34 | }) 35 | 36 | data object Settings : BottomNavRoute(R.string.settings, "settings", { 37 | Icon(Icons.Default.Settings, null) 38 | }) 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/FrpTopAppBar.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.AddBusiness 6 | import androidx.compose.material.icons.filled.MoreVert 7 | import androidx.compose.material3.DropdownMenu 8 | import androidx.compose.material3.DropdownMenuItem 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.material3.TopAppBar 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.platform.LocalContext 21 | import androidx.compose.ui.res.stringResource 22 | import com.github.jing332.frpandroid.R 23 | import com.github.jing332.frpandroid.ui.AboutDialog 24 | import com.github.jing332.frpandroid.ui.LocalMainViewModel 25 | 26 | @OptIn(ExperimentalMaterial3Api::class) 27 | @Composable 28 | fun FrpTopAppBar(type: String, subTitle: String, version: String, onAddShortcut: () -> Unit) { 29 | val context = LocalContext.current 30 | val mainVM = LocalMainViewModel.current 31 | 32 | var showMoreOptions by remember { mutableStateOf(false) } 33 | 34 | var showAboutDialog by remember { mutableStateOf(false) } 35 | if (showAboutDialog) { 36 | AboutDialog { 37 | showAboutDialog = false 38 | } 39 | } 40 | 41 | TopAppBar( 42 | title = { 43 | Column { 44 | Text("$type - v${version}", maxLines = 1) 45 | Text(subTitle, style = MaterialTheme.typography.bodyMedium) 46 | } 47 | }, 48 | actions = { 49 | IconButton(onClick = { 50 | onAddShortcut() 51 | }) { 52 | Icon( 53 | Icons.Default.AddBusiness, 54 | stringResource(R.string.add_desktop_shortcut) 55 | ) 56 | } 57 | 58 | 59 | IconButton(onClick = { 60 | showMoreOptions = true 61 | }) { 62 | DropdownMenu( 63 | expanded = showMoreOptions, 64 | onDismissRequest = { showMoreOptions = false }) { 65 | DropdownMenuItem( 66 | text = { Text(stringResource(R.string.check_update)) }, 67 | onClick = { 68 | showMoreOptions = false 69 | mainVM.checkAppUpdate() 70 | } 71 | ) 72 | 73 | DropdownMenuItem( 74 | text = { Text(stringResource(R.string.about)) }, 75 | onClick = { 76 | showMoreOptions = false 77 | showAboutDialog = true 78 | } 79 | ) 80 | } 81 | Icon(Icons.Default.MoreVert, stringResource(R.string.more_options)) 82 | } 83 | } 84 | ) 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/NavigationGraph.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.navigation.NavHostController 6 | import androidx.navigation.compose.NavHost 7 | import androidx.navigation.compose.composable 8 | import com.github.jing332.frpandroid.ui.nav.frpc.FrpcScreen 9 | import com.github.jing332.frpandroid.ui.nav.frps.FrpsScreen 10 | import com.github.jing332.frpandroid.ui.nav.settings.SettingsScreen 11 | 12 | @Composable 13 | fun NavigationGraph(navController: NavHostController, modifier: Modifier) { 14 | NavHost(navController, startDestination = BottomNavRoute.Frpc.id, modifier = modifier) { 15 | composable(BottomNavRoute.Frps.id) { 16 | FrpsScreen() 17 | } 18 | 19 | composable(BottomNavRoute.Frpc.id) { 20 | FrpcScreen() 21 | } 22 | 23 | composable(BottomNavRoute.Settings.id) { 24 | SettingsScreen() 25 | } 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/frpc/ConfigEditDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav.frpc 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.OutlinedTextField 11 | import androidx.compose.material3.Text 12 | import androidx.compose.material3.TextButton 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | import com.github.jing332.frpandroid.R 24 | import com.github.jing332.frpandroid.ui.widgets.AppDialog 25 | 26 | @OptIn(ExperimentalMaterial3Api::class) 27 | @Composable 28 | fun ConfigEditDialog( 29 | onDismissRequest: () -> Unit, 30 | section: String, 31 | key: String, 32 | value: String, 33 | onSave: (String) -> Unit 34 | ) { 35 | var text by remember { mutableStateOf(value) } 36 | AppDialog( 37 | title = { 38 | Text( 39 | text = stringResource(R.string.edit_config), 40 | style = MaterialTheme.typography.titleLarge, 41 | modifier = Modifier.padding(8.dp) 42 | ) 43 | }, 44 | content = { 45 | // Text( 46 | // section, 47 | // 48 | // modifier = Modifier.align(Alignment.Center) 49 | // ) 50 | 51 | OutlinedTextField( 52 | modifier = Modifier 53 | .padding(vertical = 24.dp, horizontal = 24.dp) 54 | .fillMaxWidth(), 55 | value = text, 56 | onValueChange = { text = it }, 57 | singleLine = true, 58 | label = { 59 | Text( 60 | key, 61 | fontWeight = FontWeight.Bold, 62 | ) 63 | } 64 | ) 65 | }, 66 | buttons = { 67 | Row( 68 | Modifier 69 | // .padding(end = 12.dp, bottom = 12.dp) 70 | ) { 71 | TextButton(onClick = onDismissRequest) { 72 | Text(text = stringResource(id = R.string.cancel)) 73 | } 74 | Spacer(modifier = Modifier.width(8.dp)) 75 | TextButton(onClick = { onSave(text) }) { 76 | Text(text = stringResource(id = R.string.save)) 77 | } 78 | } 79 | }, 80 | onDismissRequest = onDismissRequest 81 | ) 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/frpc/ConfigListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav.frpc 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.mutableStateListOf 5 | import androidx.compose.runtime.snapshots.SnapshotStateList 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.github.jing332.frpandroid.model.frp.DocumentTableManager 9 | import com.github.jing332.frpandroid.model.frp.config.IniConfig 10 | import com.github.jing332.frpandroid.model.frp.config.IniConfigManager 11 | import com.github.jing332.frpandroid.model.frp.config.IniConfigParser 12 | import com.github.jing332.frpandroid.util.FileUtils.readAllText 13 | import kotlinx.coroutines.flow.conflate 14 | import kotlinx.coroutines.launch 15 | 16 | class ConfigListViewModel : ViewModel() { 17 | private val groupedItems = mutableStateListOf>>() 18 | val filteredItems = mutableStateListOf>>() 19 | val cfgManager = IniConfigManager() 20 | 21 | private fun loadDocument(context: Context) { 22 | viewModelScope.launch { 23 | DocumentTableManager.load(context) 24 | } 25 | } 26 | 27 | fun hasRepeat(section: String, key: String): Boolean { 28 | for (v in groupedItems) { 29 | for (item in v.second) { 30 | if (item.section == section && item.key == key) { 31 | return true 32 | } 33 | } 34 | } 35 | 36 | return false 37 | } 38 | 39 | fun initFromIniString(context: Context, iniStr: String) { 40 | loadDocument(context) 41 | 42 | val fullIni = context.assets.open("defaultData/frpc_full.ini").readAllText() 43 | val map = IniConfigParser.load(fullIni) 44 | map.setToModels() 45 | } 46 | 47 | fun initFromFile(context: Context, iniFilePath: String) { 48 | loadDocument(context) 49 | 50 | cfgManager.iniFilePath = iniFilePath 51 | viewModelScope.launch { 52 | cfgManager.flowConfig().conflate().collect { 53 | it.setToModels() 54 | } 55 | } 56 | } 57 | 58 | fun saveConfig(section: String, key: String, value: String) { 59 | cfgManager.edit(section, key, value) 60 | } 61 | 62 | private fun IniConfig.setToModels() { 63 | groupedItems.clear() 64 | toGrouped().forEach { entry -> 65 | val list = mutableStateListOf() 66 | groupedItems.add(Pair(entry.key, list)) 67 | entry.value.forEach { 68 | list.add(it) 69 | } 70 | } 71 | 72 | filter() 73 | } 74 | 75 | suspend fun findDocumentToMarkdown(name: String): String { 76 | return DocumentTableManager.findDocumentToMarkdown(name) 77 | } 78 | 79 | private fun IniConfig.toGrouped(): HashMap> { 80 | val map = hashMapOf>() 81 | this.forEach { sections -> 82 | val section = sections.key 83 | 84 | sections.value.forEach { kv -> 85 | val key = kv.key 86 | val value = kv.value 87 | val group = Group(section) 88 | map[group] = map[group] ?: mutableListOf() 89 | (map[group]!! as MutableList).add(Item(section, key, value)) 90 | } 91 | } 92 | 93 | return map 94 | } 95 | 96 | fun filter(searchKey: String = "") { 97 | filteredItems.clear() 98 | 99 | fun filter(onFilter: (Item) -> Boolean) { 100 | for (item in groupedItems) { 101 | val subList = mutableStateListOf() 102 | filteredItems.add(Pair(item.first, subList)) 103 | item.second.forEach { 104 | if (onFilter(it)) subList.add(it) 105 | } 106 | 107 | if (subList.isEmpty()) 108 | filteredItems.removeAt(filteredItems.size - 1) 109 | } 110 | } 111 | 112 | if (searchKey.isBlank()) 113 | filter { true } 114 | else 115 | filter { 116 | it.key.contains(searchKey, true) || it.value.contains(searchKey, true) 117 | } 118 | } 119 | 120 | data class Group(val section: String) 121 | 122 | data class Item( 123 | val section: String, 124 | val key: String, 125 | val value: String, 126 | val id: String = "${section}_${key}" 127 | ) 128 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/frpc/ConfigSelectionDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav.frpc 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.imePadding 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.ModalBottomSheet 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.platform.LocalContext 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.compose.ui.unit.dp 23 | import com.github.jing332.frpandroid.R 24 | import com.github.jing332.frpandroid.ui.widgets.DenseOutlinedField 25 | import com.github.jing332.frpandroid.util.FileUtils.readAllText 26 | 27 | 28 | @OptIn(ExperimentalMaterial3Api::class) 29 | @Composable 30 | fun ConfigSelectionDialog( 31 | onDismissRequest: () -> Unit, 32 | onClick: (ConfigListViewModel.Item) -> Unit 33 | ) { 34 | val context = LocalContext.current 35 | val iniString = remember { 36 | context.assets.open("defaultData/frpc_full.ini").readAllText() 37 | } 38 | 39 | ModalBottomSheet(onDismissRequest = onDismissRequest) { 40 | Column( 41 | Modifier 42 | .fillMaxWidth() 43 | .padding(horizontal = 8.dp) 44 | .imePadding() 45 | ) { 46 | var searchKey by remember { mutableStateOf("") } 47 | DenseOutlinedField( 48 | value = searchKey, 49 | onValueChange = { searchKey = it }, 50 | modifier = Modifier.align(Alignment.CenterHorizontally), 51 | label = { Text(stringResource(id = R.string.search_filter)) } 52 | ) 53 | Spacer(modifier = Modifier.height(8.dp)) 54 | ConfigListScreen( 55 | modifier = Modifier.fillMaxSize(), 56 | iniString = iniString, 57 | filterKey = searchKey, 58 | onClickItem = onClick 59 | ) 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/frpc/ConfigViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav.frpc 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | class ConfigViewModel : ViewModel() { 6 | 7 | 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/frpc/LogScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav.frpc 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.foundation.lazy.itemsIndexed 10 | import androidx.compose.foundation.text.selection.SelectionContainer 11 | import androidx.compose.material3.AlertDialog 12 | import androidx.compose.material3.LocalTextStyle 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TextButton 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.collectAsState 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.text.style.TextOverflow 25 | import androidx.compose.ui.unit.Dp 26 | import androidx.compose.ui.unit.dp 27 | import com.github.jing332.frpandroid.R 28 | import com.github.jing332.frpandroid.constant.LogLevel 29 | import com.github.jing332.frpandroid.constant.LogLevel.Companion.toLevelString 30 | import com.github.jing332.frpandroid.data.entities.FrpLog 31 | import kotlinx.coroutines.flow.Flow 32 | 33 | @Composable 34 | fun LogScreen(modifier: Modifier, flowList: Flow>, paddingBottom: Dp = 48.dp) { 35 | // val list by appDb.serverLogDao.flowAll().collectAsState(initial = emptyList()) 36 | val list by flowList.collectAsState(initial = emptyList()) 37 | 38 | var showDescDialog by remember { mutableStateOf(null) } 39 | if (showDescDialog != null) 40 | AlertDialog( 41 | onDismissRequest = { showDescDialog = null }, 42 | title = { Text(stringResource(R.string.description)) }, 43 | text = { Text(showDescDialog.toString()) }, 44 | confirmButton = { 45 | TextButton( 46 | onClick = { showDescDialog = null }) { 47 | Text(stringResource(R.string.ok)) 48 | } 49 | } 50 | ) 51 | LazyColumn( 52 | Modifier 53 | .padding(8.dp) 54 | .fillMaxSize(), 55 | ) { 56 | itemsIndexed(list, key = { _, v -> v.id }) { index, item -> 57 | SelectionContainer { 58 | Text( 59 | "[${item.level.toLevelString()}] " + item.message, 60 | modifier = Modifier 61 | .fillMaxWidth() 62 | .padding(vertical = 2.dp), 63 | color = when (item.level) { 64 | LogLevel.DEBUG -> MaterialTheme.colorScheme.onBackground.copy( 65 | alpha = 0.5f 66 | ) 67 | 68 | LogLevel.INFO -> MaterialTheme.colorScheme.onBackground.copy( 69 | alpha = 0.8f 70 | ) 71 | 72 | LogLevel.WARN -> MaterialTheme.colorScheme.tertiary 73 | LogLevel.ERROR -> MaterialTheme.colorScheme.error 74 | else -> MaterialTheme.colorScheme.primary 75 | }, 76 | overflow = TextOverflow.Visible, 77 | lineHeight = LocalTextStyle.current.lineHeight * 0.8 78 | ) 79 | } 80 | 81 | if (index == list.size - 1) // 防止浮动按钮遮挡log 82 | Spacer(modifier = Modifier.height(paddingBottom)) 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/frps/FrpsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav.frps 2 | 3 | import androidx.compose.material3.Scaffold 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.platform.LocalContext 6 | import androidx.compose.ui.res.stringResource 7 | import com.github.jing332.frpandroid.R 8 | import com.github.jing332.frpandroid.model.frp.Frps 9 | import com.github.jing332.frpandroid.ui.nav.FrpTopAppBar 10 | 11 | @Composable 12 | fun FrpsScreen() { 13 | val context = LocalContext.current 14 | Scaffold( 15 | topBar = { 16 | FrpTopAppBar( 17 | type = stringResource(id = R.string.frps), 18 | subTitle = "Server", 19 | version = Frps(context).version() 20 | ) { 21 | } 22 | } 23 | ) { 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/nav/settings/SettingsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.nav.settings 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.statusBarsPadding 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.ArrowCircleUp 7 | import androidx.compose.material.icons.filled.Lock 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.LocalContext 15 | import com.github.jing332.frpandroid.config.AppConfig 16 | import com.github.jing332.frpandroid.ui.MyTools.killBattery 17 | import com.github.jing332.frpandroid.util.ToastUtils.longToast 18 | 19 | @Composable 20 | fun SettingsScreen() { 21 | val context = LocalContext.current 22 | Column(Modifier.statusBarsPadding()) { 23 | var checkUpdate by remember { AppConfig.isAutoCheckUpdate } 24 | PreferenceSwitch( 25 | title = { Text("自动检查更新") }, 26 | subTitle = { Text("打开程序主界面时从Github检查更新") }, 27 | checked = checkUpdate, 28 | onCheckedChange = { checkUpdate = it }, 29 | icon = { 30 | Icon(Icons.Default.ArrowCircleUp, contentDescription = null) 31 | } 32 | ) 33 | 34 | var wakeLock by remember { AppConfig.enabledWakeLock } 35 | PreferenceSwitch( 36 | title = { Text("唤醒锁") }, 37 | subTitle = { Text("打开后可防止锁屏后CPU休眠,但在部分系统可能会导致杀后台") }, 38 | checked = wakeLock, 39 | onCheckedChange = { wakeLock = it }, 40 | icon = { 41 | Icon(Icons.Default.Lock, contentDescription = null) 42 | } 43 | ) 44 | 45 | PreferenceWidget(onClick = { 46 | if (context.killBattery()){ 47 | context.longToast("程序已在电池优化白名单") 48 | } 49 | }, title = { 50 | Text("请求设置电池优化白名单") 51 | }, subTitle = { 52 | Text("如果程序在后台运行时会被系统杀死,可以尝试设置。") 53 | }) { 54 | 55 | } 56 | 57 | 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.toArgb 5 | 6 | val Color.colorCode: String 7 | get() { 8 | val colorCode = this.toArgb() 9 | return String.format("#%06X", 0xFFFFFF and colorCode) 10 | } 11 | 12 | val Color.androidColor: Int 13 | get() { 14 | return android.graphics.Color.parseColor(this.colorCode) 15 | } 16 | 17 | val Purple80 = Color(0xFFD0BCFF) 18 | val PurpleGrey80 = Color(0xFFCCC2DC) 19 | val Pink80 = Color(0xFFEFB8C8) 20 | 21 | val Purple40 = Color(0xFF6650a4) 22 | val PurpleGrey40 = Color(0xFF625b71) 23 | val Pink40 = Color(0xFF7D5260) 24 | 25 | 26 | val seed = Color(0xFF006590) 27 | val md_theme_light_primary = Color(0xFF006590) 28 | val md_theme_light_onPrimary = Color(0xFFFFFFFF) 29 | val md_theme_light_primaryContainer = Color(0xFFC8E6FF) 30 | val md_theme_light_onPrimaryContainer = Color(0xFF001E2E) 31 | val md_theme_light_secondary = Color(0xFF4F606E) 32 | val md_theme_light_onSecondary = Color(0xFFFFFFFF) 33 | val md_theme_light_secondaryContainer = Color(0xFFD2E5F5) 34 | val md_theme_light_onSecondaryContainer = Color(0xFF0B1D29) 35 | val md_theme_light_tertiary = Color(0xFF63597C) 36 | val md_theme_light_onTertiary = Color(0xFFFFFFFF) 37 | val md_theme_light_tertiaryContainer = Color(0xFFE9DDFF) 38 | val md_theme_light_onTertiaryContainer = Color(0xFF1F1635) 39 | val md_theme_light_error = Color(0xFFBA1A1A) 40 | val md_theme_light_errorContainer = Color(0xFFFFDAD6) 41 | val md_theme_light_onError = Color(0xFFFFFFFF) 42 | val md_theme_light_onErrorContainer = Color(0xFF410002) 43 | val md_theme_light_background = Color(0xFFFCFCFF) 44 | val md_theme_light_onBackground = Color(0xFF191C1E) 45 | val md_theme_light_surface = Color(0xFFFCFCFF) 46 | val md_theme_light_onSurface = Color(0xFF191C1E) 47 | val md_theme_light_surfaceVariant = Color(0xFFDDE3EA) 48 | val md_theme_light_onSurfaceVariant = Color(0xFF41484D) 49 | val md_theme_light_outline = Color(0xFF71787E) 50 | val md_theme_light_inverseOnSurface = Color(0xFFF0F0F3) 51 | val md_theme_light_inverseSurface = Color(0xFF2E3133) 52 | val md_theme_light_inversePrimary = Color(0xFF87CEFF) 53 | val md_theme_light_shadow = Color(0xFF000000) 54 | val md_theme_light_surfaceTint = Color(0xFF006590) 55 | val md_theme_light_outlineVariant = Color(0xFFC1C7CE) 56 | val md_theme_light_scrim = Color(0xFF000000) 57 | 58 | val md_theme_dark_primary = Color(0xFF87CEFF) 59 | val md_theme_dark_onPrimary = Color(0xFF00344D) 60 | val md_theme_dark_primaryContainer = Color(0xFF004C6D) 61 | val md_theme_dark_onPrimaryContainer = Color(0xFFC8E6FF) 62 | val md_theme_dark_secondary = Color(0xFFB7C9D8) 63 | val md_theme_dark_onSecondary = Color(0xFF21323E) 64 | val md_theme_dark_secondaryContainer = Color(0xFF384956) 65 | val md_theme_dark_onSecondaryContainer = Color(0xFFD2E5F5) 66 | val md_theme_dark_tertiary = Color(0xFFCDC0E9) 67 | val md_theme_dark_onTertiary = Color(0xFF342B4B) 68 | val md_theme_dark_tertiaryContainer = Color(0xFF4B4163) 69 | val md_theme_dark_onTertiaryContainer = Color(0xFFE9DDFF) 70 | val md_theme_dark_error = Color(0xFFFFB4AB) 71 | val md_theme_dark_errorContainer = Color(0xFF93000A) 72 | val md_theme_dark_onError = Color(0xFF690005) 73 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 74 | val md_theme_dark_background = Color(0xFF191C1E) 75 | val md_theme_dark_onBackground = Color(0xFFE2E2E5) 76 | val md_theme_dark_surface = Color(0xFF191C1E) 77 | val md_theme_dark_onSurface = Color(0xFFE2E2E5) 78 | val md_theme_dark_surfaceVariant = Color(0xFF41484D) 79 | val md_theme_dark_onSurfaceVariant = Color(0xFFC1C7CE) 80 | val md_theme_dark_outline = Color(0xFF8B9198) 81 | val md_theme_dark_inverseOnSurface = Color(0xFF191C1E) 82 | val md_theme_dark_inverseSurface = Color(0xFFE2E2E5) 83 | val md_theme_dark_inversePrimary = Color(0xFF006590) 84 | val md_theme_dark_shadow = Color(0xFF000000) 85 | val md_theme_dark_surfaceTint = Color(0xFF87CEFF) 86 | val md_theme_dark_outlineVariant = Color(0xFF41484D) 87 | val md_theme_dark_scrim = Color(0xFF000000) 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | import com.github.jing332.text_searcher.ui.theme.* 18 | 19 | private val LightColorScheme = lightColorScheme( 20 | primary = md_theme_light_primary, 21 | onPrimary = md_theme_light_onPrimary, 22 | primaryContainer = md_theme_light_primaryContainer, 23 | onPrimaryContainer = md_theme_light_onPrimaryContainer, 24 | secondary = md_theme_light_secondary, 25 | onSecondary = md_theme_light_onSecondary, 26 | secondaryContainer = md_theme_light_secondaryContainer, 27 | onSecondaryContainer = md_theme_light_onSecondaryContainer, 28 | tertiary = md_theme_light_tertiary, 29 | onTertiary = md_theme_light_onTertiary, 30 | tertiaryContainer = md_theme_light_tertiaryContainer, 31 | onTertiaryContainer = md_theme_light_onTertiaryContainer, 32 | error = md_theme_light_error, 33 | errorContainer = md_theme_light_errorContainer, 34 | onError = md_theme_light_onError, 35 | onErrorContainer = md_theme_light_onErrorContainer, 36 | background = md_theme_light_background, 37 | onBackground = md_theme_light_onBackground, 38 | surface = md_theme_light_surface, 39 | onSurface = md_theme_light_onSurface, 40 | surfaceVariant = md_theme_light_surfaceVariant, 41 | onSurfaceVariant = md_theme_light_onSurfaceVariant, 42 | outline = md_theme_light_outline, 43 | inverseOnSurface = md_theme_light_inverseOnSurface, 44 | inverseSurface = md_theme_light_inverseSurface, 45 | inversePrimary = md_theme_light_inversePrimary, 46 | surfaceTint = md_theme_light_surfaceTint, 47 | outlineVariant = md_theme_light_outlineVariant, 48 | scrim = md_theme_light_scrim, 49 | ) 50 | 51 | 52 | private val DarkColorScheme = darkColorScheme( 53 | primary = md_theme_dark_primary, 54 | onPrimary = md_theme_dark_onPrimary, 55 | primaryContainer = md_theme_dark_primaryContainer, 56 | onPrimaryContainer = md_theme_dark_onPrimaryContainer, 57 | secondary = md_theme_dark_secondary, 58 | onSecondary = md_theme_dark_onSecondary, 59 | secondaryContainer = md_theme_dark_secondaryContainer, 60 | onSecondaryContainer = md_theme_dark_onSecondaryContainer, 61 | tertiary = md_theme_dark_tertiary, 62 | onTertiary = md_theme_dark_onTertiary, 63 | tertiaryContainer = md_theme_dark_tertiaryContainer, 64 | onTertiaryContainer = md_theme_dark_onTertiaryContainer, 65 | error = md_theme_dark_error, 66 | errorContainer = md_theme_dark_errorContainer, 67 | onError = md_theme_dark_onError, 68 | onErrorContainer = md_theme_dark_onErrorContainer, 69 | background = md_theme_dark_background, 70 | onBackground = md_theme_dark_onBackground, 71 | surface = md_theme_dark_surface, 72 | onSurface = md_theme_dark_onSurface, 73 | surfaceVariant = md_theme_dark_surfaceVariant, 74 | onSurfaceVariant = md_theme_dark_onSurfaceVariant, 75 | outline = md_theme_dark_outline, 76 | inverseOnSurface = md_theme_dark_inverseOnSurface, 77 | inverseSurface = md_theme_dark_inverseSurface, 78 | inversePrimary = md_theme_dark_inversePrimary, 79 | surfaceTint = md_theme_dark_surfaceTint, 80 | outlineVariant = md_theme_dark_outlineVariant, 81 | scrim = md_theme_dark_scrim, 82 | ) 83 | 84 | 85 | @Composable 86 | fun AppTheme( 87 | darkTheme: Boolean = isSystemInDarkTheme(), 88 | // Dynamic color is available on Android 12+ 89 | dynamicColor: Boolean = true, 90 | content: @Composable () -> Unit 91 | ) { 92 | val colorScheme = when { 93 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 94 | val context = LocalContext.current 95 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 96 | } 97 | 98 | darkTheme -> DarkColorScheme 99 | else -> LightColorScheme 100 | } 101 | val view = LocalView.current 102 | if (!view.isInEditMode) { 103 | SideEffect { 104 | val window = (view.context as Activity).window 105 | window.statusBarColor = colorScheme.primary.toArgb() 106 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 107 | } 108 | } 109 | 110 | MaterialTheme( 111 | colorScheme = colorScheme, 112 | typography = Typography, 113 | content = content 114 | ) 115 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.text_searcher.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/widgets/ErrorDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.text_searcher.ui.widgets 2 | 3 | import androidx.compose.foundation.horizontalScroll 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.filled.ErrorOutline 10 | import androidx.compose.material3.AlertDialog 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.text.font.FontStyle 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import com.github.jing332.frpandroid.R 27 | 28 | 29 | @OptIn(ExperimentalMaterial3Api::class) 30 | @Composable 31 | fun ErrorDialog( 32 | t: Throwable? = null, 33 | title: String = stringResource(R.string.error), 34 | message: String = t?.localizedMessage ?: "", 35 | onDismiss: () -> Unit = {} 36 | ) { 37 | var isShow by remember { mutableStateOf(true) } 38 | if (isShow) 39 | AlertDialog( 40 | icon = { Icon(Icons.Filled.ErrorOutline, "", tint = MaterialTheme.colorScheme.error) }, 41 | title = { Text(title) }, 42 | text = { 43 | Column { 44 | Text( 45 | text = message, 46 | style = MaterialTheme.typography.titleSmall, 47 | color = MaterialTheme.colorScheme.error, 48 | modifier = Modifier.padding(bottom = 8.dp) 49 | ) 50 | t?.stackTraceToString()?.let { traceString -> 51 | val lines = traceString.lines() 52 | LazyColumn(modifier = Modifier.horizontalScroll(rememberScrollState())) { 53 | item { 54 | lines.forEach { 55 | Text( 56 | text = it, 57 | fontStyle = FontStyle.Italic, 58 | style = MaterialTheme.typography.bodySmall 59 | ) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | onDismissRequest = { 67 | isShow = false 68 | onDismiss.invoke() 69 | }, 70 | confirmButton = { 71 | Button(onClick = { 72 | isShow = false 73 | onDismiss.invoke() 74 | }) { 75 | Text(stringResource(android.R.string.ok)) 76 | } 77 | } 78 | ) 79 | } 80 | 81 | @Preview 82 | @Composable 83 | private fun PreviewErrorDialog() { 84 | ErrorDialog(Throwable("error")) 85 | } 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/widgets/ExpandableText.kt: -------------------------------------------------------------------------------- 1 | /* 2 | package com.github.jing332.text_searcher.ui.widgets 3 | 4 | import androidx.compose.animation.animateContentSize 5 | import androidx.compose.foundation.ExperimentalFoundationApi 6 | import androidx.compose.foundation.combinedClickable 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.material3.LocalTextStyle 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableIntStateOf 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.text.SpanStyle 21 | import androidx.compose.ui.text.TextStyle 22 | import androidx.compose.ui.text.buildAnnotatedString 23 | import androidx.compose.ui.text.font.FontFamily 24 | import androidx.compose.ui.text.font.FontStyle 25 | import androidx.compose.ui.text.font.FontWeight 26 | import androidx.compose.ui.text.style.TextAlign 27 | import androidx.compose.ui.text.withStyle 28 | import androidx.compose.ui.unit.TextUnit 29 | import com.github.jing332.text_searcher.R 30 | 31 | 32 | // from https://stackoverflow.com/a/72982110/13197001 33 | @OptIn(ExperimentalFoundationApi::class) 34 | @Composable 35 | fun ExpandableText( 36 | modifier: Modifier = Modifier, 37 | textModifier: Modifier = Modifier, 38 | style: TextStyle = LocalTextStyle.current, 39 | fontFamily: FontFamily = FontFamily.Default, 40 | fontStyle: FontStyle? = null, 41 | fontSize: TextUnit = LocalTextStyle.current.fontSize, 42 | fontWeight: FontWeight = LocalTextStyle.current.fontWeight ?: FontWeight.Normal, 43 | lineHeight: TextUnit = LocalTextStyle.current.lineHeight, 44 | text: String, 45 | collapsedMaxLine: Int = 2, 46 | showMoreText: String = stringResource(R.string.expandable_text_more), 47 | showMoreStyle: SpanStyle = SpanStyle( 48 | fontWeight = FontWeight.ExtraBold, 49 | color = MaterialTheme.colorScheme.primary 50 | ), 51 | showLessText: String = stringResource(R.string.expandable_text_less), 52 | showLessStyle: SpanStyle = showMoreStyle, 53 | textAlign: TextAlign? = null, 54 | 55 | onLongClick: (() -> Unit)? = null, 56 | onLongClickLabel: String? = null 57 | ) { 58 | var isExpanded by remember { mutableStateOf(false) } 59 | var clickable by remember { mutableStateOf(false) } 60 | var lastCharIndex by remember { mutableIntStateOf(0) } 61 | 62 | Box( 63 | modifier = Modifier 64 | .combinedClickable( 65 | onClick = { 66 | if (clickable) 67 | isExpanded = !isExpanded 68 | }, 69 | onLongClick = onLongClick, 70 | onLongClickLabel = onLongClickLabel, 71 | onClickLabel = if (isExpanded) showLessText else showMoreText 72 | ) 73 | .then(modifier) 74 | ) { 75 | Text( 76 | modifier = textModifier 77 | .fillMaxWidth() 78 | .animateContentSize(), 79 | text = buildAnnotatedString { 80 | if (clickable) { 81 | if (isExpanded) { 82 | append(text) 83 | withStyle(style = showLessStyle) { append(showLessText) } 84 | } else { 85 | val adjustText = text.substring(startIndex = 0, endIndex = lastCharIndex) 86 | .dropLast(showMoreText.length) 87 | .dropLastWhile { Character.isWhitespace(it) || it == '.' } 88 | append(adjustText) 89 | withStyle(style = showMoreStyle) { append(showMoreText) } 90 | } 91 | } else { 92 | append(text) 93 | } 94 | }, 95 | maxLines = if (isExpanded) Int.MAX_VALUE else collapsedMaxLine, 96 | fontStyle = fontStyle, 97 | onTextLayout = { textLayoutResult -> 98 | if (!isExpanded && textLayoutResult.hasVisualOverflow) { 99 | clickable = true 100 | lastCharIndex = textLayoutResult.getLineEnd(collapsedMaxLine - 1) 101 | } 102 | }, 103 | style = style, 104 | textAlign = textAlign, 105 | fontSize = fontSize, 106 | fontWeight = fontWeight, 107 | lineHeight = lineHeight, 108 | fontFamily = fontFamily 109 | ) 110 | } 111 | 112 | }*/ 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/widgets/ExposedDropTextField.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.text_searcher.ui.widgets 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.material3.DropdownMenuItem 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.material3.ExposedDropdownMenuBox 7 | import androidx.compose.material3.ExposedDropdownMenuDefaults 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TextField 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableIntStateOf 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.platform.LocalTextInputService 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import kotlin.math.max 23 | 24 | @OptIn(ExperimentalMaterial3Api::class) 25 | @Composable 26 | fun ExposedDropTextField( 27 | modifier: Modifier = Modifier, 28 | label: @Composable() (() -> Unit), 29 | key: Any, 30 | keys: List, 31 | values: List, 32 | onKeyChange: (key: Any) -> Unit, 33 | ) { 34 | var selectedText = values.getOrNull(max(0, keys.indexOf(key))) ?: "" 35 | var expanded by remember { mutableStateOf(false) } 36 | 37 | LaunchedEffect(keys){ 38 | keys.getOrNull(values.indexOf(selectedText))?.let(onKeyChange) 39 | } 40 | 41 | CompositionLocalProvider( 42 | LocalTextInputService provides null // Disable Keyboard 43 | ) { 44 | ExposedDropdownMenuBox( 45 | expanded = expanded, 46 | onExpandedChange = { 47 | expanded = !expanded 48 | }, 49 | ) { 50 | TextField( 51 | modifier = modifier.menuAnchor(), 52 | readOnly = true, 53 | value = selectedText, 54 | onValueChange = { }, 55 | label = label, 56 | trailingIcon = { 57 | ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) 58 | }, 59 | colors = ExposedDropdownMenuDefaults.textFieldColors() 60 | ) 61 | ExposedDropdownMenu( 62 | expanded = expanded, 63 | onDismissRequest = { expanded = false } 64 | ) { 65 | values.forEachIndexed { index, text -> 66 | val checked = key == keys[index] 67 | DropdownMenuItem( 68 | text = { 69 | Text( 70 | text, 71 | fontWeight = if (checked) FontWeight.Bold else FontWeight.Normal 72 | ) 73 | }, 74 | onClick = { 75 | expanded = false 76 | selectedText = text 77 | onKeyChange.invoke(keys[index]) 78 | }/*, modifier = Modifier.background( 79 | if (checked) MaterialTheme.colorScheme.surfaceVariant 80 | else Color.TRANSPARENT*/ 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | 89 | @Preview 90 | @Composable 91 | private fun ExposedDropTextFieldPreview() { 92 | var key by remember { mutableIntStateOf(1) } 93 | ExposedDropTextField( 94 | label = { Text("所属分组") }, 95 | key = key, 96 | keys = listOf(1, 2, 3), 97 | values = listOf("1", "2", "3"), 98 | ) { 99 | key = it as Int 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/widgets/LabelSlider.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.text_searcher.ui.widgets 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.BoxScope 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.wrapContentHeight 7 | import androidx.compose.material3.Slider 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | import androidx.constraintlayout.compose.ConstraintLayout 14 | 15 | @Composable 16 | fun LabelSlider( 17 | value: Float, 18 | onValueChange: (Float) -> Unit, 19 | modifier: Modifier = Modifier, 20 | enabled: Boolean = true, 21 | valueRange: ClosedFloatingPointRange = 0f..1f, 22 | steps: Int = 0, 23 | onValueChangeFinished: (() -> Unit)? = null, 24 | text: @Composable BoxScope.() -> Unit 25 | ) { 26 | ConstraintLayout(modifier) { 27 | val (textRef, sliderRef) = createRefs() 28 | Box( 29 | modifier = Modifier 30 | .constrainAs(textRef) { 31 | start.linkTo(parent.start) 32 | top.linkTo(parent.top) 33 | end.linkTo(parent.end) 34 | } 35 | .wrapContentHeight() 36 | ) { text() } 37 | Slider( 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .constrainAs(sliderRef) { 41 | start.linkTo(parent.start) 42 | end.linkTo(parent.end) 43 | 44 | top.linkTo(textRef.bottom, margin = (-16).dp) 45 | }, 46 | value = value, 47 | onValueChange = onValueChange, 48 | enabled = enabled, 49 | valueRange = valueRange, 50 | steps = steps, 51 | onValueChangeFinished = onValueChangeFinished 52 | ) 53 | } 54 | } 55 | 56 | @Preview 57 | @Composable 58 | fun PreviewSlider() { 59 | LabelSlider(value = 0f, onValueChange = {}) { 60 | Text("Hello World") 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/widgets/LoadingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.text_searcher.ui.widgets 2 | 3 | import androidx.compose.animation.core.animateFloat 4 | import androidx.compose.animation.core.infiniteRepeatable 5 | import androidx.compose.animation.core.keyframes 6 | import androidx.compose.animation.core.rememberInfiniteTransition 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.wrapContentWidth 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.material3.CircularProgressIndicator 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Surface 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.draw.rotate 26 | import androidx.compose.ui.graphics.Brush 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.tooling.preview.Preview 29 | import androidx.compose.ui.unit.Dp 30 | import androidx.compose.ui.unit.dp 31 | import androidx.compose.ui.window.Dialog 32 | import androidx.compose.ui.window.DialogProperties 33 | 34 | 35 | @Composable 36 | fun ProgressIndicatorLoading(progressIndicatorSize: Dp, progressIndicatorColor: Color) { 37 | val infiniteTransition = rememberInfiniteTransition(label = "") 38 | 39 | val angle by infiniteTransition.animateFloat( 40 | initialValue = 0f, 41 | targetValue = 360f, 42 | animationSpec = infiniteRepeatable( 43 | animation = keyframes { 44 | durationMillis = 600 45 | } 46 | ), label = "" 47 | ) 48 | 49 | CircularProgressIndicator( 50 | progress = 1f, 51 | modifier = Modifier 52 | .size(progressIndicatorSize) 53 | .rotate(angle) 54 | .border( 55 | 12.dp, 56 | brush = Brush.sweepGradient( 57 | listOf( 58 | Color.Transparent, 59 | progressIndicatorColor.copy(alpha = 0.1f), 60 | progressIndicatorColor 61 | ) 62 | ), 63 | shape = CircleShape 64 | ), 65 | strokeWidth = 1.dp, 66 | color = Color.Transparent 67 | ) 68 | } 69 | 70 | @Composable 71 | fun LoadingDialog(onDismissRequest: () -> Unit, dismissOnBackPress: Boolean = false) { 72 | Dialog( 73 | onDismissRequest = onDismissRequest, 74 | properties = DialogProperties(dismissOnBackPress = dismissOnBackPress) 75 | ) { 76 | Surface( 77 | tonalElevation = 4.dp, 78 | shape = MaterialTheme.shapes.medium, 79 | ) { 80 | Column(modifier = Modifier.padding(horizontal = 48.dp, vertical = 12.dp).wrapContentWidth()) { 81 | ProgressIndicatorLoading( 82 | progressIndicatorSize = 64.dp, 83 | progressIndicatorColor = MaterialTheme.colorScheme.primary 84 | ) 85 | Spacer( 86 | modifier = Modifier 87 | .height(16.dp) 88 | ) 89 | Text( 90 | text = "加载中", 91 | ) 92 | } 93 | } 94 | } 95 | } 96 | 97 | @Preview 98 | @Composable 99 | fun PreviewDialog() { 100 | var isShow by remember { mutableStateOf(true) } 101 | if (isShow) 102 | LoadingDialog(onDismissRequest = { isShow = false }) 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/widgets/Markdown.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.widgets 2 | 3 | import android.text.Spanned 4 | import android.text.method.LinkMovementMethod 5 | import android.view.View 6 | import android.widget.TextView 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.graphics.toArgb 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.viewinterop.AndroidView 15 | import io.noties.markwon.AbstractMarkwonPlugin 16 | import io.noties.markwon.LinkResolverDef 17 | import io.noties.markwon.Markwon 18 | import io.noties.markwon.MarkwonConfiguration 19 | import io.noties.markwon.MarkwonPlugin 20 | import io.noties.markwon.image.AsyncDrawableScheduler 21 | import io.noties.markwon.image.DefaultMediaDecoder 22 | import io.noties.markwon.image.ImagesPlugin 23 | import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler 24 | import io.noties.markwon.image.svg.SvgMediaDecoder 25 | import io.noties.markwon.linkify.LinkifyPlugin 26 | 27 | @Composable 28 | fun Markdown( 29 | content: String, 30 | modifier: Modifier = Modifier, 31 | isSelectable: Boolean = true, 32 | textColor: Color = MaterialTheme.colorScheme.onBackground, 33 | onLinkResolve: (url: String) -> Boolean = { false }, 34 | onTextViewConfiguration: (TextView) -> Unit = {}, 35 | ) { 36 | val context = LocalContext.current 37 | 38 | val markwon = remember { 39 | Markwon.builder(context) 40 | .usePlugin(ImagesPlugin.create { 41 | it.addSchemeHandler(OkHttpNetworkSchemeHandler.create()) 42 | it.addMediaDecoder(DefaultMediaDecoder.create()) 43 | it.addMediaDecoder(SvgMediaDecoder.create()) 44 | it.errorHandler { url, throwable -> 45 | throwable.printStackTrace() 46 | println(url) 47 | null 48 | } 49 | }) 50 | // .usePlugin(HtmlPlugin.create()) 51 | .usePlugin(object : AbstractMarkwonPlugin() { 52 | override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { 53 | // builder.linkResolver { view, link -> 54 | // Log.d(TAG, "configureConfiguration: $link") 55 | // } 56 | 57 | // or subclass default instance 58 | builder.linkResolver(object : LinkResolverDef() { 59 | override fun resolve(view: View, link: String) { 60 | if (!onLinkResolve(link)) 61 | super.resolve(view, link) 62 | } 63 | }) 64 | } 65 | }) 66 | .usePlugin(object : AbstractMarkwonPlugin(), MarkwonPlugin { 67 | override fun beforeSetText(textView: TextView, markdown: Spanned) { 68 | AsyncDrawableScheduler.unschedule(textView) 69 | } 70 | 71 | override fun afterSetText(textView: TextView) { 72 | AsyncDrawableScheduler.schedule(textView); 73 | } 74 | }) 75 | .usePlugin(LinkifyPlugin.create()) 76 | .build() 77 | } 78 | 79 | AndroidView( 80 | modifier = modifier, 81 | factory = { 82 | TextView(it).apply { 83 | movementMethod = LinkMovementMethod.getInstance() 84 | onTextViewConfiguration(this) 85 | } 86 | }) { 87 | it.setTextIsSelectable(isSelectable) 88 | 89 | it.setTextColor(textColor.toArgb()) 90 | markwon.setMarkdown(it, content) 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/widgets/TextFieldDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.text_searcher.ui.widgets 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.OutlinedTextField 5 | import androidx.compose.material3.Text 6 | import androidx.compose.material3.TextButton 7 | import androidx.compose.runtime.Composable 8 | 9 | @Composable 10 | fun TextFieldDialog( 11 | title: String, 12 | text: String, 13 | onTextChange: (String) -> Unit, 14 | onDismissRequest: () -> Unit, 15 | onConfirm: () -> Unit 16 | ) { 17 | AlertDialog(onDismissRequest = onDismissRequest, 18 | title = { 19 | Text(title) 20 | }, 21 | text = { 22 | OutlinedTextField(value = text, onValueChange = onTextChange) 23 | }, 24 | confirmButton = { 25 | TextButton(onClick = onConfirm) { 26 | Text("确定") 27 | } 28 | } 29 | ) 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/ui/widgets/Widgets.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.ui.widgets 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import androidx.compose.foundation.isSystemInDarkTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.DisposableEffect 10 | import androidx.compose.runtime.SideEffect 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.rememberUpdatedState 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.core.content.ContextCompat 16 | import androidx.localbroadcastmanager.content.LocalBroadcastManager 17 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 18 | 19 | 20 | @Composable 21 | fun TransparentSystemBars() { 22 | val systemUiController = rememberSystemUiController() 23 | val useDarkIcons = !isSystemInDarkTheme() 24 | SideEffect { 25 | systemUiController.setSystemBarsColor( 26 | color = Color.Transparent, 27 | darkIcons = useDarkIcons, 28 | isNavigationBarContrastEnforced = false, 29 | ) 30 | } 31 | } 32 | 33 | @Composable 34 | fun BasicBroadcaseReceiver( 35 | intentFilter: IntentFilter, 36 | onReceive: (Intent?) -> Unit, 37 | onRegister: (BroadcastReceiver, Context) -> Unit, 38 | onUnregister: (BroadcastReceiver, Context) -> Unit 39 | ) { 40 | val context = LocalContext.current 41 | val currentReceive by rememberUpdatedState(onReceive) 42 | 43 | DisposableEffect(context, intentFilter) { 44 | val receiver = object : BroadcastReceiver() { 45 | override fun onReceive(context: Context?, intent: Intent?) { 46 | currentReceive(intent) 47 | } 48 | } 49 | onRegister(receiver, context) 50 | 51 | onDispose { 52 | onUnregister(receiver, context) 53 | } 54 | } 55 | } 56 | 57 | @Suppress("DEPRECATION") 58 | @Composable 59 | fun LocalBroadcastReceiver(intentFilter: IntentFilter, onReceive: (Intent?) -> Unit) { 60 | BasicBroadcaseReceiver( 61 | intentFilter, 62 | onReceive, 63 | { obj, context -> 64 | LocalBroadcastManager.getInstance(context).registerReceiver(obj, intentFilter) 65 | }, 66 | { obj, context -> LocalBroadcastManager.getInstance(context).unregisterReceiver(obj) } 67 | ) 68 | } 69 | 70 | @Composable 71 | fun SystemBroadcastReceiver( 72 | intentFilter: IntentFilter, 73 | onSystemEvent: (intent: Intent?) -> Unit 74 | ) { 75 | BasicBroadcaseReceiver( 76 | intentFilter = intentFilter, onReceive = onSystemEvent, 77 | onRegister = { obj, context -> 78 | ContextCompat.registerReceiver( 79 | context, 80 | obj, 81 | intentFilter, 82 | ContextCompat.RECEIVER_EXPORTED 83 | ) 84 | }, 85 | onUnregister = { obj, context -> context.unregisterReceiver(obj) } 86 | ) 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/util/AndroidUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.util 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import android.net.Uri 8 | import android.os.Build 9 | import androidx.core.content.ContextCompat 10 | 11 | object AndroidUtils { 12 | const val ABI_ARM = "armeabi-v7a" 13 | const val ABI_ARM64 = "arm64-v8a" 14 | const val ABI_X86 = "x86" 15 | const val ABI_X86_64 = "x86_64" 16 | 17 | fun getABI(): String? { 18 | return Build.SUPPORTED_ABIS[0] 19 | } 20 | 21 | fun Context.registerGlobalReceiver( 22 | receiver: BroadcastReceiver, 23 | filter: IntentFilter, 24 | flags: Int = ContextCompat.RECEIVER_EXPORTED 25 | ) { 26 | ContextCompat.registerReceiver(this, receiver, filter, flags) 27 | } 28 | 29 | fun Context.openUri(uri: String) { 30 | val intent = Intent(Intent.ACTION_VIEW) 31 | intent.data = Uri.parse(uri) 32 | startActivity(intent) 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/util/ClipBoardUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.util 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.ClipboardManager.OnPrimaryClipChangedListener 6 | import android.content.Context 7 | import com.github.jing332.frpandroid.app 8 | 9 | 10 | /** 11 | *
12 |  * author: Blankj
13 |  * blog  : http://blankj.com
14 |  * time  : 2016/09/25
15 |  * desc  : utils about clipboard
16 | 
* 17 | */ 18 | object ClipboardUtils { 19 | 20 | /** 21 | * Copy the text to clipboard. 22 | * 23 | * The label equals name of package. 24 | * 25 | * @param text The text. 26 | */ 27 | fun copyText(text: CharSequence?) { 28 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 29 | cm.setPrimaryClip(ClipData.newPlainText(app.getPackageName(), text)) 30 | } 31 | 32 | /** 33 | * Copy the text to clipboard. 34 | * 35 | * @param label The label. 36 | * @param text The text. 37 | */ 38 | fun copyText(label: CharSequence?, text: CharSequence?) { 39 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 40 | cm.setPrimaryClip(ClipData.newPlainText(label, text)) 41 | } 42 | 43 | /** 44 | * Clear the clipboard. 45 | */ 46 | fun clear() { 47 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 48 | cm.setPrimaryClip(ClipData.newPlainText(null, "")) 49 | } 50 | 51 | /** 52 | * Return the label for clipboard. 53 | * 54 | * @return the label for clipboard 55 | */ 56 | fun getLabel(): CharSequence { 57 | val cm = app 58 | .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 59 | val des = cm.primaryClipDescription ?: return "" 60 | return des.label ?: return "" 61 | } 62 | 63 | /** 64 | * Return the text for clipboard. 65 | * 66 | * @return the text for clipboard 67 | */ 68 | val text: CharSequence 69 | get() { 70 | val cm = 71 | app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 72 | val clip = cm.primaryClip 73 | if (clip != null && clip.itemCount > 0) { 74 | val text = clip.getItemAt(0).coerceToText(app) 75 | if (text != null) { 76 | return text 77 | } 78 | } 79 | return "" 80 | } 81 | 82 | /** 83 | * Add the clipboard changed listener. 84 | */ 85 | fun addChangedListener(listener: OnPrimaryClipChangedListener?) { 86 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 87 | cm.addPrimaryClipChangedListener(listener) 88 | } 89 | 90 | /** 91 | * Remove the clipboard changed listener. 92 | */ 93 | fun removeChangedListener(listener: OnPrimaryClipChangedListener?) { 94 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 95 | cm.removePrimaryClipChangedListener(listener) 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/util/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.util 2 | 3 | import java.io.File 4 | import java.io.InputStream 5 | import java.net.URLConnection 6 | 7 | object FileUtils { 8 | val File.mimeType: String? 9 | get() { 10 | val fileNameMap = URLConnection.getFileNameMap() 11 | return fileNameMap.getContentTypeFor(name) 12 | } 13 | 14 | /** 15 | * 按行读取txt 16 | */ 17 | fun InputStream.readAllText(): String { 18 | this.use { ins -> 19 | ins.bufferedReader().use { reader -> 20 | val buffer = StringBuffer("") 21 | var str: String? 22 | while (reader.readLine().also { str = it } != null) { 23 | buffer.append(str) 24 | buffer.append("\n") 25 | } 26 | return buffer.toString() 27 | } 28 | } 29 | } 30 | 31 | fun copyFolder(src: File, target: File, overwrite: Boolean = true) { 32 | val folder = File(target.absolutePath + File.separator + src.name) 33 | folder.mkdirs() 34 | 35 | src.listFiles()?.forEach { 36 | if (it.isFile) { 37 | val newFile = File(folder.absolutePath + File.separator + it.name) 38 | it.copyTo(newFile, overwrite) 39 | } else if (it.isDirectory) { 40 | copyFolder(it, folder) 41 | } 42 | } 43 | 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/util/NetworkUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.util 2 | 3 | import java.net.InetAddress 4 | import java.net.NetworkInterface 5 | import java.net.SocketException 6 | import java.util.LinkedList 7 | import java.util.Locale 8 | 9 | 10 | object NetworkUtils { 11 | fun getIPAddress(useIPv4: Boolean): String? { 12 | try { 13 | val nis = NetworkInterface.getNetworkInterfaces() 14 | val adds = LinkedList() 15 | while (nis.hasMoreElements()) { 16 | val ni = nis.nextElement() 17 | // To prevent phone of xiaomi return "10.0.2.15" 18 | if (!ni.isUp || ni.isLoopback) continue 19 | val addresses = ni.inetAddresses 20 | while (addresses.hasMoreElements()) { 21 | adds.addFirst(addresses.nextElement()) 22 | } 23 | } 24 | for (add in adds) { 25 | if (!add.isLoopbackAddress) { 26 | val hostAddress = add.hostAddress ?: break 27 | val isIPv4 = hostAddress.indexOf(':') < 0 28 | if (useIPv4) { 29 | if (isIPv4) return hostAddress 30 | } else { 31 | if (!isIPv4) { 32 | val index = hostAddress.indexOf('%') 33 | return if (index < 0) hostAddress.uppercase(Locale.getDefault()) else hostAddress.substring( 34 | 0, 35 | index 36 | ).uppercase( 37 | Locale.getDefault() 38 | ) 39 | } 40 | } 41 | } 42 | } 43 | } catch (e: SocketException) { 44 | e.printStackTrace() 45 | } 46 | return "" 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/util/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.util 2 | 3 | object StringUtils { 4 | fun String.removeAnsiCodes(): String { 5 | val ansiRegex = Regex("\\x1B\\[[0-9;]*[m|K]") 6 | return this.replace(ansiRegex, "") 7 | } 8 | 9 | private fun paramsParseInternal(params: String): HashMap { 10 | val parameters: HashMap = hashMapOf() 11 | if (params.isBlank()) return parameters 12 | 13 | for (param in params.split("&")) { 14 | val entry = param.split("=".toRegex()).dropLastWhile { it.isEmpty() } 15 | if (entry.size > 1) { 16 | parameters[entry[0]] = entry[1] 17 | } else { 18 | parameters[entry[0]] = "" 19 | } 20 | } 21 | return parameters 22 | } 23 | 24 | fun String.paramsParse() = paramsParseInternal(this) 25 | 26 | fun String.toNumberInt(): Int { 27 | return this.replace(Regex("[^0-9]"), "").toIntOrNull() ?: 0 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/util/ThrottleUtil.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.util 2 | 3 | import kotlinx.coroutines.* 4 | 5 | @OptIn(DelicateCoroutinesApi::class) 6 | class ThrottleUtil(private val scope: CoroutineScope = GlobalScope, val time: Long = 100L) { 7 | private var job: Job? = null 8 | 9 | fun runAction( 10 | dispatcher: CoroutineDispatcher = Dispatchers.Main, 11 | action: suspend () -> Unit, 12 | ) { 13 | job?.cancel() 14 | job = null 15 | job = scope.launch(dispatcher) { 16 | delay(time) 17 | action.invoke() 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/frpandroid/util/ToastUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.frpandroid.util 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.annotation.StringRes 6 | import com.drake.net.utils.runMain 7 | 8 | object ToastUtils { 9 | fun Context.toast(@StringRes strId: String) { 10 | runMain { 11 | Toast.makeText(this, strId, Toast.LENGTH_SHORT).show() 12 | } 13 | } 14 | 15 | fun Context.toast(@StringRes strId: Int, vararg args: Any) { 16 | runMain { 17 | Toast.makeText( 18 | this, 19 | getString(strId, *args), 20 | Toast.LENGTH_SHORT 21 | ).show() 22 | } 23 | } 24 | 25 | fun Context.longToast(str: String) { 26 | runMain { 27 | Toast.makeText(this, str, Toast.LENGTH_LONG).show() 28 | } 29 | } 30 | 31 | fun Context.longToast(@StringRes strId: Int) { 32 | runMain { 33 | Toast.makeText(this, strId, Toast.LENGTH_LONG).show() 34 | } 35 | } 36 | 37 | fun Context.longToast(@StringRes strId: Int, vararg args: Any) { 38 | runMain { 39 | Toast.makeText( 40 | this, 41 | getString(strId, *args), 42 | Toast.LENGTH_LONG 43 | ).show() 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v24/ic_notification_frpc.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | 15 | 17 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v24/ic_notification_frps.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | 15 | 17 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notification_frpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/drawable-hdpi/ic_notification_frpc.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notification_frps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/drawable-hdpi/ic_notification_frps.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_notification_frpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/drawable-mdpi/ic_notification_frpc.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_notification_frps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/drawable-mdpi/ic_notification_frps.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notification_frpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/drawable-xhdpi/ic_notification_frpc.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notification_frps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/drawable-xhdpi/ic_notification_frps.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notification_frpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/drawable-xxhdpi/ic_notification_frpc.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notification_frps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/drawable-xxhdpi/ic_notification_frps.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_trending_up_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/client.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/edit.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_app_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | 14 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_frpc.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | 15 | 17 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_frps.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | 15 | 17 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/server.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_app_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_app_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-hdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-hdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-mdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-mdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-xhdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-xhdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-xxhdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-xxhdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-xxxhdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/FrpAndroid/d69fc5e4f893eef152340aabe0db06db13dc5df0/app/src/main/res/mipmap-xxxhdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/ic_app_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | FrpAndroid 4 | 5 | 开源许可 6 | 返回 7 | 未选择文件 8 | ❌ 错误 9 | 确定 10 | 描述 11 | 日志 12 | AList服务器 13 | 添加桌面快捷方式 14 | 关闭 15 | 复制地址 16 | AList运行中 17 | 取消 18 | admin 密码已设为:\n %1$s 19 | admin 密码 20 | 关闭失败:%1$s 21 | 已复制地址 22 | ⚠️启动服务器才可设置admin密码 23 | AList配置 24 | 设置 25 | 密码 26 | 启动中 27 | 关闭中 28 | FRPC开关 29 | FRPS开关 30 | 更多选项 31 | 关于 32 | 监听地址 33 | 编辑 %1$s 34 | 请至少启用一个服务器! 35 | AList提供者 36 | account 37 | 路径已复制 38 | 检查更新 39 | FRP服务 40 | FRPS 41 | FRPC 42 | ⚠️ FRP错误 (%1$d),请查看日志 43 | 配置 44 | 启动 45 | 帮助文档 46 | FRP在线文档 47 | 添加配置 48 | 搜索过滤 49 | 保存 50 | 编辑配置 51 | 警告 52 | 已存在相同的配置项,是否覆盖? 53 | 推荐使用MT管理器进行编辑 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |