├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── github
│ │ └── nrfr
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── github
│ │ │ └── nrfr
│ │ │ ├── MainActivity.kt
│ │ │ ├── data
│ │ │ ├── CountryPresets.kt
│ │ │ └── PresetCarriers.kt
│ │ │ ├── manager
│ │ │ └── CarrierConfigManager.kt
│ │ │ ├── model
│ │ │ └── SimCardInfo.kt
│ │ │ └── ui
│ │ │ ├── screens
│ │ │ ├── AboutScreen.kt
│ │ │ ├── MainScreen.kt
│ │ │ └── ShizukuNotReadyScreen.kt
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable
│ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── github
│ └── nrfr
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── docs
└── images
│ ├── app.png
│ └── client.png
├── gradle.properties
├── gradle
└── wrapper
│ └── gradle-wrapper.properties
├── nrfr-client
├── .gitignore
├── README.md
├── app.go
├── build
│ ├── README.md
│ ├── appicon.png
│ ├── darwin
│ │ ├── Info.dev.plist
│ │ └── Info.plist
│ └── windows
│ │ ├── icon.ico
│ │ ├── info.json
│ │ ├── installer
│ │ ├── project.nsi
│ │ └── wails_tools.nsh
│ │ └── wails.exe.manifest
├── frontend
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── package.json.md5
│ ├── postcss.config.js
│ ├── src
│ │ ├── App.tsx
│ │ ├── assets
│ │ │ └── images
│ │ │ │ └── logo.png
│ │ ├── components
│ │ │ ├── layout
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── ErrorMessage.tsx
│ │ │ │ ├── StepIndicator.tsx
│ │ │ │ └── TitleBar.tsx
│ │ │ └── steps
│ │ │ │ ├── AppCheck.tsx
│ │ │ │ ├── AppInstall.tsx
│ │ │ │ ├── Complete.tsx
│ │ │ │ ├── DeviceSelection.tsx
│ │ │ │ └── ServiceStart.tsx
│ │ ├── main.tsx
│ │ ├── styles
│ │ │ └── global.css
│ │ ├── types
│ │ │ └── index.ts
│ │ └── vite-env.d.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── wailsjs
│ │ ├── go
│ │ ├── main
│ │ │ ├── App.d.ts
│ │ │ └── App.js
│ │ └── models.ts
│ │ └── runtime
│ │ ├── package.json
│ │ ├── runtime.d.ts
│ │ └── runtime.js
├── go.mod
├── go.sum
├── main.go
└── wails.json
└── settings.gradle.kts
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*"
7 | workflow_dispatch:
8 |
9 | # 添加权限配置
10 | permissions:
11 | contents: write
12 |
13 | jobs:
14 | build:
15 | runs-on: windows-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 | with:
19 | fetch-depth: 0
20 |
21 | # 获取版本号和提交信息
22 | - name: Get Version Info
23 | id: version
24 | run: |
25 | VERSION=${GITHUB_REF#refs/tags/}
26 | echo "version=${VERSION}" >> $GITHUB_OUTPUT
27 |
28 | if git rev-list --tags --max-count=1 > /dev/null 2>&1; then
29 | LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || git rev-list --max-parents=0 HEAD)
30 |
31 | # 获取所有提交并过滤掉CI相关提交和文档提交
32 | ALL_COMMITS=$(git log --pretty=format:"%h %s (%an)" $LAST_TAG..HEAD | grep -v -E "ci:|docs:" || true)
33 |
34 | # 分别获取不同类型的提交
35 | FEAT_COMMITS=$(echo "$ALL_COMMITS" | grep "^[a-f0-9]\+ feat:" || true)
36 | FIX_COMMITS=$(echo "$ALL_COMMITS" | grep "^[a-f0-9]\+ fix:" || true)
37 | OTHER_COMMITS=$(echo "$ALL_COMMITS" | grep -v "^[a-f0-9]\+ \(feat:\|fix:\)" || true)
38 |
39 | # 组装changelog
40 | CHANGELOG=""
41 | if [ ! -z "$FEAT_COMMITS" ]; then
42 | CHANGELOG+="### 功能改进\n$FEAT_COMMITS\n"
43 | fi
44 | if [ ! -z "$FIX_COMMITS" ]; then
45 | CHANGELOG+="### 问题修复\n$FIX_COMMITS\n"
46 | fi
47 | if [ ! -z "$OTHER_COMMITS" ]; then
48 | CHANGELOG+="### 其他更新\n$OTHER_COMMITS"
49 | fi
50 | else
51 | CHANGELOG="### 首次发布\n$(git log --pretty=format:"- %h %s (%an)" | grep -v "ci:" || true)"
52 | fi
53 |
54 | # 如果changelog为空,添加默认信息
55 | if [ -z "$CHANGELOG" ]; then
56 | CHANGELOG="### 无更新内容"
57 | fi
58 |
59 | # 输出
60 | echo -e "$CHANGELOG"
61 |
62 | # 将 changelog 写入文件
63 | echo -e "$CHANGELOG" > CHANGELOG.md
64 | shell: bash
65 |
66 | # 设置 Java 环境
67 | - name: Set up JDK
68 | uses: actions/setup-java@v4
69 | with:
70 | distribution: "temurin"
71 | java-version: "21"
72 |
73 | # 设置 Android SDK
74 | - name: Set up Android SDK
75 | uses: android-actions/setup-android@v3
76 |
77 | # 下载替换 android.jar
78 | - name: Download android.jar
79 | run: |
80 | mkdir -p $ANDROID_HOME/platforms/android-34
81 | curl -L https://raw.githubusercontent.com/Reginer/aosp-android-jar/refs/heads/main/android-34/android.jar -o $ANDROID_HOME/platforms/android-34/android.jar
82 | shell: bash
83 |
84 | # 构建 Android APK
85 | - name: Build Android APK
86 | run: |
87 | gradle wrapper
88 | chmod +x ./gradlew
89 | ./gradlew assembleDebug
90 | shell: bash
91 |
92 | # 设置 Go 环境
93 | - name: Set up Go
94 | uses: actions/setup-go@v5
95 | with:
96 | go-version: "1.23"
97 |
98 | # 设置 Node.js 环境
99 | - name: Set up Node.js
100 | uses: actions/setup-node@v4
101 | with:
102 | node-version: "21"
103 |
104 | # 安装 Wails
105 | - name: Install Wails
106 | run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
107 |
108 | # 构建 Wails 应用
109 | - name: Build Wails App
110 | run: |
111 | cd nrfr-client/frontend
112 | npm install
113 | cd ..
114 | wails build
115 | shell: bash
116 |
117 | # 下载最新的 Shizuku APK
118 | - name: Download Shizuku APK
119 | run: |
120 | mkdir -p nrfr-client/build/bin/resources
121 | curl -L $(curl -s https://api.github.com/repos/RikkaApps/Shizuku/releases/latest | grep "browser_download_url.*apk" | cut -d '"' -f 4) -o nrfr-client/build/bin/resources/shizuku.apk
122 | shell: bash
123 |
124 | # 复制 Android APK
125 | - name: Copy Android APK
126 | run: |
127 | cp app/build/outputs/apk/debug/app-debug.apk nrfr-client/build/bin/resources/nrfr.apk
128 | cp app/build/outputs/apk/debug/app-debug.apk nrfr-${{ steps.version.outputs.version }}.apk
129 | shell: bash
130 |
131 | # 下载 platform-tools
132 | - name: Download platform-tools
133 | run: |
134 | mkdir -p nrfr-client/build/bin/platform-tools
135 | if [ "$RUNNER_OS" == "Windows" ]; then
136 | curl -L https://dl.google.com/android/repository/platform-tools-latest-windows.zip -o platform-tools.zip
137 | elif [ "$RUNNER_OS" == "Linux" ]; then
138 | curl -L https://dl.google.com/android/repository/platform-tools-latest-linux.zip -o platform-tools.zip
139 | else
140 | curl -L https://dl.google.com/android/repository/platform-tools-latest-darwin.zip -o platform-tools.zip
141 | fi
142 | unzip platform-tools.zip
143 | cp -r platform-tools/* nrfr-client/build/bin/platform-tools/
144 | rm -rf platform-tools platform-tools.zip
145 | shell: bash
146 |
147 | # 打包发布文件
148 | - name: Create Release Archive
149 | run: |
150 | cd nrfr-client/build/bin
151 | 7z a "../../nrfr-${{ steps.version.outputs.version }}-release.zip" ./*
152 | shell: bash
153 |
154 | # 创建 Release
155 | - name: Create Release
156 | uses: softprops/action-gh-release@v1
157 | if: startsWith(github.ref, 'refs/tags/')
158 | with:
159 | files: |
160 | nrfr-client/nrfr-${{ steps.version.outputs.version }}-release.zip
161 | nrfr-${{ steps.version.outputs.version }}.apk
162 | body_path: CHANGELOG.md
163 | env:
164 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
165 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | *.log
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Nrfr
3 |
🌍 免 Root 的 SIM 卡国家码修改工具,让你的网络更自由
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Nrfr 是一款强大的 SIM 卡国家码修改工具,无需 Root 权限即可修改 SIM 卡国家码。本项目完全基于 Android 系统原生 API 实现,不依赖
34 | Xposed、Magisk 等任何第三方框架,仅通过调用系统级接口实现功能。通过修改国家码,你可以:
35 |
36 | - 🌏 解锁运营商限制,使用更多本地功能
37 | - 🔓 突破某些区域限制的应用和服务
38 | - 🛠️ 解决国际漫游时的兼容性问题
39 | - 🌐 帮助使用海外 SIM 卡获得更好的本地化体验
40 | - ⚙️ 解决部分应用识别 SIM 卡地区错误的问题
41 |
42 | ## 📱 使用案例
43 |
44 | ### 运营商配置优化
45 |
46 | - 手机无法正确识别运营商配置
47 | - 某些运营商特定功能无法使用
48 | - 网络配置与当地运营商不匹配
49 |
50 | ### 运营商参数适配
51 |
52 | - 运营商功能配置不完整
53 | - 网络参数与运营商默认配置不匹配
54 | - 运营商特定服务无法正常启用
55 |
56 | ### 漫游网络识别
57 |
58 | - 漫游时运营商名称显示异常
59 | - 网络配置与漫游地运营商不匹配
60 | - 运营商特定功能无法使用
61 |
62 | ### TikTok 区域限制解除
63 |
64 | - TikTok 网络错误
65 | - 无法正常使用 TikTok 的完整功能
66 |
67 | 你可以:
68 |
69 | 1. 使用 Nrfr 修改 SIM 卡国家码为支持的地区(如 JP、US 等)
70 | 2. 重新打开 TikTok,就可以正常使用了
71 |
72 | ## 💡 实现原理
73 |
74 | Nrfr 通过调用 Android 系统级 API(CarrierConfigLoader)修改系统内的运营商配置参数,而**不是直接修改 SIM 卡**。这种实现方式:
75 |
76 | - 完全在系统层面工作,不会对 SIM 卡本身进行任何修改或造成损坏
77 | - 仅改变系统对 SIM 卡信息的读取方式
78 | - 基于 Android 原生 API 实现,不依赖任何第三方框架(如 Xposed、Magisk 等)
79 | - 通过 Shizuku 仅提供必要的权限支持
80 | - 所有修改都是可逆的,随时可以还原
81 |
82 | ## ✨ 特性
83 |
84 | - 🔒 安全可靠
85 | - 无需 Root 权限
86 | - 不修改系统文件
87 | - 不影响系统稳定性
88 | - 不会对 SIM 卡造成任何影响
89 | - 🔄 功能完善
90 | - 支持随时还原修改
91 | - 支持双卡设备,可分别配置
92 | - 一次修改永久生效,重启后保持
93 | - 🚀 简单易用
94 | - 一键启动工具
95 | - 智能检测设备和 SIM 卡状态
96 | - 自动安装所需应用
97 | - 简洁优雅的用户界面
98 | - 轻量且高效,安装包体积小
99 |
100 | ## ⚠️ 注意事项
101 |
102 | - 需要安装并启用 Shizuku
103 | - 修改国家码可能会影响运营商服务,请谨慎操作
104 | - 部分设备可能不支持修改国家码
105 | - 如需还原设置,请使用应用内的还原功能
106 |
107 | ## 🚀 快速开始
108 |
109 | 下载页面有两个文件,一个是含快速启动工具的压缩包,另一个就只是 APK 安装包。**推荐使用快速启动工具**,请按照以下步骤操作:
110 |
111 | 1. 准备手机
112 | - 启用开发者选项(具体的自己查一下)
113 | - 进入开发者选项,开启 USB 调试
114 | - 开启 USB 调试(安全设置),如果有就开启
115 | - 开启 USB 安装(允许通过 USB 安装应用)
116 | - 如果提示未知来源应用安装,请允许从此来源安装
117 |
118 | 2. 连接手机到电脑
119 | - 使用数据线将手机连接到电脑
120 | - 在手机上允许 USB 调试授权
121 |
122 | 3. 下载并启动 Nrfr 快速启动工具
123 | - 从 Release 页面下载最新版本的快速启动工具
124 | - 解压并运行 Nrfr 快速启动工具
125 | - 工具会自动检测已连接的设备
126 |
127 | 4. 安装必要组件
128 | - 工具会自动安装 Shizuku 到手机
129 | - 按照提示启用 Shizuku
130 | - 等待工具自动安装 Nrfr 应用
131 |
132 | 5. 修改国家码
133 | - 在手机上打开 Nrfr 应用
134 | - 选择需要修改的 SIM 卡
135 | - 设置目标国家码
136 | - 应用修改
137 |
138 | 修改完成后无需重启设备,设置会立即生效并永久保持。如需还原,请使用应用内的还原功能。
139 |
140 | ## 📦 构建
141 |
142 | 项目包含两个部分:快速启动工具(桌面端)和手机应用(Android)。
143 |
144 | ### 快速启动工具 (nrfr-client)
145 |
146 | ```bash
147 | # 进入客户端目录
148 | cd nrfr-client
149 |
150 | # 安装依赖
151 | npm install
152 |
153 | # 开发模式
154 | wails dev
155 |
156 | # 构建发布版本
157 | wails build
158 | ```
159 |
160 | ### Android 应用 (app)
161 |
162 | ```bash
163 | # 进入 Android 应用目录
164 | cd app
165 |
166 | # 使用 Gradle 构建 Debug 版本
167 | ./gradlew assembleDebug
168 | ```
169 |
170 | 构建完成后,可以在以下位置找到生成的文件:
171 |
172 | - 快速启动工具: `nrfr-client/build/bin/`
173 | - Android 应用: `app/build/outputs/apk/`
174 |
175 | ## 📝 依赖项
176 |
177 | - [Shizuku](https://shizuku.rikka.app/) - 用于提供特权服务
178 | - [ADB](https://developer.android.com/tools/adb) - Android 调试桥接
179 |
180 | ## 🤝 贡献
181 |
182 | 欢迎提交 Pull Request 和 Issue!在提交之前,请确保:
183 |
184 | - 代码经过测试
185 | - 遵循现有的代码风格
186 | - 更新相关文档
187 | - 描述清楚改动的目的和影响
188 |
189 | ## 📄 许可证
190 |
191 | 本项目采用 [Apache-2.0](LICENSE) 许可证。
192 |
193 | ## ⚠️ 免责声明
194 |
195 | 本工具仅供学习和研究使用。使用本工具修改系统设置可能会影响设备的正常使用,请自行承担风险。作者不对任何可能的损失负责。
196 |
197 | ## 💖 支持
198 |
199 | 如果你觉得这个项目有帮助:
200 |
201 | - 在 X 上关注 [@actkites](https://x.com/intent/follow?screen_name=actkites)
202 | - 给项目点个 Star ⭐
203 | - 分享给更多的人
204 |
205 | ## ⭐ Star History
206 |
207 | [](https://star-history.com/#Ackites/Nrfr&Date)
208 |
209 | ## 🙏 鸣谢
210 |
211 | - [Shizuku](https://shizuku.rikka.app/) - 感谢 Shizuku 提供的特权服务支持
212 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | *.apk
3 | *.aab
4 | *.ap_
5 | *.dex
6 | *.class
7 | bin/
8 | gen/
9 | out/
10 | release/
11 | debug/
12 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id("org.jetbrains.kotlin.plugin.compose")
5 | }
6 |
7 | android {
8 | namespace = "com.github.nrfr"
9 | compileSdk = 34
10 |
11 | defaultConfig {
12 | applicationId = "com.github.nrfr"
13 | minSdk = 26
14 | targetSdk = 34
15 | versionCode = 3 //版本更新 +1
16 | versionName = "1.0.3" //同步更新版本号 rfr-client/app.go
17 |
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | isMinifyEnabled = true
24 | isShrinkResources = true
25 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
26 | }
27 | }
28 | compileOptions {
29 | sourceCompatibility = JavaVersion.VERSION_1_8
30 | targetCompatibility = JavaVersion.VERSION_1_8
31 | }
32 | kotlinOptions {
33 | jvmTarget = "1.8"
34 | }
35 | buildFeatures {
36 | compose = true
37 | }
38 | }
39 |
40 | dependencies {
41 | implementation("androidx.core:core-ktx:1.12.0")
42 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
43 | implementation("androidx.activity:activity-compose:1.8.2")
44 | implementation(platform("androidx.compose:compose-bom:2023.10.01"))
45 | implementation("androidx.compose.ui:ui")
46 | implementation("androidx.compose.ui:ui-graphics")
47 | implementation("androidx.compose.ui:ui-tooling-preview")
48 | implementation("androidx.compose.material3:material3")
49 |
50 | // Shizuku
51 | implementation("dev.rikka.shizuku:api:13.1.5")
52 | implementation("dev.rikka.shizuku:provider:13.1.5")
53 | implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3")
54 |
55 | testImplementation("junit:junit:4.13.2")
56 | androidTestImplementation("androidx.test.ext:junit:1.1.5")
57 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
58 | androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01"))
59 | androidTestImplementation("androidx.compose.ui:ui-test-junit4")
60 | debugImplementation("androidx.compose.ui:ui-tooling")
61 | debugImplementation("androidx.compose.ui:ui-test-manifest")
62 | }
63 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/github/nrfr/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.github.nrfr", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr
2 |
3 | import android.content.pm.PackageManager
4 | import android.os.Bundle
5 | import android.widget.Toast
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.enableEdgeToEdge
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.setValue
12 | import com.github.nrfr.ui.screens.AboutScreen
13 | import com.github.nrfr.ui.screens.MainScreen
14 | import com.github.nrfr.ui.screens.ShizukuNotReadyScreen
15 | import com.github.nrfr.ui.theme.NrfrTheme
16 | import org.lsposed.hiddenapibypass.HiddenApiBypass
17 | import rikka.shizuku.Shizuku
18 |
19 | class MainActivity : ComponentActivity() {
20 | private var isShizukuReady by mutableStateOf(false)
21 | private var showAbout by mutableStateOf(false)
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | enableEdgeToEdge()
26 |
27 | // 初始化 Hidden API 访问
28 | HiddenApiBypass.addHiddenApiExemptions("L")
29 | HiddenApiBypass.addHiddenApiExemptions("I")
30 |
31 | // 检查 Shizuku 状态
32 | checkShizukuStatus()
33 |
34 | // 添加 Shizuku 权限监听器
35 | Shizuku.addRequestPermissionResultListener { _, grantResult ->
36 | isShizukuReady = grantResult == PackageManager.PERMISSION_GRANTED
37 | if (!isShizukuReady) {
38 | Toast.makeText(this, "需要 Shizuku 权限才能运行", Toast.LENGTH_LONG).show()
39 | }
40 | }
41 |
42 | // 添加 Shizuku 绑定监听器
43 | Shizuku.addBinderReceivedListener {
44 | checkShizukuStatus()
45 | }
46 |
47 | setContent {
48 | NrfrTheme {
49 | if (showAbout) {
50 | AboutScreen(onBack = { showAbout = false })
51 | } else if (isShizukuReady) {
52 | MainScreen(onShowAbout = { showAbout = true })
53 | } else {
54 | ShizukuNotReadyScreen()
55 | }
56 | }
57 | }
58 | }
59 |
60 | private fun checkShizukuStatus() {
61 | isShizukuReady = if (Shizuku.getBinder() == null) {
62 | Toast.makeText(this, "请先安装并启用 Shizuku", Toast.LENGTH_LONG).show()
63 | false
64 | } else {
65 | val hasPermission = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
66 | if (!hasPermission) {
67 | Shizuku.requestPermission(0)
68 | }
69 | hasPermission
70 | }
71 | }
72 |
73 | override fun onDestroy() {
74 | super.onDestroy()
75 | Shizuku.removeRequestPermissionResultListener { _, _ -> }
76 | Shizuku.removeBinderReceivedListener { }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/data/CountryPresets.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.data
2 |
3 | object CountryPresets {
4 | data class CountryInfo(
5 | val code: String,
6 | val name: String
7 | )
8 |
9 | val countries = listOf(
10 | CountryInfo("CN", "中国"),
11 | CountryInfo("HK", "中国香港"),
12 | CountryInfo("MO", "中国澳门"),
13 | CountryInfo("TW", "中国台湾"),
14 | CountryInfo("JP", "日本"),
15 | CountryInfo("KR", "韩国"),
16 | CountryInfo("US", "美国"),
17 | CountryInfo("GB", "英国"),
18 | CountryInfo("DE", "德国"),
19 | CountryInfo("FR", "法国"),
20 | CountryInfo("IT", "意大利"),
21 | CountryInfo("ES", "西班牙"),
22 | CountryInfo("PT", "葡萄牙"),
23 | CountryInfo("RU", "俄罗斯"),
24 | CountryInfo("IN", "印度"),
25 | CountryInfo("AU", "澳大利亚"),
26 | CountryInfo("NZ", "新西兰"),
27 | CountryInfo("SG", "新加坡"),
28 | CountryInfo("MY", "马来西亚"),
29 | CountryInfo("TH", "泰国"),
30 | CountryInfo("VN", "越南"),
31 | CountryInfo("ID", "印度尼西亚"),
32 | CountryInfo("PH", "菲律宾"),
33 | CountryInfo("CA", "加拿大"),
34 | CountryInfo("MX", "墨西哥"),
35 | CountryInfo("BR", "巴西"),
36 | CountryInfo("AR", "阿根廷"),
37 | CountryInfo("ZA", "南非")
38 | ).sortedBy { it.name } // 按国家名称排序
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/data/PresetCarriers.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.data
2 |
3 | object PresetCarriers {
4 | data class CarrierPreset(
5 | val name: String,
6 | val displayName: String,
7 | val region: String
8 | )
9 |
10 | val presets = listOf(
11 | // 中国大陆运营商
12 | CarrierPreset("中国移动", "China Mobile", "CN"),
13 | CarrierPreset("中国联通", "China Unicom", "CN"),
14 | CarrierPreset("中国电信", "China Telecom", "CN"),
15 |
16 | // 中国香港运营商
17 | CarrierPreset("中国移动香港", "CMHK", "HK"),
18 | CarrierPreset("香港电讯", "HKT", "HK"),
19 | CarrierPreset("3香港", "3HK", "HK"),
20 | CarrierPreset("SmarTone", "SmarTone", "HK"),
21 |
22 | // 中国澳门运营商
23 | CarrierPreset("澳门电讯", "CTM", "MO"),
24 | CarrierPreset("3澳门", "3 Macau", "MO"),
25 |
26 | // 中国台湾运营商
27 | CarrierPreset("中华电信", "Chunghwa Telecom", "TW"),
28 | CarrierPreset("台湾大哥大", "Taiwan Mobile", "TW"),
29 | CarrierPreset("远传电信", "FarEasTone", "TW"),
30 |
31 | // 日本运营商
32 | CarrierPreset("NTT docomo", "NTT docomo", "JP"),
33 | CarrierPreset("au", "au by KDDI", "JP"),
34 | CarrierPreset("Softbank", "Softbank", "JP"),
35 | CarrierPreset("Rakuten", "Rakuten Mobile", "JP"),
36 |
37 | // 韩国运营商
38 | CarrierPreset("SK Telecom", "SK Telecom", "KR"),
39 | CarrierPreset("KT", "KT Corporation", "KR"),
40 | CarrierPreset("LG U+", "LG U+", "KR"),
41 |
42 | // 美国运营商
43 | CarrierPreset("AT&T", "AT&T", "US"),
44 | CarrierPreset("T-Mobile", "T-Mobile USA", "US"),
45 | CarrierPreset("Verizon", "Verizon", "US"),
46 | CarrierPreset("Sprint", "Sprint", "US"),
47 |
48 | // 英国运营商
49 | CarrierPreset("EE", "EE", "GB"),
50 | CarrierPreset("O2", "O2 UK", "GB"),
51 | CarrierPreset("Three", "Three UK", "GB"),
52 | CarrierPreset("Vodafone", "Vodafone UK", "GB"),
53 |
54 | // 新加坡运营商
55 | CarrierPreset("Singtel", "Singtel", "SG"),
56 | CarrierPreset("StarHub", "StarHub", "SG"),
57 | CarrierPreset("M1", "M1", "SG"),
58 |
59 | // 马来西亚运营商
60 | CarrierPreset("Maxis", "Maxis", "MY"),
61 | CarrierPreset("Celcom", "Celcom", "MY"),
62 | CarrierPreset("Digi", "Digi", "MY"),
63 | CarrierPreset("U Mobile", "U Mobile", "MY"),
64 |
65 | // 泰国运营商
66 | CarrierPreset("AIS", "AIS", "TH"),
67 | CarrierPreset("DTAC", "DTAC", "TH"),
68 | CarrierPreset("True Move H", "True Move H", "TH"),
69 |
70 | // 越南运营商
71 | CarrierPreset("Viettel", "Viettel Mobile", "VN"),
72 | CarrierPreset("Vinaphone", "Vinaphone", "VN"),
73 | CarrierPreset("Mobifone", "Mobifone", "VN"),
74 |
75 | // 印度尼西亚运营商
76 | CarrierPreset("Telkomsel", "Telkomsel", "ID"),
77 | CarrierPreset("Indosat", "Indosat Ooredoo", "ID"),
78 | CarrierPreset("XL Axiata", "XL Axiata", "ID"),
79 |
80 | // 菲律宾运营商
81 | CarrierPreset("Globe", "Globe Telecom", "PH"),
82 | CarrierPreset("Smart", "Smart Communications", "PH"),
83 | CarrierPreset("DITO", "DITO Telecommunity", "PH"),
84 |
85 | // 印度运营商
86 | CarrierPreset("Jio", "Reliance Jio", "IN"),
87 | CarrierPreset("Airtel", "Bharti Airtel", "IN"),
88 | CarrierPreset("Vi", "Vodafone Idea", "IN"),
89 |
90 | // 澳大利亚运营商
91 | CarrierPreset("Telstra", "Telstra", "AU"),
92 | CarrierPreset("Optus", "Optus", "AU"),
93 | CarrierPreset("Vodafone", "Vodafone AU", "AU"),
94 |
95 | // 加拿大运营商
96 | CarrierPreset("Bell", "Bell Mobility", "CA"),
97 | CarrierPreset("Rogers", "Rogers Wireless", "CA"),
98 | CarrierPreset("Telus", "Telus Mobility", "CA"),
99 |
100 | // 德国运营商
101 | CarrierPreset("Telekom", "T-Mobile DE", "DE"),
102 | CarrierPreset("Vodafone", "Vodafone DE", "DE"),
103 | CarrierPreset("O2", "O2 DE", "DE"),
104 |
105 | // 法国运营商
106 | CarrierPreset("Orange", "Orange FR", "FR"),
107 | CarrierPreset("SFR", "SFR", "FR"),
108 | CarrierPreset("Free", "Free Mobile", "FR"),
109 | CarrierPreset("Bouygues", "Bouygues Telecom", "FR"),
110 |
111 | // 意大利运营商
112 | CarrierPreset("TIM", "Telecom Italia", "IT"),
113 | CarrierPreset("Vodafone", "Vodafone IT", "IT"),
114 | CarrierPreset("Wind Tre", "Wind Tre", "IT"),
115 |
116 | // 西班牙运营商
117 | CarrierPreset("Movistar", "Movistar", "ES"),
118 | CarrierPreset("Vodafone", "Vodafone ES", "ES"),
119 | CarrierPreset("Orange", "Orange ES", "ES"),
120 |
121 | // 俄罗斯运营商
122 | CarrierPreset("MTS", "MTS", "RU"),
123 | CarrierPreset("MegaFon", "MegaFon", "RU"),
124 | CarrierPreset("Beeline", "Beeline", "RU"),
125 |
126 | // 巴西运营商
127 | CarrierPreset("Vivo", "Vivo", "BR"),
128 | CarrierPreset("Claro", "Claro", "BR"),
129 | CarrierPreset("TIM", "TIM Brasil", "BR"),
130 |
131 | // 自定义选项
132 | CarrierPreset("自定义", "", "")
133 | )
134 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/manager/CarrierConfigManager.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.manager
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.os.PersistableBundle
6 | import android.telephony.CarrierConfigManager
7 | import android.telephony.SubscriptionManager
8 | import android.telephony.TelephonyFrameworkInitializer
9 | import android.telephony.TelephonyManager
10 | import com.android.internal.telephony.ICarrierConfigLoader
11 | import com.github.nrfr.model.SimCardInfo
12 | import rikka.shizuku.ShizukuBinderWrapper
13 |
14 | object CarrierConfigManager {
15 | fun getSimCards(context: Context): List {
16 | val simCards = mutableListOf()
17 | val subId1 = SubscriptionManager.getSubId(0)
18 | val subId2 = SubscriptionManager.getSubId(1)
19 |
20 | if (subId1 != null) {
21 | val config1 = getCurrentConfig(subId1[0])
22 | simCards.add(SimCardInfo(1, subId1[0], getCarrierNameBySubId(context, subId1[0]), config1))
23 | }
24 | if (subId2 != null) {
25 | val config2 = getCurrentConfig(subId2[0])
26 | simCards.add(SimCardInfo(2, subId2[0], getCarrierNameBySubId(context, subId2[0]), config2))
27 | }
28 |
29 | return simCards
30 | }
31 |
32 | private fun getCurrentConfig(subId: Int): Map {
33 | try {
34 | val carrierConfigLoader = ICarrierConfigLoader.Stub.asInterface(
35 | ShizukuBinderWrapper(
36 | TelephonyFrameworkInitializer
37 | .getTelephonyServiceManager()
38 | .carrierConfigServiceRegisterer
39 | .get()
40 | )
41 | )
42 | val config = carrierConfigLoader.getConfigForSubId(subId, "com.github.nrfr") ?: return emptyMap()
43 |
44 | val result = mutableMapOf()
45 |
46 | // 获取国家码配置
47 | config.getString(CarrierConfigManager.KEY_SIM_COUNTRY_ISO_OVERRIDE_STRING)?.let {
48 | result["国家码"] = it
49 | }
50 |
51 | // 获取运营商名称配置
52 | if (config.getBoolean(CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL, false)) {
53 | config.getString(CarrierConfigManager.KEY_CARRIER_NAME_STRING)?.let {
54 | result["运营商名称"] = it
55 | }
56 | }
57 |
58 | return result
59 | } catch (e: Exception) {
60 | return emptyMap()
61 | }
62 | }
63 |
64 | private fun getCarrierNameBySubId(context: Context, subId: Int): String {
65 | val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
66 | ?: return ""
67 |
68 | return try {
69 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
70 | // Android 10 及以上使用新 API
71 | telephonyManager.getNetworkOperatorName(subId)
72 | } else {
73 | // Android 8-9 使用反射获取运营商名称
74 | val createForSubscriptionId = TelephonyManager::class.java.getMethod(
75 | "createForSubscriptionId",
76 | Int::class.javaPrimitiveType
77 | )
78 | val subTelephonyManager = createForSubscriptionId.invoke(telephonyManager, subId) as TelephonyManager
79 | subTelephonyManager.networkOperatorName
80 | }
81 | } catch (e: Exception) {
82 | // 如果获取失败,回退到默认的 TelephonyManager
83 | telephonyManager.networkOperatorName
84 | }
85 | }
86 |
87 | fun setCarrierConfig(subId: Int, countryCode: String?, carrierName: String? = null) {
88 | val bundle = PersistableBundle()
89 |
90 | // 设置国家码
91 | if (!countryCode.isNullOrEmpty() && countryCode.length == 2) {
92 | bundle.putString(
93 | CarrierConfigManager.KEY_SIM_COUNTRY_ISO_OVERRIDE_STRING,
94 | countryCode.lowercase()
95 | )
96 | }
97 |
98 | // 设置运营商名称
99 | if (!carrierName.isNullOrEmpty()) {
100 | bundle.putBoolean(CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL, true)
101 | bundle.putString(CarrierConfigManager.KEY_CARRIER_NAME_STRING, carrierName)
102 | }
103 |
104 | overrideCarrierConfig(subId, bundle)
105 | }
106 |
107 | fun resetCarrierConfig(subId: Int) {
108 | overrideCarrierConfig(subId, null)
109 | }
110 |
111 | private fun overrideCarrierConfig(subId: Int, bundle: PersistableBundle?) {
112 | val carrierConfigLoader = ICarrierConfigLoader.Stub.asInterface(
113 | ShizukuBinderWrapper(
114 | TelephonyFrameworkInitializer
115 | .getTelephonyServiceManager()
116 | .carrierConfigServiceRegisterer
117 | .get()
118 | )
119 | )
120 | carrierConfigLoader.overrideConfig(subId, bundle, true)
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/model/SimCardInfo.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.model
2 |
3 | data class SimCardInfo(
4 | val slot: Int,
5 | val subId: Int,
6 | val carrierName: String,
7 | val currentConfig: Map = emptyMap()
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/ui/screens/AboutScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.ui.screens
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.rememberScrollState
8 | import androidx.compose.foundation.verticalScroll
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.filled.ArrowBack
11 | import androidx.compose.material3.*
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.platform.LocalContext
16 | import androidx.compose.ui.res.painterResource
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.compose.ui.unit.dp
19 | import com.github.nrfr.R
20 |
21 | @OptIn(ExperimentalMaterial3Api::class)
22 | @Composable
23 | fun AboutScreen(onBack: () -> Unit) {
24 | val context = LocalContext.current
25 |
26 | Scaffold(
27 | topBar = {
28 | CenterAlignedTopAppBar(
29 | title = {
30 | Row(
31 | verticalAlignment = Alignment.CenterVertically,
32 | horizontalArrangement = Arrangement.Center
33 | ) {
34 | Icon(
35 | painter = painterResource(id = R.drawable.ic_launcher_foreground),
36 | modifier = Modifier.size(48.dp),
37 | contentDescription = "App Icon",
38 | tint = MaterialTheme.colorScheme.primary
39 | )
40 | Spacer(modifier = Modifier.width(4.dp))
41 | Text("关于")
42 | }
43 | },
44 | navigationIcon = {
45 | IconButton(onClick = onBack) {
46 | Icon(Icons.Default.ArrowBack, contentDescription = "返回")
47 | }
48 | }
49 | )
50 | }
51 | ) { innerPadding ->
52 | Column(
53 | modifier = Modifier
54 | .fillMaxSize()
55 | .padding(innerPadding)
56 | ) {
57 | Column(
58 | modifier = Modifier
59 | .verticalScroll(rememberScrollState())
60 | .padding(16.dp)
61 | .weight(1f, fill = false),
62 | horizontalAlignment = Alignment.CenterHorizontally,
63 | verticalArrangement = Arrangement.spacedBy(16.dp)
64 | ) {
65 | // 应用信息
66 | Card(
67 | modifier = Modifier.fillMaxWidth()
68 | ) {
69 | Column(
70 | modifier = Modifier.padding(16.dp),
71 | verticalArrangement = Arrangement.spacedBy(8.dp)
72 | ) {
73 | Text(
74 | "功能介绍",
75 | style = MaterialTheme.typography.titleMedium
76 | )
77 | Text(
78 | "• 修改 SIM 卡的国家码配置,可用于解除部分应用的地区限制\n" +
79 | "• 帮助使用海外 SIM 卡时获得更好的本地化体验\n" +
80 | "• 解决部分应用识别 SIM 卡地区错误的问题\n" +
81 | "• 无需 Root 权限,无需修改系统文件,安全且可随时还原\n" +
82 | "• 支持 Android 8 及以上系统版本\n" +
83 | "• 支持双卡设备,可分别配置不同国家码",
84 | style = MaterialTheme.typography.bodyMedium
85 | )
86 | }
87 | }
88 |
89 | Divider(modifier = Modifier.padding(vertical = 8.dp))
90 |
91 | // 作者信息
92 | Text(
93 | "作者信息",
94 | style = MaterialTheme.typography.titleMedium
95 | )
96 | Card(
97 | modifier = Modifier.fillMaxWidth()
98 | ) {
99 | Column(
100 | modifier = Modifier.padding(16.dp),
101 | verticalArrangement = Arrangement.spacedBy(8.dp)
102 | ) {
103 | Text("作者: Antkites")
104 | Text(
105 | "GitHub: Ackites",
106 | modifier = Modifier.clickable {
107 | context.startActivity(
108 | Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Ackites"))
109 | )
110 | },
111 | color = MaterialTheme.colorScheme.primary
112 | )
113 | Text(
114 | "X (Twitter): @actkites",
115 | modifier = Modifier.clickable {
116 | context.startActivity(
117 | Intent(
118 | Intent.ACTION_VIEW,
119 | Uri.parse("https://x.com/intent/follow?screen_name=actkites")
120 | )
121 | )
122 | },
123 | color = MaterialTheme.colorScheme.primary
124 | )
125 | }
126 | }
127 |
128 | Divider(modifier = Modifier.padding(vertical = 8.dp))
129 |
130 | // 开源信息
131 | Text(
132 | "开源信息",
133 | style = MaterialTheme.typography.titleMedium
134 | )
135 | Card(
136 | modifier = Modifier.fillMaxWidth()
137 | ) {
138 | Column(
139 | modifier = Modifier.padding(16.dp),
140 | verticalArrangement = Arrangement.spacedBy(8.dp)
141 | ) {
142 | Text(
143 | "本项目已在 GitHub 开源",
144 | textAlign = TextAlign.Center,
145 | modifier = Modifier.fillMaxWidth()
146 | )
147 | Text(
148 | "访问项目主页",
149 | color = MaterialTheme.colorScheme.primary,
150 | textAlign = TextAlign.Center,
151 | modifier = Modifier
152 | .fillMaxWidth()
153 | .clickable {
154 | context.startActivity(
155 | Intent(
156 | Intent.ACTION_VIEW,
157 | Uri.parse("https://github.com/Ackites/Nrfr")
158 | )
159 | )
160 | }
161 | )
162 | }
163 | }
164 |
165 | Spacer(modifier = Modifier.weight(1f))
166 |
167 | // 版权信息
168 | Text(
169 | "© 2024 Antkites. All rights reserved.",
170 | style = MaterialTheme.typography.bodySmall,
171 | color = MaterialTheme.colorScheme.onSurfaceVariant
172 | )
173 | }
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/ui/screens/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.ui.screens
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.text.KeyboardActions
6 | import androidx.compose.foundation.text.KeyboardOptions
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.filled.Info
9 | import androidx.compose.material3.*
10 | import androidx.compose.runtime.*
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.platform.LocalFocusManager
15 | import androidx.compose.ui.res.painterResource
16 | import androidx.compose.ui.text.input.ImeAction
17 | import androidx.compose.ui.text.input.KeyboardType
18 | import androidx.compose.ui.unit.dp
19 | import com.github.nrfr.R
20 | import com.github.nrfr.data.CountryPresets
21 | import com.github.nrfr.data.PresetCarriers
22 | import com.github.nrfr.manager.CarrierConfigManager
23 | import com.github.nrfr.model.SimCardInfo
24 |
25 | @OptIn(ExperimentalMaterial3Api::class)
26 | @Composable
27 | fun MainScreen(onShowAbout: () -> Unit) {
28 | val context = LocalContext.current
29 | var selectedSimCard by remember { mutableStateOf(null) }
30 | var selectedCountryCode by remember { mutableStateOf("") }
31 | var customCountryCode by remember { mutableStateOf("") }
32 | var isCustomCountryCode by remember { mutableStateOf(false) }
33 | var selectedCarrier by remember { mutableStateOf(null) }
34 | var customCarrierName by remember { mutableStateOf("") }
35 | var isSimCardMenuExpanded by remember { mutableStateOf(false) }
36 | var isCountryCodeMenuExpanded by remember { mutableStateOf(false) }
37 | var isCarrierMenuExpanded by remember { mutableStateOf(false) }
38 | var refreshTrigger by remember { mutableStateOf(0) }
39 |
40 | // 获取实际的 SIM 卡信息
41 | val simCards = remember(context, refreshTrigger) { CarrierConfigManager.getSimCards(context) }
42 |
43 | // 当 simCards 更新时,更新选中的 SIM 卡信息
44 | LaunchedEffect(simCards, selectedSimCard) {
45 | if (selectedSimCard != null) {
46 | selectedSimCard = simCards.find { it.slot == selectedSimCard?.slot }
47 | }
48 | }
49 |
50 | Scaffold(
51 | modifier = Modifier.fillMaxSize(),
52 | topBar = {
53 | CenterAlignedTopAppBar(
54 | title = {
55 | Row(
56 | verticalAlignment = Alignment.CenterVertically,
57 | horizontalArrangement = Arrangement.Center
58 | ) {
59 | Icon(
60 | painter = painterResource(id = R.drawable.ic_launcher_foreground),
61 | modifier = Modifier.size(48.dp),
62 | contentDescription = "App Icon",
63 | tint = MaterialTheme.colorScheme.primary
64 | )
65 | Spacer(modifier = Modifier.width(4.dp))
66 | Text("Nrfr")
67 | }
68 | },
69 | actions = {
70 | IconButton(onClick = onShowAbout) {
71 | Icon(Icons.Default.Info, contentDescription = "关于")
72 | }
73 | }
74 | )
75 | }
76 | ) { innerPadding ->
77 | Column(
78 | modifier = Modifier
79 | .fillMaxSize()
80 | .padding(innerPadding)
81 | .padding(16.dp),
82 | horizontalAlignment = Alignment.CenterHorizontally,
83 | verticalArrangement = Arrangement.spacedBy(16.dp)
84 | ) {
85 | // SIM卡选择
86 | SimCardSelector(
87 | simCards = simCards,
88 | selectedSimCard = selectedSimCard,
89 | isExpanded = isSimCardMenuExpanded,
90 | onExpandedChange = { isSimCardMenuExpanded = it },
91 | onSimCardSelected = { selectedSimCard = it }
92 | )
93 |
94 | // 显示当前选中的 SIM 卡的配置信息
95 | selectedSimCard?.let { simCard ->
96 | CurrentConfigCard(simCard = simCard)
97 | }
98 |
99 | // 国家码选择
100 | CountryCodeSelector(
101 | selectedCountryCode = selectedCountryCode,
102 | isCustomCountryCode = isCustomCountryCode,
103 | customCountryCode = customCountryCode,
104 | isExpanded = isCountryCodeMenuExpanded,
105 | onExpandedChange = { isCountryCodeMenuExpanded = it },
106 | onCountryCodeSelected = { code ->
107 | selectedCountryCode = code
108 | isCustomCountryCode = false
109 | },
110 | onCustomSelected = {
111 | isCustomCountryCode = true
112 | selectedCountryCode = customCountryCode
113 | }
114 | )
115 |
116 | // 自定义国家码输入框
117 | if (isCustomCountryCode) {
118 | CustomCountryCodeInput(
119 | value = customCountryCode,
120 | onValueChange = {
121 | if (it.length <= 2 && it.all { char -> char.isLetter() }) {
122 | customCountryCode = it.uppercase()
123 | selectedCountryCode = it.uppercase()
124 | }
125 | }
126 | )
127 | }
128 |
129 | // 运营商选择
130 | CarrierSelector(
131 | selectedCarrier = selectedCarrier,
132 | isExpanded = isCarrierMenuExpanded,
133 | onExpandedChange = { isCarrierMenuExpanded = it },
134 | onCarrierSelected = { carrier ->
135 | selectedCarrier = carrier
136 | customCarrierName = carrier.displayName
137 | }
138 | )
139 |
140 | // 自定义运营商名称输入框
141 | if (selectedCarrier?.name == "自定义") {
142 | CustomCarrierNameInput(
143 | value = customCarrierName,
144 | onValueChange = { customCarrierName = it }
145 | )
146 | }
147 |
148 | Spacer(modifier = Modifier.weight(1f))
149 |
150 | // 按钮行
151 | ActionButtons(
152 | selectedSimCard = selectedSimCard,
153 | selectedCountryCode = selectedCountryCode,
154 | isCustomCountryCode = isCustomCountryCode,
155 | customCountryCode = customCountryCode,
156 | selectedCarrier = selectedCarrier,
157 | customCarrierName = customCarrierName,
158 | onReset = {
159 | try {
160 | CarrierConfigManager.resetCarrierConfig(it.subId)
161 | Toast.makeText(context, "设置已还原", Toast.LENGTH_SHORT).show()
162 | refreshTrigger += 1
163 | selectedCountryCode = ""
164 | selectedCarrier = null
165 | customCarrierName = ""
166 | } catch (e: Exception) {
167 | Toast.makeText(context, "还原失败: ${e.message}", Toast.LENGTH_SHORT).show()
168 | }
169 | },
170 | onSave = { simCard ->
171 | try {
172 | val carrierName = if (selectedCarrier?.name == "自定义") {
173 | customCarrierName.takeIf { it.isNotEmpty() }
174 | } else {
175 | selectedCarrier?.displayName
176 | }
177 | val countryCode = if (isCustomCountryCode) {
178 | customCountryCode.takeIf { it.length == 2 }
179 | } else {
180 | selectedCountryCode
181 | }
182 | CarrierConfigManager.setCarrierConfig(
183 | simCard.subId,
184 | countryCode,
185 | carrierName
186 | )
187 | Toast.makeText(context, "设置已保存", Toast.LENGTH_SHORT).show()
188 | refreshTrigger += 1
189 | } catch (e: Exception) {
190 | Toast.makeText(context, "保存失败: ${e.message}", Toast.LENGTH_SHORT).show()
191 | }
192 | }
193 | )
194 | }
195 | }
196 | }
197 |
198 | @OptIn(ExperimentalMaterial3Api::class)
199 | @Composable
200 | private fun SimCardSelector(
201 | simCards: List,
202 | selectedSimCard: SimCardInfo?,
203 | isExpanded: Boolean,
204 | onExpandedChange: (Boolean) -> Unit,
205 | onSimCardSelected: (SimCardInfo) -> Unit
206 | ) {
207 | ExposedDropdownMenuBox(
208 | expanded = isExpanded,
209 | onExpandedChange = onExpandedChange
210 | ) {
211 | OutlinedTextField(
212 | value = selectedSimCard?.let { "SIM ${it.slot} (${it.carrierName})" } ?: "",
213 | onValueChange = {},
214 | readOnly = true,
215 | label = { Text("选择SIM卡") },
216 | trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
217 | modifier = Modifier
218 | .fillMaxWidth()
219 | .menuAnchor()
220 | )
221 | ExposedDropdownMenu(
222 | expanded = isExpanded,
223 | onDismissRequest = { onExpandedChange(false) }
224 | ) {
225 | simCards.forEach { simCard ->
226 | DropdownMenuItem(
227 | text = {
228 | Column {
229 | Text("SIM ${simCard.slot} (${simCard.carrierName})")
230 | if (simCard.currentConfig.isEmpty()) {
231 | Text(
232 | "无覆盖配置",
233 | style = MaterialTheme.typography.bodySmall,
234 | color = MaterialTheme.colorScheme.onSurfaceVariant
235 | )
236 | } else {
237 | simCard.currentConfig.forEach { (key, value) ->
238 | Text(
239 | "$key: $value",
240 | style = MaterialTheme.typography.bodySmall,
241 | color = MaterialTheme.colorScheme.onSurfaceVariant
242 | )
243 | }
244 | }
245 | }
246 | },
247 | onClick = {
248 | onSimCardSelected(simCard)
249 | onExpandedChange(false)
250 | }
251 | )
252 | }
253 | }
254 | }
255 | }
256 |
257 | @Composable
258 | private fun CurrentConfigCard(simCard: SimCardInfo) {
259 | Card(
260 | modifier = Modifier
261 | .fillMaxWidth()
262 | .padding(vertical = 8.dp)
263 | ) {
264 | Column(
265 | modifier = Modifier
266 | .fillMaxWidth()
267 | .padding(16.dp)
268 | ) {
269 | Text(
270 | "当前配置",
271 | style = MaterialTheme.typography.titleMedium
272 | )
273 | Spacer(modifier = Modifier.height(8.dp))
274 | if (simCard.currentConfig.isEmpty()) {
275 | Text(
276 | "无覆盖配置",
277 | style = MaterialTheme.typography.bodyMedium,
278 | color = MaterialTheme.colorScheme.onSurfaceVariant
279 | )
280 | } else {
281 | simCard.currentConfig.forEach { (key, value) ->
282 | Text(
283 | "$key: $value",
284 | style = MaterialTheme.typography.bodyMedium
285 | )
286 | }
287 | }
288 | }
289 | }
290 | }
291 |
292 | @OptIn(ExperimentalMaterial3Api::class)
293 | @Composable
294 | private fun CountryCodeSelector(
295 | selectedCountryCode: String,
296 | isCustomCountryCode: Boolean,
297 | customCountryCode: String,
298 | isExpanded: Boolean,
299 | onExpandedChange: (Boolean) -> Unit,
300 | onCountryCodeSelected: (String) -> Unit,
301 | onCustomSelected: () -> Unit
302 | ) {
303 | ExposedDropdownMenuBox(
304 | expanded = isExpanded,
305 | onExpandedChange = onExpandedChange
306 | ) {
307 | OutlinedTextField(
308 | value = when {
309 | isCustomCountryCode -> "自定义"
310 | selectedCountryCode.isEmpty() -> ""
311 | else -> CountryPresets.countries.find { it.code == selectedCountryCode }
312 | ?.let { "${it.name} (${it.code})" }
313 | ?: selectedCountryCode
314 | },
315 | onValueChange = {},
316 | readOnly = true,
317 | label = { Text("选择国家码") },
318 | trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
319 | modifier = Modifier
320 | .fillMaxWidth()
321 | .menuAnchor()
322 | )
323 | ExposedDropdownMenu(
324 | expanded = isExpanded,
325 | onDismissRequest = { onExpandedChange(false) }
326 | ) {
327 | // 预设国家码列表
328 | CountryPresets.countries.forEach { countryInfo ->
329 | DropdownMenuItem(
330 | text = { Text("${countryInfo.name} (${countryInfo.code})") },
331 | onClick = {
332 | onCountryCodeSelected(countryInfo.code)
333 | onExpandedChange(false)
334 | }
335 | )
336 | }
337 | // 自定义选项
338 | DropdownMenuItem(
339 | text = { Text("自定义") },
340 | onClick = {
341 | onCustomSelected()
342 | onExpandedChange(false)
343 | }
344 | )
345 | }
346 | }
347 | }
348 |
349 | @OptIn(ExperimentalMaterial3Api::class)
350 | @Composable
351 | private fun CustomCountryCodeInput(
352 | value: String,
353 | onValueChange: (String) -> Unit
354 | ) {
355 | val focusManager = LocalFocusManager.current
356 | OutlinedTextField(
357 | value = value,
358 | onValueChange = onValueChange,
359 | label = { Text("自定义国家码 (2位字母)") },
360 | keyboardOptions = KeyboardOptions(
361 | keyboardType = KeyboardType.Text,
362 | imeAction = ImeAction.Done
363 | ),
364 | keyboardActions = KeyboardActions(
365 | onDone = {
366 | focusManager.clearFocus()
367 | }
368 | ),
369 | singleLine = true,
370 | modifier = Modifier.fillMaxWidth()
371 | )
372 | }
373 |
374 | @OptIn(ExperimentalMaterial3Api::class)
375 | @Composable
376 | private fun CarrierSelector(
377 | selectedCarrier: PresetCarriers.CarrierPreset?,
378 | isExpanded: Boolean,
379 | onExpandedChange: (Boolean) -> Unit,
380 | onCarrierSelected: (PresetCarriers.CarrierPreset) -> Unit
381 | ) {
382 | ExposedDropdownMenuBox(
383 | expanded = isExpanded,
384 | onExpandedChange = onExpandedChange
385 | ) {
386 | OutlinedTextField(
387 | value = selectedCarrier?.name ?: "",
388 | onValueChange = {},
389 | readOnly = true,
390 | label = { Text("选择运营商") },
391 | trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
392 | modifier = Modifier
393 | .fillMaxWidth()
394 | .menuAnchor()
395 | )
396 | ExposedDropdownMenu(
397 | expanded = isExpanded,
398 | onDismissRequest = { onExpandedChange(false) }
399 | ) {
400 | // 分组显示运营商
401 | PresetCarriers.presets
402 | .groupBy { it.region }
403 | .forEach { (region, carriers) ->
404 | if (region.isNotEmpty()) {
405 | val regionName = CountryPresets.countries.find { it.code == region }?.name ?: region
406 | Text(
407 | regionName,
408 | style = MaterialTheme.typography.titleSmall,
409 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
410 | color = MaterialTheme.colorScheme.primary
411 | )
412 | carriers.forEach { carrier ->
413 | DropdownMenuItem(
414 | text = { Text(carrier.name) },
415 | onClick = {
416 | onCarrierSelected(carrier)
417 | onExpandedChange(false)
418 | }
419 | )
420 | }
421 | Divider(modifier = Modifier.padding(vertical = 4.dp))
422 | }
423 | }
424 |
425 | // 自定义选项
426 | PresetCarriers.presets
427 | .filter { it.region.isEmpty() }
428 | .forEach { carrier ->
429 | DropdownMenuItem(
430 | text = { Text(carrier.name) },
431 | onClick = {
432 | onCarrierSelected(carrier)
433 | onExpandedChange(false)
434 | }
435 | )
436 | }
437 | }
438 | }
439 | }
440 |
441 | @OptIn(ExperimentalMaterial3Api::class)
442 | @Composable
443 | private fun CustomCarrierNameInput(
444 | value: String,
445 | onValueChange: (String) -> Unit
446 | ) {
447 | OutlinedTextField(
448 | value = value,
449 | onValueChange = onValueChange,
450 | label = { Text("自定义运营商名称") },
451 | modifier = Modifier.fillMaxWidth()
452 | )
453 | }
454 |
455 | @Composable
456 | private fun ActionButtons(
457 | selectedSimCard: SimCardInfo?,
458 | selectedCountryCode: String,
459 | isCustomCountryCode: Boolean,
460 | customCountryCode: String,
461 | selectedCarrier: PresetCarriers.CarrierPreset?,
462 | customCarrierName: String,
463 | onReset: (SimCardInfo) -> Unit,
464 | onSave: (SimCardInfo) -> Unit
465 | ) {
466 | Row(
467 | modifier = Modifier.fillMaxWidth(),
468 | horizontalArrangement = Arrangement.spacedBy(16.dp)
469 | ) {
470 | // 还原按钮
471 | OutlinedButton(
472 | onClick = { selectedSimCard?.let(onReset) },
473 | modifier = Modifier.weight(1f),
474 | enabled = selectedSimCard != null
475 | ) {
476 | Text("还原设置")
477 | }
478 |
479 | // 保存按钮
480 | Button(
481 | onClick = { selectedSimCard?.let(onSave) },
482 | modifier = Modifier.weight(1f),
483 | enabled = selectedSimCard != null && (
484 | (isCustomCountryCode && customCountryCode.length == 2) ||
485 | (!isCustomCountryCode && selectedCountryCode.isNotEmpty()) ||
486 | (selectedCarrier != null && (selectedCarrier.name != "自定义" || customCarrierName.isNotEmpty()))
487 | )
488 | ) {
489 | Text("保存生效")
490 | }
491 | }
492 | }
493 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/ui/screens/ShizukuNotReadyScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.ui.screens
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 |
11 | @Composable
12 | fun ShizukuNotReadyScreen() {
13 | Column(
14 | modifier = Modifier
15 | .fillMaxSize()
16 | .padding(16.dp),
17 | horizontalAlignment = Alignment.CenterHorizontally,
18 | verticalArrangement = Arrangement.Center
19 | ) {
20 | Text(
21 | text = "需要 Shizuku 权限",
22 | style = MaterialTheme.typography.headlineMedium
23 | )
24 | Spacer(modifier = Modifier.height(8.dp))
25 | Text(
26 | text = "请安装并启用 Shizuku,然后重启应用",
27 | style = MaterialTheme.typography.bodyLarge
28 | )
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.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.ui.platform.LocalContext
13 |
14 | private val DarkColorScheme = darkColorScheme(
15 | primary = Purple80,
16 | secondary = PurpleGrey80,
17 | tertiary = Pink80
18 | )
19 |
20 | private val LightColorScheme = lightColorScheme(
21 | primary = Purple40,
22 | secondary = PurpleGrey40,
23 | tertiary = Pink40
24 |
25 | /* Other default colors to override
26 | background = Color(0xFFFFFBFE),
27 | surface = Color(0xFFFFFBFE),
28 | onPrimary = Color.White,
29 | onSecondary = Color.White,
30 | onTertiary = Color.White,
31 | onBackground = Color(0xFF1C1B1F),
32 | onSurface = Color(0xFF1C1B1F),
33 | */
34 | )
35 |
36 | @Composable
37 | fun NrfrTheme(
38 | darkTheme: Boolean = isSystemInDarkTheme(),
39 | // Dynamic color is available on Android 12+
40 | dynamicColor: Boolean = true,
41 | content: @Composable () -> Unit
42 | ) {
43 | val colorScheme = when {
44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
45 | val context = LocalContext.current
46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
47 | }
48 |
49 | darkTheme -> DarkColorScheme
50 | else -> LightColorScheme
51 | }
52 |
53 | MaterialTheme(
54 | colorScheme = colorScheme,
55 | typography = Typography,
56 | content = content
57 | )
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/nrfr/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr.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/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Nrfr
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
13 |
14 |
20 |
--------------------------------------------------------------------------------
/app/src/test/java/com/github/nrfr/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.nrfr
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id("com.android.application") version "8.7.0-rc01" apply false
4 | id("org.jetbrains.kotlin.android") version "2.0.0" apply false
5 | id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
6 | }
7 |
--------------------------------------------------------------------------------
/docs/images/app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/docs/images/app.png
--------------------------------------------------------------------------------
/docs/images/client.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/docs/images/client.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 30 20:02:19 CST 2024
2 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
3 |
--------------------------------------------------------------------------------
/nrfr-client/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries
2 | build/bin
3 | *.exe
4 | *.exe~
5 | *.dll
6 | *.so
7 | *.dylib
8 |
9 | # Wails
10 | wails.exe
11 | frontend/dist
12 | frontend/node_modules
13 | frontend/.env
14 | frontend/.env.*
15 |
16 | # Resources
17 | resources/*.apk
18 |
19 | # IDE
20 | .idea
21 | .vscode
22 | *.swp
23 | *.swo
24 |
25 | # Logs
26 | *.log
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | # System
32 | .DS_Store
33 | Thumbs.db
34 |
--------------------------------------------------------------------------------
/nrfr-client/README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | ## About
4 |
5 | This is the official Wails React-TS template.
6 |
7 | You can configure the project by editing `wails.json`. More information about the project settings can be found
8 | here: https://wails.io/docs/reference/project-config
9 |
10 | ## Live Development
11 |
12 | To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
13 | server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
14 | and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
15 | to this in your browser, and you can call your Go code from devtools.
16 |
17 | ## Building
18 |
19 | To build a redistributable, production mode package, use `wails build`.
20 |
--------------------------------------------------------------------------------
/nrfr-client/app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 | "runtime"
10 | "strconv"
11 | "strings"
12 | "syscall"
13 | "time"
14 |
15 | "github.com/electricbubble/gadb"
16 | wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
17 | )
18 |
19 | // App struct
20 | type App struct {
21 | ctx context.Context
22 | adbClient gadb.Client
23 | selectedDevice *gadb.Device
24 | adbPath string // 保存 adb 路径以便关闭时使用
25 | }
26 |
27 | // DeviceInfo 设备信息结构
28 | type DeviceInfo struct {
29 | Serial string `json:"serial"`
30 | State string `json:"state"`
31 | Product string `json:"product"`
32 | Model string `json:"model"`
33 | }
34 |
35 | // AppStatus 应用状态结构
36 | type AppStatus struct {
37 | Shizuku bool `json:"shizuku"`
38 | Nrfr struct {
39 | Installed bool `json:"installed"`
40 | NeedUpdate bool `json:"needUpdate"`
41 | } `json:"nrfr"`
42 | }
43 |
44 | // NewApp creates a new App application struct
45 | func NewApp() *App {
46 | return &App{}
47 | }
48 |
49 | // startup is called when the app starts. The context is saved
50 | func (a *App) startup(ctx context.Context) {
51 | a.ctx = ctx
52 |
53 | // 尝试启动 ADB 服务器
54 | execPath, err := os.Executable()
55 | if err != nil {
56 | wailsruntime.LogError(ctx, fmt.Sprintf("获取执行路径失败: %v", err))
57 | return
58 | }
59 | execDir := filepath.Dir(execPath)
60 |
61 | // 优先使用程序目录下的 platform-tools
62 | adbPath := filepath.Join(execDir, "platform-tools", "adb")
63 | if runtime.GOOS == "windows" {
64 | adbPath = filepath.Join(execDir, "platform-tools", "adb.exe")
65 | }
66 |
67 | // 如果程序目录下的 adb 不存在,尝试使用系统环境中的 adb
68 | if _, err := os.Stat(adbPath); os.IsNotExist(err) {
69 | if runtime.GOOS == "windows" {
70 | // 尝试从环境变量中获取 ANDROID_HOME
71 | androidHome := os.Getenv("ANDROID_HOME")
72 | if androidHome == "" {
73 | // 如果没有环境变量,尝试从默认安装路径获取
74 | androidHome = filepath.Join(os.Getenv("LOCALAPPDATA"), "Android", "Sdk")
75 | }
76 | adbPath = filepath.Join(androidHome, "platform-tools", "adb.exe")
77 | } else {
78 | adbPath = "adb" // 在类 Unix 系统上使用 PATH 中的 adb
79 | }
80 | }
81 |
82 | // 保存 adb 路径
83 | a.adbPath = adbPath
84 |
85 | // 检查 ADB 服务器是否已在运行
86 | cmd := exec.Command(adbPath, "devices")
87 | if runtime.GOOS == "windows" {
88 | cmd.SysProcAttr = &syscall.SysProcAttr{
89 | HideWindow: true,
90 | CreationFlags: 0x08000000, // CREATE_NO_WINDOW
91 | }
92 | }
93 | if err := cmd.Run(); err != nil {
94 | // ADB 服务器未运行,启动它
95 | startCmd := exec.Command(adbPath, "start-server")
96 | if runtime.GOOS == "windows" {
97 | startCmd.SysProcAttr = &syscall.SysProcAttr{
98 | HideWindow: true,
99 | CreationFlags: 0x08000000, // CREATE_NO_WINDOW
100 | }
101 | }
102 | if err := startCmd.Run(); err != nil {
103 | wailsruntime.LogError(ctx, fmt.Sprintf("启动ADB服务器失败: %v", err))
104 | }
105 | }
106 |
107 | // 初始化 ADB 客户端
108 | client, err := gadb.NewClient()
109 | if err != nil {
110 | wailsruntime.LogError(ctx, fmt.Sprintf("初始化ADB失败: %v", err))
111 | return
112 | }
113 | a.adbClient = client
114 | }
115 |
116 | // shutdown is called when the app is closing
117 | func (a *App) shutdown(ctx context.Context) {
118 | // 关闭 ADB 服务器
119 | if a.adbPath != "" {
120 | cmd := exec.Command(a.adbPath, "kill-server")
121 | if runtime.GOOS == "windows" {
122 | cmd.SysProcAttr = &syscall.SysProcAttr{
123 | HideWindow: true,
124 | CreationFlags: 0x08000000, // CREATE_NO_WINDOW
125 | }
126 | }
127 | if err := cmd.Run(); err != nil {
128 | wailsruntime.LogError(ctx, fmt.Sprintf("关闭ADB服务器失败: %v", err))
129 | }
130 | }
131 | }
132 |
133 | // GetDevices 获取已连接的设备列表
134 | func (a *App) GetDevices() []DeviceInfo {
135 | devices, err := a.adbClient.DeviceList()
136 | if err != nil {
137 | wailsruntime.LogError(a.ctx, fmt.Sprintf("获取设备列表失败: %v", err))
138 | return nil
139 | }
140 |
141 | var deviceInfos []DeviceInfo
142 | for _, device := range devices {
143 | state, _ := device.State()
144 | product, _ := device.Product()
145 | model, _ := device.Model()
146 |
147 | info := DeviceInfo{
148 | Serial: device.Serial(),
149 | State: string(state),
150 | Product: product,
151 | Model: model,
152 | }
153 | deviceInfos = append(deviceInfos, info)
154 | }
155 | return deviceInfos
156 | }
157 |
158 | // SelectDevice 选择设备
159 | func (a *App) SelectDevice(serial string) error {
160 | devices, err := a.adbClient.DeviceList()
161 | if err != nil {
162 | return fmt.Errorf("获取设备列表失败: %v", err)
163 | }
164 |
165 | for _, device := range devices {
166 | if device.Serial() == serial {
167 | a.selectedDevice = &device
168 | return nil
169 | }
170 | }
171 | return fmt.Errorf("未找到设备: %s", serial)
172 | }
173 |
174 | // CheckApps 检查必要的应用是否已安装
175 | func (a *App) CheckApps() AppStatus {
176 | if a.selectedDevice == nil {
177 | return AppStatus{
178 | Shizuku: false,
179 | Nrfr: struct {
180 | Installed bool `json:"installed"`
181 | NeedUpdate bool `json:"needUpdate"`
182 | }{
183 | Installed: false,
184 | NeedUpdate: false,
185 | },
186 | }
187 | }
188 |
189 | // 检查 Shizuku
190 | shizukuInstalled, _ := a.isPackageInstalled("moe.shizuku.privileged.api")
191 |
192 | // 检查 Nrfr
193 | nrfrInstalled, _ := a.isPackageInstalled("com.github.nrfr")
194 | needUpdate := false
195 | if nrfrInstalled {
196 | needUpdate, _ = a.CheckNrfrUpdate()
197 | }
198 |
199 | return AppStatus{
200 | Shizuku: shizukuInstalled,
201 | Nrfr: struct {
202 | Installed bool `json:"installed"`
203 | NeedUpdate bool `json:"needUpdate"`
204 | }{
205 | Installed: nrfrInstalled,
206 | NeedUpdate: needUpdate,
207 | },
208 | }
209 | }
210 |
211 | // isPackageInstalled 检查包是否已安装
212 | func (a *App) isPackageInstalled(packageName string) (bool, error) {
213 | output, err := a.selectedDevice.RunShellCommand("pm", "list", "packages", packageName)
214 | if err != nil {
215 | return false, err
216 | }
217 | return strings.Contains(output, packageName), nil
218 | }
219 |
220 | // InstallShizuku 安装 Shizuku
221 | func (a *App) InstallShizuku() error {
222 | if a.selectedDevice == nil {
223 | return fmt.Errorf("未选择设备")
224 | }
225 |
226 | // 从本地资源目录推送 APK 到设备
227 | execPath, err := os.Executable()
228 | if err != nil {
229 | return fmt.Errorf("获取执行路径失败: %v", err)
230 | }
231 | execDir := filepath.Dir(execPath)
232 | localApk := filepath.Join(execDir, "resources", "shizuku.apk")
233 | remoteApk := "/data/local/tmp/shizuku.apk"
234 |
235 | // 检查文件是否存在
236 | if _, err := os.Stat(localApk); os.IsNotExist(err) {
237 | return fmt.Errorf("shizuku apk 文件不存在: %s", localApk)
238 | }
239 |
240 | // 推送 APK 文件
241 | file, err := os.Open(localApk)
242 | if err != nil {
243 | return fmt.Errorf("打开 Shizuku APK 文件失败: %v", err)
244 | }
245 | defer file.Close()
246 |
247 | err = a.selectedDevice.Push(file, remoteApk, time.Now())
248 | if err != nil {
249 | return fmt.Errorf("推送 Shizuku APK 文件失败: %v", err)
250 | }
251 |
252 | // 安装 APK
253 | _, err = a.selectedDevice.RunShellCommand("pm", "install", "-r", remoteApk)
254 | if err != nil {
255 | return fmt.Errorf("安装 Shizuku 失败: %v", err)
256 | }
257 |
258 | // 清理临时文件
259 | _, err = a.selectedDevice.RunShellCommand("rm", remoteApk)
260 | if err != nil {
261 | wailsruntime.LogWarning(a.ctx, fmt.Sprintf("清理临时文件失败: %v", err))
262 | }
263 |
264 | return nil
265 | }
266 |
267 | // InstallNrfr 安装 Nrfr
268 | func (a *App) InstallNrfr() error {
269 | if a.selectedDevice == nil {
270 | return fmt.Errorf("未选择设备")
271 | }
272 |
273 | // 从本地资源目录推送 APK 到设备
274 | execPath, err := os.Executable()
275 | if err != nil {
276 | return fmt.Errorf("获取执行路径失败: %v", err)
277 | }
278 | execDir := filepath.Dir(execPath)
279 | localApk := filepath.Join(execDir, "resources", "nrfr.apk")
280 | remoteApk := "/data/local/tmp/nrfr.apk"
281 |
282 | // 检查文件是否存在
283 | if _, err := os.Stat(localApk); os.IsNotExist(err) {
284 | return fmt.Errorf("nrfr apk 文件不存在: %s", localApk)
285 | }
286 |
287 | // 推送 APK 文件
288 | file, err := os.Open(localApk)
289 | if err != nil {
290 | return fmt.Errorf("打开 Nrfr APK 文件失败: %v", err)
291 | }
292 | defer file.Close()
293 |
294 | err = a.selectedDevice.Push(file, remoteApk, time.Now())
295 | if err != nil {
296 | return fmt.Errorf("推送 Nrfr APK 文件失败: %v", err)
297 | }
298 |
299 | // 安装 APK
300 | _, err = a.selectedDevice.RunShellCommand("pm", "install", "-r", remoteApk)
301 | if err != nil {
302 | return fmt.Errorf("安装 Nrfr 失败: %v", err)
303 | }
304 |
305 | // 清理临时文件
306 | _, err = a.selectedDevice.RunShellCommand("rm", remoteApk)
307 | if err != nil {
308 | wailsruntime.LogWarning(a.ctx, fmt.Sprintf("清理临时文件失败: %v", err))
309 | }
310 |
311 | return nil
312 | }
313 |
314 | // StartShizuku 启动 Shizuku
315 | func (a *App) StartShizuku() error {
316 | if a.selectedDevice == nil {
317 | return fmt.Errorf("未选择设备")
318 | }
319 |
320 | // 先启动 Shizuku 应用
321 | _, err := a.selectedDevice.RunShellCommand("monkey", "-p", "moe.shizuku.privileged.api", "1")
322 | if err != nil {
323 | return fmt.Errorf("启动 shizuku 应用失败: %v", err)
324 | }
325 |
326 | // 等待应用启动
327 | time.Sleep(time.Second * 2)
328 |
329 | // 执行启动脚本
330 | output, err := a.selectedDevice.RunShellCommand("sh", "/sdcard/Android/data/moe.shizuku.privileged.api/start.sh")
331 | if err != nil {
332 | return fmt.Errorf("启动 shizuku 服务失败: %v", err)
333 | }
334 | wailsruntime.LogInfo(a.ctx, fmt.Sprintf("shizuku 启动输出: %s", output))
335 | return nil
336 | }
337 |
338 | // WindowMinimise 最小化窗口
339 | func (a *App) WindowMinimise() {
340 | wailsruntime.WindowMinimise(a.ctx)
341 | }
342 |
343 | // WindowMaximise 最大化窗口
344 | func (a *App) WindowMaximise() {
345 | wailsruntime.WindowToggleMaximise(a.ctx)
346 | }
347 |
348 | // WindowClose 关闭窗口
349 | func (a *App) WindowClose() {
350 | // 先关闭 ADB 服务器
351 | if a.adbPath != "" {
352 | cmd := exec.Command(a.adbPath, "kill-server")
353 | if runtime.GOOS == "windows" {
354 | cmd.SysProcAttr = &syscall.SysProcAttr{
355 | HideWindow: true,
356 | CreationFlags: 0x08000000, // CREATE_NO_WINDOW
357 | }
358 | }
359 | _ = cmd.Run() // 忽略错误,因为我们即将退出程序
360 | }
361 | wailsruntime.Quit(a.ctx)
362 | }
363 |
364 | // StartNrfr 启动 Nrfr 应用
365 | func (a *App) StartNrfr() error {
366 | if a.selectedDevice == nil {
367 | return fmt.Errorf("no device selected")
368 | }
369 |
370 | // 使用 monkey 启动 Nrfr
371 | _, err := a.selectedDevice.RunShellCommand("monkey", "-p", "com.github.nrfr", "1")
372 | if err != nil {
373 | return fmt.Errorf("failed to start nrfr: %v", err)
374 | }
375 |
376 | return nil
377 | }
378 |
379 | // GetAppVersion 获取已安装应用的版本号
380 | func (a *App) GetAppVersion(packageName string) (string, error) {
381 | if a.selectedDevice == nil {
382 | return "", fmt.Errorf("未选择设备")
383 | }
384 |
385 | output, err := a.selectedDevice.RunShellCommand("dumpsys", "package", packageName, "|", "grep", "versionName")
386 | if err != nil {
387 | return "", fmt.Errorf("获取版本号失败: %v", err)
388 | }
389 |
390 | // 解析版本号
391 | parts := strings.Split(strings.TrimSpace(output), "=")
392 | if len(parts) != 2 {
393 | return "", fmt.Errorf("解析版本号失败")
394 | }
395 | return strings.TrimSpace(parts[1]), nil
396 | }
397 |
398 | // compareVersions 比较两个版本号,如果 v1 < v2 返回 -1,v1 = v2 返回 0,v1 > v2 返回 1
399 | func compareVersions(v1, v2 string) int {
400 | // 移除可能的 'v' 前缀
401 | v1 = strings.TrimPrefix(v1, "v")
402 | v2 = strings.TrimPrefix(v2, "v")
403 |
404 | // 分割版本号
405 | parts1 := strings.Split(v1, ".")
406 | parts2 := strings.Split(v2, ".")
407 |
408 | // 确保两个版本号都有三个部分
409 | for len(parts1) < 3 {
410 | parts1 = append(parts1, "0")
411 | }
412 | for len(parts2) < 3 {
413 | parts2 = append(parts2, "0")
414 | }
415 |
416 | // 比较每个部分
417 | for i := 0; i < 3; i++ {
418 | num1, _ := strconv.Atoi(parts1[i])
419 | num2, _ := strconv.Atoi(parts2[i])
420 |
421 | if num1 < num2 {
422 | return -1
423 | }
424 | if num1 > num2 {
425 | return 1
426 | }
427 | }
428 |
429 | return 0
430 | }
431 |
432 | // CheckNrfrUpdate 检查Nrfr是否需要更新
433 | func (a *App) CheckNrfrUpdate() (bool, error) {
434 | if a.selectedDevice == nil {
435 | return false, fmt.Errorf("未选择设备")
436 | }
437 |
438 | // 检查是否已安装
439 | installed, err := a.isPackageInstalled("com.github.nrfr")
440 | if err != nil {
441 | return false, err
442 | }
443 |
444 | if !installed {
445 | return true, nil // 未安装,需要安装
446 | }
447 |
448 | // 获取已安装版本
449 | currentVersion, err := a.GetAppVersion("com.github.nrfr")
450 | if err != nil {
451 | return false, err
452 | }
453 |
454 | // 最新版本号(从build.gradle.kts中获取)
455 | latestVersion := "1.0.3" // 这里硬编码为当前最新版本
456 |
457 | // 比较版本号
458 | return compareVersions(currentVersion, latestVersion) < 0, nil
459 | }
460 |
--------------------------------------------------------------------------------
/nrfr-client/build/README.md:
--------------------------------------------------------------------------------
1 | # Build Directory
2 |
3 | The build directory is used to house all the build files and assets for your application.
4 |
5 | The structure is:
6 |
7 | * bin - Output directory
8 | * darwin - macOS specific files
9 | * windows - Windows specific files
10 |
11 | ## Mac
12 |
13 | The `darwin` directory holds files specific to Mac builds.
14 | These may be customised and used as part of the build. To return these files to the default state, simply delete them
15 | and
16 | build with `wails build`.
17 |
18 | The directory contains the following files:
19 |
20 | - `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
21 | - `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
22 |
23 | ## Windows
24 |
25 | The `windows` directory contains the manifest and rc files used when building with `wails build`.
26 | These may be customised for your application. To return these files to the default state, simply delete them and
27 | build with `wails build`.
28 |
29 | - `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
30 | use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
31 | will be created using the `appicon.png` file in the build directory.
32 | - `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
33 | - `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
34 | as well as the application itself (right click the exe -> properties -> details)
35 | - `wails.exe.manifest` - The main application manifest file.
36 |
--------------------------------------------------------------------------------
/nrfr-client/build/appicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/nrfr-client/build/appicon.png
--------------------------------------------------------------------------------
/nrfr-client/build/darwin/Info.dev.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundlePackageType
5 | APPL
6 | CFBundleName
7 | {{.Info.ProductName}}
8 | CFBundleExecutable
9 | {{.Name}}
10 | CFBundleIdentifier
11 | com.wails.{{.Name}}
12 | CFBundleVersion
13 | {{.Info.ProductVersion}}
14 | CFBundleGetInfoString
15 | {{.Info.Comments}}
16 | CFBundleShortVersionString
17 | {{.Info.ProductVersion}}
18 | CFBundleIconFile
19 | iconfile
20 | LSMinimumSystemVersion
21 | 10.13.0
22 | NSHighResolutionCapable
23 | true
24 | NSHumanReadableCopyright
25 | {{.Info.Copyright}}
26 | {{if .Info.FileAssociations}}
27 | CFBundleDocumentTypes
28 |
29 | {{range .Info.FileAssociations}}
30 |
31 | CFBundleTypeExtensions
32 |
33 | {{.Ext}}
34 |
35 | CFBundleTypeName
36 | {{.Name}}
37 | CFBundleTypeRole
38 | {{.Role}}
39 | CFBundleTypeIconFile
40 | {{.IconName}}
41 |
42 | {{end}}
43 |
44 | {{end}}
45 | {{if .Info.Protocols}}
46 | CFBundleURLTypes
47 |
48 | {{range .Info.Protocols}}
49 |
50 | CFBundleURLName
51 | com.wails.{{.Scheme}}
52 | CFBundleURLSchemes
53 |
54 | {{.Scheme}}
55 |
56 | CFBundleTypeRole
57 | {{.Role}}
58 |
59 | {{end}}
60 |
61 | {{end}}
62 | NSAppTransportSecurity
63 |
64 | NSAllowsLocalNetworking
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/nrfr-client/build/darwin/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundlePackageType
5 | APPL
6 | CFBundleName
7 | {{.Info.ProductName}}
8 | CFBundleExecutable
9 | {{.Name}}
10 | CFBundleIdentifier
11 | com.wails.{{.Name}}
12 | CFBundleVersion
13 | {{.Info.ProductVersion}}
14 | CFBundleGetInfoString
15 | {{.Info.Comments}}
16 | CFBundleShortVersionString
17 | {{.Info.ProductVersion}}
18 | CFBundleIconFile
19 | iconfile
20 | LSMinimumSystemVersion
21 | 10.13.0
22 | NSHighResolutionCapable
23 | true
24 | NSHumanReadableCopyright
25 | {{.Info.Copyright}}
26 | {{if .Info.FileAssociations}}
27 | CFBundleDocumentTypes
28 |
29 | {{range .Info.FileAssociations}}
30 |
31 | CFBundleTypeExtensions
32 |
33 | {{.Ext}}
34 |
35 | CFBundleTypeName
36 | {{.Name}}
37 | CFBundleTypeRole
38 | {{.Role}}
39 | CFBundleTypeIconFile
40 | {{.IconName}}
41 |
42 | {{end}}
43 |
44 | {{end}}
45 | {{if .Info.Protocols}}
46 | CFBundleURLTypes
47 |
48 | {{range .Info.Protocols}}
49 |
50 | CFBundleURLName
51 | com.wails.{{.Scheme}}
52 | CFBundleURLSchemes
53 |
54 | {{.Scheme}}
55 |
56 | CFBundleTypeRole
57 | {{.Role}}
58 |
59 | {{end}}
60 |
61 | {{end}}
62 |
63 |
64 |
--------------------------------------------------------------------------------
/nrfr-client/build/windows/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/nrfr-client/build/windows/icon.ico
--------------------------------------------------------------------------------
/nrfr-client/build/windows/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "fixed": {
3 | "file_version": "{{.Info.ProductVersion}}"
4 | },
5 | "info": {
6 | "0000": {
7 | "ProductVersion": "{{.Info.ProductVersion}}",
8 | "CompanyName": "{{.Info.CompanyName}}",
9 | "FileDescription": "{{.Info.ProductName}}",
10 | "LegalCopyright": "{{.Info.Copyright}}",
11 | "ProductName": "{{.Info.ProductName}}",
12 | "Comments": "{{.Info.Comments}}"
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/nrfr-client/build/windows/installer/project.nsi:
--------------------------------------------------------------------------------
1 | Unicode true
2 |
3 | ####
4 | ## Please note: Template replacements don't work in this file. They are provided with default defines like
5 | ## mentioned underneath.
6 | ## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
7 | ## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
8 | ## from outside of Wails for debugging and development of the installer.
9 | ##
10 | ## For development first make a wails nsis build to populate the "wails_tools.nsh":
11 | ## > wails build --target windows/amd64 --nsis
12 | ## Then you can call makensis on this file with specifying the path to your binary:
13 | ## For a AMD64 only installer:
14 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
15 | ## For a ARM64 only installer:
16 | ## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
17 | ## For a installer with both architectures:
18 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
19 | ####
20 | ## The following information is taken from the ProjectInfo file, but they can be overwritten here.
21 | ####
22 | ## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
23 | ## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
24 | ## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
25 | ## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
26 | ## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
27 | ###
28 | ## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
29 | ## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
30 | ####
31 | ## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
32 | ####
33 | ## Include the wails tools
34 | ####
35 | !include "wails_tools.nsh"
36 |
37 | # The version information for this two must consist of 4 parts
38 | VIProductVersion "${INFO_PRODUCTVERSION}.0"
39 | VIFileVersion "${INFO_PRODUCTVERSION}.0"
40 |
41 | VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
42 | VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
43 | VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
44 | VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
45 | VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
46 | VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
47 |
48 | # Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
49 | ManifestDPIAware true
50 |
51 | !include "MUI.nsh"
52 |
53 | !define MUI_ICON "..\icon.ico"
54 | !define MUI_UNICON "..\icon.ico"
55 | # !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
56 | !define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
57 | !define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
58 |
59 | !insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
60 | # !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
61 | !insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
62 | !insertmacro MUI_PAGE_INSTFILES # Installing page.
63 | !insertmacro MUI_PAGE_FINISH # Finished installation page.
64 |
65 | !insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
66 |
67 | !insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
68 |
69 | ## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
70 | #!uninstfinalize 'signtool --file "%1"'
71 | #!finalize 'signtool --file "%1"'
72 |
73 | Name "${INFO_PRODUCTNAME}"
74 | OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
75 | InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
76 | ShowInstDetails show # This will always show the installation details.
77 |
78 | Function .onInit
79 | !insertmacro wails.checkArchitecture
80 | FunctionEnd
81 |
82 | Section
83 | !insertmacro wails.setShellContext
84 |
85 | !insertmacro wails.webview2runtime
86 |
87 | SetOutPath $INSTDIR
88 |
89 | !insertmacro wails.files
90 |
91 | CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
92 | CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
93 |
94 | !insertmacro wails.associateFiles
95 | !insertmacro wails.associateCustomProtocols
96 |
97 | !insertmacro wails.writeUninstaller
98 | SectionEnd
99 |
100 | Section "uninstall"
101 | !insertmacro wails.setShellContext
102 |
103 | RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
104 |
105 | RMDir /r $INSTDIR
106 |
107 | Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
108 | Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
109 |
110 | !insertmacro wails.unassociateFiles
111 | !insertmacro wails.unassociateCustomProtocols
112 |
113 | !insertmacro wails.deleteUninstaller
114 | SectionEnd
115 |
--------------------------------------------------------------------------------
/nrfr-client/build/windows/installer/wails_tools.nsh:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT - Generated automatically by `wails build`
2 |
3 | !include "x64.nsh"
4 | !include "WinVer.nsh"
5 | !include "FileFunc.nsh"
6 |
7 | !ifndef INFO_PROJECTNAME
8 | !define INFO_PROJECTNAME "{{.Name}}"
9 | !endif
10 | !ifndef INFO_COMPANYNAME
11 | !define INFO_COMPANYNAME "{{.Info.CompanyName}}"
12 | !endif
13 | !ifndef INFO_PRODUCTNAME
14 | !define INFO_PRODUCTNAME "{{.Info.ProductName}}"
15 | !endif
16 | !ifndef INFO_PRODUCTVERSION
17 | !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
18 | !endif
19 | !ifndef INFO_COPYRIGHT
20 | !define INFO_COPYRIGHT "{{.Info.Copyright}}"
21 | !endif
22 | !ifndef PRODUCT_EXECUTABLE
23 | !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
24 | !endif
25 | !ifndef UNINST_KEY_NAME
26 | !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
27 | !endif
28 | !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
29 |
30 | !ifndef REQUEST_EXECUTION_LEVEL
31 | !define REQUEST_EXECUTION_LEVEL "admin"
32 | !endif
33 |
34 | RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
35 |
36 | !ifdef ARG_WAILS_AMD64_BINARY
37 | !define SUPPORTS_AMD64
38 | !endif
39 |
40 | !ifdef ARG_WAILS_ARM64_BINARY
41 | !define SUPPORTS_ARM64
42 | !endif
43 |
44 | !ifdef SUPPORTS_AMD64
45 | !ifdef SUPPORTS_ARM64
46 | !define ARCH "amd64_arm64"
47 | !else
48 | !define ARCH "amd64"
49 | !endif
50 | !else
51 | !ifdef SUPPORTS_ARM64
52 | !define ARCH "arm64"
53 | !else
54 | !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
55 | !endif
56 | !endif
57 |
58 | !macro wails.checkArchitecture
59 | !ifndef WAILS_WIN10_REQUIRED
60 | !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
61 | !endif
62 |
63 | !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
64 | !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
65 | !endif
66 |
67 | ${If} ${AtLeastWin10}
68 | !ifdef SUPPORTS_AMD64
69 | ${if} ${IsNativeAMD64}
70 | Goto ok
71 | ${EndIf}
72 | !endif
73 |
74 | !ifdef SUPPORTS_ARM64
75 | ${if} ${IsNativeARM64}
76 | Goto ok
77 | ${EndIf}
78 | !endif
79 |
80 | IfSilent silentArch notSilentArch
81 | silentArch:
82 | SetErrorLevel 65
83 | Abort
84 | notSilentArch:
85 | MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
86 | Quit
87 | ${else}
88 | IfSilent silentWin notSilentWin
89 | silentWin:
90 | SetErrorLevel 64
91 | Abort
92 | notSilentWin:
93 | MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
94 | Quit
95 | ${EndIf}
96 |
97 | ok:
98 | !macroend
99 |
100 | !macro wails.files
101 | !ifdef SUPPORTS_AMD64
102 | ${if} ${IsNativeAMD64}
103 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
104 | ${EndIf}
105 | !endif
106 |
107 | !ifdef SUPPORTS_ARM64
108 | ${if} ${IsNativeARM64}
109 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
110 | ${EndIf}
111 | !endif
112 | !macroend
113 |
114 | !macro wails.writeUninstaller
115 | WriteUninstaller "$INSTDIR\uninstall.exe"
116 |
117 | SetRegView 64
118 | WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
119 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
120 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
121 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
122 | WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
123 | WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
124 |
125 | ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
126 | IntFmt $0 "0x%08X" $0
127 | WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
128 | !macroend
129 |
130 | !macro wails.deleteUninstaller
131 | Delete "$INSTDIR\uninstall.exe"
132 |
133 | SetRegView 64
134 | DeleteRegKey HKLM "${UNINST_KEY}"
135 | !macroend
136 |
137 | !macro wails.setShellContext
138 | ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
139 | SetShellVarContext all
140 | ${else}
141 | SetShellVarContext current
142 | ${EndIf}
143 | !macroend
144 |
145 | # Install webview2 by launching the bootstrapper
146 | # See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
147 | !macro wails.webview2runtime
148 | !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
149 | !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
150 | !endif
151 |
152 | SetRegView 64
153 | # If the admin key exists and is not empty then webview2 is already installed
154 | ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
155 | ${If} $0 != ""
156 | Goto ok
157 | ${EndIf}
158 |
159 | ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
160 | # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
161 | ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
162 | ${If} $0 != ""
163 | Goto ok
164 | ${EndIf}
165 | ${EndIf}
166 |
167 | SetDetailsPrint both
168 | DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
169 | SetDetailsPrint listonly
170 |
171 | InitPluginsDir
172 | CreateDirectory "$pluginsdir\webview2bootstrapper"
173 | SetOutPath "$pluginsdir\webview2bootstrapper"
174 | File "tmp\MicrosoftEdgeWebview2Setup.exe"
175 | ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
176 |
177 | SetDetailsPrint both
178 | ok:
179 | !macroend
180 |
181 | # Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
182 | !macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
183 | ; Backup the previously associated file class
184 | ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
185 | WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
186 |
187 | WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
188 |
189 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
190 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
191 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
192 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
193 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
194 | !macroend
195 |
196 | !macro APP_UNASSOCIATE EXT FILECLASS
197 | ; Backup the previously associated file class
198 | ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
199 | WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
200 |
201 | DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
202 | !macroend
203 |
204 | !macro wails.associateFiles
205 | ; Create file associations
206 | {{range .Info.FileAssociations}}
207 | !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
208 |
209 | File "..\{{.IconName}}.ico"
210 | {{end}}
211 | !macroend
212 |
213 | !macro wails.unassociateFiles
214 | ; Delete app associations
215 | {{range .Info.FileAssociations}}
216 | !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
217 |
218 | Delete "$INSTDIR\{{.IconName}}.ico"
219 | {{end}}
220 | !macroend
221 |
222 | !macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
223 | DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
224 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
225 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
226 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
227 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
228 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
229 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
230 | !macroend
231 |
232 | !macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
233 | DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
234 | !macroend
235 |
236 | !macro wails.associateCustomProtocols
237 | ; Create custom protocols associations
238 | {{range .Info.Protocols}}
239 | !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
240 |
241 | {{end}}
242 | !macroend
243 |
244 | !macro wails.unassociateCustomProtocols
245 | ; Delete app custom protocol associations
246 | {{range .Info.Protocols}}
247 | !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
248 | {{end}}
249 | !macroend
250 |
--------------------------------------------------------------------------------
/nrfr-client/build/windows/wails.exe.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | true/pm
12 | permonitorv2,permonitor
13 |
14 |
15 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | nrfr-client
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "react": "^19.0.0",
13 | "react-dom": "^19.0.0"
14 | },
15 | "devDependencies": {
16 | "@types/react": "^19.0.2",
17 | "@types/react-dom": "^19.0.2",
18 | "@vitejs/plugin-react": "^4.3.4",
19 | "autoprefixer": "^10.4.20",
20 | "postcss": "^8.4.49",
21 | "tailwindcss": "^3.4.17",
22 | "typescript": "^5.7.2",
23 | "vite": "^6.0.6"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/package.json.md5:
--------------------------------------------------------------------------------
1 | b0eaf1f8114c42148fbbebcee15cba7f
--------------------------------------------------------------------------------
/nrfr-client/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useEffect, useState} from 'react';
2 | import {CheckApps, GetDevices, InstallNrfr, InstallShizuku, SelectDevice, StartShizuku} from '../wailsjs/go/main/App';
3 | import type {AppStatus, DeviceInfo, Step} from './types';
4 | import {ErrorBoundary} from './components/layout/ErrorBoundary';
5 | import {TitleBar} from './components/layout/TitleBar';
6 | import {StepIndicator} from './components/layout/StepIndicator';
7 | import {ErrorMessage} from './components/layout/ErrorMessage';
8 | import {DeviceSelection} from './components/steps/DeviceSelection';
9 | import {AppCheck} from './components/steps/AppCheck';
10 | import {AppInstall} from './components/steps/AppInstall';
11 | import {ServiceStart} from './components/steps/ServiceStart';
12 | import {Complete} from './components/steps/Complete';
13 |
14 | function App() {
15 | const [step, setStep] = useState(1);
16 | const [devices, setDevices] = useState([]);
17 | const [selectedDevice, setSelectedDevice] = useState(null);
18 | const [appsStatus, setAppsStatus] = useState({
19 | shizuku: false,
20 | nrfr: {
21 | installed: false,
22 | needUpdate: false
23 | }
24 | });
25 | const [isLoading, setIsLoading] = useState(false);
26 | const [error, setError] = useState('');
27 |
28 | const loadDevices = useCallback(async () => {
29 | try {
30 | setIsLoading(true);
31 | setError('');
32 | const deviceList = await GetDevices();
33 | if (!deviceList) {
34 | setDevices([]);
35 | return;
36 | }
37 | setDevices(Array.isArray(deviceList) ? deviceList : []);
38 | } catch (err: any) {
39 | setError(err.message || '获取设备列表失败');
40 | setDevices([]);
41 | } finally {
42 | setIsLoading(false);
43 | }
44 | }, []);
45 |
46 | useEffect(() => {
47 | loadDevices();
48 | }, [loadDevices]);
49 |
50 | const handleDeviceSelect = async (device: DeviceInfo) => {
51 | try {
52 | setIsLoading(true);
53 | setError('');
54 | await SelectDevice(device.serial);
55 | setSelectedDevice(device);
56 | const status = await CheckApps();
57 | setAppsStatus(status);
58 | setStep(2);
59 | } catch (err: any) {
60 | setError(err.message || '选择设备失败');
61 | setSelectedDevice(null);
62 | } finally {
63 | setIsLoading(false);
64 | }
65 | };
66 |
67 | const handleInstallApps = async () => {
68 | try {
69 | setIsLoading(true);
70 | setError('');
71 |
72 | // 只安装必需的应用
73 | if (!appsStatus.shizuku) {
74 | await InstallShizuku();
75 | }
76 | if (!appsStatus.nrfr.installed) {
77 | await InstallNrfr();
78 | }
79 |
80 | // 检查安装结果
81 | const newStatus = await CheckApps();
82 | setAppsStatus(newStatus);
83 |
84 | // 只验证必需应用是否安装成功
85 | if (!newStatus.shizuku || !newStatus.nrfr.installed) {
86 | setError('部分应用安装失败,请重试');
87 | }
88 | } catch (err: any) {
89 | setError(err.message || '安装应用失败');
90 | } finally {
91 | setIsLoading(false);
92 | }
93 | };
94 |
95 | const handleUpdateApp = async () => {
96 | try {
97 | setIsLoading(true);
98 | setError('');
99 |
100 | // 执行更新
101 | await InstallNrfr();
102 |
103 | // 检查更新结果
104 | const newStatus = await CheckApps();
105 | setAppsStatus(newStatus);
106 | } catch (err: any) {
107 | setError(err.message || '更新应用失败');
108 | } finally {
109 | setIsLoading(false);
110 | }
111 | };
112 |
113 | const handleStartService = async () => {
114 | try {
115 | setIsLoading(true);
116 | setError('');
117 | await StartShizuku();
118 | setStep(5);
119 | } catch (err: any) {
120 | setError(err.message || '启动服务失败');
121 | } finally {
122 | setIsLoading(false);
123 | }
124 | };
125 |
126 | const handleAppCheck = async () => {
127 | try {
128 | const status = await CheckApps();
129 | setAppsStatus(status);
130 | } catch (err: any) {
131 | setError(err.message || '检查应用状态失败');
132 | }
133 | };
134 |
135 | const handleNext = async (nextStep: Step) => {
136 | if (nextStep === 3) {
137 | // 在切换到步骤3之前先检查状态
138 | await handleAppCheck();
139 | }
140 | setStep(nextStep);
141 | };
142 |
143 | const renderStepContent = () => {
144 | if (!selectedDevice && step > 1) {
145 | return null;
146 | }
147 |
148 | switch (step) {
149 | case 1:
150 | return (
151 |
157 | );
158 | case 2:
159 | return (
160 | handleNext(3)}
164 | />
165 | );
166 | case 3:
167 | return (
168 | handleNext(4)}
175 | />
176 | );
177 | case 4:
178 | return (
179 |
184 | );
185 | case 5:
186 | return ;
187 | default:
188 | return null;
189 | }
190 | };
191 |
192 | return (
193 |
194 |
195 |
196 |
197 |
198 |
199 | setError('')}/>
200 |
201 | {renderStepContent()}
202 |
203 |
204 |
205 |
206 |
207 | );
208 | }
209 |
210 | export default App;
211 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/nrfr-client/frontend/src/assets/images/logo.png
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/components/layout/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface Props {
4 | children: React.ReactNode;
5 | }
6 |
7 | interface State {
8 | hasError: boolean;
9 | error: Error | null;
10 | }
11 |
12 | export class ErrorBoundary extends React.Component {
13 | constructor(props: Props) {
14 | super(props);
15 | this.state = {hasError: false, error: null};
16 | }
17 |
18 | static getDerivedStateFromError(error: Error) {
19 | return {hasError: true, error};
20 | }
21 |
22 | render() {
23 | if (this.state.hasError) {
24 | return (
25 |
26 |
出现错误
27 |
{this.state.error?.message}
28 |
window.location.reload()}
31 | >
32 | 重新加载
33 |
34 |
35 | );
36 | }
37 |
38 | return this.props.children;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/components/layout/ErrorMessage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface Props {
4 | message: string;
5 | onClose: () => void;
6 | }
7 |
8 | export const ErrorMessage: React.FC = ({message, onClose}) => {
9 | if (!message) return null;
10 |
11 | return (
12 |
13 | {message}
14 |
18 | 关闭
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/components/layout/StepIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface Props {
4 | currentStep: number;
5 | }
6 |
7 | export const StepIndicator: React.FC = ({currentStep}) => {
8 | return (
9 |
10 |
11 | {[1, 2, 3, 4, 5].map((step) => (
12 |
22 | {step < currentStep ? '✓' : step}
23 |
24 | ))}
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/components/layout/TitleBar.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import logo from '../../assets/images/logo.png';
3 | import {WindowClose, WindowMaximise, WindowMinimise} from '../../../wailsjs/go/main/App';
4 | import {BrowserOpenURL} from '../../../wailsjs/runtime/runtime';
5 |
6 | export const TitleBar: React.FC = () => {
7 | const [showAbout, setShowAbout] = useState(false);
8 |
9 | const handleFollow = () => {
10 | BrowserOpenURL('https://x.com/intent/follow?screen_name=actkites');
11 | };
12 |
13 | return (
14 |
15 |
17 |
18 |
19 |
Nrfr - 快速启动工具
20 |
21 |
22 |
setShowAbout(!showAbout)}
25 | title="关于"
26 | aria-label="打开关于页面"
27 | >
28 |
30 |
33 |
34 |
35 |
WindowMinimise()}>
37 |
WindowMaximise()}>
39 |
WindowClose()}>
41 |
42 |
43 |
44 | {showAbout && (
45 |
48 |
49 |
setShowAbout(false)}
51 | className="absolute right-2 top-2 text-gray-400 hover:text-gray-600 transition-colors"
52 | title="关闭"
53 | aria-label="关闭关于页面"
54 | >
55 |
57 |
59 |
60 |
61 |
62 |
63 |
关注作者:
64 |
68 |
70 |
72 |
73 | @actkites
74 |
75 |
76 |
77 |
78 | )}
79 |
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/components/steps/AppCheck.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import type {AppStatus, DeviceInfo} from '../../types';
3 |
4 | interface Props {
5 | device: DeviceInfo;
6 | appsStatus: AppStatus;
7 | onNext: () => void;
8 | }
9 |
10 | export const AppCheck: React.FC = ({device, appsStatus, onNext}) => {
11 | const [isConfirmed, setIsConfirmed] = useState(false);
12 |
13 | const handleConfirm = () => {
14 | setIsConfirmed(true);
15 | onNext();
16 | };
17 |
18 | return (
19 |
20 |
检查应用
21 |
22 | 当前设备:{device.model || '未知设备'} ({device.serial})
23 |
24 |
25 |
26 |
27 |
Shizuku
28 |
29 |
30 | {appsStatus.shizuku ? '已安装' : '未安装'}
31 |
32 | {appsStatus.shizuku && (
33 |
35 |
37 |
38 | )}
39 |
40 |
41 |
42 |
43 |
44 |
Nrfr
45 |
46 |
47 | {appsStatus.nrfr.installed ? '已安装' : '未安装'}
48 |
49 | {appsStatus.nrfr.installed && (
50 |
52 |
54 |
55 | )}
56 |
57 |
58 |
59 |
60 |
65 | {isConfirmed ? '已确认' : '确认并继续'}
66 |
67 |
68 | );
69 | };
70 |
71 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/components/steps/AppInstall.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type {AppStatus, DeviceInfo} from '../../types';
3 |
4 | interface Props {
5 | device: DeviceInfo;
6 | appsStatus: AppStatus;
7 | isLoading: boolean;
8 | onInstall: () => void;
9 | onUpdate: () => void;
10 | onNext: () => void;
11 | }
12 |
13 | export const AppInstall: React.FC = ({device, appsStatus, isLoading, onInstall, onUpdate, onNext}) => {
14 | // 只有完全没安装的应用才是必需操作
15 | const needsRequiredAction = !appsStatus.shizuku || !appsStatus.nrfr.installed;
16 | // 有可更新的应用
17 | const hasOptionalUpdate = appsStatus.nrfr.installed && appsStatus.nrfr.needUpdate;
18 | // 安装完成(不考虑更新状态)
19 | const isComplete = appsStatus.shizuku && appsStatus.nrfr.installed;
20 |
21 | return (
22 |
23 |
安装应用
24 |
25 | 当前设备:{device.model || '未知设备'} ({device.serial})
26 |
27 |
28 | {!appsStatus.shizuku && (
29 |
30 |
需要安装 Shizuku
31 | {isLoading && (
32 |
41 | )}
42 |
43 | )}
44 | {!appsStatus.nrfr.installed && (
45 |
46 |
需要安装 Nrfr
47 | {isLoading && (
48 |
57 | )}
58 |
59 | )}
60 | {isComplete && (
61 |
62 |
63 |
64 |
66 |
67 |
所有必需应用已安装完成
68 |
69 |
70 | )}
71 | {hasOptionalUpdate && (
72 |
73 |
74 |
75 |
77 |
79 |
80 |
发现 Nrfr 新版本可用
81 |
82 |
83 |
84 | )}
85 |
86 |
87 | {needsRequiredAction && (
88 |
93 | {isLoading ? '安装中...' : '安装必需应用'}
94 |
95 | )}
96 | {hasOptionalUpdate && !needsRequiredAction && (
97 |
102 | {isLoading ? '更新中...' : '更新到最新版本'}
103 |
104 | )}
105 | {isComplete && (
106 |
111 | 继续下一步 {hasOptionalUpdate ? '(暂不更新)' : ''}
112 |
113 | )}
114 |
115 |
116 | );
117 | };
118 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/components/steps/Complete.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useState} from 'react';
2 | import type {DeviceInfo} from '../../types';
3 | import {StartNrfr} from '../../../wailsjs/go/main/App';
4 |
5 | interface Props {
6 | device: DeviceInfo;
7 | }
8 |
9 | export const Complete: React.FC = ({device}) => {
10 | const [isStarting, setIsStarting] = useState(false);
11 | const [error, setError] = useState('');
12 |
13 | const handleStartNrfr = useCallback(async () => {
14 | try {
15 | setIsStarting(true);
16 | setError('');
17 | await StartNrfr();
18 | } catch (err: any) {
19 | setError(err.message || '启动 Nrfr 失败');
20 | } finally {
21 | setIsStarting(false);
22 | }
23 | }, []);
24 |
25 | return (
26 |
27 |
设置完成
28 |
29 | 当前设备:{device.model || '未知设备'} ({device.serial})
30 |
31 |
32 |
✓ 全部完成
33 |
现在可以开始使用了
34 |
35 |
36 | {error && (
37 |
38 | {error}
39 |
40 | )}
41 |
42 |
43 |
48 | {isStarting ? (
49 | <>
50 |
59 | 启动中...
60 | >
61 | ) : (
62 | '启动 Nrfr'
63 | )}
64 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/components/steps/DeviceSelection.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useEffect, useRef, useState} from 'react';
2 | import type {DeviceInfo} from '../../types';
3 |
4 | interface Props {
5 | devices: DeviceInfo[];
6 | onSelect: (device: DeviceInfo) => void;
7 | onRefresh: () => void;
8 | isLoading?: boolean;
9 | }
10 |
11 | export const DeviceSelection: React.FC = ({devices, onSelect, onRefresh, isLoading = false}) => {
12 | const [isOpen, setIsOpen] = useState(false);
13 | const [searchTerm, setSearchTerm] = useState('');
14 | const [selectedDevice, setSelectedDevice] = useState(null);
15 | const [isConfirmed, setIsConfirmed] = useState(false);
16 | const [lastDevices, setLastDevices] = useState([]);
17 | const [showLoading, setShowLoading] = useState(false);
18 | const loadingTimer = useRef>(null);
19 |
20 | // 处理加载状态的延迟显示
21 | useEffect(() => {
22 | if (isLoading) {
23 | // 延迟显示加载状态,避免闪烁
24 | loadingTimer.current = setTimeout(() => {
25 | setShowLoading(true);
26 | }, 300);
27 | } else {
28 | if (loadingTimer.current) {
29 | clearTimeout(loadingTimer.current);
30 | }
31 | setShowLoading(false);
32 | }
33 | return () => {
34 | if (loadingTimer.current) {
35 | clearTimeout(loadingTimer.current);
36 | }
37 | };
38 | }, [isLoading]);
39 |
40 | // 使用防抖处理设备列表更新
41 | useEffect(() => {
42 | const devicesStr = JSON.stringify(devices);
43 | const lastDevicesStr = JSON.stringify(lastDevices);
44 |
45 | if (devicesStr !== lastDevicesStr) {
46 | setLastDevices(devices);
47 |
48 | // 只在设备列表发生实质性变化时更新选中状态
49 | if (devices.length > 0 && !selectedDevice) {
50 | handleSelect(devices[0]);
51 | } else if (selectedDevice) {
52 | const deviceStillExists = devices.find(d => d.serial === selectedDevice.serial);
53 | if (!deviceStillExists) {
54 | setSelectedDevice(null);
55 | setIsConfirmed(false);
56 | }
57 | }
58 | }
59 | }, [devices, lastDevices, selectedDevice]);
60 |
61 | const filteredDevices = devices.filter(device =>
62 | device.model?.toLowerCase().includes(searchTerm.toLowerCase()) ||
63 | device.product?.toLowerCase().includes(searchTerm.toLowerCase()) ||
64 | device.serial.toLowerCase().includes(searchTerm.toLowerCase())
65 | );
66 |
67 | const handleSelect = useCallback((device: DeviceInfo) => {
68 | setSelectedDevice(device);
69 | setIsOpen(false);
70 | setIsConfirmed(false);
71 | }, []);
72 |
73 | const handleConfirm = useCallback(() => {
74 | if (selectedDevice) {
75 | setIsConfirmed(true);
76 | onSelect(selectedDevice);
77 | }
78 | }, [selectedDevice, onSelect]);
79 |
80 | // 自动刷新定时器
81 | useEffect(() => {
82 | const timer = setInterval(onRefresh, 2000);
83 | return () => clearInterval(timer);
84 | }, [onRefresh]);
85 |
86 | // 处理点击外部关闭下拉框
87 | useEffect(() => {
88 | const handleClickOutside = (event: MouseEvent) => {
89 | const target = event.target as HTMLElement;
90 | if (!target.closest('.device-dropdown')) {
91 | setIsOpen(false);
92 | }
93 | };
94 |
95 | document.addEventListener('mousedown', handleClickOutside);
96 | return () => document.removeEventListener('mousedown', handleClickOutside);
97 | }, []);
98 |
99 | const renderDeviceInfo = () => {
100 | if (selectedDevice) {
101 | return (
102 |
103 |
{selectedDevice.model || '未知设备'}
104 |
105 | {selectedDevice.product ? `${selectedDevice.product} • ` : ''}{selectedDevice.serial}
106 |
107 |
108 | );
109 | }
110 |
111 | return (
112 |
113 |
114 | {showLoading ? '正在检测设备...' : '请选择设备'}
115 |
116 |
117 | );
118 | };
119 |
120 | return (
121 |
122 |
选择设备
123 |
124 |
125 | {/* 选择框 */}
126 |
!isConfirmed && setIsOpen(!isOpen)}
132 | disabled={isConfirmed}
133 | >
134 | {renderDeviceInfo()}
135 |
136 | {showLoading && (
137 |
138 |
140 |
142 |
144 |
145 |
146 | )}
147 | {!isConfirmed && (
148 |
154 |
155 |
156 | )}
157 | {isConfirmed && (
158 |
160 |
161 |
162 | )}
163 |
164 |
165 |
166 | {/* 下拉面板 */}
167 | {isOpen && !isConfirmed && (
168 |
5 ? 'auto' : undefined
173 | }}
174 | >
175 | {/* 搜索框 */}
176 |
177 | setSearchTerm(e.target.value)}
183 | />
184 |
185 |
186 | {/* 设备列表 */}
187 |
188 | {filteredDevices.length > 0 ? (
189 | filteredDevices.map((device) => (
190 |
handleSelect(device)}
196 | >
197 |
198 |
{device.model || '未知设备'}
199 |
200 | {device.product ? `${device.product} • ` : ''}{device.serial}
201 |
202 |
203 |
204 | ))
205 | ) : (
206 |
208 | {devices.length === 0 ? (
209 | <>
210 | {showLoading ? '正在检测设备...' : '未检测到设备'}
211 | {!showLoading && (
212 | {
215 | setIsOpen(false);
216 | onRefresh();
217 | }}
218 | >
219 | 刷新
220 |
221 | )}
222 | >
223 | ) : (
224 | '没有找到匹配的设备'
225 | )}
226 |
227 | )}
228 |
229 |
230 | )}
231 |
232 |
233 | {/* 确认按钮 */}
234 | {selectedDevice && !isConfirmed && (
235 |
240 | 确认选择
241 |
242 | )}
243 |
244 | {/* 刷新按钮 */}
245 |
{
248 | setIsOpen(false);
249 | onRefresh();
250 | }}
251 | disabled={showLoading || isConfirmed}
252 | >
253 | {showLoading ? (
254 | <>
255 |
256 |
257 |
259 |
261 |
262 |
263 | 正在检测设备...
264 | >
265 | ) : (
266 | '刷新设备列表'
267 | )}
268 |
269 |
270 | );
271 | };
272 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/components/steps/ServiceStart.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import type {DeviceInfo} from '../../types';
3 |
4 | interface Props {
5 | device: DeviceInfo;
6 | isLoading: boolean;
7 | onStart: () => void;
8 | }
9 |
10 | export const ServiceStart: React.FC = ({device, isLoading, onStart}) => {
11 | const [isConfirmed, setIsConfirmed] = useState(false);
12 |
13 | const handleConfirm = () => {
14 | setIsConfirmed(true);
15 | onStart();
16 | };
17 |
18 | return (
19 |
20 |
启动服务
21 |
22 | 当前设备:{device.model || '未知设备'} ({device.serial})
23 |
24 |
25 |
准备启动 Shizuku 服务
26 | {isLoading && (
27 |
36 | )}
37 |
38 |
43 | {isLoading ? '启动中...' : isConfirmed ? '已确认' : '确认并启动'}
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {createRoot} from 'react-dom/client'
3 | import './styles/global.css'
4 | import App from './App'
5 |
6 | const container = document.getElementById('root')
7 |
8 | const root = createRoot(container!)
9 |
10 | root.render(
11 |
12 |
13 |
14 | )
15 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* 窗口控制按钮样式 */
6 | .window-control-button {
7 | @apply w-3 h-3 rounded-full transition-colors;
8 | cursor: pointer;
9 | }
10 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import type {main} from '../../wailsjs/go/models';
2 |
3 | export type DeviceInfo = main.DeviceInfo;
4 |
5 | export interface AppStatus {
6 | shizuku: boolean;
7 | nrfr: {
8 | installed: boolean;
9 | needUpdate: boolean;
10 | };
11 | }
12 |
13 | export type Step = 1 | 2 | 3 | 4 | 5;
14 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
--------------------------------------------------------------------------------
/nrfr-client/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": [
6 | "DOM",
7 | "DOM.Iterable",
8 | "ESNext"
9 | ],
10 | "allowJs": false,
11 | "skipLibCheck": true,
12 | "esModuleInterop": false,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "ESNext",
17 | "moduleResolution": "Node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ],
26 | "references": [
27 | {
28 | "path": "./tsconfig.node.json"
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": [
9 | "vite.config.ts"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | port: 3000,
9 | strictPort: true,
10 | host: true,
11 | }
12 | })
13 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/wailsjs/go/main/App.d.ts:
--------------------------------------------------------------------------------
1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2 | // This file is automatically generated. DO NOT EDIT
3 | import {main} from '../models';
4 |
5 | export function CheckApps(): Promise;
6 |
7 | export function CheckNrfrUpdate(): Promise;
8 |
9 | export function GetAppVersion(arg1: string): Promise;
10 |
11 | export function GetDevices(): Promise>;
12 |
13 | export function InstallNrfr(): Promise;
14 |
15 | export function InstallShizuku(): Promise;
16 |
17 | export function SelectDevice(arg1: string): Promise;
18 |
19 | export function StartNrfr(): Promise;
20 |
21 | export function StartShizuku(): Promise;
22 |
23 | export function WindowClose(): Promise;
24 |
25 | export function WindowMaximise(): Promise;
26 |
27 | export function WindowMinimise(): Promise;
28 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/wailsjs/go/main/App.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
3 | // This file is automatically generated. DO NOT EDIT
4 |
5 | export function CheckApps() {
6 | return window['go']['main']['App']['CheckApps']();
7 | }
8 |
9 | export function CheckNrfrUpdate() {
10 | return window['go']['main']['App']['CheckNrfrUpdate']();
11 | }
12 |
13 | export function GetAppVersion(arg1) {
14 | return window['go']['main']['App']['GetAppVersion'](arg1);
15 | }
16 |
17 | export function GetDevices() {
18 | return window['go']['main']['App']['GetDevices']();
19 | }
20 |
21 | export function InstallNrfr() {
22 | return window['go']['main']['App']['InstallNrfr']();
23 | }
24 |
25 | export function InstallShizuku() {
26 | return window['go']['main']['App']['InstallShizuku']();
27 | }
28 |
29 | export function SelectDevice(arg1) {
30 | return window['go']['main']['App']['SelectDevice'](arg1);
31 | }
32 |
33 | export function StartNrfr() {
34 | return window['go']['main']['App']['StartNrfr']();
35 | }
36 |
37 | export function StartShizuku() {
38 | return window['go']['main']['App']['StartShizuku']();
39 | }
40 |
41 | export function WindowClose() {
42 | return window['go']['main']['App']['WindowClose']();
43 | }
44 |
45 | export function WindowMaximise() {
46 | return window['go']['main']['App']['WindowMaximise']();
47 | }
48 |
49 | export function WindowMinimise() {
50 | return window['go']['main']['App']['WindowMinimise']();
51 | }
52 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/wailsjs/go/models.ts:
--------------------------------------------------------------------------------
1 | export namespace main {
2 |
3 | export class AppStatus {
4 | shizuku: boolean;
5 | // Go type: struct { Installed bool "json:\"installed\""; NeedUpdate bool "json:\"needUpdate\"" }
6 | nrfr: any;
7 |
8 | static createFrom(source: any = {}) {
9 | return new AppStatus(source);
10 | }
11 |
12 | constructor(source: any = {}) {
13 | if ('string' === typeof source) source = JSON.parse(source);
14 | this.shizuku = source["shizuku"];
15 | this.nrfr = this.convertValues(source["nrfr"], Object);
16 | }
17 |
18 | convertValues(a: any, classs: any, asMap: boolean = false): any {
19 | if (!a) {
20 | return a;
21 | }
22 | if (a.slice && a.map) {
23 | return (a as any[]).map(elem => this.convertValues(elem, classs));
24 | } else if ("object" === typeof a) {
25 | if (asMap) {
26 | for (const key of Object.keys(a)) {
27 | a[key] = new classs(a[key]);
28 | }
29 | return a;
30 | }
31 | return new classs(a);
32 | }
33 | return a;
34 | }
35 | }
36 |
37 | export class DeviceInfo {
38 | serial: string;
39 | state: string;
40 | product: string;
41 | model: string;
42 |
43 | static createFrom(source: any = {}) {
44 | return new DeviceInfo(source);
45 | }
46 |
47 | constructor(source: any = {}) {
48 | if ('string' === typeof source) source = JSON.parse(source);
49 | this.serial = source["serial"];
50 | this.state = source["state"];
51 | this.product = source["product"];
52 | this.model = source["model"];
53 | }
54 | }
55 |
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/wailsjs/runtime/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wailsapp/runtime",
3 | "version": "2.0.0",
4 | "description": "Wails Javascript runtime library",
5 | "main": "runtime.js",
6 | "types": "runtime.d.ts",
7 | "scripts": {
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/wailsapp/wails.git"
12 | },
13 | "keywords": [
14 | "Wails",
15 | "Javascript",
16 | "Go"
17 | ],
18 | "author": "Lea Anthony ",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/wailsapp/wails/issues"
22 | },
23 | "homepage": "https://github.com/wailsapp/wails#readme"
24 | }
25 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/wailsjs/runtime/runtime.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | _ __ _ __
3 | | | / /___ _(_) /____
4 | | | /| / / __ `/ / / ___/
5 | | |/ |/ / /_/ / / (__ )
6 | |__/|__/\__,_/_/_/____/
7 | The electron alternative for Go
8 | (c) Lea Anthony 2019-present
9 | */
10 |
11 | export interface Position {
12 | x: number;
13 | y: number;
14 | }
15 |
16 | export interface Size {
17 | w: number;
18 | h: number;
19 | }
20 |
21 | export interface Screen {
22 | isCurrent: boolean;
23 | isPrimary: boolean;
24 | width: number
25 | height: number
26 | }
27 |
28 | // Environment information such as platform, buildtype, ...
29 | export interface EnvironmentInfo {
30 | buildType: string;
31 | platform: string;
32 | arch: string;
33 | }
34 |
35 | // [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
36 | // emits the given event. Optional data may be passed with the event.
37 | // This will trigger any event listeners.
38 | export function EventsEmit(eventName: string, ...data: any): void;
39 |
40 | // [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
41 | export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
42 |
43 | // [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
44 | // sets up a listener for the given event name, but will only trigger a given number times.
45 | export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
46 |
47 | // [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
48 | // sets up a listener for the given event name, but will only trigger once.
49 | export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
50 |
51 | // [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
52 | // unregisters the listener for the given event name.
53 | export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
54 |
55 | // [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
56 | // unregisters all listeners.
57 | export function EventsOffAll(): void;
58 |
59 | // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
60 | // logs the given message as a raw message
61 | export function LogPrint(message: string): void;
62 |
63 | // [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
64 | // logs the given message at the `trace` log level.
65 | export function LogTrace(message: string): void;
66 |
67 | // [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
68 | // logs the given message at the `debug` log level.
69 | export function LogDebug(message: string): void;
70 |
71 | // [LogError](https://wails.io/docs/reference/runtime/log#logerror)
72 | // logs the given message at the `error` log level.
73 | export function LogError(message: string): void;
74 |
75 | // [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
76 | // logs the given message at the `fatal` log level.
77 | // The application will quit after calling this method.
78 | export function LogFatal(message: string): void;
79 |
80 | // [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
81 | // logs the given message at the `info` log level.
82 | export function LogInfo(message: string): void;
83 |
84 | // [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
85 | // logs the given message at the `warning` log level.
86 | export function LogWarning(message: string): void;
87 |
88 | // [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
89 | // Forces a reload by the main application as well as connected browsers.
90 | export function WindowReload(): void;
91 |
92 | // [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
93 | // Reloads the application frontend.
94 | export function WindowReloadApp(): void;
95 |
96 | // [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
97 | // Sets the window AlwaysOnTop or not on top.
98 | export function WindowSetAlwaysOnTop(b: boolean): void;
99 |
100 | // [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
101 | // *Windows only*
102 | // Sets window theme to system default (dark/light).
103 | export function WindowSetSystemDefaultTheme(): void;
104 |
105 | // [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
106 | // *Windows only*
107 | // Sets window to light theme.
108 | export function WindowSetLightTheme(): void;
109 |
110 | // [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
111 | // *Windows only*
112 | // Sets window to dark theme.
113 | export function WindowSetDarkTheme(): void;
114 |
115 | // [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
116 | // Centers the window on the monitor the window is currently on.
117 | export function WindowCenter(): void;
118 |
119 | // [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
120 | // Sets the text in the window title bar.
121 | export function WindowSetTitle(title: string): void;
122 |
123 | // [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
124 | // Makes the window full screen.
125 | export function WindowFullscreen(): void;
126 |
127 | // [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
128 | // Restores the previous window dimensions and position prior to full screen.
129 | export function WindowUnfullscreen(): void;
130 |
131 | // [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
132 | // Returns the state of the window, i.e. whether the window is in full screen mode or not.
133 | export function WindowIsFullscreen(): Promise;
134 |
135 | // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
136 | // Sets the width and height of the window.
137 | export function WindowSetSize(width: number, height: number): Promise;
138 |
139 | // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
140 | // Gets the width and height of the window.
141 | export function WindowGetSize(): Promise;
142 |
143 | // [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
144 | // Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
145 | // Setting a size of 0,0 will disable this constraint.
146 | export function WindowSetMaxSize(width: number, height: number): void;
147 |
148 | // [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
149 | // Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
150 | // Setting a size of 0,0 will disable this constraint.
151 | export function WindowSetMinSize(width: number, height: number): void;
152 |
153 | // [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
154 | // Sets the window position relative to the monitor the window is currently on.
155 | export function WindowSetPosition(x: number, y: number): void;
156 |
157 | // [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
158 | // Gets the window position relative to the monitor the window is currently on.
159 | export function WindowGetPosition(): Promise;
160 |
161 | // [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
162 | // Hides the window.
163 | export function WindowHide(): void;
164 |
165 | // [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
166 | // Shows the window, if it is currently hidden.
167 | export function WindowShow(): void;
168 |
169 | // [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
170 | // Maximises the window to fill the screen.
171 | export function WindowMaximise(): void;
172 |
173 | // [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
174 | // Toggles between Maximised and UnMaximised.
175 | export function WindowToggleMaximise(): void;
176 |
177 | // [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
178 | // Restores the window to the dimensions and position prior to maximising.
179 | export function WindowUnmaximise(): void;
180 |
181 | // [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
182 | // Returns the state of the window, i.e. whether the window is maximised or not.
183 | export function WindowIsMaximised(): Promise;
184 |
185 | // [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
186 | // Minimises the window.
187 | export function WindowMinimise(): void;
188 |
189 | // [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
190 | // Restores the window to the dimensions and position prior to minimising.
191 | export function WindowUnminimise(): void;
192 |
193 | // [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
194 | // Returns the state of the window, i.e. whether the window is minimised or not.
195 | export function WindowIsMinimised(): Promise;
196 |
197 | // [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
198 | // Returns the state of the window, i.e. whether the window is normal or not.
199 | export function WindowIsNormal(): Promise;
200 |
201 | // [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
202 | // Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
203 | export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
204 |
205 | // [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
206 | // Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
207 | export function ScreenGetAll(): Promise;
208 |
209 | // [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
210 | // Opens the given URL in the system browser.
211 | export function BrowserOpenURL(url: string): void;
212 |
213 | // [Environment](https://wails.io/docs/reference/runtime/intro#environment)
214 | // Returns information about the environment
215 | export function Environment(): Promise;
216 |
217 | // [Quit](https://wails.io/docs/reference/runtime/intro#quit)
218 | // Quits the application.
219 | export function Quit(): void;
220 |
221 | // [Hide](https://wails.io/docs/reference/runtime/intro#hide)
222 | // Hides the application.
223 | export function Hide(): void;
224 |
225 | // [Show](https://wails.io/docs/reference/runtime/intro#show)
226 | // Shows the application.
227 | export function Show(): void;
228 |
229 | // [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
230 | // Returns the current text stored on clipboard
231 | export function ClipboardGetText(): Promise;
232 |
233 | // [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
234 | // Sets a text on the clipboard
235 | export function ClipboardSetText(text: string): Promise;
236 |
237 | // [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
238 | // OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
239 | export function OnFileDrop(callback: (x: number, y: number, paths: string[]) => void, useDropTarget: boolean): void
240 |
241 | // [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
242 | // OnFileDropOff removes the drag and drop listeners and handlers.
243 | export function OnFileDropOff(): void
244 |
245 | // Check if the file path resolver is available
246 | export function CanResolveFilePaths(): boolean;
247 |
248 | // Resolves file paths for an array of files
249 | export function ResolveFilePaths(files: File[]): void
250 |
--------------------------------------------------------------------------------
/nrfr-client/frontend/wailsjs/runtime/runtime.js:
--------------------------------------------------------------------------------
1 | /*
2 | _ __ _ __
3 | | | / /___ _(_) /____
4 | | | /| / / __ `/ / / ___/
5 | | |/ |/ / /_/ / / (__ )
6 | |__/|__/\__,_/_/_/____/
7 | The electron alternative for Go
8 | (c) Lea Anthony 2019-present
9 | */
10 |
11 | export function LogPrint(message) {
12 | window.runtime.LogPrint(message);
13 | }
14 |
15 | export function LogTrace(message) {
16 | window.runtime.LogTrace(message);
17 | }
18 |
19 | export function LogDebug(message) {
20 | window.runtime.LogDebug(message);
21 | }
22 |
23 | export function LogInfo(message) {
24 | window.runtime.LogInfo(message);
25 | }
26 |
27 | export function LogWarning(message) {
28 | window.runtime.LogWarning(message);
29 | }
30 |
31 | export function LogError(message) {
32 | window.runtime.LogError(message);
33 | }
34 |
35 | export function LogFatal(message) {
36 | window.runtime.LogFatal(message);
37 | }
38 |
39 | export function EventsOnMultiple(eventName, callback, maxCallbacks) {
40 | return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
41 | }
42 |
43 | export function EventsOn(eventName, callback) {
44 | return EventsOnMultiple(eventName, callback, -1);
45 | }
46 |
47 | export function EventsOff(eventName, ...additionalEventNames) {
48 | return window.runtime.EventsOff(eventName, ...additionalEventNames);
49 | }
50 |
51 | export function EventsOnce(eventName, callback) {
52 | return EventsOnMultiple(eventName, callback, 1);
53 | }
54 |
55 | export function EventsEmit(eventName) {
56 | let args = [eventName].slice.call(arguments);
57 | return window.runtime.EventsEmit.apply(null, args);
58 | }
59 |
60 | export function WindowReload() {
61 | window.runtime.WindowReload();
62 | }
63 |
64 | export function WindowReloadApp() {
65 | window.runtime.WindowReloadApp();
66 | }
67 |
68 | export function WindowSetAlwaysOnTop(b) {
69 | window.runtime.WindowSetAlwaysOnTop(b);
70 | }
71 |
72 | export function WindowSetSystemDefaultTheme() {
73 | window.runtime.WindowSetSystemDefaultTheme();
74 | }
75 |
76 | export function WindowSetLightTheme() {
77 | window.runtime.WindowSetLightTheme();
78 | }
79 |
80 | export function WindowSetDarkTheme() {
81 | window.runtime.WindowSetDarkTheme();
82 | }
83 |
84 | export function WindowCenter() {
85 | window.runtime.WindowCenter();
86 | }
87 |
88 | export function WindowSetTitle(title) {
89 | window.runtime.WindowSetTitle(title);
90 | }
91 |
92 | export function WindowFullscreen() {
93 | window.runtime.WindowFullscreen();
94 | }
95 |
96 | export function WindowUnfullscreen() {
97 | window.runtime.WindowUnfullscreen();
98 | }
99 |
100 | export function WindowIsFullscreen() {
101 | return window.runtime.WindowIsFullscreen();
102 | }
103 |
104 | export function WindowGetSize() {
105 | return window.runtime.WindowGetSize();
106 | }
107 |
108 | export function WindowSetSize(width, height) {
109 | window.runtime.WindowSetSize(width, height);
110 | }
111 |
112 | export function WindowSetMaxSize(width, height) {
113 | window.runtime.WindowSetMaxSize(width, height);
114 | }
115 |
116 | export function WindowSetMinSize(width, height) {
117 | window.runtime.WindowSetMinSize(width, height);
118 | }
119 |
120 | export function WindowSetPosition(x, y) {
121 | window.runtime.WindowSetPosition(x, y);
122 | }
123 |
124 | export function WindowGetPosition() {
125 | return window.runtime.WindowGetPosition();
126 | }
127 |
128 | export function WindowHide() {
129 | window.runtime.WindowHide();
130 | }
131 |
132 | export function WindowShow() {
133 | window.runtime.WindowShow();
134 | }
135 |
136 | export function WindowMaximise() {
137 | window.runtime.WindowMaximise();
138 | }
139 |
140 | export function WindowToggleMaximise() {
141 | window.runtime.WindowToggleMaximise();
142 | }
143 |
144 | export function WindowUnmaximise() {
145 | window.runtime.WindowUnmaximise();
146 | }
147 |
148 | export function WindowIsMaximised() {
149 | return window.runtime.WindowIsMaximised();
150 | }
151 |
152 | export function WindowMinimise() {
153 | window.runtime.WindowMinimise();
154 | }
155 |
156 | export function WindowUnminimise() {
157 | window.runtime.WindowUnminimise();
158 | }
159 |
160 | export function WindowSetBackgroundColour(R, G, B, A) {
161 | window.runtime.WindowSetBackgroundColour(R, G, B, A);
162 | }
163 |
164 | export function ScreenGetAll() {
165 | return window.runtime.ScreenGetAll();
166 | }
167 |
168 | export function WindowIsMinimised() {
169 | return window.runtime.WindowIsMinimised();
170 | }
171 |
172 | export function WindowIsNormal() {
173 | return window.runtime.WindowIsNormal();
174 | }
175 |
176 | export function BrowserOpenURL(url) {
177 | window.runtime.BrowserOpenURL(url);
178 | }
179 |
180 | export function Environment() {
181 | return window.runtime.Environment();
182 | }
183 |
184 | export function Quit() {
185 | window.runtime.Quit();
186 | }
187 |
188 | export function Hide() {
189 | window.runtime.Hide();
190 | }
191 |
192 | export function Show() {
193 | window.runtime.Show();
194 | }
195 |
196 | export function ClipboardGetText() {
197 | return window.runtime.ClipboardGetText();
198 | }
199 |
200 | export function ClipboardSetText(text) {
201 | return window.runtime.ClipboardSetText(text);
202 | }
203 |
204 | /**
205 | * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
206 | *
207 | * @export
208 | * @callback OnFileDropCallback
209 | * @param {number} x - x coordinate of the drop
210 | * @param {number} y - y coordinate of the drop
211 | * @param {string[]} paths - A list of file paths.
212 | */
213 |
214 | /**
215 | * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
216 | *
217 | * @export
218 | * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
219 | * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
220 | */
221 | export function OnFileDrop(callback, useDropTarget) {
222 | return window.runtime.OnFileDrop(callback, useDropTarget);
223 | }
224 |
225 | /**
226 | * OnFileDropOff removes the drag and drop listeners and handlers.
227 | */
228 | export function OnFileDropOff() {
229 | return window.runtime.OnFileDropOff();
230 | }
231 |
232 | export function CanResolveFilePaths() {
233 | return window.runtime.CanResolveFilePaths();
234 | }
235 |
236 | export function ResolveFilePaths(files) {
237 | return window.runtime.ResolveFilePaths(files);
238 | }
--------------------------------------------------------------------------------
/nrfr-client/go.mod:
--------------------------------------------------------------------------------
1 | module nrfr-client
2 |
3 | go 1.22.0
4 |
5 | toolchain go1.23.2
6 |
7 | require (
8 | github.com/electricbubble/gadb v0.1.0
9 | github.com/wailsapp/wails/v2 v2.9.2
10 | )
11 |
12 | require (
13 | github.com/bep/debounce v1.2.1 // indirect
14 | github.com/go-ole/go-ole v1.3.0 // indirect
15 | github.com/godbus/dbus/v5 v5.1.0 // indirect
16 | github.com/google/uuid v1.6.0 // indirect
17 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
18 | github.com/labstack/echo/v4 v4.13.3 // indirect
19 | github.com/labstack/gommon v0.4.2 // indirect
20 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
21 | github.com/leaanthony/gosod v1.0.4 // indirect
22 | github.com/leaanthony/slicer v1.6.0 // indirect
23 | github.com/leaanthony/u v1.1.1 // indirect
24 | github.com/mattn/go-colorable v0.1.13 // indirect
25 | github.com/mattn/go-isatty v0.0.20 // indirect
26 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
27 | github.com/pkg/errors v0.9.1 // indirect
28 | github.com/rivo/uniseg v0.4.7 // indirect
29 | github.com/samber/lo v1.47.0 // indirect
30 | github.com/tkrajina/go-reflector v0.5.8 // indirect
31 | github.com/valyala/bytebufferpool v1.0.0 // indirect
32 | github.com/valyala/fasttemplate v1.2.2 // indirect
33 | github.com/wailsapp/go-webview2 v1.0.18 // indirect
34 | github.com/wailsapp/mimetype v1.4.1 // indirect
35 | golang.org/x/crypto v0.31.0 // indirect
36 | golang.org/x/net v0.33.0 // indirect
37 | golang.org/x/sys v0.28.0 // indirect
38 | golang.org/x/text v0.21.0 // indirect
39 | )
40 |
--------------------------------------------------------------------------------
/nrfr-client/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
2 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/electricbubble/gadb v0.1.0 h1:h7RKlToMlFtGW4rUkAd4GSiFAHioMH5Nx7jtbb2nKi4=
6 | github.com/electricbubble/gadb v0.1.0/go.mod h1:3293YJ6OWHv/Q6NA5dwSbK43MbmYm8+Vz2d7h5J3IA8=
7 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
8 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
9 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
10 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
13 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
14 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
15 | github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
16 | github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
17 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
18 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
19 | github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
20 | github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
21 | github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
22 | github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
23 | github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
24 | github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
25 | github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
26 | github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
27 | github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
28 | github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
29 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
30 | github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
31 | github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
32 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
33 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
34 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
35 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
36 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
37 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
38 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
39 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
40 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
43 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
44 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
45 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
46 | github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
47 | github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
48 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
49 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
50 | github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
51 | github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
52 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
53 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
54 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
55 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
56 | github.com/wailsapp/go-webview2 v1.0.18 h1:SSSCoLA+MYikSp1U0WmvELF/4c3x5kH8Vi31TKyZ4yk=
57 | github.com/wailsapp/go-webview2 v1.0.18/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
58 | github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
59 | github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
60 | github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
61 | github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
62 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
63 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
64 | golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
65 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
66 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
67 | golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
68 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
69 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
70 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
71 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
72 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
73 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
74 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
75 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
76 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
77 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
78 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
79 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
82 |
--------------------------------------------------------------------------------
/nrfr-client/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 |
6 | "github.com/wailsapp/wails/v2"
7 | "github.com/wailsapp/wails/v2/pkg/options"
8 | "github.com/wailsapp/wails/v2/pkg/options/assetserver"
9 | "github.com/wailsapp/wails/v2/pkg/options/windows"
10 | )
11 |
12 | //go:embed all:frontend/dist
13 | var assets embed.FS
14 |
15 | func main() {
16 | // Create an instance of the app structure
17 | app := NewApp()
18 |
19 | // Create application with options
20 | err := wails.Run(&options.App{
21 | Title: "Nrfr",
22 | Width: 640,
23 | Height: 480,
24 | Frameless: true,
25 | AssetServer: &assetserver.Options{
26 | Assets: assets,
27 | },
28 | BackgroundColour: &options.RGBA{R: 0, G: 0, B: 0, A: 0},
29 | OnStartup: app.startup,
30 | OnShutdown: app.shutdown,
31 | CSSDragProperty: "--wails-draggable",
32 | CSSDragValue: "drag",
33 | Bind: []interface{}{
34 | app,
35 | },
36 | Windows: &windows.Options{
37 | WindowIsTranslucent: true,
38 | BackdropType: windows.Mica,
39 | DisableWindowIcon: false,
40 | Theme: windows.SystemDefault,
41 | },
42 | Debug: options.Debug{
43 | OpenInspectorOnStartup: true,
44 | },
45 | })
46 |
47 | if err != nil {
48 | println("Error:", err.Error())
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/nrfr-client/wails.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://wails.io/schemas/config.v2.json",
3 | "name": "nrfr-client",
4 | "outputfilename": "Nrfr 快速启动工具",
5 | "frontend:install": "npm install",
6 | "frontend:build": "npm run build",
7 | "frontend:dev:watcher": "npm run dev",
8 | "frontend:dev:serverUrl": "auto",
9 | "author": {
10 | "name": "Antkites"
11 | },
12 | "info": {
13 | "companyName": "Antkites",
14 | "productName": "Nrfr Client",
15 | "productVersion": "1.0.2",
16 | "copyright": "Copyright © 2024 Antkites",
17 | "comments": "Nrfr 快速启动工具"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Nrfr"
23 | include(":app")
24 |
--------------------------------------------------------------------------------