├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ └── Action CI.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── composeApp
├── build.gradle.kts
├── proguard-rules-android.pro
├── proguard-rules-jvm.pro
├── src
│ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ ├── Main.android.kt
│ │ │ ├── misc
│ │ │ │ └── KeyStoreUtils.kt
│ │ │ ├── platform
│ │ │ │ ├── Clipboard.android.kt
│ │ │ │ ├── Crypto.android.kt
│ │ │ │ ├── Download.android.kt
│ │ │ │ ├── HttpClient.android.kt
│ │ │ │ ├── Preferences.android.kt
│ │ │ │ └── Toast.android.kt
│ │ │ └── top
│ │ │ │ └── yukonga
│ │ │ │ └── updater
│ │ │ │ └── kmp
│ │ │ │ ├── AndroidAppContext.kt
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── values-night
│ │ │ └── themes.xml
│ │ │ ├── values
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ │ └── xml
│ │ │ └── locales_config.xml
│ ├── commonMain
│ │ ├── composeResources
│ │ │ ├── drawable
│ │ │ │ └── icon.webp
│ │ │ ├── values-ja-rJP
│ │ │ │ └── strings.xml
│ │ │ ├── values-pt-rBR
│ │ │ │ └── strings.xml
│ │ │ ├── values-zh-rCN
│ │ │ │ └── strings.xml
│ │ │ ├── values-zh-rTW
│ │ │ │ └── strings.xml
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ └── kotlin
│ │ │ ├── App.kt
│ │ │ ├── Info.kt
│ │ │ ├── Login.kt
│ │ │ ├── Metadata.kt
│ │ │ ├── Theme.kt
│ │ │ ├── data
│ │ │ ├── DataHelper.kt
│ │ │ ├── DeviceInfoHelper.kt
│ │ │ ├── FileInfoHelper.kt
│ │ │ └── RomInfoHelper.kt
│ │ │ ├── misc
│ │ │ ├── AppUtils.kt
│ │ │ ├── MessageUtils.kt
│ │ │ └── ZipFile.kt
│ │ │ ├── platform
│ │ │ ├── Clipboard.kt
│ │ │ ├── Crypto.kt
│ │ │ ├── Download.kt
│ │ │ ├── HttpClient.kt
│ │ │ ├── Preferences.kt
│ │ │ └── Toast.kt
│ │ │ └── ui
│ │ │ ├── AboutDialog.kt
│ │ │ ├── BasicViews.kt
│ │ │ ├── LoginCardView.kt
│ │ │ ├── LoginDialog.kt
│ │ │ ├── ResultViews.kt
│ │ │ └── components
│ │ │ ├── AutoCompleteTextField.kt
│ │ │ └── TextWithIcon.kt
│ ├── desktopMain
│ │ ├── java
│ │ │ └── platform
│ │ │ │ ├── Clipboard.desktop.kt
│ │ │ │ ├── Crypto.desktop.kt
│ │ │ │ ├── Download.desktop.kt
│ │ │ │ ├── HttpClient.desktop.kt
│ │ │ │ ├── Preferences.desktop.kt
│ │ │ │ └── Toast.desktop.kt
│ │ ├── kotlin
│ │ │ ├── Main.desktop.kt
│ │ │ ├── misc
│ │ │ │ └── KeyStoreUtils.kt
│ │ │ └── theme
│ │ │ │ ├── MacOSThemeManager.kt
│ │ │ │ └── WindowsThemeManager.kt
│ │ └── resources
│ │ │ ├── linux
│ │ │ └── Icon.png
│ │ │ ├── macos
│ │ │ └── Icon.icns
│ │ │ └── windows
│ │ │ └── Icon.ico
│ ├── iosMain
│ │ └── kotlin
│ │ │ ├── Main.ios.kt
│ │ │ ├── ResourceEnvironmentFix.kt
│ │ │ └── platform
│ │ │ ├── Clipboard.ios.kt
│ │ │ ├── Crypto.ios.kt
│ │ │ ├── Download.ios.kt
│ │ │ ├── HttpClient.ios.kt
│ │ │ ├── Preferences.ios.kt
│ │ │ └── Toast.ios.kt
│ ├── jsMain
│ │ ├── kotlin
│ │ │ ├── Main.js.kt
│ │ │ └── platform
│ │ │ │ ├── Clipboard.js.kt
│ │ │ │ ├── Crypto.js.kt
│ │ │ │ ├── Download.js.kt
│ │ │ │ ├── HttpClient.js.kt
│ │ │ │ ├── Preferences.js.kt
│ │ │ │ └── Toast.js.kt
│ │ └── resources
│ │ │ ├── MiSans VF.woff2
│ │ │ ├── app.js
│ │ │ ├── favicon.ico
│ │ │ ├── index.html
│ │ │ └── styles.css
│ ├── macosMain
│ │ ├── kotlin
│ │ │ ├── Main.macos.kt
│ │ │ └── platform
│ │ │ │ ├── Clipboard.macos.kt
│ │ │ │ ├── Crypto.macos.kt
│ │ │ │ ├── Download.macos.kt
│ │ │ │ ├── HttpClient.macos.kt
│ │ │ │ ├── Preferences.macos.kt
│ │ │ │ └── Toast.macos.kt
│ │ └── resources
│ │ │ └── Updater.icns
│ └── wasmJsMain
│ │ ├── kotlin
│ │ ├── Main.wasmJs.kt
│ │ └── platform
│ │ │ ├── Clipboard.wasmJs.kt
│ │ │ ├── Crypto.wasmJs.kt
│ │ │ ├── Download.wasmJs.kt
│ │ │ ├── HttpClient.wasmJs.kt
│ │ │ ├── Preferences.wasmJs.kt
│ │ │ └── Toast.wasmJs.kt
│ │ └── resources
│ │ ├── MiSans VF.woff2
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── styles.css
└── webpack.config.d
│ └── config.js
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── iosApp
├── Configuration
│ └── Config.xcconfig
├── Podfile
├── iosApp.xcodeproj
│ └── project.pbxproj
└── iosApp
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ └── app-icon-1024.png
│ └── Contents.json
│ ├── ContentView.swift
│ ├── Info.plist
│ └── iosApp.swift
└── settings.gradle.kts
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "gradle"
9 | directory: "/"
10 | schedule:
11 | interval: "daily"
12 |
13 | - package-ecosystem: "github-actions"
14 | directory: "/"
15 | schedule:
16 | interval: "daily"
17 |
--------------------------------------------------------------------------------
/.github/workflows/Action CI.yml:
--------------------------------------------------------------------------------
1 | name: Action CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths-ignore:
7 | - 'README.md'
8 | - 'LICENSE'
9 |
10 | permissions:
11 | contents: read
12 | actions: write
13 |
14 | jobs:
15 | build:
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | matrix:
19 | os: [ macos-latest, ubuntu-latest, windows-latest ]
20 | include:
21 | - os: windows-latest
22 | platform: windows x64
23 | build-command: ./gradlew createReleaseDistributable
24 | artifact-path: composeApp/build/compose/binaries/main-release/app/Updater
25 | artifact-name: Updater-windows-x64-exe
26 | jdk-distribution: jetbrains
27 | - os: macos-latest
28 | platform: macos arm64
29 | build-command: ./gradlew packageDmgNativeReleaseMacosArm64
30 | artifact-path: composeApp/build/compose/binaries/main/native-macosArm64-release-dmg
31 | artifact-name: Updater-darwin-arm64-dmg
32 | jdk-distribution: zulu
33 | - os: ubuntu-latest
34 | platform: linux x64
35 | platformEx: android aarch64
36 | build-command: ./gradlew createReleaseDistributable
37 | build-commandEx: ./gradlew assembleDebug && ./gradlew assembleRelease
38 | artifact-path: composeApp/build/compose/binaries/main-release/app/Updater
39 | artifact-pathEx: composeApp/build/outputs/apk/release
40 | artifact-name: Updater-linux-x64-bin
41 | artifact-nameEx: Updater-android-aarch64-apk
42 | jdk-distribution: zulu
43 |
44 | steps:
45 | - name: Checkout sources
46 | uses: actions/checkout@v4
47 | with:
48 | fetch-depth: 0
49 |
50 | - name: Setup JDK
51 | uses: actions/setup-java@v4
52 | with:
53 | distribution: ${{ matrix.jdk-distribution }}
54 | java-version: '21'
55 |
56 | - name: Setup Gradle
57 | uses: gradle/actions/setup-gradle@v4
58 |
59 | - name: Decode android signing key
60 | if: matrix.platformEx == 'android aarch64'
61 | run: echo ${{ secrets.SIGNING_KEY }} | base64 -d > keystore.jks
62 |
63 | - name: Build ${{ matrix.platform }} platform
64 | run: ${{ matrix.build-command }}
65 |
66 | - name: Build ${{ matrix.platformEx }} platform
67 | if: matrix.platformEx == 'android aarch64'
68 | run: ${{ matrix.build-commandEx }}
69 | env:
70 | KEYSTORE_PATH: "../keystore.jks"
71 | KEYSTORE_PASS: ${{ secrets.KEY_STORE_PASSWORD }}
72 | KEY_ALIAS: ${{ secrets.ALIAS }}
73 | KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
74 |
75 | - name: Upload Updater ${{ matrix.platform }} artifact
76 | uses: actions/upload-artifact@v4
77 | with:
78 | name: ${{ matrix.artifact-name }}
79 | path: ${{ matrix.artifact-path }}
80 | compression-level: 9
81 |
82 | - name: Upload Updater ${{ matrix.platformEx }} artifact
83 | if: matrix.platformEx == 'android aarch64'
84 | uses: actions/upload-artifact@v4
85 | with:
86 | name: ${{ matrix.artifact-nameEx }}
87 | path: ${{ matrix.artifact-pathEx }}
88 | compression-level: 9
89 |
90 | - name: Post to Telegram ci channel
91 | if: ${{ success() && matrix.platformEx == 'android aarch64' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && github.ref_type != 'tag' }}
92 | env:
93 | CHANNEL_ID: ${{ secrets.CHANNEL_ID }}
94 | BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
95 | COMMIT_MESSAGE: |+
96 | New CI from Updater\-KMP
97 |
98 | ```
99 | ${{ github.event.head_commit.message }}
100 | ```
101 | run: |
102 | if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
103 | export RELEASE=$(find ./composeApp/build/outputs/apk/release -name "*.apk")
104 | export DEBUG=$(find ./composeApp/build/outputs/apk/debug -name "*.apk")
105 | ESCAPED=`python3 -c 'import json,os,urllib.parse; print(urllib.parse.quote(json.dumps(os.environ["COMMIT_MESSAGE"])))'`
106 | curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2Frelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2Fdebug%22%2C%22parse_mode%22%3A%22MarkdownV2%22%2C%22caption%22%3A${ESCAPED}%7D%5D" -F release="@$RELEASE" -F debug="@$DEBUG"
107 | fi
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/java,linux,macos,kotlin,android,windows,composer,jetbrains,androidstudio,visualstudiocode,xcode
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=java,linux,macos,kotlin,android,windows,composer,jetbrains,androidstudio,visualstudiocode,xcode
3 |
4 | ### Android ###
5 | # Gradle files
6 | .gradle/
7 | build/
8 |
9 | # Local configuration file (sdk path, etc)
10 | local.properties
11 |
12 | # Log/OS Files
13 | *.log
14 |
15 | # Android Studio generated files and folders
16 | captures/
17 | .externalNativeBuild/
18 | .cxx/
19 | *.apk
20 | output.json
21 |
22 | # IntelliJ
23 | *.iml
24 | .idea/
25 | misc.xml
26 | deploymentTargetDropDown.xml
27 | render.experimental.xml
28 |
29 | # Keystore files
30 | *.jks
31 | *.keystore
32 |
33 | # Google Services (e.g. APIs or Firebase)
34 | google-services.json
35 |
36 | # Android Profiling
37 | *.hprof
38 |
39 | ### Android Patch ###
40 | gen-external-apklibs
41 |
42 | # Replacement of .externalNativeBuild directories introduced
43 | # with Android Studio 3.5.
44 |
45 | ### Composer ###
46 | composer.phar
47 | /vendor/
48 |
49 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
50 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
51 | # composer.lock
52 |
53 | ### Java ###
54 | # Compiled class file
55 | *.class
56 |
57 | # Log file
58 |
59 | # BlueJ files
60 | *.ctxt
61 |
62 | # Mobile Tools for Java (J2ME)
63 | .mtj.tmp/
64 |
65 | # Package Files #
66 | *.jar
67 | *.war
68 | *.nar
69 | *.ear
70 | *.zip
71 | *.tar.gz
72 | *.rar
73 |
74 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
75 | hs_err_pid*
76 | replay_pid*
77 |
78 | ### JetBrains ###
79 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
80 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
81 |
82 | # User-specific stuff
83 | .idea/**/workspace.xml
84 | .idea/**/tasks.xml
85 | .idea/**/usage.statistics.xml
86 | .idea/**/dictionaries
87 | .idea/**/shelf
88 |
89 | # AWS User-specific
90 | .idea/**/aws.xml
91 |
92 | # Generated files
93 | .idea/**/contentModel.xml
94 |
95 | # Sensitive or high-churn files
96 | .idea/**/dataSources/
97 | .idea/**/dataSources.ids
98 | .idea/**/dataSources.local.xml
99 | .idea/**/sqlDataSources.xml
100 | .idea/**/dynamic.xml
101 | .idea/**/uiDesigner.xml
102 | .idea/**/dbnavigator.xml
103 |
104 | # Gradle
105 | .idea/**/gradle.xml
106 | .idea/**/libraries
107 |
108 | # Gradle and Maven with auto-import
109 | # When using Gradle or Maven with auto-import, you should exclude module files,
110 | # since they will be recreated, and may cause churn. Uncomment if using
111 | # auto-import.
112 | # .idea/artifacts
113 | # .idea/compiler.xml
114 | # .idea/jarRepositories.xml
115 | # .idea/modules.xml
116 | # .idea/*.iml
117 | # .idea/modules
118 | # *.iml
119 | # *.ipr
120 |
121 | # CMake
122 | cmake-build-*/
123 |
124 | # Mongo Explorer plugin
125 | .idea/**/mongoSettings.xml
126 |
127 | # File-based project format
128 | *.iws
129 |
130 | # IntelliJ
131 | out/
132 |
133 | # mpeltonen/sbt-idea plugin
134 | .idea_modules/
135 |
136 | # JIRA plugin
137 | atlassian-ide-plugin.xml
138 |
139 | # Cursive Clojure plugin
140 | .idea/replstate.xml
141 |
142 | # SonarLint plugin
143 | .idea/sonarlint/
144 |
145 | # Crashlytics plugin (for Android Studio and IntelliJ)
146 | com_crashlytics_export_strings.xml
147 | crashlytics.properties
148 | crashlytics-build.properties
149 | fabric.properties
150 |
151 | # Editor-based Rest Client
152 | .idea/httpRequests
153 |
154 | # Android studio 3.1+ serialized cache file
155 | .idea/caches/build_file_checksums.ser
156 |
157 | ### JetBrains Patch ###
158 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
159 |
160 | # *.iml
161 | # modules.xml
162 | # .idea/misc.xml
163 | # *.ipr
164 |
165 | # Sonarlint plugin
166 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
167 | .idea/**/sonarlint/
168 |
169 | # SonarQube Plugin
170 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
171 | .idea/**/sonarIssues.xml
172 |
173 | # Markdown Navigator plugin
174 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
175 | .idea/**/markdown-navigator.xml
176 | .idea/**/markdown-navigator-enh.xml
177 | .idea/**/markdown-navigator/
178 |
179 | # Cache file creation bug
180 | # See https://youtrack.jetbrains.com/issue/JBR-2257
181 | .idea/$CACHE_FILE$
182 |
183 | # CodeStream plugin
184 | # https://plugins.jetbrains.com/plugin/12206-codestream
185 | .idea/codestream.xml
186 |
187 | # Azure Toolkit for IntelliJ plugin
188 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
189 | .idea/**/azureSettings.xml
190 |
191 | ### Kotlin ###
192 | /.kotlin
193 | # Compiled class file
194 |
195 | # Log file
196 |
197 | # BlueJ files
198 |
199 | # Mobile Tools for Java (J2ME)
200 |
201 | # Package Files #
202 |
203 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
204 |
205 | ### Linux ###
206 | *~
207 |
208 | # temporary files which can be created if a process still has a handle open of a deleted file
209 | .fuse_hidden*
210 |
211 | # KDE directory preferences
212 | .directory
213 |
214 | # Linux trash folder which might appear on any partition or disk
215 | .Trash-*
216 |
217 | # .nfs files are created when an open file is removed but is still being accessed
218 | .nfs*
219 |
220 | ### macOS ###
221 | # General
222 | .DS_Store
223 | .AppleDouble
224 | .LSOverride
225 |
226 | # Icon must end with two \r
227 | Icon
228 |
229 |
230 | # Thumbnails
231 | ._*
232 |
233 | # Files that might appear in the root of a volume
234 | .DocumentRevisions-V100
235 | .fseventsd
236 | .Spotlight-V100
237 | .TemporaryItems
238 | .Trashes
239 | .VolumeIcon.icns
240 | .com.apple.timemachine.donotpresent
241 |
242 | # Directories potentially created on remote AFP share
243 | .AppleDB
244 | .AppleDesktop
245 | Network Trash Folder
246 | Temporary Items
247 | .apdisk
248 |
249 | ### macOS Patch ###
250 | # iCloud generated files
251 | *.icloud
252 |
253 | ### VisualStudioCode ###
254 | .vscode/*
255 | !.vscode/settings.json
256 | !.vscode/tasks.json
257 | !.vscode/launch.json
258 | !.vscode/extensions.json
259 | !.vscode/*.code-snippets
260 |
261 | # Local History for Visual Studio Code
262 | .history/
263 |
264 | # Built Visual Studio Code Extensions
265 | *.vsix
266 |
267 | ### VisualStudioCode Patch ###
268 | # Ignore all local history of files
269 | .history
270 | .ionide
271 |
272 | ### Windows ###
273 | # Windows thumbnail cache files
274 | Thumbs.db
275 | Thumbs.db:encryptable
276 | ehthumbs.db
277 | ehthumbs_vista.db
278 |
279 | # Dump file
280 | *.stackdump
281 |
282 | # Folder config file
283 | [Dd]esktop.ini
284 |
285 | # Recycle Bin used on file shares
286 | $RECYCLE.BIN/
287 |
288 | # Windows Installer files
289 | *.cab
290 | *.msi
291 | *.msix
292 | *.msm
293 | *.msp
294 |
295 | # Windows shortcuts
296 | *.lnk
297 |
298 | ### Xcode ###
299 | ## User settings
300 | xcuserdata/
301 |
302 | ## Xcode 8 and earlier
303 | *.xcscmblueprint
304 | *.xccheckout
305 |
306 | ### Xcode Patch ###
307 |
308 | # Ignore cocoapods files
309 | iosApp/Podfile.lock
310 | iosApp/Pods/*
311 | iosApp/iosApp.xcworkspace/*
312 | iosApp/iosApp.xcodeproj/*
313 | !iosApp/iosApp.xcodeproj/project.pbxproj
314 | composeApp/composeApp.podspec
315 |
316 | ### AndroidStudio ###
317 | # Covers files to be ignored for android development using Android Studio.
318 |
319 | # Built application files
320 | *.ap_
321 | *.aab
322 |
323 | # Files for the ART/Dalvik VM
324 | *.dex
325 |
326 | # Java class files
327 |
328 | # Generated files
329 | bin/
330 | gen/
331 |
332 | # Gradle files
333 | .gradle
334 |
335 | # Signing files
336 | .signing/
337 |
338 | # Local configuration file (sdk path, etc)
339 |
340 | # Proguard folder generated by Eclipse
341 | proguard/
342 |
343 | # Log Files
344 |
345 | # Android Studio
346 | /*/build/
347 | /*/local.properties
348 | /*/out
349 | /*/*/build
350 | /*/*/production
351 | .navigation/
352 | *.ipr
353 | *.swp
354 |
355 | # Keystore files
356 |
357 | # Google Services (e.g. APIs or Firebase)
358 | # google-services.json
359 |
360 | # Android Patch
361 |
362 | # External native build folder generated in Android Studio 2.2 and later
363 | .externalNativeBuild
364 |
365 | # NDK
366 | obj/
367 |
368 | # IntelliJ IDEA
369 | /out/
370 |
371 | # User-specific configurations
372 | .idea/caches/
373 | .idea/libraries/
374 | .idea/shelf/
375 | .idea/workspace.xml
376 | .idea/tasks.xml
377 | .idea/.name
378 | .idea/compiler.xml
379 | .idea/copyright/profiles_settings.xml
380 | .idea/encodings.xml
381 | .idea/misc.xml
382 | .idea/modules.xml
383 | .idea/scopes/scope_settings.xml
384 | .idea/dictionaries
385 | .idea/vcs.xml
386 | .idea/jsLibraryMappings.xml
387 | .idea/datasources.xml
388 | .idea/dataSources.ids
389 | .idea/sqlDataSources.xml
390 | .idea/dynamic.xml
391 | .idea/uiDesigner.xml
392 | .idea/assetWizardSettings.xml
393 | .idea/gradle.xml
394 | .idea/jarRepositories.xml
395 | .idea/navEditor.xml
396 |
397 | # Legacy Eclipse project files
398 | .classpath
399 | .project
400 | .cproject
401 | .settings/
402 |
403 | # Mobile Tools for Java (J2ME)
404 |
405 | # Package Files #
406 |
407 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
408 |
409 | ## Plugin-specific files:
410 |
411 | # mpeltonen/sbt-idea plugin
412 |
413 | # JIRA plugin
414 |
415 | # Mongo Explorer plugin
416 | .idea/mongoSettings.xml
417 |
418 | # Crashlytics plugin (for Android Studio and IntelliJ)
419 |
420 | ### AndroidStudio Patch ###
421 |
422 | !/gradle/wrapper/gradle-wrapper.jar
423 |
424 | # End of https://www.toptal.com/developers/gitignore/api/java,linux,macos,kotlin,android,windows,composer,jetbrains,androidstudio,visualstudiocode,xcode
425 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Updater-KMP
2 | This is an app to get Xiaomi official recovery rom information.
3 | Use [Kotlin Multiplatform](https://www.jetbrains.com/kotlin-multiplatform/) + [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/).
4 | **Android** / **Desktop(JVM)** / **iOS** / **macOS** are fully supported.
5 | **Webpage([Js](https://yukonga.github.io/Updater-JsCanvas/)/[WasmJs](https://yukonga.github.io/Updater-WasmJs/))** is also basically supported.
6 |
7 | ## Usage:
8 | When obtaining the release version, system version suffix can be automatically completed using `AUTO`.
9 | For example: `OS2.0.100.0.AUTO` / `V14.0.4.0.AUTO`.
10 |
11 | When obtaining the other version, please enter the complete system version yourself.
12 | For example: `OS1.0.23.12.19.DEV` / `V14.0.23.5.8.DEV`.
13 |
14 | ## Notes:
15 | Only supported `MIUI9` and above versions. The most extreme case is: Redmi 1S (armani), MIUI9, Android4.4.
16 |
17 | Only devices in the list of [DeviceInfoHelper](https://github.com/YuKongA/Updater-KMP/blob/main/composeApp/src/commonMain/kotlin/data/DeviceInfoHelper.kt#L28) are supported use `AUTO` to complete automatically, other devices still need to manually enter the full system version.
18 |
19 | When you are not logged in with a Xiaomi account, you can use the miotaV3-v1 interface to obtain any detailed information of the `Pubilc Release Version` of any model.
20 |
21 | After logging in to your Xiaomi account, you will use the miotaV3-v2 interface to obtain detailed information about the `Beta Release Version` or the `Public Development Version`, corresponding to the internal test permissions you have.
22 |
23 | ## Credits:
24 | - [compose-imageloader](https://github.com/qdsfdhvh/compose-imageloader) with MIT License
25 | - [compose-multiplatform](https://github.com/JetBrains/compose-multiplatform) with Apache-2.0 license
26 | - [cryptography-kotlin](https://github.com/whyoleg/cryptography-kotlin) with Apache-2.0 license
27 | - [haze](https://github.com/chrisbanes/haze) with Apache-2.0 license
28 | - [ktor](https://github.com/ktorio/ktor) with Apache-2.0 license
29 | - [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) with Apache-2.0 license
30 | - [miuix](https://github.com/miuix-kotlin-multiplatform/miuix) with Apache-2.0 license
31 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application) apply false
3 | alias(libs.plugins.compose.compiler) apply false
4 | alias(libs.plugins.jetbrains.compose) apply false
5 | alias(libs.plugins.kotlin.cocoapods) apply false
6 | alias(libs.plugins.kotlin.multiplatform) apply false
7 | alias(libs.plugins.kotlin.serialization) apply false
8 | }
--------------------------------------------------------------------------------
/composeApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | import com.android.build.gradle.internal.api.BaseVariantOutputImpl
4 | import com.android.build.gradle.internal.tasks.factory.dependsOn
5 | import org.gradle.kotlin.dsl.support.uppercaseFirstChar
6 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
7 | import org.jetbrains.compose.desktop.application.tasks.AbstractNativeMacApplicationPackageAppDirTask
8 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
9 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
10 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
11 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
12 | import org.jetbrains.kotlin.konan.target.KonanTarget
13 | import java.util.Properties
14 |
15 | plugins {
16 | alias(libs.plugins.android.application)
17 | alias(libs.plugins.compose.compiler)
18 | alias(libs.plugins.jetbrains.compose)
19 | alias(libs.plugins.kotlin.cocoapods)
20 | alias(libs.plugins.kotlin.multiplatform)
21 | alias(libs.plugins.kotlin.serialization)
22 | }
23 |
24 | val appName = "Updater"
25 | val pkgName = "top.yukonga.updater.kmp"
26 | val verName = "1.5.2"
27 | val verCode = getVersionCode()
28 | val generatedSrcDir = layout.buildDirectory.dir("generated").get().asFile.resolve("updater")
29 |
30 | java {
31 | toolchain.languageVersion = JavaLanguageVersion.of(21)
32 | }
33 |
34 | kotlin {
35 | jvmToolchain(21)
36 |
37 | androidTarget()
38 |
39 | jvm("desktop")
40 |
41 | iosX64()
42 | iosArm64()
43 | iosSimulatorArm64()
44 |
45 | fun macosTargets(config: KotlinNativeTarget.() -> Unit) {
46 | macosX64(config)
47 | macosArm64(config)
48 | }
49 | macosTargets {
50 | compilerOptions {
51 | freeCompilerArgs.add("-Xbinary=preCodegenInlineThreshold=40")
52 | }
53 | binaries.executable {
54 | entryPoint = "main"
55 | }
56 | }
57 |
58 | cocoapods {
59 | version = verName
60 | summary = "Get HyperOS/MIUI recovery ROM info"
61 | homepage = "https://github.com/YuKongA/Updater-KMP"
62 | authors = "YuKongA"
63 | license = "AGPL-3.0"
64 | podfile = project.file("../iosApp/Podfile")
65 | compilerOptions {
66 | freeCompilerArgs.add("-Xbinary=preCodegenInlineThreshold=40")
67 | }
68 | framework {
69 | baseName = appName + "Framework"
70 | isStatic = true
71 | }
72 | }
73 |
74 | @OptIn(ExperimentalWasmDsl::class)
75 | wasmJs {
76 | browser {
77 | outputModuleName = "updater"
78 | commonWebpackConfig {
79 | outputFileName = "updater.js"
80 | }
81 | }
82 | binaries.executable()
83 | }
84 |
85 | js(IR) {
86 | browser {
87 | outputModuleName = "updater"
88 | commonWebpackConfig {
89 | outputFileName = "updater.js"
90 | }
91 | }
92 | binaries.executable()
93 | }
94 |
95 | sourceSets {
96 | val desktopMain by getting
97 | val commonMain by getting {
98 | kotlin.srcDir(generatedSrcDir.resolve("kotlin").absolutePath)
99 | }
100 | commonMain.dependencies {
101 | implementation(compose.runtime)
102 | implementation(compose.foundation)
103 | implementation(compose.material3)
104 | implementation(compose.ui)
105 | implementation(compose.components.resources)
106 | // Added
107 | implementation(libs.cryptography.core)
108 | implementation(libs.image.loader)
109 | implementation(libs.kotlinx.serialization.json)
110 | implementation(libs.kotlinx.datetime)
111 | implementation(libs.ktor.client.core)
112 | implementation(libs.miuix)
113 | implementation(libs.haze)
114 | }
115 | androidMain.dependencies {
116 | implementation(libs.androidx.activity.compose)
117 | // Added
118 | implementation(libs.cryptography.provider.jdk)
119 | implementation(libs.ktor.client.cio)
120 | }
121 | iosMain.dependencies {
122 | // Added
123 | implementation(libs.cryptography.provider.apple)
124 | implementation(libs.ktor.client.darwin)
125 | }
126 | macosMain.dependencies {
127 | // Added
128 | implementation(libs.cryptography.provider.apple)
129 | implementation(libs.ktor.client.darwin)
130 | }
131 | jsMain.dependencies {
132 | // Added
133 | implementation(libs.cryptography.provider.webcrypto)
134 | implementation(libs.ktor.client.js)
135 | }
136 | wasmJsMain.dependencies {
137 | // Added
138 | implementation(libs.cryptography.provider.webcrypto)
139 | implementation(libs.ktor.client.js)
140 | }
141 | desktopMain.dependencies {
142 | implementation(compose.desktop.currentOs)
143 | // Added
144 | implementation(libs.cryptography.provider.jdk)
145 | implementation(libs.ktor.client.cio)
146 | implementation(libs.jna)
147 | implementation(libs.jna.platform)
148 | }
149 | }
150 | }
151 |
152 | android {
153 | namespace = pkgName
154 | compileSdk = 36
155 | defaultConfig {
156 | applicationId = pkgName
157 | minSdk = 26
158 | targetSdk = compileSdk
159 | versionCode = verCode
160 | versionName = verName
161 | }
162 | val properties = Properties()
163 | runCatching { properties.load(project.rootProject.file("local.properties").inputStream()) }
164 | val keystorePath = properties.getProperty("KEYSTORE_PATH") ?: System.getenv("KEYSTORE_PATH")
165 | val keystorePwd = properties.getProperty("KEYSTORE_PASS") ?: System.getenv("KEYSTORE_PASS")
166 | val alias = properties.getProperty("KEY_ALIAS") ?: System.getenv("KEY_ALIAS")
167 | val pwd = properties.getProperty("KEY_PASSWORD") ?: System.getenv("KEY_PASSWORD")
168 | if (keystorePath != null) {
169 | signingConfigs {
170 | create("release") {
171 | storeFile = file(keystorePath)
172 | storePassword = keystorePwd
173 | keyAlias = alias
174 | keyPassword = pwd
175 | enableV2Signing = true
176 | enableV3Signing = true
177 | enableV4Signing = true
178 | }
179 | }
180 | }
181 | buildTypes {
182 | release {
183 | isMinifyEnabled = true
184 | isShrinkResources = true
185 | vcsInfo.include = false
186 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules-android.pro")
187 | if (keystorePath != null) signingConfig = signingConfigs.getByName("release")
188 | }
189 | debug {
190 | if (keystorePath != null) signingConfig = signingConfigs.getByName("release")
191 | }
192 | }
193 | dependenciesInfo.includeInApk = false
194 | packaging {
195 | applicationVariants.all {
196 | outputs.all {
197 | (this as BaseVariantOutputImpl).outputFileName = "$appName-v$versionName($versionCode)-$name.apk"
198 | }
199 | }
200 | resources.excludes += "**"
201 | }
202 | }
203 |
204 | compose.desktop {
205 | application {
206 | mainClass = "Main_desktopKt"
207 |
208 | buildTypes.release.proguard {
209 | optimize = false
210 | configurationFiles.from("proguard-rules-jvm.pro")
211 | }
212 |
213 | nativeDistributions {
214 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
215 |
216 | packageName = appName
217 | packageVersion = verName
218 | description = "Get HyperOS/MIUI recovery ROM info"
219 | copyright = "Copyright © 2024-2025 YuKongA"
220 | linux {
221 | iconFile = file("src/desktopMain/resources/linux/Icon.png")
222 | }
223 | macOS {
224 | bundleID = pkgName
225 | iconFile = file("src/desktopMain/resources/macos/Icon.icns")
226 | }
227 | windows {
228 | dirChooser = true
229 | perUserInstall = true
230 | iconFile = file("src/desktopMain/resources/windows/Icon.ico")
231 | }
232 | }
233 | }
234 | nativeApplication {
235 | targets(kotlin.targets.getByName("macosArm64"), kotlin.targets.getByName("macosX64"))
236 | distributions {
237 | targetFormats(TargetFormat.Dmg)
238 | packageName = appName
239 | packageVersion = verName
240 | description = "Get HyperOS/MIUI recovery ROM info"
241 | copyright = "Copyright © 2024-2025 YuKongA"
242 | macOS {
243 | bundleID = pkgName
244 | iconFile = file("src/macosMain/resources/Updater.icns")
245 | }
246 | }
247 | }
248 | }
249 |
250 | fun getGitCommitCount(): Int {
251 | val process = Runtime.getRuntime().exec(arrayOf("git", "rev-list", "--count", "HEAD"))
252 | return process.inputStream.bufferedReader().use { it.readText().trim().toInt() }
253 | }
254 |
255 | fun getVersionCode(): Int {
256 | val commitCount = getGitCommitCount()
257 | val major = 5
258 | return major + commitCount
259 | }
260 |
261 | val generateVersionInfo by tasks.registering {
262 | doLast {
263 | val file = generatedSrcDir.resolve("kotlin/misc/VersionInfo.kt")
264 | if (!file.exists()) {
265 | file.parentFile.mkdirs()
266 | file.createNewFile()
267 | }
268 | file.writeText(
269 | """
270 | package misc
271 |
272 | object VersionInfo {
273 | const val VERSION_NAME = "$verName"
274 | const val VERSION_CODE = $verCode
275 | }
276 | """.trimIndent()
277 | )
278 | }
279 | }
280 |
281 | tasks.named("generateComposeResClass").configure {
282 | dependsOn(generateVersionInfo)
283 | }
284 |
285 | afterEvaluate {
286 | project.extensions.getByType().targets
287 | .withType()
288 | .filter { it.konanTarget == KonanTarget.MACOS_ARM64 || it.konanTarget == KonanTarget.MACOS_X64 }
289 | .forEach { target ->
290 | val targetName = target.targetName.uppercaseFirstChar()
291 | val buildTypes = mapOf(
292 | NativeBuildType.RELEASE to target.binaries.getExecutable(NativeBuildType.RELEASE),
293 | NativeBuildType.DEBUG to target.binaries.getExecutable(NativeBuildType.DEBUG)
294 | )
295 | buildTypes.forEach { (buildType, executable) ->
296 | val buildTypeName = buildType.name.lowercase().uppercaseFirstChar()
297 | target.binaries.withType()
298 | .filter { it.buildType == buildType }
299 | .forEach {
300 | val taskName = "copy${buildTypeName}ComposeResourcesFor${targetName}"
301 | val copyTask = tasks.register(taskName) {
302 | from({
303 | (executable.compilation.associatedCompilations + executable.compilation).flatMap { compilation ->
304 | compilation.allKotlinSourceSets.map { it.resources }
305 | }
306 | })
307 | into(executable.outputDirectory.resolve("compose-resources"))
308 | exclude("*.icns")
309 | }
310 | it.linkTaskProvider.dependsOn(copyTask)
311 | }
312 | }
313 | }
314 | }
315 |
316 | tasks.withType().configureEach {
317 | doLast {
318 | val packageName = packageName.get()
319 | val destinationDir = outputs.files.singleFile
320 | val appDir = destinationDir.resolve("$packageName.app")
321 | val resourcesDir = appDir.resolve("Contents/Resources")
322 | val currentMacosTarget = kotlin.targets.withType()
323 | .find { it.konanTarget == KonanTarget.MACOS_ARM64 || it.konanTarget == KonanTarget.MACOS_X64 }?.targetName
324 | val composeResourcesDir = project.rootDir
325 | .resolve("composeApp/build/bin/$currentMacosTarget/releaseExecutable/compose-resources")
326 | if (composeResourcesDir.exists()) {
327 | project.copy {
328 | from(composeResourcesDir)
329 | into(resourcesDir.resolve("compose-resources"))
330 | }
331 | }
332 | }
333 | }
334 |
--------------------------------------------------------------------------------
/composeApp/proguard-rules-android.pro:
--------------------------------------------------------------------------------
1 | -dontwarn org.slf4j.helpers.SubstituteLogger
--------------------------------------------------------------------------------
/composeApp/proguard-rules-jvm.pro:
--------------------------------------------------------------------------------
1 | -dontwarn org.slf4j.helpers.SubstituteLogger
2 | -dontwarn okhttp3.internal.platform.**
3 | -dontwarn io.ktor.network.sockets.SocketBase**
4 |
5 | -keep class com.sun.jna.** { *; }
6 | -keep class * implements com.sun.jna.** { *; }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/Main.android.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.Composable
2 |
3 | @Composable
4 | fun MainView() = App()
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/misc/KeyStoreUtils.kt:
--------------------------------------------------------------------------------
1 | package misc
2 |
3 | import android.security.keystore.KeyGenParameterSpec
4 | import android.security.keystore.KeyProperties
5 | import java.security.KeyStore
6 | import javax.crypto.Cipher
7 | import javax.crypto.KeyGenerator
8 | import javax.crypto.SecretKey
9 | import javax.crypto.spec.GCMParameterSpec
10 |
11 | object KeyStoreUtils {
12 |
13 | private const val ANDROID_KEY_STORE = "AndroidKeyStore"
14 | private const val UPDATER_KEY_ALIAS = "updater_key_alias"
15 | private const val AES_MODE = "AES/GCM/NoPadding"
16 |
17 | fun generateKey() {
18 | val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
19 | keyGenerator.init(
20 | KeyGenParameterSpec.Builder(UPDATER_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
21 | .setBlockModes(KeyProperties.BLOCK_MODE_GCM).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE).setRandomizedEncryptionRequired(false)
22 | .build()
23 | )
24 | keyGenerator.generateKey()
25 | }
26 |
27 | private fun getSecretKey(): SecretKey {
28 | val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
29 | keyStore.load(null)
30 | return keyStore.getKey(UPDATER_KEY_ALIAS, null) as SecretKey
31 | }
32 |
33 | fun getEncryptionCipher(): Cipher {
34 | val cipher = Cipher.getInstance(AES_MODE)
35 | cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
36 | return cipher
37 | }
38 |
39 | fun getDecryptionCipher(iv: ByteArray): Cipher {
40 | val cipher = Cipher.getInstance(AES_MODE)
41 | cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), GCMParameterSpec(128, iv))
42 | return cipher
43 | }
44 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/platform/Clipboard.android.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import android.content.ClipData
4 | import androidx.compose.ui.platform.ClipEntry
5 | import androidx.compose.ui.platform.Clipboard
6 |
7 | actual suspend fun Clipboard.copyToClipboard(string: String) {
8 | val clipData = ClipData.newPlainText("Clipboard", string)
9 | setClipEntry(ClipEntry(clipData))
10 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/platform/Crypto.android.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.jdk.JDK
5 | import misc.KeyStoreUtils
6 | import kotlin.io.encoding.Base64
7 | import kotlin.io.encoding.ExperimentalEncodingApi
8 |
9 | actual suspend fun provider() = CryptographyProvider.JDK
10 |
11 | @OptIn(ExperimentalEncodingApi::class)
12 | actual fun ownEncrypt(string: String): Pair {
13 | val cipher = KeyStoreUtils.getEncryptionCipher()
14 | val encrypted = cipher.doFinal(string.toByteArray())
15 | val iv = cipher.iv
16 | return Pair(Base64.encode(encrypted), Base64.encode(iv))
17 | }
18 |
19 | @OptIn(ExperimentalEncodingApi::class)
20 | actual fun ownDecrypt(encryptedText: String, encodedIv: String): String {
21 | val encrypted = Base64.decode(encryptedText)
22 | val iv = Base64.decode(encodedIv)
23 | val cipher = KeyStoreUtils.getDecryptionCipher(iv)
24 | return String(cipher.doFinal(encrypted))
25 | }
26 |
27 | actual fun generateKey() {
28 | KeyStoreUtils.generateKey()
29 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/platform/Download.android.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import android.app.DownloadManager
4 | import android.content.Context
5 | import android.os.Environment
6 | import androidx.core.net.toUri
7 | import top.yukonga.updater.kmp.AndroidAppContext
8 |
9 | actual fun downloadToLocal(url: String, fileName: String) {
10 | val request = DownloadManager.Request(url.toUri()).apply {
11 | setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
12 | setTitle(fileName)
13 | setDescription(fileName)
14 | setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
15 | setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
16 | }
17 | val context = AndroidAppContext.getApplicationContext()
18 | val downloadManager = context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
19 | downloadManager.enqueue(request)
20 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/platform/HttpClient.android.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.cio.CIO
5 | import io.ktor.client.plugins.HttpTimeout
6 |
7 | actual fun httpClientPlatform(): HttpClient {
8 | return HttpClient(CIO).config {
9 | install(HttpTimeout) {
10 | requestTimeoutMillis = 10000
11 | connectTimeoutMillis = 10000
12 | socketTimeoutMillis = 10000
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/platform/Preferences.android.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import top.yukonga.updater.kmp.AndroidAppContext
7 |
8 | @SuppressLint("StaticFieldLeak")
9 | private val context = AndroidAppContext.getApplicationContext()
10 | private val sharedPreferences: SharedPreferences? = context?.getSharedPreferences("UpdaterKMP", Context.MODE_PRIVATE)
11 |
12 | actual fun perfSet(key: String, value: String) {
13 | sharedPreferences?.edit()?.putString(key, value)?.apply()
14 | }
15 |
16 | actual fun perfGet(key: String): String? {
17 | return sharedPreferences?.getString(key, null)
18 | }
19 |
20 | actual fun perfRemove(key: String) {
21 | sharedPreferences?.edit()?.remove(key)?.apply()
22 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/platform/Toast.android.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import android.widget.Toast
4 | import top.yukonga.updater.kmp.AndroidAppContext
5 |
6 | private var lastToast: Toast? = null
7 |
8 | actual fun useToast(): Boolean = true
9 |
10 | actual fun showToast(message: String, duration: Long) {
11 | val context = AndroidAppContext.getApplicationContext()
12 | lastToast?.cancel()
13 | lastToast = Toast.makeText(context, message, duration.toInt()).apply { show() }
14 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/top/yukonga/updater/kmp/AndroidAppContext.kt:
--------------------------------------------------------------------------------
1 | package top.yukonga.updater.kmp
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 |
6 | @SuppressLint("StaticFieldLeak")
7 | object AndroidAppContext {
8 | private var context: Context? = null
9 |
10 | fun init(context: Context) {
11 | AndroidAppContext.context = context.applicationContext
12 | }
13 |
14 | fun getApplicationContext(): Context? {
15 | return context
16 | }
17 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/top/yukonga/updater/kmp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package top.yukonga.updater.kmp
2 |
3 | import MainView
4 | import android.os.Build
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.enableEdgeToEdge
9 |
10 | class MainActivity : ComponentActivity() {
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 |
14 | AndroidAppContext.init(this)
15 | enableEdgeToEdge()
16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
17 | window.isNavigationBarContrastEnforced = false
18 | }
19 | setContent {
20 | MainView()
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 | #3482FF
5 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/xml/locales_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/composeApp/src/commonMain/composeResources/drawable/icon.webp
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/values-ja-rJP/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 概要
4 | ログイン
5 | ソース コードを表示:
6 | チャンネルに参加:
7 | コードネーム
8 | デバイス名
9 | システムバージョン
10 | Android バージョン
11 | 送信
12 | 検索履歴
13 | 検索履歴を消去
14 | メジャーバージョン
15 | ファイル名
16 | ファイルサイズ
17 | ダウンロード URL
18 | 変更履歴
19 | ブランチ
20 | タグ
21 | フィンガープリント
22 | セキュリティ パッチ レベル
23 | ビルド時間
24 | 注意
25 | 検索中…
26 | 情報がありません!
27 | 要求されたバージョンは存在しません!
28 | リクエストが成功しました!
29 | ネットワーク接続がありません!
30 | ultimate リンクを取得できません!
31 | アカウント
32 | パスワード
33 | パスワードを保存
34 | キャンセル
35 | ログイン中…
36 | ログイン成功
37 | アカウントまたはパスワードが空です!
38 | アカウントがありません
39 | v1 インターフェースを使用
40 | v2 インターフェースを使用
41 | ログイン済み
42 | ログイン期限切れ
43 | 再ログインをお勧めします
44 | コピー成功
45 | ダウンロード開始
46 | セキュリティキーの取得に失敗しました
47 | ログアウト
48 | ログアウトしてもよろしいですか?
49 | 確認
50 | ログアウト成功
51 | ログイン認証に失敗しました!
52 | 地域コード
53 | グローバルアカウント
54 | 拡張機能の設定
55 | 表示モード
56 | システムに従ってください
57 | ライトモード
58 | ダークモード
59 | 著作権 © 2024-2025 YuKongA
60 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sobre
4 | Entrar
5 | Ver Código Fonte:
6 | Junte-se ao canal:
7 | Codename
8 | Nome do Dispositivo
9 | Versão do Sistema
10 | Versão do Android
11 | Enviar
12 | Histórico de Pesquisa
13 | Limpar histórico de Pesquisa
14 | Versão Principal
15 | Nome do Arquivo
16 | Tamanho do Arquivo
17 | URL de Download
18 | Registro de Alterações
19 | Ramo
20 | Etiquetas
21 | Impressão digital
22 | Nível do patch de segurança
23 | Hora da construção
24 | ATENÇÃO
25 | Pesquisando…
26 | Sem informações!
27 | A versão solicitada não existe!
28 | Solicitação bem-sucedida!
29 | Sem conexão com a rede!
30 | Não é possível obter o link ultimate!
31 | Conta
32 | Senha
33 | Salvar senha
34 | Cancelar
35 | Entrando…
36 | Login bem-sucedido
37 | Conta ou Senha vazia!
38 | Sem conta
39 | Usando interface v1
40 | Usando interface v2
41 | Logado
42 | Login expirado
43 | Recomendado fazer login novamente
44 | Cópia bem-sucedida
45 | Iniciar download
46 | Falha ao obter chave de segurança
47 | Sair
48 | Tem certeza de que deseja sair?
49 | Confirmar
50 | Logout bem-sucedido
51 | Falha na verificação de login!
52 | Código de Regiões
53 | Conta global
54 | Configurações de Extensão
55 | Modo de exibição
56 | Padrão do sistema
57 | Modo claro
58 | Modo escuro
59 | Copyright © 2024-2025 YuKongA
60 |
61 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 关于
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 | 未获取到 ultimate 链接!
31 | 账号
32 | 密码
33 | 保存密码
34 | 取消
35 | 正在登录…
36 | 登录已过期
37 | 建议重新登录
38 | 登录成功
39 | 账号或密码为空!
40 | 未登录
41 | 正在使用 v1 接口
42 | 正在使用 v2 接口
43 | 已登录
44 | 复制成功
45 | 开始下载
46 | 获取密钥失败
47 | 退出
48 | 确定要退出登录么?
49 | 确定
50 | 退出成功
51 | 区域代号
52 | 全球账号
53 | 登录验证失败!
54 | 扩展设置
55 | 显示模式
56 | 跟随系统
57 | 浅色模式
58 | 深色模式
59 | 版权所有 © 2024-2025 YuKongA
60 |
61 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 關於
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 | 未獲取到 ultimate 連結!
31 | 帳號
32 | 密碼
33 | 儲存密碼
34 | 取消
35 | 登入中…
36 | 登入已過期
37 | 建議重新登入
38 | 登入成功
39 | 帳號或密碼為空!
40 | 未登入
41 | 正在使用 v1 埠
42 | 正在使用 v2 埠
43 | 已登入
44 | 複製成功
45 | 開始下載
46 | 取得金鑰失敗
47 | 退出
48 | 確定要退出登入嗎?
49 | 確定
50 | 退出成功
51 | 區域代號
52 | 全球帳號
53 | 登入驗證失敗!
54 | 擴展設定
55 | 顯示模式
56 | 跟隨系統
57 | 淺色模式
58 | 深色模式
59 | 版權所有 © 2024-2025 YuKongA
60 |
61 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Updater
4 | About
5 | View Source:
6 | Join Channel:
7 | Login
8 | Code Name
9 | Device Name
10 | System Version
11 | Android Version
12 | Submit
13 | Search History
14 | Clear Search History
15 | Major Version
16 | File Name
17 | File Size
18 | Download Url
19 | Changelog
20 | Branch
21 | Tags
22 | Fingerprint
23 | Security Patch Level
24 | Build Time
25 | Attention
26 | Searching…
27 | No information!
28 | Requested version does not exist!
29 | Request successful!
30 | No network connection!
31 | Unable to get ultimate link!
32 | Account
33 | Password
34 | Save password
35 | Cancel
36 | Logging in…
37 | Login successful
38 | Account or Password empty!
39 | No account
40 | Using v1 interface
41 | Using v2 interface
42 | Logged in
43 | Login expired
44 | Recommended to login again
45 | Copy successful
46 | Start download
47 | Failed to get security key
48 | Logout
49 | Are you sure you want to logout?
50 | Confirm
51 | Logout successful
52 | Login verification failed!
53 | Regions code
54 | Global account
55 | Extension settings
56 | Display mode
57 | System default
58 | Light mode
59 | Dark mode
60 | Copyright © 2024-2025 YuKongA
61 |
62 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/Info.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.MutableState
2 | import data.DataHelper
3 | import io.ktor.client.call.body
4 | import io.ktor.client.request.forms.FormDataContent
5 | import io.ktor.client.request.post
6 | import io.ktor.http.Parameters
7 | import io.ktor.utils.io.InternalAPI
8 | import kotlinx.serialization.json.Json
9 | import misc.json
10 | import platform.httpClientPlatform
11 | import platform.miuiDecrypt
12 | import platform.miuiEncrypt
13 | import platform.perfGet
14 | import kotlin.io.encoding.Base64
15 | import kotlin.io.encoding.ExperimentalEncodingApi
16 |
17 | val CN_RECOVERY_URL = if (isWeb()) "https://updater.yukonga.top/updates/miotaV3.php" else "https://update.miui.com/updates/miotaV3.php"
18 | val INTL_RECOVERY_URL = if (isWeb()) "https://updater.yukonga.top/intl-updates/miotaV3.php" else "https://update.intl.miui.com/updates/miotaV3.php"
19 | var accountType = "CN"
20 | var port = "1"
21 | var security = ""
22 | var securityKey = "miuiotavalided11".encodeToByteArray()
23 | var serviceToken = ""
24 | var userId = ""
25 |
26 | /**
27 | * Generate JSON data for recovery ROM info request.
28 | *
29 | * @param branch: Branch name
30 | * @param codeNameExt: CodeName with region extension
31 | * @param regionCode: Region code
32 | * @param romVersion: ROM version
33 | * @param androidVersion: Android version
34 | * @param userId: Xiaomi ID
35 | * @param security: Security key
36 | * @param token: Service token
37 | *
38 | * @return JSON data
39 | */
40 | fun generateJson(
41 | branch: String,
42 | codeNameExt: String,
43 | regionCode: String,
44 | romVersion: String,
45 | androidVersion: String,
46 | userId: String,
47 | security: String,
48 | token: String
49 | ): String {
50 | val data = DataHelper.RequestData(
51 | b = branch,
52 | c = androidVersion,
53 | d = codeNameExt,
54 | f = "1",
55 | id = userId,
56 | l = if (!codeNameExt.contains("_global")) "zh_CN" else "en_US",
57 | ov = romVersion,
58 | p = codeNameExt,
59 | pn = codeNameExt,
60 | r = regionCode,
61 | security = security,
62 | token = token,
63 | unlock = "0",
64 | v = "MIUI-$romVersion"
65 | )
66 | return Json.encodeToString(data)
67 | }
68 |
69 | /**
70 | * Get recovery ROM info form xiaomi server.
71 | *
72 | * @param branch: Branch name
73 | * @param codeNameExt: CodeName with region extension
74 | * @param regionCode: Region code
75 | * @param romVersion: ROM version
76 | * @param androidVersion: Android version
77 | * @param isLogin: Xiaomi account login status
78 | *
79 | * @return Recovery ROM info
80 | */
81 | @OptIn(ExperimentalEncodingApi::class, InternalAPI::class)
82 | suspend fun getRecoveryRomInfo(
83 | branch: String,
84 | codeNameExt: String,
85 | regionCode: String,
86 | romVersion: String,
87 | androidVersion: String,
88 | isLogin: MutableState
89 | ): String {
90 | if (perfGet("loginInfo") != null && isLogin.value == 1) {
91 | val loginInfo = perfGet("loginInfo")?.let { json.decodeFromString(it) }
92 | val authResult = loginInfo?.authResult
93 | if (authResult != "3") {
94 | accountType = loginInfo?.accountType.toString().ifEmpty { "CN" }
95 | port = "2"
96 | security = loginInfo?.ssecurity.toString()
97 | securityKey = Base64.Mime.decode(security)
98 | serviceToken = loginInfo?.serviceToken.toString()
99 | userId = loginInfo?.userId.toString()
100 | } else setDefaultRequestInfo()
101 | } else setDefaultRequestInfo()
102 |
103 | val jsonData = generateJson(branch, codeNameExt, regionCode, romVersion, androidVersion, userId, security, serviceToken)
104 | val encryptedText = miuiEncrypt(jsonData, securityKey)
105 | val client = httpClientPlatform()
106 | val parameters = Parameters.build {
107 | append("q", encryptedText)
108 | append("t", serviceToken)
109 | append("s", port)
110 | }
111 | val recoveryUrl = if (accountType != "CN") INTL_RECOVERY_URL else CN_RECOVERY_URL
112 | try {
113 | val response = client.post(recoveryUrl) {
114 | body = FormDataContent(parameters)
115 | }
116 | val requestedEncryptedText = response.body()
117 | client.close()
118 | return miuiDecrypt(requestedEncryptedText, securityKey)
119 | } catch (e: Exception) {
120 | e.printStackTrace()
121 | return ""
122 | }
123 | }
124 |
125 | /**
126 | * Set default request info.
127 | */
128 | fun setDefaultRequestInfo() {
129 | accountType = "CN"
130 | port = "1"
131 | security = ""
132 | securityKey = "miuiotavalided11".encodeToByteArray()
133 | serviceToken = ""
134 | userId = ""
135 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/Login.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.MutableState
2 | import data.DataHelper
3 | import io.ktor.client.call.body
4 | import io.ktor.client.request.get
5 | import io.ktor.client.request.parameter
6 | import io.ktor.client.request.post
7 | import misc.json
8 | import misc.md5Hash
9 | import platform.generateKey
10 | import platform.httpClientPlatform
11 | import platform.ownDecrypt
12 | import platform.ownEncrypt
13 | import platform.perfGet
14 | import platform.perfRemove
15 | import platform.perfSet
16 | import top.yukonga.miuix.kmp.utils.Platform
17 | import top.yukonga.miuix.kmp.utils.platform
18 |
19 | private const val loginAuth2Url = "https://account.xiaomi.com/pass/serviceLoginAuth2"
20 |
21 | fun isWeb(): Boolean = platform() == Platform.WasmJs || platform() == Platform.Js
22 |
23 | /**
24 | * Login Xiaomi account.
25 | *
26 | * @param account: Xiaomi account
27 | * @param password: Password
28 | * @param global: Global or China account
29 | * @param savePassword: Save password or not
30 | * @param isLogin: Login status
31 | *
32 | * @return Login status
33 | */
34 | suspend fun login(
35 | account: String,
36 | password: String,
37 | global: Boolean,
38 | savePassword: String,
39 | isLogin: MutableState
40 | ): Int {
41 | if (account.isEmpty() || password.isEmpty()) return 1
42 | if (savePassword != "1") deletePassword()
43 |
44 | val client = httpClientPlatform()
45 | val sid = if (global) "miuiota_intl" else "miuiromota"
46 | val md5Hash = md5Hash(password)
47 |
48 | try {
49 | client.get(loginAuth2Url)
50 | val response = client.post(loginAuth2Url) {
51 | parameter("_json", "true")
52 | parameter("user", account)
53 | parameter("hash", md5Hash)
54 | parameter("sid", sid)
55 | }
56 |
57 | val authStr = response.body().replace("&&&START&&&", "")
58 | val authJson = json.decodeFromString(authStr)
59 | val description = authJson.description
60 | val ssecurity = authJson.ssecurity
61 | val location = authJson.location
62 | val userId = authJson.userId.toString()
63 | val accountType = if (global) "GL" else "CN"
64 | val authResult = if (authJson.result == "ok") "1" else "0"
65 |
66 | if (description != "成功") return 3
67 | if (ssecurity == null || location == null || userId.isEmpty()) return 4
68 |
69 | if (savePassword == "1") {
70 | perfSet("savePassword", "1")
71 | savePassword(account, password)
72 | }
73 |
74 | val response2 = client.get(location) { parameter("_userIdNeedEncrypt", true) }
75 | val cookies = response2.headers["Set-Cookie"].toString().split("; ")[0].split("; ")[0]
76 | val serviceToken = cookies.split("serviceToken=")[1].split(";")[0]
77 |
78 | val loginInfo = DataHelper.LoginData(accountType, authResult, description, ssecurity, serviceToken, userId)
79 | perfSet("loginInfo", json.encodeToString(loginInfo))
80 | isLogin.value = 1
81 | return 0
82 | } catch (_: Exception) {
83 | return 2
84 | }
85 | }
86 |
87 | /**
88 | * Logout Xiaomi account.
89 | *
90 | * @param isLogin: Login status
91 | *
92 | * @return Logout status
93 | */
94 | fun logout(isLogin: MutableState): Boolean {
95 | perfRemove("loginInfo")
96 | isLogin.value = 0
97 | return true
98 | }
99 |
100 | /**
101 | * Save Xiaomi's account & password.
102 | *
103 | * @param account: Xiaomi account
104 | * @param password: Password
105 | */
106 | fun savePassword(account: String, password: String) {
107 | generateKey()
108 | val encryptedAccount = ownEncrypt(account)
109 | val encryptedPassword = ownEncrypt(password)
110 | perfSet("account", encryptedAccount.first)
111 | perfSet("accountIv", encryptedAccount.second)
112 | perfSet("password", encryptedPassword.first)
113 | perfSet("passwordIv", encryptedPassword.second)
114 | }
115 |
116 | /**
117 | * Delete Xiaomi's account & password.
118 | */
119 | fun deletePassword() {
120 | perfRemove("account")
121 | perfRemove("accountIv")
122 | perfRemove("password")
123 | perfRemove("passwordIv")
124 | }
125 |
126 | /**
127 | * Get Xiaomi's account & password.
128 | *
129 | * @return Pair of Xiaomi's account & password
130 | */
131 | fun getPassword(): Pair {
132 | if (perfGet("account") != null && perfGet("password") != null && perfGet("accountIv") != null && perfGet("passwordIv") != null) {
133 | val encryptedAccount = perfGet("account").toString()
134 | val encodedAccountKey = perfGet("accountIv").toString()
135 | val encryptedPassword = perfGet("password").toString()
136 | val encodedPasswordKey = perfGet("passwordIv").toString()
137 | val account = ownDecrypt(encryptedAccount, encodedAccountKey)
138 | val password = ownDecrypt(encryptedPassword, encodedPasswordKey)
139 | return Pair(account, password)
140 | } else return Pair("", "")
141 | }
142 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/Metadata.kt:
--------------------------------------------------------------------------------
1 | import io.ktor.client.request.get
2 | import io.ktor.client.request.head
3 | import io.ktor.client.request.header
4 | import io.ktor.client.statement.bodyAsChannel
5 | import io.ktor.http.HttpHeaders
6 | import io.ktor.utils.io.readAvailable
7 | import kotlinx.coroutines.withTimeout
8 | import misc.ZipFileUtil.locateCentralDirectory
9 | import misc.ZipFileUtil.locateLocalFileHeader
10 | import misc.ZipFileUtil.locateLocalFileOffset
11 | import platform.httpClientPlatform
12 | import kotlin.math.min
13 |
14 | private const val METADATA_PATH = "META-INF/com/android/metadata"
15 | private const val CHUNK_SIZE = 1024
16 | private const val END_BYTES_SIZE = 4096
17 | private const val LOCAL_HEADER_SIZE = 256
18 | private const val TIMEOUT_MS = 20000L
19 |
20 | class Metadata private constructor() {
21 |
22 | companion object {
23 | private val instance by lazy { Metadata() }
24 |
25 | suspend fun getMetadata(url: String): String = instance.fetchMetadata(url)
26 |
27 | fun getMetadataValue(metadata: String, prefix: String): String =
28 | metadata.lineSequence().firstOrNull { it.startsWith(prefix) }?.substringAfter(prefix).orEmpty()
29 | }
30 |
31 | private val client = httpClientPlatform()
32 |
33 | private suspend fun fetchMetadata(url: String): String {
34 | return withTimeout(TIMEOUT_MS) {
35 | try {
36 | extractMetadata(url)
37 | } catch (_: Exception) {
38 | ""
39 | }
40 | }
41 | }
42 |
43 | private suspend fun extractMetadata(url: String): String {
44 | val fileLength = getFileLength(url) ?: return ""
45 | if (fileLength == 0L) return ""
46 |
47 | val actualEndBytesSize = min(fileLength, END_BYTES_SIZE.toLong()).toInt()
48 | val endBytes = readRange(url, fileLength - actualEndBytesSize, actualEndBytesSize) ?: return ""
49 |
50 | val centralDirectoryInfo = locateCentralDirectory(endBytes, fileLength)
51 | if (centralDirectoryInfo.offset == -1L || centralDirectoryInfo.size == -1L ||
52 | centralDirectoryInfo.offset < 0 || centralDirectoryInfo.size <= 0 ||
53 | centralDirectoryInfo.offset + centralDirectoryInfo.size > fileLength
54 | ) return ""
55 |
56 | val centralDirectory = readRange(url, centralDirectoryInfo.offset, centralDirectoryInfo.size.toInt()) ?: return ""
57 |
58 | val localHeaderOffset = locateLocalFileHeader(centralDirectory, METADATA_PATH)
59 | if (localHeaderOffset == -1L || localHeaderOffset < 0 || localHeaderOffset >= fileLength) return ""
60 |
61 | val maxBytesForLocalHeader = min(fileLength - localHeaderOffset, LOCAL_HEADER_SIZE.toLong()).toInt()
62 |
63 | if (maxBytesForLocalHeader < 30) return ""
64 | val localHeaderBytes = readRange(url, localHeaderOffset, maxBytesForLocalHeader) ?: return ""
65 |
66 | val metadataInternalOffset = locateLocalFileOffset(localHeaderBytes)
67 | if (metadataInternalOffset == -1L || metadataInternalOffset > maxBytesForLocalHeader) return ""
68 |
69 | val metadataActualOffset = localHeaderOffset + metadataInternalOffset
70 | val metadataSize = localHeaderBytes.getUncompressedSize()
71 |
72 | if (metadataSize < 0 || metadataActualOffset + metadataSize > fileLength) return ""
73 |
74 | return readContent(url, metadataActualOffset, metadataSize) ?: return ""
75 | }
76 |
77 | private suspend fun getFileLength(url: String): Long? {
78 | return try {
79 | val response = client.head(url) {
80 | header(HttpHeaders.Range, "bytes=0-0")
81 | }
82 |
83 | response.headers[HttpHeaders.ContentRange]?.let { contentRange ->
84 | val parts = contentRange.split("/")
85 | if (parts.size > 1) {
86 | parts[1].toLongOrNull()?.let { if (it > 0) return it }
87 | }
88 | }
89 | response.headers[HttpHeaders.ContentLength]?.toLongOrNull()?.let { if (it > 0) return it }
90 |
91 | null
92 | } catch (_: Exception) {
93 | null
94 | }
95 | }
96 |
97 | private suspend fun readRange(url: String, start: Long, size: Int): ByteArray? {
98 | if (size == 0) return ByteArray(0)
99 | if (size < 0 || start < 0) return null
100 |
101 | val bytes = ByteArray(size)
102 | return try {
103 | val response = client.get(url) {
104 | header(HttpHeaders.Range, "bytes=$start-${start + size - 1}")
105 | }
106 |
107 | val channel = response.bodyAsChannel()
108 | var totalBytesRead = 0
109 | while (totalBytesRead < size) {
110 | val bytesReadThisTurn = channel.readAvailable(bytes, totalBytesRead, size - totalBytesRead)
111 | if (bytesReadThisTurn == -1) return null
112 |
113 | totalBytesRead += bytesReadThisTurn
114 | }
115 | bytes
116 | } catch (_: Exception) {
117 | null
118 | }
119 |
120 | }
121 |
122 | private suspend fun readContent(url: String, offset: Long, size: Int): String? {
123 | if (size == 0) return ""
124 | if (size < 0 || offset < 0) return null
125 |
126 | val contentBytes = ByteArray(size)
127 | var totalBytesRead = 0
128 | return try {
129 | while (totalBytesRead < size) {
130 | val remaining = size - totalBytesRead
131 | val currentChunkSize = min(CHUNK_SIZE, remaining)
132 |
133 | val bytesReadInChunk = executeStreamedRangeRequest(
134 | url,
135 | offset + totalBytesRead,
136 | contentBytes,
137 | totalBytesRead,
138 | currentChunkSize
139 | )
140 |
141 | if (bytesReadInChunk <= 0) return null
142 | totalBytesRead += bytesReadInChunk
143 | }
144 | contentBytes.decodeToString()
145 | } catch (_: Exception) {
146 | null
147 | }
148 | }
149 |
150 | private suspend fun executeStreamedRangeRequest(
151 | url: String,
152 | fileOffset: Long,
153 | buffer: ByteArray,
154 | bufferOffset: Int,
155 | length: Int
156 | ): Int {
157 | if (length == 0) return 0
158 | if (length < 0 || fileOffset < 0 || bufferOffset < 0 || bufferOffset + length > buffer.size) return -1 // Invalid params
159 |
160 | return try {
161 | val response = client.get(url) {
162 | header(HttpHeaders.Range, "bytes=$fileOffset-${fileOffset + length - 1}")
163 | }
164 | response.bodyAsChannel().readAvailable(buffer, bufferOffset, length)
165 | } catch (_: Exception) {
166 | -1
167 | }
168 | }
169 | }
170 |
171 | private fun ByteArray.getUncompressedSize(): Int {
172 | if (this.size < 22 + 4) return -1
173 | return (this[22].toInt() and 0xff) or
174 | ((this[23].toInt() and 0xff) shl 8) or
175 | ((this[24].toInt() and 0xff) shl 16) or
176 | ((this[25].toInt() and 0xff) shl 24)
177 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/Theme.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.Composable
2 | import top.yukonga.miuix.kmp.theme.MiuixTheme
3 | import top.yukonga.miuix.kmp.theme.darkColorScheme
4 | import top.yukonga.miuix.kmp.theme.lightColorScheme
5 |
6 | @Composable
7 | fun AppTheme(
8 | isDarkTheme: Boolean,
9 | content: @Composable () -> Unit
10 | ) {
11 | MiuixTheme(
12 | colors = if (isDarkTheme) {
13 | darkColorScheme()
14 | } else {
15 | lightColorScheme()
16 | }
17 | ) {
18 | content()
19 | }
20 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/DataHelper.kt:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | object DataHelper {
6 | @Serializable
7 | data class AuthorizeData(
8 | val description: String? = null,
9 | val location: String? = null,
10 | val result: String? = null,
11 | val ssecurity: String? = null,
12 | val userId: Long? = null,
13 | )
14 |
15 | @Serializable
16 | data class IconInfoData(
17 | val changelog: String,
18 | val iconName: String,
19 | val iconLink: String,
20 | )
21 |
22 |
23 | @Serializable
24 | data class LoginData(
25 | val accountType: String? = null,
26 | var authResult: String? = null,
27 | val description: String? = null,
28 | val ssecurity: String? = null,
29 | val serviceToken: String? = null,
30 | val userId: String? = null,
31 | )
32 |
33 | @Serializable
34 | data class RequestData(
35 | val b: String,
36 | val c: String,
37 | val d: String,
38 | val f: String,
39 | val id: String,
40 | val l: String,
41 | val ov: String,
42 | val p: String,
43 | val pn: String,
44 | val r: String,
45 | val security: String,
46 | val token: String,
47 | val v: String,
48 | val unlock: String,
49 | )
50 |
51 | @Serializable
52 | data class RomInfoData(
53 | var type: String = "",
54 | var device: String = "",
55 | var version: String = "",
56 | var codebase: String = "",
57 | var branch: String = "",
58 | var bigVersion: String = "",
59 | var fileName: String = "",
60 | var fileSize: String = "",
61 | var isBeta: Boolean = false,
62 | var isGov: Boolean = false,
63 | var official1Download: String = "",
64 | var official2Download: String = "",
65 | var cdn1Download: String = "",
66 | var cdn2Download: String = "",
67 | var changelog: String = "",
68 | var gentleNotice: String = "",
69 | var fingerprint: String = "",
70 | var securityPatchLevel: String = "",
71 | var timestamp: String = "",
72 | )
73 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/FileInfoHelper.kt:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | class FileInfoHelper {
4 | data class FileInfo(
5 | val offset: Long,
6 | val size: Long,
7 | )
8 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/RomInfoHelper.kt:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | object RomInfoHelper {
7 | @Serializable
8 | data class RomInfo(
9 | @SerialName("AuthResult") val authResult: Int? = null,
10 | @SerialName("CurrentRom") val currentRom: Rom? = null,
11 | @SerialName("LatestRom") val latestRom: Rom? = null,
12 | @SerialName("IncrementRom") val incrementRom: Rom? = null,
13 | @SerialName("CrossRom") val crossRom: Rom? = null,
14 | @SerialName("Icon") val icon: Map? = null,
15 | @SerialName("FileMirror") val fileMirror: FileMirror? = null,
16 | @SerialName("GentleNotice") val gentleNotice: GentleNotice? = null,
17 | )
18 |
19 | @Serializable
20 | data class Rom(
21 | val bigversion: String? = null,
22 | val branch: String? = null,
23 | val changelog: HashMap? = null,
24 | val codebase: String? = null,
25 | val device: String? = null,
26 | val filename: String? = null,
27 | val filesize: String? = null,
28 | val md5: String? = null,
29 | val name: String? = null,
30 | val osbigversion: String? = null,
31 | val type: String? = null,
32 | val version: String? = null,
33 | val isBeta: Int = 0,
34 | val isGov: Int = 0,
35 | )
36 |
37 | @Serializable
38 | data class Changelog(
39 | val txt: List,
40 | )
41 |
42 | @Serializable
43 | data class FileMirror(
44 | val icon: String,
45 | val image: String,
46 | val video: String,
47 | val headimage: String,
48 | )
49 |
50 | @Serializable
51 | data class GentleNotice(
52 | val text: String,
53 | )
54 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/misc/MessageUtils.kt:
--------------------------------------------------------------------------------
1 | package misc
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.systemBarsPadding
6 | import androidx.compose.material3.Snackbar
7 | import androidx.compose.material3.SnackbarDuration
8 | import androidx.compose.material3.SnackbarHost
9 | import androidx.compose.material3.SnackbarHostState
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.LaunchedEffect
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.rememberCoroutineScope
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import kotlinx.coroutines.Job
18 | import kotlinx.coroutines.delay
19 | import kotlinx.coroutines.launch
20 | import platform.showToast
21 | import top.yukonga.miuix.kmp.theme.MiuixTheme
22 | import platform.useToast
23 |
24 | class MessageUtils {
25 |
26 | companion object {
27 | private val snackbarMessage = mutableStateOf("")
28 | private var snackbarDuration = mutableStateOf(1000L)
29 | private var isSnackbarVisible = mutableStateOf(false)
30 | private var snackbarCoroutineJob: Job? = null
31 | private var snackbarKey = mutableStateOf(0)
32 |
33 | fun showMessage(message: String, duration: Long = 1000L) {
34 | if (useToast()) {
35 | showToast(message, duration)
36 | } else {
37 | snackbarCoroutineJob?.cancel()
38 | snackbarMessage.value = message
39 | snackbarDuration.value = duration
40 | isSnackbarVisible.value = true
41 | snackbarKey.value++
42 | }
43 | }
44 |
45 | @Composable
46 | fun Snackbar() {
47 | val snackbarHostState = remember { SnackbarHostState() }
48 | val snackCoroutineScope = rememberCoroutineScope()
49 |
50 | Box(
51 | modifier = Modifier
52 | .fillMaxSize()
53 | .systemBarsPadding(),
54 | contentAlignment = Alignment.BottomCenter
55 | ) {
56 | SnackbarHost(
57 | hostState = snackbarHostState
58 | ) {
59 | Snackbar(
60 | snackbarData = it,
61 | containerColor = MiuixTheme.colorScheme.onBackground,
62 | contentColor = MiuixTheme.colorScheme.background
63 | )
64 | }
65 | }
66 | if (snackbarMessage.value.isNotEmpty()) {
67 | LaunchedEffect(snackbarKey.value) {
68 | snackbarCoroutineJob = snackCoroutineScope.launch {
69 | snackbarHostState.showSnackbar(message = snackbarMessage.value, duration = SnackbarDuration.Indefinite)
70 | isSnackbarVisible.value = false
71 | }
72 | delay(snackbarDuration.value)
73 | snackbarHostState.currentSnackbarData?.dismiss()
74 | }
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/misc/ZipFile.kt:
--------------------------------------------------------------------------------
1 | package misc
2 |
3 | import data.FileInfoHelper
4 | import okio.ByteString
5 | import okio.ByteString.Companion.toByteString
6 |
7 | object ZipFileUtil {
8 | private const val CENSIG = 0x02014b50L // "PK\001\002" - Central directory file header signature
9 | private const val LOCSIG = 0x04034b50L // "PK\003\004" - Local file header signature
10 | private const val ENDSIG = 0x06054b50L // "PK\005\006" - End of central directory record signature
11 | private const val ENDHDR = 22 // Minimum size of end of central directory record
12 | private const val ZIP64_ENDSIG = 0x06064b50L // "PK\006\006" - Zip64 end of central directory record signature
13 | private const val ZIP64_LOCSIG = 0x07064b50L // "PK\006\007" - Zip64 end of central directory locator signature
14 | private const val ZIP64_LOCHDR = 20 // Size of Zip64 end of central directory locator
15 | private const val ZIP64_MAGICVAL = 0xFFFFFFFFL // Marker for Zip64 fields
16 |
17 | fun locateCentralDirectory(bytes: ByteArray, fileLength: Long): FileInfoHelper.FileInfo {
18 | val byteString = bytes.toByteString()
19 | val searchStartPos = bytes.size - ENDHDR
20 | var cenSize = -1L
21 | var cenOffset = -1L
22 |
23 | for (currentScanPos in searchStartPos downTo 0) {
24 | if ((byteString.getIntLe(currentScanPos).toLong() and 0xFFFFFFFFL) == ENDSIG) {
25 | val cenDirOffsetFieldPos = currentScanPos + 16
26 | val cenDirSizeFieldPos = currentScanPos + 12
27 |
28 | val offsetOfCentralDir = byteString.getIntLe(cenDirOffsetFieldPos).toLong() and 0xFFFFFFFFL
29 | val sizeOfCentralDir = byteString.getIntLe(cenDirSizeFieldPos).toLong() and 0xFFFFFFFFL
30 |
31 | if (offsetOfCentralDir == ZIP64_MAGICVAL || sizeOfCentralDir == ZIP64_MAGICVAL) {
32 | val zip64LocatorPos = currentScanPos - ZIP64_LOCHDR
33 | if (zip64LocatorPos >= 0 && (byteString.getIntLe(zip64LocatorPos).toLong() and 0xFFFFFFFFL) == ZIP64_LOCSIG) {
34 | val zip64EocdRecordOffsetInFile = byteString.getLongLe(zip64LocatorPos + 8)
35 | val zip64EocdRecordOffsetInBuffer = bytes.size - (fileLength - zip64EocdRecordOffsetInFile).toInt()
36 | if (zip64EocdRecordOffsetInBuffer >= 0
37 | && (zip64EocdRecordOffsetInBuffer + 56) <= bytes.size
38 | && (byteString.getIntLe(zip64EocdRecordOffsetInBuffer).toLong() and 0xFFFFFFFFL) == ZIP64_ENDSIG
39 | ) {
40 | cenSize = byteString.getLongLe(zip64EocdRecordOffsetInBuffer + 40)
41 | cenOffset = byteString.getLongLe(zip64EocdRecordOffsetInBuffer + 48)
42 | break
43 | }
44 | }
45 | } else {
46 | cenSize = sizeOfCentralDir
47 | cenOffset = offsetOfCentralDir
48 | break
49 | }
50 | }
51 | }
52 | return FileInfoHelper.FileInfo(cenOffset, cenSize)
53 | }
54 |
55 | fun locateLocalFileHeader(bytes: ByteArray, fileName: String): Long {
56 | val byteString = bytes.toByteString()
57 | var pos = 0
58 | var localHeaderOffset = -1L
59 |
60 | while (pos + 46 <= bytes.size) {
61 | if ((byteString.getIntLe(pos).toLong() and 0xFFFFFFFFL) == CENSIG) {
62 | val fileNameLength = byteString.getShortLe(pos + 28).toInt() and 0xFFFF
63 | val extraFieldLength = byteString.getShortLe(pos + 30).toInt() and 0xFFFF
64 | val fileCommentLength = byteString.getShortLe(pos + 32).toInt() and 0xFFFF
65 | val relativeOffsetOfLocalHeader = byteString.getIntLe(pos + 42).toLong() and 0xFFFFFFFFL
66 |
67 | val fileNameStartPos = pos + 46
68 | if (fileNameStartPos + fileNameLength > bytes.size) break
69 |
70 | val currentFileName = byteString.substring(fileNameStartPos, fileNameStartPos + fileNameLength).utf8()
71 | if (fileName == currentFileName) {
72 | localHeaderOffset = relativeOffsetOfLocalHeader
73 | break
74 | }
75 | pos = fileNameStartPos + fileNameLength + extraFieldLength + fileCommentLength
76 | } else {
77 | break
78 | }
79 | }
80 | return localHeaderOffset
81 | }
82 |
83 | fun locateLocalFileOffset(bytes: ByteArray): Long {
84 | val byteString = bytes.toByteString()
85 | if ((byteString.getIntLe(0).toLong() and 0xFFFFFFFFL) == LOCSIG) {
86 | val fileNameLength = byteString.getShortLe(26).toInt() and 0xFFFF
87 | val extraFieldLength = byteString.getShortLe(28).toInt() and 0xFFFF
88 | return (30L + fileNameLength + extraFieldLength)
89 | }
90 | return -1L
91 | }
92 |
93 | private fun ByteString.getIntLe(pos: Int): Int {
94 | return (get(pos).toInt() and 0xFF) or
95 | ((get(pos + 1).toInt() and 0xFF) shl 8) or
96 | ((get(pos + 2).toInt() and 0xFF) shl 16) or
97 | ((get(pos + 3).toInt() and 0xFF) shl 24)
98 | }
99 |
100 | private fun ByteString.getShortLe(pos: Int): Short {
101 | return ((get(pos).toInt() and 0xFF) or
102 | ((get(pos + 1).toInt() and 0xFF) shl 8)).toShort()
103 | }
104 |
105 | private fun ByteString.getLongLe(pos: Int): Long {
106 | return (getIntLe(pos).toLong() and ZIP64_MAGICVAL) or
107 | (getIntLe(pos + 4).toLong() shl 32)
108 | }
109 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/platform/Clipboard.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import androidx.compose.ui.platform.Clipboard
4 |
5 | internal expect suspend fun Clipboard.copyToClipboard(string: String)
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/platform/Crypto.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.DelicateCryptographyApi
5 | import dev.whyoleg.cryptography.algorithms.AES
6 | import kotlin.io.encoding.Base64
7 | import kotlin.io.encoding.ExperimentalEncodingApi
8 |
9 | private val iv = "0102030405060708".encodeToByteArray()
10 |
11 | expect suspend fun provider(): CryptographyProvider
12 |
13 | expect fun generateKey()
14 |
15 | /**
16 | * Generate a Cipher to be used by the xiaomi server.
17 | */
18 | suspend fun miuiCipher(securityKey: ByteArray): AES.IvCipher {
19 | val provider = provider()
20 | val aesCBC = provider.get(AES.CBC) // AES CBC
21 | val key = aesCBC.keyDecoder().decodeFromByteArray(AES.Key.Format.RAW, securityKey)
22 | return key.cipher(true) // PKCS5Padding
23 | }
24 |
25 | /**
26 | * Encrypt the JSON used for the request using AES.
27 | *
28 | * @param jsonRequest: JSON used for the request
29 | * @param securityKey: Security key
30 | *
31 | * @return Encrypted JSON text
32 | */
33 | @OptIn(ExperimentalEncodingApi::class, DelicateCryptographyApi::class)
34 | suspend fun miuiEncrypt(jsonRequest: String, securityKey: ByteArray): String {
35 | val cipher = miuiCipher(securityKey)
36 | val encrypted = cipher.encryptWithIv(iv, jsonRequest.encodeToByteArray())
37 | return Base64.UrlSafe.encode(encrypted)
38 | }
39 |
40 | /**
41 | * Decrypt the returned content using AES.
42 | *
43 | * @param encryptedText: Returned content
44 | * @param securityKey: Security key
45 | *
46 | * @return Decrypted return content text
47 | */
48 | @OptIn(DelicateCryptographyApi::class, ExperimentalEncodingApi::class)
49 | suspend fun miuiDecrypt(encryptedText: String, securityKey: ByteArray): String {
50 | val cipher = miuiCipher(securityKey)
51 | val encryptedTextBytes = Base64.Mime.decode(encryptedText)
52 | val decryptedTextBytes = cipher.decryptWithIv(iv, encryptedTextBytes)
53 | return decryptedTextBytes.decodeToString()
54 | }
55 |
56 | expect fun ownEncrypt(string: String): Pair
57 |
58 | expect fun ownDecrypt(encryptedText: String, encodedIv: String): String
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/platform/Download.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | expect fun downloadToLocal(url: String, fileName: String)
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/platform/HttpClient.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import io.ktor.client.HttpClient
4 |
5 | expect fun httpClientPlatform(): HttpClient
6 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/platform/Preferences.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | expect fun perfSet(key: String, value: String)
4 | expect fun perfGet(key: String): String?
5 | expect fun perfRemove(key: String)
6 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/platform/Toast.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | expect fun useToast(): Boolean
4 | expect fun showToast(message: String, duration: Long)
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/AboutDialog.kt:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.platform.LocalFocusManager
16 | import androidx.compose.ui.platform.LocalUriHandler
17 | import androidx.compose.ui.text.AnnotatedString
18 | import androidx.compose.ui.text.SpanStyle
19 | import androidx.compose.ui.text.font.FontWeight
20 | import androidx.compose.ui.text.style.TextDecoration
21 | import androidx.compose.ui.unit.dp
22 | import androidx.compose.ui.unit.sp
23 | import misc.VersionInfo
24 | import org.jetbrains.compose.resources.painterResource
25 | import org.jetbrains.compose.resources.stringResource
26 | import top.yukonga.miuix.kmp.basic.IconButton
27 | import top.yukonga.miuix.kmp.basic.Text
28 | import top.yukonga.miuix.kmp.extra.SuperDialog
29 | import top.yukonga.miuix.kmp.theme.MiuixTheme
30 | import top.yukonga.miuix.kmp.utils.Platform
31 | import top.yukonga.miuix.kmp.utils.platform
32 | import updater.composeapp.generated.resources.Res
33 | import updater.composeapp.generated.resources.about
34 | import updater.composeapp.generated.resources.app_name
35 | import updater.composeapp.generated.resources.icon
36 | import updater.composeapp.generated.resources.join_channel
37 | import updater.composeapp.generated.resources.opensource_info
38 | import updater.composeapp.generated.resources.view_source
39 |
40 | @Composable
41 | fun AboutDialog(
42 | ) {
43 | val showDialog = remember { mutableStateOf(false) }
44 | val focusManager = LocalFocusManager.current
45 |
46 | IconButton(
47 | modifier = Modifier.padding(start = if (platform() != Platform.IOS && platform() != Platform.Android) 10.dp else 20.dp),
48 | onClick = {
49 | showDialog.value = true
50 | focusManager.clearFocus()
51 | },
52 | holdDownState = showDialog.value
53 | ) {
54 | Image(
55 | painter = painterResource(Res.drawable.icon),
56 | contentDescription = "About",
57 | modifier = Modifier
58 | .size(32.dp)
59 | .padding(4.dp),
60 | )
61 | }
62 |
63 | SuperDialog(
64 | show = showDialog,
65 | title = stringResource(Res.string.about),
66 | onDismissRequest = {
67 | showDialog.value = false
68 | }
69 | ) {
70 | Row(
71 | modifier = Modifier.padding(bottom = 10.dp),
72 | horizontalArrangement = Arrangement.spacedBy(16.dp),
73 | verticalAlignment = Alignment.CenterVertically
74 | ) {
75 | Image(
76 | painter = painterResource(Res.drawable.icon),
77 | contentDescription = "Icon",
78 | modifier = Modifier.size(45.dp),
79 | )
80 | Column {
81 | Text(
82 | text = stringResource(Res.string.app_name),
83 | fontSize = 22.sp,
84 | fontWeight = FontWeight.SemiBold
85 | )
86 | Text(
87 | text = VersionInfo.VERSION_NAME + " (" + VersionInfo.VERSION_CODE + ")",
88 | )
89 | }
90 | }
91 | val uriHandler = LocalUriHandler.current
92 | Row(
93 | verticalAlignment = Alignment.CenterVertically
94 | ) {
95 | Text(
96 | text = stringResource(Res.string.view_source) + " ",
97 | )
98 | Text(
99 | text = AnnotatedString(
100 | text = "GitHub",
101 | spanStyle = SpanStyle(
102 | textDecoration = TextDecoration.Underline,
103 | color = MiuixTheme.colorScheme.primary
104 | )
105 | ),
106 | modifier = Modifier.clickable(
107 | onClick = {
108 | uriHandler.openUri("https://github.com/YuKongA/Updater-KMP")
109 | }
110 | )
111 | )
112 | }
113 | Row(
114 | verticalAlignment = Alignment.CenterVertically
115 | ) {
116 | Text(
117 | text = stringResource(Res.string.join_channel) + " ",
118 | )
119 | Text(
120 | text = AnnotatedString(
121 | text = "Telegram",
122 | spanStyle = SpanStyle(
123 | textDecoration = TextDecoration.Underline,
124 | color = MiuixTheme.colorScheme.primary
125 | )
126 | ),
127 | modifier = Modifier.clickable(
128 | onClick = {
129 | uriHandler.openUri("https://t.me/YuKongA13579")
130 | },
131 | )
132 | )
133 | }
134 | Text(
135 | modifier = Modifier.padding(top = 10.dp),
136 | text = stringResource(Res.string.opensource_info)
137 | )
138 | }
139 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/BasicViews.kt:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.expandVertically
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.animation.fadeOut
7 | import androidx.compose.animation.shrinkVertically
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.text.KeyboardActions
12 | import androidx.compose.foundation.text.KeyboardOptions
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.LaunchedEffect
15 | import androidx.compose.runtime.MutableState
16 | import androidx.compose.runtime.mutableStateOf
17 | import androidx.compose.runtime.remember
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.platform.LocalFocusManager
21 | import androidx.compose.ui.text.input.ImeAction
22 | import androidx.compose.ui.unit.DpSize
23 | import androidx.compose.ui.unit.dp
24 | import data.DeviceInfoHelper
25 | import kotlinx.coroutines.flow.MutableStateFlow
26 | import misc.MessageUtils.Companion.showMessage
27 | import org.jetbrains.compose.resources.stringResource
28 | import top.yukonga.miuix.kmp.basic.ButtonDefaults
29 | import top.yukonga.miuix.kmp.basic.Card
30 | import top.yukonga.miuix.kmp.basic.TextButton
31 | import top.yukonga.miuix.kmp.basic.TextField
32 | import top.yukonga.miuix.kmp.extra.DropDownMode
33 | import top.yukonga.miuix.kmp.extra.SpinnerEntry
34 | import top.yukonga.miuix.kmp.extra.SpinnerMode
35 | import top.yukonga.miuix.kmp.extra.SuperDropdown
36 | import top.yukonga.miuix.kmp.extra.SuperSpinner
37 | import top.yukonga.miuix.kmp.theme.MiuixTheme
38 | import ui.components.AutoCompleteTextField
39 | import updater.composeapp.generated.resources.Res
40 | import updater.composeapp.generated.resources.android_version
41 | import updater.composeapp.generated.resources.code_name
42 | import updater.composeapp.generated.resources.device_name
43 | import updater.composeapp.generated.resources.regions_code
44 | import updater.composeapp.generated.resources.search_history
45 | import updater.composeapp.generated.resources.submit
46 | import updater.composeapp.generated.resources.system_version
47 | import updater.composeapp.generated.resources.toast_no_info
48 |
49 | @Composable
50 | private fun SearchHistoryView(
51 | searchKeywords: MutableState>,
52 | searchKeywordsSelected: MutableState,
53 | onHistorySelect: (String) -> Unit
54 | ) {
55 | AnimatedVisibility(
56 | visible = searchKeywords.value.isNotEmpty(),
57 | enter = fadeIn() + expandVertically(),
58 | exit = fadeOut() + shrinkVertically()
59 | ) {
60 | val localFocusManager = LocalFocusManager.current
61 | val spinnerOptions = searchKeywords.value.map { keyword ->
62 | val parts = keyword.split("-")
63 | SpinnerEntry(
64 | icon = null,
65 | title = "${parts.getOrElse(0) { "" }.ifEmpty { "Unknown" }} (${parts.getOrElse(1) { "" }})",
66 | summary = "${parts.getOrElse(2) { "" }}-${parts.getOrElse(3) { "" }}-${parts.getOrElse(4) { "" }}",
67 | )
68 | }
69 | Card(
70 | modifier = Modifier
71 | .padding(horizontal = 12.dp)
72 | .padding(top = 12.dp)
73 | ) {
74 | SuperSpinner(
75 | title = stringResource(Res.string.search_history),
76 | items = spinnerOptions,
77 | selectedIndex = searchKeywordsSelected.value,
78 | mode = SpinnerMode.AlwaysOnRight,
79 | showValue = false,
80 | onSelectedIndexChange = { index ->
81 | onHistorySelect(searchKeywords.value[index])
82 | searchKeywordsSelected.value = index
83 | },
84 | onClick = {
85 | localFocusManager.clearFocus()
86 | },
87 | maxHeight = 280.dp
88 | )
89 | }
90 | }
91 | }
92 |
93 | @Composable
94 | fun BasicViews(
95 | deviceName: MutableState,
96 | codeName: MutableState,
97 | androidVersion: MutableState,
98 | deviceRegion: MutableState,
99 | systemVersion: MutableState,
100 | updateRomInfo: MutableState,
101 | searchKeywords: MutableState>,
102 | searchKeywordsSelected: MutableState
103 | ) {
104 | val androidVersionSelected = remember {
105 | mutableStateOf(DeviceInfoHelper.androidVersions.indexOf(androidVersion.value).takeIf { it >= 0 } ?: 0)
106 | }
107 | val regionSelected = remember {
108 | mutableStateOf(DeviceInfoHelper.regionNames.indexOf(deviceRegion.value).takeIf { it >= 0 } ?: 0)
109 | }
110 |
111 | val deviceNameFlow = remember { MutableStateFlow(deviceName.value) }
112 | val codeNameFlow = remember { MutableStateFlow(codeName.value) }
113 |
114 | val toastNoInfo = stringResource(Res.string.toast_no_info)
115 |
116 | val focusManager = LocalFocusManager.current
117 |
118 | LaunchedEffect(deviceNameFlow) {
119 | deviceNameFlow.collect { newValue ->
120 | if (deviceName.value != newValue) {
121 | deviceName.value = newValue
122 | val text = DeviceInfoHelper.codeName(newValue)
123 | if (text.isNotEmpty()) codeName.value = text
124 | }
125 | }
126 | }
127 |
128 | LaunchedEffect(codeNameFlow) {
129 | codeNameFlow.collect { newValue ->
130 | if (codeName.value != newValue) {
131 | codeName.value = newValue
132 | val text = DeviceInfoHelper.deviceName(newValue)
133 | if (text.isNotEmpty()) deviceName.value = text
134 | }
135 | }
136 | }
137 |
138 | Column(
139 | modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp),
140 | horizontalAlignment = Alignment.CenterHorizontally
141 | ) {
142 | AutoCompleteTextField(
143 | text = deviceName,
144 | items = DeviceInfoHelper.deviceNames,
145 | onValueChange = deviceNameFlow,
146 | label = stringResource(Res.string.device_name)
147 | )
148 | AutoCompleteTextField(
149 | text = codeName,
150 | items = DeviceInfoHelper.codeNames,
151 | onValueChange = codeNameFlow,
152 | label = stringResource(Res.string.code_name)
153 | )
154 | TextField(
155 | insideMargin = DpSize(16.dp, 20.dp),
156 | modifier = Modifier
157 | .fillMaxWidth()
158 | .padding(horizontal = 12.dp)
159 | .padding(bottom = 12.dp),
160 | value = systemVersion.value,
161 | onValueChange = { systemVersion.value = it },
162 | label = stringResource(Res.string.system_version),
163 | singleLine = true,
164 | backgroundColor = MiuixTheme.colorScheme.surface,
165 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
166 | keyboardActions = KeyboardActions(onSearch = {
167 | focusManager.clearFocus()
168 | if (codeName.value != "" && androidVersion.value != "" && systemVersion.value != "") {
169 | updateRomInfo.value++
170 | } else {
171 | showMessage(toastNoInfo)
172 | }
173 | })
174 | )
175 | Card(
176 | modifier = Modifier
177 | .padding(horizontal = 12.dp)
178 | ) {
179 | SuperDropdown(
180 | title = stringResource(Res.string.android_version),
181 | items = DeviceInfoHelper.androidVersions,
182 | selectedIndex = androidVersionSelected.value,
183 | onSelectedIndexChange = { index ->
184 | androidVersionSelected.value = index
185 | androidVersion.value = DeviceInfoHelper.androidVersions[index]
186 | },
187 | onClick = {
188 | focusManager.clearFocus()
189 | },
190 | mode = DropDownMode.AlwaysOnRight,
191 | maxHeight = 280.dp
192 | )
193 | SuperDropdown(
194 | title = stringResource(Res.string.regions_code),
195 | items = DeviceInfoHelper.regionNames,
196 | selectedIndex = regionSelected.value,
197 | onSelectedIndexChange = { index ->
198 | regionSelected.value = index
199 | deviceRegion.value = DeviceInfoHelper.regionNames[index]
200 | },
201 | onClick = {
202 | focusManager.clearFocus()
203 | },
204 | mode = DropDownMode.AlwaysOnRight,
205 | maxHeight = 280.dp
206 | )
207 | }
208 | SearchHistoryView(
209 | searchKeywords = searchKeywords,
210 | searchKeywordsSelected = searchKeywordsSelected,
211 | onHistorySelect = { keyword ->
212 | val parts = keyword.split("-")
213 | deviceName.value = parts[0]
214 | codeName.value = parts[1]
215 | deviceRegion.value = parts[2]
216 | androidVersion.value = parts[3]
217 | systemVersion.value = parts[4]
218 | regionSelected.value = DeviceInfoHelper.regionNames.indexOf(parts[2])
219 | androidVersionSelected.value = DeviceInfoHelper.androidVersions.indexOf(parts[3])
220 | }
221 | )
222 | TextButton(
223 | modifier = Modifier
224 | .fillMaxWidth()
225 | .padding(top = 12.dp)
226 | .padding(horizontal = 12.dp),
227 | colors = ButtonDefaults.textButtonColorsPrimary(),
228 | onClick = {
229 | focusManager.clearFocus()
230 | if (codeName.value != "" && androidVersion.value != "" && systemVersion.value != "") {
231 | updateRomInfo.value++
232 | } else {
233 | showMessage(toastNoInfo)
234 | }
235 | },
236 | text = stringResource(Res.string.submit)
237 | )
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/LoginCardView.kt:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.MutableState
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.text.font.FontWeight
15 | import androidx.compose.ui.unit.dp
16 | import isWeb
17 | import org.jetbrains.compose.resources.stringResource
18 | import top.yukonga.miuix.kmp.basic.Card
19 | import top.yukonga.miuix.kmp.basic.Icon
20 | import top.yukonga.miuix.kmp.basic.Text
21 | import top.yukonga.miuix.kmp.icon.MiuixIcons
22 | import top.yukonga.miuix.kmp.icon.icons.useful.Confirm
23 | import top.yukonga.miuix.kmp.icon.icons.useful.Info
24 | import top.yukonga.miuix.kmp.theme.MiuixTheme
25 | import updater.composeapp.generated.resources.Res
26 | import updater.composeapp.generated.resources.logged_in
27 | import updater.composeapp.generated.resources.login_desc
28 | import updater.composeapp.generated.resources.login_expired
29 | import updater.composeapp.generated.resources.login_expired_desc
30 | import updater.composeapp.generated.resources.no_account
31 | import updater.composeapp.generated.resources.using_v2
32 |
33 | @Composable
34 | fun LoginCardView(
35 | isLogin: MutableState
36 | ) {
37 | val account = when (isLogin.value) {
38 | 1 -> stringResource(Res.string.logged_in)
39 | 0 -> stringResource(Res.string.no_account)
40 | else -> stringResource(Res.string.login_expired)
41 | }
42 | val info = when (isLogin.value) {
43 | 1 -> stringResource(Res.string.using_v2)
44 | 0 -> stringResource(Res.string.login_desc)
45 | else -> stringResource(Res.string.login_expired_desc)
46 | }
47 | val icon = if (isLogin.value == 1) MiuixIcons.Useful.Confirm else MiuixIcons.Useful.Info
48 |
49 | Card(
50 | modifier = Modifier
51 | .fillMaxWidth()
52 | .padding(all = 12.dp),
53 | insideMargin = PaddingValues(16.dp)
54 | ) {
55 | Row(
56 | verticalAlignment = Alignment.CenterVertically,
57 | horizontalArrangement = Arrangement.SpaceBetween
58 | ) {
59 | Icon(
60 | modifier = Modifier.padding(start = 6.dp),
61 | imageVector = icon,
62 | tint = MiuixTheme.colorScheme.onSurface,
63 | contentDescription = null
64 | )
65 | Column(modifier = Modifier.padding(start = 20.dp)) {
66 | Text(
67 | text = if (!isWeb()) account else "WebPage",
68 | fontWeight = FontWeight.SemiBold
69 | )
70 | Text(
71 | text = info
72 | )
73 | }
74 | Spacer(modifier = Modifier.weight(1f))
75 | if (!isWeb()) LoginDialog(isLogin)
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/LoginDialog.kt:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.foundation.text.KeyboardActions
11 | import androidx.compose.foundation.text.KeyboardOptions
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.MutableState
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.runtime.rememberCoroutineScope
18 | import androidx.compose.runtime.setValue
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.focus.FocusDirection
22 | import androidx.compose.ui.platform.LocalFocusManager
23 | import androidx.compose.ui.text.input.ImeAction
24 | import androidx.compose.ui.text.input.KeyboardType
25 | import androidx.compose.ui.text.input.PasswordVisualTransformation
26 | import androidx.compose.ui.text.input.VisualTransformation
27 | import androidx.compose.ui.unit.dp
28 | import getPassword
29 | import kotlinx.coroutines.launch
30 | import login
31 | import logout
32 | import misc.MessageUtils.Companion.showMessage
33 | import org.jetbrains.compose.resources.stringResource
34 | import platform.perfGet
35 | import top.yukonga.miuix.kmp.basic.ButtonDefaults
36 | import top.yukonga.miuix.kmp.basic.Icon
37 | import top.yukonga.miuix.kmp.basic.IconButton
38 | import top.yukonga.miuix.kmp.basic.TextButton
39 | import top.yukonga.miuix.kmp.basic.TextField
40 | import top.yukonga.miuix.kmp.extra.SuperCheckbox
41 | import top.yukonga.miuix.kmp.extra.SuperDialog
42 | import top.yukonga.miuix.kmp.icon.MiuixIcons
43 | import top.yukonga.miuix.kmp.icon.icons.useful.Blocklist
44 | import top.yukonga.miuix.kmp.icon.icons.useful.RemoveBlocklist
45 | import top.yukonga.miuix.kmp.icon.icons.useful.Rename
46 | import top.yukonga.miuix.kmp.theme.MiuixTheme
47 | import updater.composeapp.generated.resources.Res
48 | import updater.composeapp.generated.resources.account
49 | import updater.composeapp.generated.resources.account_or_password_empty
50 | import updater.composeapp.generated.resources.cancel
51 | import updater.composeapp.generated.resources.global
52 | import updater.composeapp.generated.resources.logging_in
53 | import updater.composeapp.generated.resources.login
54 | import updater.composeapp.generated.resources.login_error
55 | import updater.composeapp.generated.resources.login_successful
56 | import updater.composeapp.generated.resources.logout
57 | import updater.composeapp.generated.resources.logout_confirm
58 | import updater.composeapp.generated.resources.logout_successful
59 | import updater.composeapp.generated.resources.password
60 | import updater.composeapp.generated.resources.save_password
61 | import updater.composeapp.generated.resources.security_error
62 | import updater.composeapp.generated.resources.toast_crash_info
63 |
64 | private const val PASSWORD_SAVE_KEY = "savePassword"
65 | private const val PASSWORD_SAVE_ENABLED = "1"
66 | private const val PASSWORD_SAVE_DISABLED = "0"
67 |
68 | @Composable
69 | fun LoginDialog(
70 | isLogin: MutableState
71 | ) {
72 | val coroutineScope = rememberCoroutineScope()
73 | var account by remember { mutableStateOf(getPassword().first) }
74 | var password by remember { mutableStateOf(getPassword().second) }
75 |
76 | var global by remember { mutableStateOf(false) }
77 | var savePassword by remember { mutableStateOf(perfGet(PASSWORD_SAVE_KEY) ?: PASSWORD_SAVE_DISABLED) }
78 | val showDialog = remember { mutableStateOf(false) }
79 |
80 | val icon = when (isLogin.value) {
81 | 1 -> MiuixIcons.Useful.Blocklist
82 | else -> MiuixIcons.Useful.RemoveBlocklist
83 | }
84 |
85 | val messageLoginIn = stringResource(Res.string.logging_in)
86 | val messageLoginSuccess = stringResource(Res.string.login_successful)
87 | val messageEmpty = stringResource(Res.string.account_or_password_empty)
88 | val messageError = stringResource(Res.string.login_error)
89 | val messageSecurityError = stringResource(Res.string.security_error)
90 | val messageLogoutSuccessful = stringResource(Res.string.logout_successful)
91 | val messageCrashInfo = stringResource(Res.string.toast_crash_info)
92 |
93 | val focusManager = LocalFocusManager.current
94 |
95 | IconButton(
96 | onClick = {
97 | showDialog.value = true
98 | focusManager.clearFocus()
99 | },
100 | holdDownState = showDialog.value
101 | ) {
102 | Icon(
103 | imageVector = icon,
104 | tint = MiuixTheme.colorScheme.onSurface,
105 | contentDescription = "Login"
106 | )
107 | }
108 |
109 | if (isLogin.value != 1) {
110 | SuperDialog(
111 | show = showDialog,
112 | title = stringResource(Res.string.login),
113 | onDismissRequest = {
114 | showDialog.value = false
115 | }
116 | ) {
117 | Column {
118 | TextField(
119 | value = account,
120 | onValueChange = { account = it },
121 | label = stringResource(Res.string.account),
122 | modifier = Modifier.fillMaxWidth(),
123 | singleLine = true,
124 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
125 | keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) })
126 | )
127 | var passwordVisibility by remember { mutableStateOf(false) }
128 | TextField(
129 | value = password,
130 | onValueChange = { password = it },
131 | label = stringResource(Res.string.password),
132 | modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
133 | singleLine = true,
134 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
135 | keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
136 | visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(),
137 | trailingIcon = {
138 | IconButton(
139 | modifier = Modifier.padding(end = 6.dp),
140 | onClick = {
141 | passwordVisibility = !passwordVisibility
142 | },
143 | content = {
144 | Icon(
145 | imageVector = MiuixIcons.Useful.Rename,
146 | tint = if (passwordVisibility) MiuixTheme.colorScheme.primary else MiuixTheme.colorScheme.onSecondaryContainer,
147 | contentDescription = null
148 | )
149 | }
150 | )
151 | }
152 | )
153 | Row(
154 | modifier = Modifier.padding(vertical = 16.dp),
155 | verticalAlignment = Alignment.CenterVertically
156 | ) {
157 | Row(
158 | modifier = Modifier.weight(1f),
159 | horizontalArrangement = Arrangement.Center
160 | ) {
161 | SuperCheckbox(
162 | title = stringResource(Res.string.global),
163 | checked = global,
164 | onCheckedChange = {
165 | global = it
166 | }
167 | )
168 | }
169 | Row(
170 | modifier = Modifier.weight(1f),
171 | horizontalArrangement = Arrangement.Center
172 | ) {
173 | SuperCheckbox(
174 | title = stringResource(Res.string.save_password),
175 | checked = savePassword == PASSWORD_SAVE_ENABLED,
176 | onCheckedChange = {
177 | savePassword = if (it) PASSWORD_SAVE_ENABLED else PASSWORD_SAVE_DISABLED
178 | }
179 | )
180 | }
181 | }
182 | Row {
183 | TextButton(
184 | modifier = Modifier.weight(1f),
185 | text = stringResource(Res.string.login),
186 | colors = ButtonDefaults.textButtonColorsPrimary(),
187 | onClick = {
188 | showDialog.value = false
189 | showMessage(message = messageLoginIn)
190 | coroutineScope.launch {
191 | val int = login(account, password, global, savePassword, isLogin)
192 | when (int) {
193 | 0 -> showMessage(message = messageLoginSuccess)
194 | 1 -> showMessage(message = messageEmpty)
195 | 2 -> showMessage(message = messageCrashInfo)
196 | 3 -> showMessage(message = messageError)
197 | 4 -> showMessage(message = messageSecurityError)
198 | }
199 | }
200 | }
201 | )
202 | Spacer(Modifier.width(20.dp))
203 | TextButton(
204 | modifier = Modifier.weight(1f),
205 | text = stringResource(Res.string.cancel),
206 | colors = ButtonDefaults.textButtonColors(),
207 | onClick = {
208 | showDialog.value = false
209 | }
210 | )
211 | }
212 | }
213 | }
214 | }
215 |
216 | if (isLogin.value == 1) {
217 | SuperDialog(
218 | show = showDialog,
219 | title = stringResource(Res.string.logout),
220 | summary = stringResource(Res.string.logout_confirm),
221 | onDismissRequest = {
222 | showDialog.value = false
223 | }
224 | ) {
225 | Row {
226 | TextButton(
227 | modifier = Modifier.weight(1f),
228 | text = stringResource(Res.string.logout),
229 | colors = ButtonDefaults.textButtonColorsPrimary(),
230 | onClick = {
231 | coroutineScope.launch {
232 | val boolean = logout(isLogin)
233 | if (boolean) showMessage(message = messageLogoutSuccessful)
234 | }
235 | showDialog.value = false
236 | }
237 | )
238 | Spacer(Modifier.width(20.dp))
239 | TextButton(
240 | modifier = Modifier.weight(1f),
241 | text = stringResource(Res.string.cancel),
242 | colors = ButtonDefaults.textButtonColors(),
243 | onClick = {
244 | showDialog.value = false
245 | }
246 | )
247 | }
248 | }
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/ResultViews.kt:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import androidx.compose.animation.AnimatedContent
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.animation.fadeIn
7 | import androidx.compose.animation.fadeOut
8 | import androidx.compose.animation.togetherWith
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.PaddingValues
12 | import androidx.compose.foundation.layout.Row
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.MutableState
17 | import androidx.compose.runtime.mutableStateOf
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.runtime.rememberCoroutineScope
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType
23 | import androidx.compose.ui.platform.LocalClipboard
24 | import androidx.compose.ui.platform.LocalHapticFeedback
25 | import androidx.compose.ui.text.font.FontWeight
26 | import androidx.compose.ui.text.style.TextAlign
27 | import androidx.compose.ui.unit.dp
28 | import androidx.compose.ui.unit.sp
29 | import data.DataHelper
30 | import platform.downloadToLocal
31 | import kotlinx.coroutines.launch
32 | import misc.MessageUtils.Companion.showMessage
33 | import misc.bodyFontSize
34 | import misc.bodySmallFontSize
35 | import org.jetbrains.compose.resources.stringResource
36 | import platform.copyToClipboard
37 | import top.yukonga.miuix.kmp.basic.Card
38 | import top.yukonga.miuix.kmp.basic.Icon
39 | import top.yukonga.miuix.kmp.basic.IconButton
40 | import top.yukonga.miuix.kmp.basic.Text
41 | import top.yukonga.miuix.kmp.icon.MiuixIcons
42 | import top.yukonga.miuix.kmp.icon.icons.useful.Copy
43 | import top.yukonga.miuix.kmp.icon.icons.useful.Save
44 | import top.yukonga.miuix.kmp.theme.MiuixTheme
45 | import ui.components.TextWithIcon
46 | import updater.composeapp.generated.resources.Res
47 | import updater.composeapp.generated.resources.android_version
48 | import updater.composeapp.generated.resources.attention
49 | import updater.composeapp.generated.resources.big_version
50 | import updater.composeapp.generated.resources.branch
51 | import updater.composeapp.generated.resources.build_time
52 | import updater.composeapp.generated.resources.changelog
53 | import updater.composeapp.generated.resources.code_name
54 | import updater.composeapp.generated.resources.copy_successful
55 | import updater.composeapp.generated.resources.download
56 | import updater.composeapp.generated.resources.download_start
57 | import updater.composeapp.generated.resources.filename
58 | import updater.composeapp.generated.resources.filesize
59 | import updater.composeapp.generated.resources.fingerprint
60 | import updater.composeapp.generated.resources.security_patch_level
61 | import updater.composeapp.generated.resources.system_version
62 | import updater.composeapp.generated.resources.tags
63 |
64 | @Composable
65 | fun InfoCardViews(
66 | romInfoState: MutableState,
67 | iconInfo: MutableState>,
68 | ) {
69 | val romInfo = romInfoState.value
70 | val isVisible = romInfo.type.isNotEmpty()
71 |
72 | AnimatedVisibility(
73 | visible = isVisible,
74 | enter = fadeIn(animationSpec = tween(400)),
75 | exit = fadeOut(animationSpec = tween(400))
76 | ) {
77 | Card(
78 | modifier = Modifier.padding(bottom = 12.dp),
79 | insideMargin = PaddingValues(16.dp)
80 | ) {
81 | Text(
82 | text = romInfo.type.uppercase(),
83 | fontSize = 24.sp,
84 | fontWeight = FontWeight.SemiBold,
85 | modifier = Modifier.padding(bottom = 12.dp)
86 | )
87 |
88 | BaseMessageView(
89 | romInfo.device,
90 | romInfo.version,
91 | romInfo.bigVersion,
92 | romInfo.codebase,
93 | romInfo.branch
94 | )
95 |
96 | if (romInfo.isBeta) {
97 | MessageTextView(
98 | stringResource(Res.string.tags),
99 | "Beta"
100 | )
101 | }
102 |
103 | if (romInfo.isGov) {
104 | MessageTextView(
105 | stringResource(Res.string.tags),
106 | "Government"
107 | )
108 | }
109 |
110 | AnimatedVisibility(
111 | visible = romInfo.timestamp.isNotEmpty()
112 | ) {
113 | MetadataView(
114 | romInfo.fingerprint,
115 | romInfo.securityPatchLevel,
116 | romInfo.timestamp
117 | )
118 | }
119 | MessageTextView(
120 | stringResource(Res.string.filename),
121 | romInfo.fileName
122 | )
123 | MessageTextView(
124 | stringResource(Res.string.filesize),
125 | romInfo.fileSize
126 | )
127 |
128 | Text(
129 | text = stringResource(Res.string.download),
130 | color = MiuixTheme.colorScheme.onSecondaryVariant,
131 | fontSize = bodySmallFontSize
132 | )
133 |
134 | if (romInfo.official1Download.isNotEmpty()) {
135 | Row(
136 | modifier = Modifier.fillMaxWidth(),
137 | horizontalArrangement = Arrangement.Start
138 | ) {
139 | DownloadInfoView(
140 | modifier = Modifier.weight(1f),
141 | "ultimateota",
142 | romInfo.official1Download,
143 | romInfo.fileName
144 | )
145 | DownloadInfoView(
146 | modifier = Modifier.weight(1f),
147 | "superota",
148 | romInfo.official2Download,
149 | romInfo.fileName
150 | )
151 | }
152 | }
153 |
154 | Row(
155 | modifier = Modifier.fillMaxWidth(),
156 | horizontalArrangement = Arrangement.Start
157 | ) {
158 | DownloadInfoView(
159 | modifier = Modifier.weight(1f),
160 | "aliyuncs",
161 | romInfo.cdn1Download,
162 | romInfo.fileName
163 | )
164 | DownloadInfoView(
165 | modifier = Modifier.weight(1f),
166 | "cdnorg",
167 | romInfo.cdn2Download,
168 | romInfo.fileName
169 | )
170 | }
171 |
172 | if (romInfo.changelog.isNotEmpty()) {
173 | ChangelogView(
174 | iconInfo,
175 | romInfo.changelog
176 | )
177 | }
178 |
179 | if (romInfo.gentleNotice.isNotEmpty()) {
180 | Text(
181 | modifier = Modifier.padding(top = 16.dp),
182 | text = stringResource(Res.string.attention),
183 | fontSize = 24.sp,
184 | fontWeight = FontWeight.SemiBold,
185 | )
186 | Text(
187 | text = romInfo.gentleNotice,
188 | color = MiuixTheme.colorScheme.onSecondaryVariant,
189 | fontSize = 14.5.sp,
190 | modifier = Modifier.padding(top = 12.dp)
191 | )
192 | }
193 | }
194 | }
195 | }
196 |
197 | @Composable
198 | fun MetadataView(
199 | fingerprint: String,
200 | securityPatchLevel: String,
201 | buildTime: String,
202 | ) {
203 | Column(
204 | modifier = Modifier.fillMaxWidth(),
205 | ) {
206 | MessageTextView(stringResource(Res.string.fingerprint), fingerprint)
207 | MessageTextView(stringResource(Res.string.security_patch_level), securityPatchLevel)
208 | MessageTextView(stringResource(Res.string.build_time), buildTime)
209 | }
210 | }
211 |
212 | @Composable
213 | fun BaseMessageView(
214 | device: String,
215 | version: String,
216 | bigVersion: String,
217 | codebase: String,
218 | branch: String
219 | ) {
220 | Column(
221 | modifier = Modifier.fillMaxWidth(),
222 | ) {
223 | MessageTextView(stringResource(Res.string.code_name), device)
224 | MessageTextView(stringResource(Res.string.system_version), version)
225 | MessageTextView(stringResource(Res.string.big_version), bigVersion)
226 | MessageTextView(stringResource(Res.string.android_version), codebase)
227 | MessageTextView(stringResource(Res.string.branch), branch)
228 | }
229 | }
230 |
231 | @Composable
232 | fun MessageTextView(
233 | title: String,
234 | text: String
235 | ) {
236 | val content = remember { mutableStateOf("") }
237 | content.value = text
238 |
239 | Column(
240 | modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)
241 | ) {
242 | Text(
243 | text = title,
244 | color = MiuixTheme.colorScheme.onSecondaryVariant,
245 | fontSize = bodySmallFontSize
246 | )
247 | AnimatedContent(
248 | targetState = content.value,
249 | transitionSpec = {
250 | fadeIn(animationSpec = tween(1500)) togetherWith fadeOut(animationSpec = tween(300))
251 | }
252 | ) {
253 | Text(
254 | text = it,
255 | fontSize = bodyFontSize,
256 | fontWeight = FontWeight.Medium
257 | )
258 | }
259 | }
260 | }
261 |
262 | @Composable
263 | fun DownloadInfoView(
264 | modifier: Modifier = Modifier,
265 | title: String,
266 | url: String,
267 | fileName: String
268 | ) {
269 | val hapticFeedback = LocalHapticFeedback.current
270 | val clipboard = LocalClipboard.current
271 |
272 | val coroutineScope = rememberCoroutineScope()
273 |
274 | val messageCopySuccessful = stringResource(Res.string.copy_successful)
275 | val messageDownloadStart = stringResource(Res.string.download_start)
276 |
277 | Column(
278 | modifier = modifier
279 | ) {
280 | Text(
281 | text = title,
282 | fontSize = bodyFontSize,
283 | fontWeight = FontWeight.Medium,
284 | textAlign = TextAlign.Center
285 | )
286 | Row {
287 | if (url.isNotEmpty()) {
288 | IconButton(
289 | onClick = {
290 | coroutineScope.launch {
291 | clipboard.copyToClipboard(url)
292 | }
293 | showMessage(messageCopySuccessful)
294 | hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
295 | }
296 | ) {
297 | Icon(
298 | imageVector = MiuixIcons.Useful.Copy,
299 | contentDescription = null,
300 | tint = MiuixTheme.colorScheme.onSurface
301 | )
302 | }
303 | IconButton(
304 | onClick = {
305 | downloadToLocal(url, fileName)
306 | showMessage(messageDownloadStart)
307 | hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
308 | }
309 | ) {
310 | Icon(
311 | imageVector = MiuixIcons.Useful.Save,
312 | contentDescription = null,
313 | tint = MiuixTheme.colorScheme.onSurface
314 | )
315 | }
316 | }
317 | }
318 | }
319 | }
320 |
321 | @Composable
322 | fun ChangelogView(
323 | iconInfo: MutableState>,
324 | changelog: String
325 | ) {
326 | val hapticFeedback = LocalHapticFeedback.current
327 | val clipboard = LocalClipboard.current
328 |
329 | val coroutineScope = rememberCoroutineScope()
330 |
331 | val messageCopySuccessful = stringResource(Res.string.copy_successful)
332 |
333 | Column {
334 | Row(
335 | modifier = Modifier.padding(vertical = 6.dp),
336 | horizontalArrangement = Arrangement.SpaceBetween,
337 | verticalAlignment = Alignment.CenterVertically
338 | ) {
339 | Text(
340 | modifier = Modifier.padding(end = 6.dp),
341 | text = stringResource(Res.string.changelog),
342 | fontSize = 24.sp,
343 | fontWeight = FontWeight.SemiBold,
344 | )
345 | IconButton(
346 | onClick = {
347 | coroutineScope.launch {
348 | clipboard.copyToClipboard(changelog)
349 | }
350 | showMessage(messageCopySuccessful)
351 | hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
352 | }
353 | ) {
354 | Icon(
355 | imageVector = MiuixIcons.Useful.Copy,
356 | contentDescription = null,
357 | tint = MiuixTheme.colorScheme.onSurface
358 | )
359 | }
360 | }
361 | iconInfo.value.forEachIndexed { index, it ->
362 | TextWithIcon(
363 | changelog = it.changelog,
364 | iconName = it.iconName,
365 | iconLink = it.iconLink,
366 | padding = if (index == iconInfo.value.size - 1) 0.dp else 16.dp
367 | )
368 | }
369 | }
370 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/components/AutoCompleteTextField.kt:
--------------------------------------------------------------------------------
1 | package ui.components
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.animation.core.VisibilityThreshold
5 | import androidx.compose.animation.core.spring
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.PaddingValues
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.rememberScrollState
15 | import androidx.compose.foundation.text.KeyboardActions
16 | import androidx.compose.foundation.text.KeyboardOptions
17 | import androidx.compose.foundation.verticalScroll
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.LaunchedEffect
20 | import androidx.compose.runtime.MutableState
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.runtime.setValue
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.focus.onFocusChanged
28 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType.Companion.LongPress
29 | import androidx.compose.ui.layout.SubcomposeLayout
30 | import androidx.compose.ui.platform.LocalFocusManager
31 | import androidx.compose.ui.platform.LocalHapticFeedback
32 | import androidx.compose.ui.text.font.FontWeight
33 | import androidx.compose.ui.text.input.ImeAction
34 | import androidx.compose.ui.unit.DpSize
35 | import androidx.compose.ui.unit.IntOffset
36 | import androidx.compose.ui.unit.IntRect
37 | import androidx.compose.ui.unit.IntSize
38 | import androidx.compose.ui.unit.LayoutDirection
39 | import androidx.compose.ui.unit.dp
40 | import kotlinx.coroutines.flow.MutableStateFlow
41 | import top.yukonga.miuix.kmp.basic.ListPopup
42 | import top.yukonga.miuix.kmp.basic.PopupPositionProvider
43 | import top.yukonga.miuix.kmp.basic.Text
44 | import top.yukonga.miuix.kmp.basic.TextField
45 | import top.yukonga.miuix.kmp.extra.DropdownColors
46 | import top.yukonga.miuix.kmp.extra.DropdownDefaults
47 | import top.yukonga.miuix.kmp.theme.MiuixTheme
48 | import kotlin.math.min
49 |
50 | @Composable
51 | fun AutoCompleteTextField(
52 | text: MutableState,
53 | items: List,
54 | onValueChange: MutableStateFlow,
55 | label: String
56 | ) {
57 | val filteredList = remember(text.value, items) {
58 | items.filter {
59 | it.startsWith(text.value, ignoreCase = true)
60 | || it.contains(text.value, ignoreCase = true)
61 | || it.replace(" ", "").contains(text.value, ignoreCase = true)
62 | }.sortedBy { !it.startsWith(text.value, ignoreCase = true) }
63 | }
64 | var isFocused by remember { mutableStateOf(false) }
65 | val showPopup = remember { mutableStateOf(false) }
66 | val hapticFeedback = LocalHapticFeedback.current
67 | val focusManager = LocalFocusManager.current
68 |
69 | LaunchedEffect(isFocused, onValueChange.value) {
70 | showPopup.value = isFocused && text.value.isNotEmpty()
71 | }
72 |
73 | Box(
74 | modifier = Modifier
75 | .padding(horizontal = 12.dp)
76 | .padding(bottom = 12.dp)
77 | .fillMaxWidth()
78 | ) {
79 | TextField(
80 | insideMargin = DpSize(16.dp, 20.dp),
81 | value = text.value,
82 | onValueChange = {
83 | onValueChange.value = it
84 | },
85 | singleLine = true,
86 | label = label,
87 | backgroundColor = MiuixTheme.colorScheme.surface,
88 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
89 | keyboardActions = KeyboardActions(onDone = {
90 | focusManager.clearFocus()
91 | showPopup.value = false
92 | }),
93 | modifier = Modifier.onFocusChanged { focusState ->
94 | isFocused = focusState.isFocused
95 | }
96 | )
97 | ListPopup(
98 | show = showPopup,
99 | onDismissRequest = {
100 | focusManager.clearFocus()
101 | showPopup.value = false
102 | },
103 | popupPositionProvider = AutoCompletePositionProvider,
104 | alignment = PopupPositionProvider.Align.TopLeft,
105 | enableWindowDim = false,
106 | maxHeight = 280.dp
107 | ) {
108 | AutoCompleteListPopupColumn {
109 | if (filteredList.isNotEmpty()) {
110 | filteredList.forEachIndexed { index, item ->
111 | AutoCompleteDropdownImpl(
112 | text = item,
113 | optionSize = filteredList.size,
114 | onSelectedIndexChange = {
115 | hapticFeedback.performHapticFeedback(LongPress)
116 | onValueChange.value = item
117 | focusManager.clearFocus()
118 | showPopup.value = false
119 | },
120 | isSelected = false,
121 | index = index,
122 | )
123 | }
124 | } else {
125 | AutoCompleteDropdownImpl(
126 | text = null,
127 | optionSize = 0,
128 | onSelectedIndexChange = {},
129 | isSelected = false,
130 | index = 0,
131 | )
132 | }
133 | }
134 | }
135 | }
136 | }
137 |
138 | val AutoCompletePositionProvider = object : PopupPositionProvider {
139 | override fun calculatePosition(
140 | anchorBounds: IntRect,
141 | windowBounds: IntRect,
142 | layoutDirection: LayoutDirection,
143 | popupContentSize: IntSize,
144 | popupMargin: IntRect,
145 | alignment: PopupPositionProvider.Align
146 | ): IntOffset {
147 | val offsetX: Int = anchorBounds.left
148 | val offsetY: Int = anchorBounds.bottom + popupMargin.top
149 | return IntOffset(
150 | x = offsetX.coerceIn(
151 | minimumValue = windowBounds.left,
152 | maximumValue = (windowBounds.right - popupContentSize.width - popupMargin.right).coerceAtLeast(windowBounds.left)
153 | ),
154 | y = offsetY.coerceIn(
155 | minimumValue = (windowBounds.top + popupMargin.top),
156 | maximumValue = (windowBounds.bottom - popupContentSize.height - popupMargin.bottom).coerceAtLeast(windowBounds.top + popupMargin.top)
157 | )
158 | )
159 | }
160 |
161 | override fun getMargins(): PaddingValues {
162 | return PaddingValues(horizontal = 20.dp, vertical = 0.dp)
163 | }
164 | }
165 |
166 | @Composable
167 | fun AutoCompleteListPopupColumn(
168 | content: @Composable () -> Unit
169 | ) {
170 | SubcomposeLayout(
171 | modifier = Modifier
172 | .verticalScroll(rememberScrollState())
173 | .animateContentSize(
174 | spring(
175 | stiffness = 8000f,
176 | visibilityThreshold = IntSize.VisibilityThreshold
177 | )
178 | )
179 | ) { constraints ->
180 | var listHeight = 0
181 | val tempConstraints = constraints.copy(minWidth = 0, maxWidth = 288.dp.roundToPx(), minHeight = 0)
182 | val listWidth = subcompose("miuixPopupListFake", content).map {
183 | it.measure(tempConstraints)
184 | }.maxOf { it.width }.coerceIn(0, 288.dp.roundToPx())
185 | val childConstraints = constraints.copy(minWidth = listWidth, maxWidth = listWidth, minHeight = 0)
186 | val placeables = subcompose("miuixPopupListReal", content).map {
187 | val placeable = it.measure(childConstraints)
188 | listHeight += placeable.height
189 | placeable
190 | }
191 | layout(listWidth, min(constraints.maxHeight, listHeight)) {
192 | var height = 0
193 | placeables.forEach {
194 | it.place(0, height)
195 | height += it.height
196 | }
197 | }
198 | }
199 | }
200 |
201 | @Composable
202 | fun AutoCompleteDropdownImpl(
203 | text: String?,
204 | optionSize: Int,
205 | isSelected: Boolean,
206 | index: Int,
207 | dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(),
208 | onSelectedIndexChange: (Int) -> Unit
209 | ) {
210 | val additionalTopPadding = if (index == 0) 20f.dp else 12f.dp
211 | val additionalBottomPadding = if (index == optionSize - 1) 20f.dp else 12f.dp
212 | val textColor = if (isSelected) {
213 | dropdownColors.selectedContentColor
214 | } else {
215 | dropdownColors.contentColor
216 | }
217 | val backgroundColor = if (isSelected) {
218 | dropdownColors.selectedContainerColor
219 | } else {
220 | dropdownColors.containerColor
221 | }
222 |
223 | if (text != null) {
224 | Row(
225 | verticalAlignment = Alignment.CenterVertically,
226 | horizontalArrangement = Arrangement.SpaceBetween,
227 | modifier = Modifier
228 | .clickable {
229 | onSelectedIndexChange(index)
230 | }
231 | .background(backgroundColor)
232 | .padding(horizontal = 20.dp)
233 | .padding(top = additionalTopPadding, bottom = additionalBottomPadding)
234 | ) {
235 | Text(
236 | text = text,
237 | fontSize = MiuixTheme.textStyles.body1.fontSize,
238 | fontWeight = FontWeight.Medium,
239 | color = textColor,
240 | )
241 | }
242 | } else {
243 | Box(
244 | modifier = Modifier
245 | .padding(horizontal = 20.dp)
246 | ) {}
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/components/TextWithIcon.kt:
--------------------------------------------------------------------------------
1 | package ui.components
2 |
3 | import androidx.compose.animation.AnimatedContent
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.animation.fadeOut
7 | import androidx.compose.animation.togetherWith
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.Row
11 | import androidx.compose.foundation.layout.Spacer
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.size
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.unit.Dp
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.sp
21 | import com.seiko.imageloader.rememberImagePainter
22 | import misc.bodyFontSize
23 | import top.yukonga.miuix.kmp.basic.Text
24 | import top.yukonga.miuix.kmp.theme.MiuixTheme
25 |
26 | @Composable
27 | fun TextWithIcon(
28 | changelog: String,
29 | iconName: String,
30 | iconLink: String,
31 | padding: Dp
32 | ) {
33 | val imagePainter = rememberImagePainter(iconLink)
34 |
35 | AnimatedContent(
36 | targetState = changelog,
37 | transitionSpec = {
38 | fadeIn(animationSpec = tween(1500)) togetherWith fadeOut(animationSpec = tween(300))
39 | }
40 | ) { content ->
41 | Column {
42 | Row(
43 | modifier = Modifier.padding(bottom = 8.dp),
44 | verticalAlignment = Alignment.CenterVertically
45 | ) {
46 | if (iconLink.isNotEmpty()) {
47 | Image(
48 | modifier = Modifier.size(24.dp),
49 | painter = imagePainter,
50 | contentDescription = iconName,
51 | )
52 | Text(
53 | modifier = Modifier.padding(horizontal = 6.dp),
54 | text = iconName,
55 | fontSize = bodyFontSize,
56 | )
57 | } else if (content.isNotEmpty() && content != " ") {
58 | Text(
59 | text = iconName,
60 | fontSize = bodyFontSize,
61 | )
62 | }
63 | }
64 | if (content.isNotEmpty() && content != " ") {
65 | Text(
66 | text = content,
67 | color = MiuixTheme.colorScheme.onSecondaryVariant,
68 | fontSize = 14.5.sp
69 | )
70 | Spacer(modifier = Modifier.height(padding))
71 | }
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/java/platform/Clipboard.desktop.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import androidx.compose.ui.platform.ClipEntry
4 | import androidx.compose.ui.platform.Clipboard
5 | import java.awt.datatransfer.StringSelection
6 |
7 | internal actual suspend fun Clipboard.copyToClipboard(string: String) {
8 | setClipEntry(ClipEntry(StringSelection(string)))
9 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/java/platform/Crypto.desktop.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.jdk.JDK
5 | import misc.KeyStoreUtils
6 | import kotlin.io.encoding.Base64
7 | import kotlin.io.encoding.ExperimentalEncodingApi
8 |
9 | actual suspend fun provider() = CryptographyProvider.JDK
10 |
11 | @OptIn(ExperimentalEncodingApi::class)
12 | actual fun ownEncrypt(string: String): Pair {
13 | val cipher = KeyStoreUtils.getEncryptionCipher()
14 | val encrypted = cipher.doFinal(string.toByteArray())
15 | val iv = cipher.iv
16 | return Pair(Base64.Mime.encode(encrypted), Base64.Mime.encode(iv))
17 | }
18 |
19 | @OptIn(ExperimentalEncodingApi::class)
20 | actual fun ownDecrypt(encryptedText: String, encodedIv: String): String {
21 | val encrypted = Base64.Mime.decode(encryptedText)
22 | val iv = Base64.Mime.decode(encodedIv)
23 | val cipher = KeyStoreUtils.getDecryptionCipher(iv) ?: return ""
24 | return String(cipher.doFinal(encrypted))
25 | }
26 |
27 | actual fun generateKey() {
28 | KeyStoreUtils.generateKey()
29 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/java/platform/Download.desktop.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import java.awt.Desktop
4 | import java.net.URI
5 |
6 | actual fun downloadToLocal(url: String, fileName: String) {
7 | if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
8 | Desktop.getDesktop().browse(URI(url))
9 | }
10 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/java/platform/HttpClient.desktop.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.cio.CIO
5 | import io.ktor.client.plugins.HttpTimeout
6 |
7 | actual fun httpClientPlatform(): HttpClient {
8 | return HttpClient(CIO).config {
9 | install(HttpTimeout) {
10 | requestTimeoutMillis = 10000
11 | connectTimeoutMillis = 10000
12 | socketTimeoutMillis = 10000
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/java/platform/Preferences.desktop.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import java.util.prefs.Preferences
4 |
5 | private val preferences = Preferences.userRoot().node("UpdaterKMP")
6 |
7 | actual fun perfSet(key: String, value: String) {
8 | preferences.put(key, value)
9 | }
10 |
11 | actual fun perfGet(key: String): String? {
12 | return preferences.get(key, null)
13 | }
14 |
15 | actual fun perfRemove(key: String) {
16 | preferences.remove(key)
17 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/java/platform/Toast.desktop.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | actual fun useToast(): Boolean = false
4 | actual fun showToast(message: String, duration: Long) {}
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/Main.desktop.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.LaunchedEffect
2 | import androidx.compose.runtime.getValue
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.runtime.setValue
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.unit.DpSize
8 | import androidx.compose.ui.unit.dp
9 | import androidx.compose.ui.window.Window
10 | import androidx.compose.ui.window.WindowPosition
11 | import androidx.compose.ui.window.application
12 | import androidx.compose.ui.window.rememberWindowState
13 | import com.sun.jna.Platform.isMac
14 | import com.sun.jna.Platform.isWindows
15 | import kotlinx.coroutines.Dispatchers
16 | import kotlinx.coroutines.withContext
17 | import org.jetbrains.compose.resources.painterResource
18 | import org.jetbrains.compose.resources.stringResource
19 | import theme.MacOSThemeManager
20 | import theme.WindowsThemeManager
21 | import updater.composeapp.generated.resources.Res
22 | import updater.composeapp.generated.resources.app_name
23 | import updater.composeapp.generated.resources.icon
24 | import javax.swing.SwingUtilities
25 |
26 | fun main() = application {
27 | val state = rememberWindowState(
28 | size = DpSize(1200.dp, 800.dp),
29 | position = WindowPosition.Aligned(Alignment.Center)
30 | )
31 |
32 | Window(
33 | state = state,
34 | onCloseRequest = ::exitApplication,
35 | title = stringResource(Res.string.app_name),
36 | icon = painterResource(Res.drawable.icon),
37 | ) {
38 | when {
39 | isWindows() -> {
40 | var isDarkTheme by remember { mutableStateOf(WindowsThemeManager.isWindowsDarkTheme()) }
41 | LaunchedEffect(Unit) {
42 | withContext(Dispatchers.IO) {
43 | WindowsThemeManager.listenWindowsThemeChanges { newSystemThemeIsDark ->
44 | if (isDarkTheme != newSystemThemeIsDark) isDarkTheme = newSystemThemeIsDark
45 | }
46 | }
47 | }
48 | LaunchedEffect(isDarkTheme, window) {
49 | SwingUtilities.invokeLater {
50 | WindowsThemeManager.setWindowsTitleBarTheme(window, isDarkTheme)
51 | }
52 | }
53 | App(isDarkTheme)
54 | }
55 |
56 | isMac() -> {
57 | var isDarkTheme by remember { mutableStateOf(MacOSThemeManager.isMacOSDarkTheme()) }
58 | LaunchedEffect(Unit) {
59 | withContext(Dispatchers.IO) {
60 | MacOSThemeManager.listenMacOSThemeChanges { newSystemThemeIsDark ->
61 | if (isDarkTheme != newSystemThemeIsDark) isDarkTheme = newSystemThemeIsDark
62 | }
63 | }
64 | }
65 | LaunchedEffect(isDarkTheme, window) {
66 | SwingUtilities.invokeLater {
67 | MacOSThemeManager.setMacOSTitleBarTheme(window, isDarkTheme)
68 | }
69 | }
70 | App(isDarkTheme)
71 | }
72 |
73 | else -> {
74 | App()
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/misc/KeyStoreUtils.kt:
--------------------------------------------------------------------------------
1 | package misc
2 |
3 | import java.io.File
4 | import java.io.FileInputStream
5 | import java.io.FileOutputStream
6 | import java.security.KeyStore
7 | import javax.crypto.Cipher
8 | import javax.crypto.KeyGenerator
9 | import javax.crypto.SecretKey
10 | import javax.crypto.spec.GCMParameterSpec
11 |
12 | object KeyStoreUtils {
13 | private const val KEY_STORE_TYPE = "JCEKS"
14 | private const val KEY_ALIAS = "updater_key_alias"
15 | private const val AES_MODE = "AES/GCM/NoPadding"
16 | private const val JVM_KEY_STORE = "JvmKeyStore"
17 | private val UPDATER_DIR = File(System.getProperty("user.home"), ".updater-kmp")
18 | private val KEY_STORE_FILE = File(UPDATER_DIR, "keystore.jks").absolutePath
19 |
20 | fun generateKey() {
21 | val keyGenerator = KeyGenerator.getInstance("AES")
22 | val secretKey = keyGenerator.generateKey()
23 | val secretKeyEntry = KeyStore.SecretKeyEntry(secretKey)
24 |
25 | val keyStore = KeyStore.getInstance(KEY_STORE_TYPE)
26 | keyStore.load(null, null)
27 | val password = KeyStore.PasswordProtection(JVM_KEY_STORE.toCharArray())
28 | keyStore.setEntry(KEY_ALIAS, secretKeyEntry, password)
29 |
30 | UPDATER_DIR.mkdirs()
31 | FileOutputStream(KEY_STORE_FILE).use { keyStore.store(it, JVM_KEY_STORE.toCharArray()) }
32 | }
33 |
34 | private val secretKey: SecretKey?
35 | get() {
36 | val keyStore = KeyStore.getInstance(KEY_STORE_TYPE)
37 |
38 | val file = File(KEY_STORE_FILE)
39 | if (!file.exists()) return null
40 |
41 | FileInputStream(file).use { keyStore.load(it, JVM_KEY_STORE.toCharArray()) }
42 |
43 | val password = KeyStore.PasswordProtection(JVM_KEY_STORE.toCharArray())
44 | val secretKeyEntry = keyStore.getEntry(KEY_ALIAS, password) as KeyStore.SecretKeyEntry
45 | return secretKeyEntry.secretKey
46 | }
47 |
48 | @Throws(Exception::class)
49 | fun getEncryptionCipher(): Cipher {
50 | val cipher = Cipher.getInstance(AES_MODE)
51 | cipher.init(Cipher.ENCRYPT_MODE, secretKey)
52 | return cipher
53 | }
54 |
55 | @Throws(Exception::class)
56 | fun getDecryptionCipher(iv: ByteArray): Cipher? {
57 | if (secretKey == null) return null
58 | val cipher = Cipher.getInstance(AES_MODE)
59 | cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv))
60 | return cipher
61 | }
62 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/theme/MacOSThemeManager.kt:
--------------------------------------------------------------------------------
1 | package theme
2 |
3 | import androidx.compose.ui.awt.ComposeWindow
4 | import kotlinx.coroutines.currentCoroutineContext
5 | import kotlinx.coroutines.isActive
6 |
7 | object MacOSThemeManager {
8 | fun isMacOSDarkTheme(): Boolean {
9 | return try {
10 | val process = ProcessBuilder("defaults", "read", "-g", "AppleInterfaceStyle").start()
11 | val result = process.inputStream.bufferedReader().readText().trim()
12 | process.waitFor()
13 | result.equals("Dark", ignoreCase = true)
14 | } catch (_: Exception) {
15 | false
16 | }
17 | }
18 |
19 | fun setMacOSTitleBarTheme(window: ComposeWindow, isDark: Boolean) {
20 | try {
21 | // This needed JetBrains Runtime
22 | window.rootPane.putClientProperty(
23 | "apple.awt.windowAppearance",
24 | if (isDark) "NSAppearanceNameVibrantDark" else "NSAppearanceNameVibrantLight",
25 | )
26 | } catch (_: Exception) {
27 | }
28 | }
29 |
30 | suspend fun listenMacOSThemeChanges(onThemeChanged: (Boolean) -> Unit) {
31 | try {
32 | while (currentCoroutineContext().isActive) {
33 | val currentSystemThemeIsDark = isMacOSDarkTheme()
34 | onThemeChanged(currentSystemThemeIsDark)
35 | }
36 | } catch (_: Exception) {
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/theme/WindowsThemeManager.kt:
--------------------------------------------------------------------------------
1 | package theme
2 |
3 | import com.sun.jna.Native
4 | import com.sun.jna.Pointer
5 | import com.sun.jna.platform.win32.Advapi32
6 | import com.sun.jna.platform.win32.Advapi32Util
7 | import com.sun.jna.platform.win32.WinDef
8 | import com.sun.jna.platform.win32.WinError
9 | import com.sun.jna.platform.win32.WinNT
10 | import com.sun.jna.platform.win32.WinReg
11 | import com.sun.jna.win32.StdCallLibrary
12 | import kotlinx.coroutines.currentCoroutineContext
13 | import kotlinx.coroutines.isActive
14 |
15 | object WindowsThemeManager {
16 | private const val REGISTRY_KEY_PATH = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"
17 | private const val REGISTRY_VALUE_NAME = "AppsUseLightTheme"
18 |
19 | private interface DwmApi : StdCallLibrary {
20 | fun DwmSetWindowAttribute(
21 | hwnd: Pointer,
22 | dwAttribute: Int,
23 | pvAttribute: Pointer,
24 | cbAttribute: Int
25 | ): Int
26 |
27 | companion object {
28 | val INSTANCE: DwmApi by lazy {
29 | Native.load("dwmapi", DwmApi::class.java)
30 | }
31 | const val DWMWA_USE_IMMERSIVE_DARK_MODE = 20
32 | }
33 | }
34 |
35 | fun isWindowsDarkTheme(): Boolean {
36 | return try {
37 | val value = Advapi32Util.registryGetIntValue(
38 | WinReg.HKEY_CURRENT_USER,
39 | REGISTRY_KEY_PATH,
40 | REGISTRY_VALUE_NAME
41 | )
42 | value == 0
43 | } catch (_: Exception) {
44 | false
45 | }
46 | }
47 |
48 | fun setWindowsTitleBarTheme(window: java.awt.Window, isDark: Boolean) {
49 | try {
50 | val hwnd = WinDef.HWND(Native.getComponentPointer(window))
51 | val darkModeValue = WinDef.BOOLByReference(WinDef.BOOL(isDark))
52 |
53 | DwmApi.INSTANCE.DwmSetWindowAttribute(
54 | hwnd.pointer,
55 | DwmApi.DWMWA_USE_IMMERSIVE_DARK_MODE,
56 | darkModeValue.pointer,
57 | 4,
58 | )
59 | } catch (_: Throwable) {
60 | }
61 | }
62 |
63 | suspend fun listenWindowsThemeChanges(onThemeChanged: (isDark: Boolean) -> Unit) {
64 | val advapi32 = Advapi32.INSTANCE
65 | val hKeyByRef = WinReg.HKEYByReference()
66 |
67 | val openResult = advapi32.RegOpenKeyEx(
68 | WinReg.HKEY_CURRENT_USER,
69 | REGISTRY_KEY_PATH,
70 | 0,
71 | WinNT.KEY_NOTIFY,
72 | hKeyByRef,
73 | )
74 |
75 | if (openResult != WinError.ERROR_SUCCESS) return
76 |
77 | val hKey = hKeyByRef.value
78 | try {
79 | while (currentCoroutineContext().isActive) {
80 | val notifyResult = advapi32.RegNotifyChangeKeyValue(
81 | hKey,
82 | false,
83 | WinNT.REG_NOTIFY_CHANGE_LAST_SET,
84 | null,
85 | false
86 | )
87 |
88 | if (!currentCoroutineContext().isActive) break
89 | if (notifyResult == WinError.ERROR_SUCCESS) {
90 | val currentSystemThemeIsDark = isWindowsDarkTheme()
91 | onThemeChanged(currentSystemThemeIsDark)
92 | } else {
93 | break
94 | }
95 | }
96 | } finally {
97 | advapi32.RegCloseKey(hKey)
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/resources/linux/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/composeApp/src/desktopMain/resources/linux/Icon.png
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/resources/macos/Icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/composeApp/src/desktopMain/resources/macos/Icon.icns
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/resources/windows/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/composeApp/src/desktopMain/resources/windows/Icon.ico
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/Main.ios.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.window.ComposeUIViewController
2 |
3 | fun main() = ComposeUIViewController {
4 | ResourceEnvironmentFix {
5 | App()
6 | }
7 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/ResourceEnvironmentFix.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
2 | @file:OptIn(ExperimentalResourceApi::class, InternalResourceApi::class)
3 |
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.CompositionLocalProvider
6 | import org.jetbrains.compose.resources.ComposeEnvironment
7 | import org.jetbrains.compose.resources.ExperimentalResourceApi
8 | import org.jetbrains.compose.resources.InternalResourceApi
9 | import org.jetbrains.compose.resources.LanguageQualifier
10 | import org.jetbrains.compose.resources.LocalComposeEnvironment
11 | import org.jetbrains.compose.resources.RegionQualifier
12 | import org.jetbrains.compose.resources.ResourceEnvironment
13 | import org.jetbrains.compose.resources.getResourceEnvironment
14 | import org.jetbrains.compose.resources.getSystemEnvironment
15 | import platform.Foundation.NSLocale
16 | import platform.Foundation.NSLocaleScriptCode
17 | import platform.Foundation.currentLocale
18 | import platform.Foundation.preferredLanguages
19 |
20 | // https://youtrack.jetbrains.com/issue/CMP-6614/iOS-Localization-strings-for-language-qualifiers-that-are-not-the-same-between-platforms-appear-not-translated
21 |
22 | val resourceEnvironmentFix: Unit = run {
23 | getResourceEnvironment = ::myResourceEnvironment
24 | }
25 |
26 | @Composable
27 | fun ResourceEnvironmentFix(content: @Composable () -> Unit) {
28 | resourceEnvironmentFix
29 |
30 | val default = LocalComposeEnvironment.current
31 | CompositionLocalProvider(
32 | LocalComposeEnvironment provides object : ComposeEnvironment {
33 | @Composable
34 | override fun rememberEnvironment(): ResourceEnvironment {
35 | val environment = default.rememberEnvironment()
36 | return mapEnvironment(environment)
37 | }
38 | }
39 | ) {
40 | content()
41 | }
42 | }
43 |
44 | private fun myResourceEnvironment(): ResourceEnvironment {
45 | val environment = getSystemEnvironment()
46 | return mapEnvironment(environment)
47 | }
48 |
49 | private fun mapEnvironment(environment: ResourceEnvironment): ResourceEnvironment {
50 | val locale = NSLocale.preferredLanguages.firstOrNull()
51 | ?.let { NSLocale(it as String) }
52 | ?: NSLocale.currentLocale
53 | val script = locale.objectForKey(NSLocaleScriptCode) as? String
54 |
55 | return ResourceEnvironment(
56 | language = when (environment.language.language) {
57 | "he" -> LanguageQualifier("iw")
58 | "id" -> LanguageQualifier("in")
59 | else -> environment.language
60 | },
61 | region = when (environment.language.language) {
62 | "en" -> when (environment.region.region) {
63 | "" -> RegionQualifier("")
64 | "US" -> RegionQualifier("")
65 | "AU" -> RegionQualifier("AU")
66 | else -> RegionQualifier("GB")
67 | }
68 |
69 | "zh" -> when (script) {
70 | "Hans" -> RegionQualifier("CN")
71 | "Hant" -> RegionQualifier("TW")
72 | else -> environment.region
73 | }
74 |
75 | else -> environment.region
76 | },
77 | theme = environment.theme,
78 | density = environment.density
79 | )
80 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/platform/Clipboard.ios.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.platform.ClipEntry
5 | import androidx.compose.ui.platform.Clipboard
6 |
7 | @OptIn(ExperimentalComposeUiApi::class)
8 | actual suspend fun Clipboard.copyToClipboard(string: String) {
9 | this.setClipEntry(ClipEntry.withPlainText(string))
10 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/platform/Crypto.ios.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.apple.Apple
5 |
6 | actual suspend fun provider() = CryptographyProvider.Apple
7 |
8 | actual fun ownEncrypt(string: String): Pair = Pair(string, "")
9 |
10 | actual fun ownDecrypt(encryptedText: String, encodedIv: String): String = encryptedText
11 |
12 | actual fun generateKey() = Unit
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/platform/Download.ios.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import platform.Foundation.NSURL
4 | import platform.UIKit.UIApplication
5 |
6 | actual fun downloadToLocal(url: String, fileName: String) {
7 | val openUrl = NSURL(string = url)
8 | UIApplication.sharedApplication.openURL(openUrl)
9 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/platform/HttpClient.ios.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.darwin.Darwin
5 | import io.ktor.client.plugins.HttpTimeout
6 |
7 | actual fun httpClientPlatform(): HttpClient {
8 | return HttpClient(Darwin).config {
9 | install(HttpTimeout) {
10 | requestTimeoutMillis = 10000
11 | connectTimeoutMillis = 10000
12 | socketTimeoutMillis = 10000
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/platform/Preferences.ios.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import platform.Foundation.NSUserDefaults
4 |
5 | private val preferences = NSUserDefaults.standardUserDefaults()
6 |
7 | actual fun perfSet(key: String, value: String) {
8 | preferences.setObject(value, key)
9 | }
10 |
11 | actual fun perfGet(key: String): String? {
12 | return preferences.stringForKey(key)
13 | }
14 |
15 | actual fun perfRemove(key: String) {
16 | preferences.removeObjectForKey(key)
17 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/platform/Toast.ios.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | actual fun useToast(): Boolean = false
4 | actual fun showToast(message: String, duration: Long) {}
--------------------------------------------------------------------------------
/composeApp/src/jsMain/kotlin/Main.js.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.LaunchedEffect
2 | import androidx.compose.runtime.mutableStateOf
3 | import androidx.compose.runtime.remember
4 | import androidx.compose.ui.ExperimentalComposeUiApi
5 | import androidx.compose.ui.platform.LocalFontFamilyResolver
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.platform.Font
8 | import androidx.compose.ui.window.ComposeViewport
9 | import kotlinx.browser.window
10 | import kotlinx.coroutines.await
11 | import org.jetbrains.skiko.wasm.onWasmReady
12 | import org.khronos.webgl.ArrayBuffer
13 | import org.khronos.webgl.Int8Array
14 |
15 | private const val MiSanVF = "./MiSans VF.woff2"
16 |
17 | @OptIn(ExperimentalComposeUiApi::class)
18 | fun main() {
19 | onWasmReady {
20 | ComposeViewport(
21 | viewportContainerId = "composeApplication"
22 | ) {
23 | val fontFamilyResolver = LocalFontFamilyResolver.current
24 | val fontsLoaded = remember { mutableStateOf(false) }
25 |
26 | if (fontsLoaded.value) {
27 | hideLoading()
28 | App()
29 | }
30 |
31 | LaunchedEffect(Unit) {
32 | val miSanVFBytes = loadRes(MiSanVF).toByteArray()
33 | val fontFamily = FontFamily(Font("MiSans VF", miSanVFBytes))
34 | fontFamilyResolver.preload(fontFamily)
35 | fontsLoaded.value = true
36 | }
37 | }
38 | }
39 | }
40 |
41 |
42 | suspend fun loadRes(url: String): ArrayBuffer {
43 | return window.fetch(url).await().arrayBuffer().await()
44 | }
45 |
46 | fun ArrayBuffer.toByteArray(): ByteArray {
47 | val source = Int8Array(this, 0, byteLength)
48 | return jsInt8ArrayToKotlinByteArray(source)
49 | }
50 |
51 | external fun hideLoading()
52 |
53 | external fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray
--------------------------------------------------------------------------------
/composeApp/src/jsMain/kotlin/platform/Clipboard.js.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import androidx.compose.ui.platform.ClipEntry
4 | import androidx.compose.ui.platform.Clipboard
5 |
6 | actual suspend fun Clipboard.copyToClipboard(string: String) {
7 | this.setClipEntry(ClipEntry.withPlainText(string))
8 | }
--------------------------------------------------------------------------------
/composeApp/src/jsMain/kotlin/platform/Crypto.js.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.webcrypto.WebCrypto
5 |
6 | actual suspend fun provider() = CryptographyProvider.WebCrypto
7 |
8 | actual fun ownEncrypt(string: String): Pair = Pair(string, "")
9 |
10 | actual fun ownDecrypt(encryptedText: String, encodedIv: String): String = encryptedText
11 |
12 | actual fun generateKey() = Unit
--------------------------------------------------------------------------------
/composeApp/src/jsMain/kotlin/platform/Download.js.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import kotlinx.browser.document
4 | import org.w3c.dom.HTMLAnchorElement
5 |
6 | actual fun downloadToLocal(url: String, fileName: String) {
7 | val anchorElement = document.createElement("a") as HTMLAnchorElement
8 | anchorElement.href = url
9 | anchorElement.download = fileName
10 | document.body?.appendChild(anchorElement)
11 | anchorElement.click()
12 | document.body?.removeChild(anchorElement)
13 | }
--------------------------------------------------------------------------------
/composeApp/src/jsMain/kotlin/platform/HttpClient.js.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.js.Js
5 | import io.ktor.client.plugins.HttpTimeout
6 |
7 | actual fun httpClientPlatform(): HttpClient {
8 | return HttpClient(Js).config {
9 | install(HttpTimeout) {
10 | requestTimeoutMillis = 10000
11 | connectTimeoutMillis = 10000
12 | socketTimeoutMillis = 10000
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/composeApp/src/jsMain/kotlin/platform/Preferences.js.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import kotlinx.browser.window
4 |
5 | actual fun perfSet(key: String, value: String) {
6 | window.localStorage.setItem(key, value)
7 | }
8 |
9 | actual fun perfGet(key: String): String? {
10 | return window.localStorage.getItem(key)
11 | }
12 |
13 | actual fun perfRemove(key: String) {
14 | window.localStorage.removeItem(key)
15 | }
--------------------------------------------------------------------------------
/composeApp/src/jsMain/kotlin/platform/Toast.js.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | actual fun useToast(): Boolean = false
4 | actual fun showToast(message: String, duration: Long) {}
--------------------------------------------------------------------------------
/composeApp/src/jsMain/resources/MiSans VF.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/composeApp/src/jsMain/resources/MiSans VF.woff2
--------------------------------------------------------------------------------
/composeApp/src/jsMain/resources/app.js:
--------------------------------------------------------------------------------
1 | function hideLoading() {
2 | document.getElementById('loading').style.display = 'none';
3 | document.getElementById('composeApplication').style.display = 'block';
4 | }
5 |
6 | function jsInt8ArrayToKotlinByteArray(x) {
7 | const size = x.length;
8 | const memBuffer = new ArrayBuffer(size);
9 | const mem8 = new Int8Array(memBuffer);
10 | mem8.set(x);
11 | const byteArray = new Uint8Array(memBuffer);
12 | return byteArray;
13 | }
14 |
15 | function writeToClipboard(text) {
16 | navigator.clipboard.writeText(text);
17 | }
--------------------------------------------------------------------------------
/composeApp/src/jsMain/resources/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/composeApp/src/jsMain/resources/favicon.ico
--------------------------------------------------------------------------------
/composeApp/src/jsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Updater
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/composeApp/src/jsMain/resources/styles.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | html, body {
18 | margin: 0;
19 | padding: 0;
20 | width: 100%;
21 | height: 100%;
22 | display: flex;
23 | overflow: hidden;
24 | }
25 |
26 | #loading {
27 | position: absolute;
28 | top: 50%;
29 | left: 50%;
30 | transform: translate(-50%, -50%);
31 | font-size: 24px;
32 | font-weight: bold;
33 | }
34 |
35 | #loading::after {
36 | content: '.';
37 | animation: ellipsis 1.5s infinite;
38 | }
39 |
40 | @keyframes ellipsis {
41 | 0% {
42 | content: '.';
43 | }
44 |
45 | 33% {
46 | content: '..';
47 | }
48 |
49 | 66% {
50 | content: '...';
51 | }
52 |
53 | 100% {
54 | content: '.';
55 | }
56 | }
57 |
58 | #composeApplication {
59 | width: 100%;
60 | height: 100%;
61 | }
62 |
--------------------------------------------------------------------------------
/composeApp/src/macosMain/kotlin/Main.macos.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.collectAsState
2 | import androidx.compose.runtime.getValue
3 | import androidx.compose.ui.unit.DpSize
4 | import androidx.compose.ui.unit.dp
5 | import androidx.compose.ui.window.Window
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import platform.AppKit.NSApp
8 | import platform.AppKit.NSApplication
9 | import platform.AppKit.NSApplicationDelegateProtocol
10 | import platform.Foundation.NSDistributedNotificationCenter
11 | import platform.Foundation.NSNotification
12 | import platform.Foundation.NSOperationQueue
13 | import platform.Foundation.NSUserDefaults
14 | import platform.darwin.NSObject
15 | import kotlin.system.exitProcess
16 |
17 | val isDarkThemeState = MutableStateFlow(false)
18 |
19 | class AppDelegate : NSObject(), NSApplicationDelegateProtocol {
20 | override fun applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication): Boolean {
21 | return true
22 | }
23 |
24 | override fun applicationWillTerminate(notification: NSNotification) {
25 | exitProcess(0)
26 | }
27 |
28 | override fun applicationDidFinishLaunching(notification: NSNotification) {
29 | updateThemeMode()
30 |
31 | NSDistributedNotificationCenter.defaultCenter().addObserverForName(
32 | name = "AppleInterfaceThemeChangedNotification",
33 | `object` = null,
34 | queue = NSOperationQueue.mainQueue,
35 | usingBlock = { _: NSNotification? ->
36 | this.updateThemeMode()
37 | }
38 | )
39 | }
40 |
41 | private fun updateThemeMode() {
42 | val defaults = NSUserDefaults.standardUserDefaults
43 | val interfaceStyle = defaults.stringForKey("AppleInterfaceStyle")
44 | val isCurrentlyDark = interfaceStyle != null && interfaceStyle.equals("Dark", ignoreCase = true)
45 | if (isDarkThemeState.value != isCurrentlyDark) isDarkThemeState.value = isCurrentlyDark
46 | }
47 | }
48 |
49 | fun main() {
50 | NSApplication.sharedApplication()
51 | val delegate = AppDelegate()
52 | NSApp?.setDelegate(delegate)
53 |
54 | Window(
55 | title = "Updater",
56 | size = DpSize(1200.dp, 800.dp),
57 | ) {
58 | val isDarkTheme by isDarkThemeState.collectAsState()
59 | App(isDarkTheme)
60 | }
61 |
62 | NSApp?.run()
63 | }
--------------------------------------------------------------------------------
/composeApp/src/macosMain/kotlin/platform/Clipboard.macos.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.platform.ClipEntry
5 | import androidx.compose.ui.platform.Clipboard
6 |
7 | @OptIn(ExperimentalComposeUiApi::class)
8 | actual suspend fun Clipboard.copyToClipboard(string: String) {
9 | this.setClipEntry(ClipEntry.withPlainText(string))
10 | }
--------------------------------------------------------------------------------
/composeApp/src/macosMain/kotlin/platform/Crypto.macos.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.apple.Apple
5 |
6 | actual suspend fun provider() = CryptographyProvider.Apple
7 |
8 | actual fun ownEncrypt(string: String): Pair = Pair(string, "")
9 |
10 | actual fun ownDecrypt(encryptedText: String, encodedIv: String): String = encryptedText
11 |
12 | actual fun generateKey() = Unit
--------------------------------------------------------------------------------
/composeApp/src/macosMain/kotlin/platform/Download.macos.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import platform.AppKit.NSWorkspace
4 | import platform.Foundation.NSURL
5 |
6 | actual fun downloadToLocal(url: String, fileName: String) {
7 | val openUrl = NSURL(string = url)
8 | NSWorkspace.sharedWorkspace().openURL(openUrl)
9 | }
--------------------------------------------------------------------------------
/composeApp/src/macosMain/kotlin/platform/HttpClient.macos.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.darwin.Darwin
5 | import io.ktor.client.plugins.HttpTimeout
6 |
7 | actual fun httpClientPlatform(): HttpClient {
8 | return HttpClient(Darwin).config {
9 | install(HttpTimeout) {
10 | requestTimeoutMillis = 10000
11 | connectTimeoutMillis = 10000
12 | socketTimeoutMillis = 10000
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/composeApp/src/macosMain/kotlin/platform/Preferences.macos.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import platform.Foundation.NSUserDefaults
4 |
5 | private val preferences = NSUserDefaults.standardUserDefaults()
6 |
7 | actual fun perfSet(key: String, value: String) {
8 | preferences.setObject(value, key)
9 | }
10 |
11 | actual fun perfGet(key: String): String? {
12 | return preferences.stringForKey(key)
13 | }
14 |
15 | actual fun perfRemove(key: String) {
16 | preferences.removeObjectForKey(key)
17 | }
--------------------------------------------------------------------------------
/composeApp/src/macosMain/kotlin/platform/Toast.macos.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | actual fun useToast(): Boolean = false
4 | actual fun showToast(message: String, duration: Long) {}
--------------------------------------------------------------------------------
/composeApp/src/macosMain/resources/Updater.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/composeApp/src/macosMain/resources/Updater.icns
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/Main.wasmJs.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.LaunchedEffect
2 | import androidx.compose.runtime.mutableStateOf
3 | import androidx.compose.runtime.remember
4 | import androidx.compose.ui.ExperimentalComposeUiApi
5 | import androidx.compose.ui.platform.LocalFontFamilyResolver
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.platform.Font
8 | import androidx.compose.ui.window.ComposeViewport
9 | import io.ktor.client.fetch.Response
10 | import kotlinx.browser.window
11 | import kotlinx.coroutines.await
12 | import org.khronos.webgl.ArrayBuffer
13 | import org.khronos.webgl.Int8Array
14 | import kotlin.wasm.unsafe.UnsafeWasmMemoryApi
15 | import kotlin.wasm.unsafe.withScopedMemoryAllocator
16 |
17 | private const val MiSanVF = "./MiSans VF.woff2"
18 |
19 | @OptIn(ExperimentalComposeUiApi::class)
20 | fun main() {
21 | ComposeViewport(
22 | viewportContainerId = "composeApplication"
23 | ) {
24 | val fontFamilyResolver = LocalFontFamilyResolver.current
25 | val fontsLoaded = remember { mutableStateOf(false) }
26 |
27 | if (fontsLoaded.value) {
28 | hideLoading()
29 | App()
30 | }
31 |
32 | LaunchedEffect(Unit) {
33 | val miSanVFBytes = loadRes(MiSanVF).toByteArray()
34 | val fontFamily = FontFamily(Font("MiSans VF", miSanVFBytes))
35 | fontFamilyResolver.preload(fontFamily)
36 | fontsLoaded.value = true
37 | }
38 | }
39 | }
40 |
41 | suspend fun loadRes(url: String): ArrayBuffer {
42 | return window.fetch(url).await().arrayBuffer().await()
43 | }
44 |
45 | fun ArrayBuffer.toByteArray(): ByteArray {
46 | val source = Int8Array(this, 0, byteLength)
47 | return jsInt8ArrayToKotlinByteArray(source)
48 | }
49 |
50 |
51 | @JsFun(
52 | """
53 | function hideLoading() {
54 | document.getElementById('loading').style.display = 'none';
55 | document.getElementById('composeApplication').style.display = 'block';
56 | }
57 | """
58 | )
59 | external fun hideLoading()
60 |
61 | @JsFun(
62 | """ (src, size, dstAddr) => {
63 | const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size);
64 | mem8.set(src);
65 | }
66 | """
67 | )
68 | external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int)
69 |
70 | internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray {
71 | val size = x.length
72 |
73 | @OptIn(UnsafeWasmMemoryApi::class)
74 | return withScopedMemoryAllocator { allocator ->
75 | val memBuffer = allocator.allocate(size)
76 | val dstAddress = memBuffer.address.toInt()
77 | jsExportInt8ArrayToWasm(x, size, dstAddress)
78 | ByteArray(size) { i -> (memBuffer + i).loadByte() }
79 | }
80 | }
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/platform/Clipboard.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import androidx.compose.ui.platform.ClipEntry
4 | import androidx.compose.ui.platform.Clipboard
5 |
6 | actual suspend fun Clipboard.copyToClipboard(string: String) {
7 | this.setClipEntry(ClipEntry.withPlainText(string))
8 | }
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/platform/Crypto.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.webcrypto.WebCrypto
5 |
6 | actual suspend fun provider() = CryptographyProvider.WebCrypto
7 |
8 | actual fun ownEncrypt(string: String): Pair = Pair(string, "")
9 |
10 | actual fun ownDecrypt(encryptedText: String, encodedIv: String): String = encryptedText
11 |
12 | actual fun generateKey() = Unit
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/platform/Download.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import kotlinx.browser.document
4 | import org.w3c.dom.HTMLAnchorElement
5 |
6 | actual fun downloadToLocal(url: String, fileName: String) {
7 | val anchorElement = document.createElement("a") as HTMLAnchorElement
8 | anchorElement.href = url
9 | anchorElement.download = fileName
10 | document.body?.appendChild(anchorElement)
11 | anchorElement.click()
12 | document.body?.removeChild(anchorElement)
13 | }
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/platform/HttpClient.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.js.Js
5 | import io.ktor.client.plugins.HttpTimeout
6 |
7 | actual fun httpClientPlatform(): HttpClient {
8 | return HttpClient(Js).config {
9 | install(HttpTimeout) {
10 | requestTimeoutMillis = 10000
11 | connectTimeoutMillis = 10000
12 | socketTimeoutMillis = 10000
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/platform/Preferences.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import kotlinx.browser.window
4 |
5 | actual fun perfSet(key: String, value: String) {
6 | window.localStorage.setItem(key, value)
7 | }
8 |
9 | actual fun perfGet(key: String): String? {
10 | return window.localStorage.getItem(key)
11 | }
12 |
13 | actual fun perfRemove(key: String) {
14 | window.localStorage.removeItem(key)
15 | }
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/platform/Toast.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | actual fun useToast(): Boolean = false
4 | actual fun showToast(message: String, duration: Long) {}
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/resources/MiSans VF.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/composeApp/src/wasmJsMain/resources/MiSans VF.woff2
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/resources/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/composeApp/src/wasmJsMain/resources/favicon.ico
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Updater
8 |
9 |
10 |
11 |
12 |
13 |
14 | Loading
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/resources/styles.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 |
18 | html,
19 | body {
20 | width: 100%;
21 | height: 100%;
22 | margin: 0;
23 | padding: 0;
24 | overflow: hidden;
25 | }
26 |
27 | #loading {
28 | position: absolute;
29 | top: 50%;
30 | left: 50%;
31 | transform: translate(-50%, -50%);
32 | font-size: 24px;
33 | font-weight: bold;
34 | }
35 |
36 | #loading::after {
37 | content: '.';
38 | animation: ellipsis 1.5s infinite;
39 | }
40 |
41 | @keyframes ellipsis {
42 | 0% {
43 | content: '.';
44 | }
45 |
46 | 33% {
47 | content: '..';
48 | }
49 |
50 | 66% {
51 | content: '...';
52 | }
53 |
54 | 100% {
55 | content: '.';
56 | }
57 | }
58 |
59 | #composeApplication {
60 | width: 100%;
61 | height: 100%;
62 | }
--------------------------------------------------------------------------------
/composeApp/webpack.config.d/config.js:
--------------------------------------------------------------------------------
1 | const TerserPlugin = require("terser-webpack-plugin");
2 |
3 | config.optimization = config.optimization || {};
4 | config.optimization.minimize = true;
5 | config.optimization.minimizer = [
6 | new TerserPlugin({
7 | terserOptions: {
8 | mangle: true, // Note: By default, mangle is set to true.
9 | compress: false, // Disable the transformations that reduce the code size.
10 | output: {
11 | beautify: false,
12 | },
13 | },
14 | }),
15 | ];
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Gradle
2 | org.gradle.jvmargs=-Xmx8g -Dfile.encoding=UTF-8
3 | org.gradle.parallel=true
4 | org.gradle.caching=true
5 | org.gradle.configureondemand=true
6 | # Android
7 | android.useAndroidX=true
8 | android.nonTransitiveRClass=true
9 | # Kotlin
10 | kotlin.code.style=official
11 | # MPP
12 | kotlin.mpp.androidSourceSetLayoutVersion=2
13 | kotlin.mpp.enableCInteropCommonization=true
14 | # Native
15 | kotlin.native.ignoreDisabledTargets=true
16 | kotlin.native.useEmbeddableCompilerJar=true
17 | kotlin.native.enableKlibsCrossCompilation=true
18 | # Incremental compilation
19 | kotlin.incremental=true
20 | kotlin.incremental.multiplatform=true
21 | kotlin.incremental.jvm.fir=true
22 | kotlin.incremental.js=true
23 | kotlin.incremental.js.klib=true
24 | kotlin.incremental.native=true
25 | kotlin.incremental.wasm=true
26 | # Experimental target
27 | org.jetbrains.compose.experimental.macos.enabled=true
28 | org.jetbrains.compose.experimental.jscanvas.enabled=true
29 | # Xcode
30 | kotlin.apple.xcodeCompatibility.nowarn=true
31 | xcodeproj=./iosApp
32 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | android-gradle-plugin = "8.10.1"
3 | androidx-activity-compose = "1.10.1"
4 | compose-plugin = "1.8.1"
5 | cryptography = "0.4.0"
6 | haze = "1.6.3"
7 | image-loader = "1.10.0"
8 | jna = "5.17.0"
9 | kotlin = "2.1.21"
10 | kotlinx-serialization-json = "1.8.1"
11 | kotlinx-datetime = "0.6.2"
12 | ktor-client = "3.1.3"
13 | miuix = "0.4.7"
14 |
15 | [libraries]
16 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
17 | cryptography-core = { module = "dev.whyoleg.cryptography:cryptography-core", version.ref = "cryptography" }
18 | cryptography-provider-apple = { module = "dev.whyoleg.cryptography:cryptography-provider-apple", version.ref = "cryptography" }
19 | cryptography-provider-jdk = { module = "dev.whyoleg.cryptography:cryptography-provider-jdk", version.ref = "cryptography" }
20 | cryptography-provider-webcrypto = { module = "dev.whyoleg.cryptography:cryptography-provider-webcrypto", version.ref = "cryptography" }
21 | haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
22 | image-loader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "image-loader" }
23 | jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
24 | jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
25 | kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
26 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
27 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-client" }
28 | ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor-client" }
29 | ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor-client" }
30 | ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor-client" }
31 | miuix = { module = "top.yukonga.miuix.kmp:miuix", version.ref = "miuix" }
32 |
33 | [plugins]
34 | android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
35 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
36 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
37 | kotlin-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }
38 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
39 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH="\\\"\\\""
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | if ! command -v java >/dev/null 2>&1
137 | then
138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
139 |
140 | Please set the JAVA_HOME variable in your environment to match the
141 | location of your Java installation."
142 | fi
143 | fi
144 |
145 | # Increase the maximum file descriptors if we can.
146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
147 | case $MAX_FD in #(
148 | max*)
149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
150 | # shellcheck disable=SC2039,SC3045
151 | MAX_FD=$( ulimit -H -n ) ||
152 | warn "Could not query maximum file descriptor limit"
153 | esac
154 | case $MAX_FD in #(
155 | '' | soft) :;; #(
156 | *)
157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
158 | # shellcheck disable=SC2039,SC3045
159 | ulimit -n "$MAX_FD" ||
160 | warn "Could not set maximum file descriptor limit to $MAX_FD"
161 | esac
162 | fi
163 |
164 | # Collect all arguments for the java command, stacking in reverse order:
165 | # * args from the command line
166 | # * the main class name
167 | # * -classpath
168 | # * -D...appname settings
169 | # * --module-path (only if needed)
170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
171 |
172 | # For Cygwin or MSYS, switch paths to Windows format before running java
173 | if "$cygwin" || "$msys" ; then
174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
176 |
177 | JAVACMD=$( cygpath --unix "$JAVACMD" )
178 |
179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
180 | for arg do
181 | if
182 | case $arg in #(
183 | -*) false ;; # don't mess with options #(
184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
185 | [ -e "$t" ] ;; #(
186 | *) false ;;
187 | esac
188 | then
189 | arg=$( cygpath --path --ignore --mixed "$arg" )
190 | fi
191 | # Roll the args list around exactly as many times as the number of
192 | # args, so each arg winds up back in the position where it started, but
193 | # possibly modified.
194 | #
195 | # NB: a `for` loop captures its iteration list before it begins, so
196 | # changing the positional parameters here affects neither the number of
197 | # iterations, nor the values presented in `arg`.
198 | shift # remove old arg
199 | set -- "$@" "$arg" # push replacement arg
200 | done
201 | fi
202 |
203 |
204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
206 |
207 | # Collect all arguments for the java command:
208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
209 | # and any embedded shellness will be escaped.
210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
211 | # treated as '${Hostname}' itself on the command line.
212 |
213 | set -- \
214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
215 | -classpath "$CLASSPATH" \
216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
217 | "$@"
218 |
219 | # Stop when "xargs" is not available.
220 | if ! command -v xargs >/dev/null 2>&1
221 | then
222 | die "xargs is not available"
223 | fi
224 |
225 | # Use "xargs" to parse quoted args.
226 | #
227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
228 | #
229 | # In Bash we could simply go:
230 | #
231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
232 | # set -- "${ARGS[@]}" "$@"
233 | #
234 | # but POSIX shell has neither arrays nor command substitution, so instead we
235 | # post-process each arg (as a line of input to sed) to backslash-escape any
236 | # character that might be a shell metacharacter, then use eval to reverse
237 | # that process (while maintaining the separation between arguments), and wrap
238 | # the whole thing up as a single "set" statement.
239 | #
240 | # This will of course break if any of these variables contains a newline or
241 | # an unmatched quote.
242 | #
243 |
244 | eval "set -- $(
245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
246 | xargs -n1 |
247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
248 | tr '\n' ' '
249 | )" '"$@"'
250 |
251 | exec "$JAVACMD" "$@"
252 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=top.yukonga.updater.kmp
3 | APP_NAME=Updater
4 |
--------------------------------------------------------------------------------
/iosApp/Podfile:
--------------------------------------------------------------------------------
1 | target 'iosApp' do
2 | use_frameworks!
3 | platform :ios, '14.1'
4 | pod 'composeApp', :path => '../composeApp'
5 | end
6 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuKongA/Updater-KMP/07507e63069ad5ecbc22a1d0fa82af4c645e9b62/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import UpdaterFramework
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | Main_iosKt.main()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
17 | .edgesIgnoringSafeArea(.all) // edge to edge
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.5.1
21 | CFBundleVersion
22 | 151
23 | LSRequiresIPhoneOS
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UIStatusBarStyle
37 |
38 | UISupportedInterfaceOrientations
39 |
40 | UIInterfaceOrientationPortrait
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UISupportedInterfaceOrientations~ipad
45 |
46 | UIInterfaceOrientationPortrait
47 | UIInterfaceOrientationPortraitUpsideDown
48 | UIInterfaceOrientationLandscapeLeft
49 | UIInterfaceOrientationLandscapeRight
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/iosApp/iosApp/iosApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct iosApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | rootProject.name = "Updater"
4 |
5 | pluginManagement {
6 | repositories {
7 | google {
8 | mavenContent {
9 | includeGroupAndSubgroups("androidx")
10 | includeGroupAndSubgroups("com.android")
11 | includeGroupAndSubgroups("com.google")
12 | }
13 | }
14 | mavenCentral()
15 | gradlePluginPortal()
16 | //maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
17 | }
18 | }
19 |
20 | dependencyResolutionManagement {
21 | repositories {
22 | google {
23 | mavenContent {
24 | includeGroupAndSubgroups("androidx")
25 | includeGroupAndSubgroups("com.android")
26 | includeGroupAndSubgroups("com.google")
27 | }
28 | }
29 | mavenCentral()
30 | //maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
31 | }
32 | }
33 |
34 | plugins {
35 | id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0")
36 | }
37 |
38 | include(":composeApp")
--------------------------------------------------------------------------------