├── .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 |