Skip to content

Commit

Permalink
Add WebAssembly support (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
YuKongA authored Jul 23, 2024
1 parent 67ada57 commit c603624
Show file tree
Hide file tree
Showing 24 changed files with 3,309 additions and 15 deletions.
25 changes: 25 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
import java.io.ByteArrayOutputStream
import java.util.Properties

Expand All @@ -21,6 +23,24 @@ val verCode = getVersionCode()
val xcf = XCFramework(appName + "Framework")

kotlin {

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
moduleName = "Updater"
browser {
commonWebpackConfig {
outputFileName = "Updater.js"
devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
static = (static ?: mutableListOf()).apply {
// Serve sources to debug inside browser
add(project.projectDir.path)
}
}
}
}
binaries.executable()
}

androidTarget()

jvm("desktop")
Expand Down Expand Up @@ -62,6 +82,11 @@ kotlin {
implementation(libs.cryptography.provider.apple)
implementation(libs.ktor.client.ios)
}
wasmJsMain.dependencies {
// Added
implementation(libs.cryptography.provider.webcrypto)
implementation(libs.ktor.client.js)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
// Added
Expand Down
4 changes: 3 additions & 1 deletion composeApp/src/androidMain/kotlin/Login.android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ actual fun md5Hash(input: String): String {
val md = MessageDigest.getInstance("MD5")
md.update(input.toByteArray())
return md.digest().joinToString("") { "%02x".format(it) }.uppercase()
}
}

actual fun isWasm(): Boolean = false
2 changes: 1 addition & 1 deletion composeApp/src/commonMain/kotlin/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ private fun TopAppBar(scrollBehavior: TopAppBarScrollBehavior, colorMode: Mutabl
},
actions = {
TuneDialog(colorMode)
LoginDialog(isLogin)
if (!isWasm()) LoginDialog(isLogin)
},
scrollBehavior = scrollBehavior
)
Expand Down
14 changes: 6 additions & 8 deletions composeApp/src/commonMain/kotlin/Info.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import androidx.compose.runtime.MutableState
import data.DataHelper
import io.ktor.client.call.body
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.post
import io.ktor.http.ContentType
import io.ktor.http.Parameters
import io.ktor.http.content.TextContent
import io.ktor.http.formUrlEncode
import io.ktor.utils.io.InternalAPI
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import misc.json
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

const val CN_RECOVERY_URL = "https://update.miui.com/updates/miotaV3.php"
const val INTL_RECOVERY_URL = "https://update.intl.miui.com/updates/miotaV3.php"
val CN_RECOVERY_URL = if (isWasm()) "https://updater.yukonga.top/updates/miotaV3.php" else "https://update.miui.com/updates/miotaV3.php"
val INTL_RECOVERY_URL = if (isWasm()) "https://updater.yukonga.top/intl-updates/miotaV3.php" else "https://update.intl.miui.com/updates/miotaV3.php"
var accountType = "CN"
var port = "1"
var security = ""
Expand Down Expand Up @@ -106,11 +104,11 @@ suspend fun getRecoveryRomInfo(
append("q", encryptedText)
append("t", serviceToken)
append("s", port)
}.formUrlEncode()
val recoveryUrl = if (accountType == "GL") INTL_RECOVERY_URL else CN_RECOVERY_URL
}
val recoveryUrl = if (accountType != "CN") INTL_RECOVERY_URL else CN_RECOVERY_URL
try {
val response = client.post(recoveryUrl) {
body = TextContent(parameters, ContentType.Application.FormUrlEncoded)
body = FormDataContent(parameters)
}
val requestedEncryptedText = response.body<String>()
client.close()
Expand Down
2 changes: 2 additions & 0 deletions composeApp/src/commonMain/kotlin/Login.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ expect fun httpClientPlatform(): HttpClient

expect fun md5Hash(input: String): String

expect fun isWasm(): Boolean

/**
* Login Xiaomi account.
*
Expand Down
4 changes: 3 additions & 1 deletion composeApp/src/commonMain/kotlin/misc/AppUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import data.DeviceInfoHelper
import data.RomInfoHelper
import getRecoveryRomInfo
import iconLink
import isWasm
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -173,10 +174,11 @@ fun handleRomInfo(
val changelog = changelogGroups.map { it.split("\n").drop(1).joinToString("\n") }
val iconNames = changelogGroups.map { it.split("\n").first() }

val iconMainLink = recoveryRomInfo.fileMirror!!.icon
val iconMainLink = if (isWasm()) "https://updater.yukonga.top/icon/10/" else recoveryRomInfo.fileMirror!!.icon
val iconNameLink = recoveryRomInfo.icon!!

val iconLinks = iconLink(iconNames, iconMainLink, iconNameLink)
println(iconLinks)

iconInfoData.value = iconNames.mapIndexed { index, iconName ->
DataHelper.IconInfoData(
Expand Down
3 changes: 2 additions & 1 deletion composeApp/src/commonMain/kotlin/ui/LoginCardView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import isWasm
import org.jetbrains.compose.resources.stringResource
import updaterkmp.composeapp.generated.resources.Res
import updaterkmp.composeapp.generated.resources.logged_in
Expand Down Expand Up @@ -64,7 +65,7 @@ fun LoginCardView(
)
Column(modifier = Modifier.padding(start = 20.dp)) {
Text(
text = account,
text = if (!isWasm()) account else "WebAssembly",
fontWeight = FontWeight.SemiBold,
fontSize = MaterialTheme.typography.titleMedium.fontSize
)
Expand Down
4 changes: 3 additions & 1 deletion composeApp/src/desktopMain/kotlin/Login.desktop.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ actual fun md5Hash(input: String): String {
val md = MessageDigest.getInstance("MD5")
md.update(input.toByteArray())
return md.digest().joinToString("") { "%02x".format(it) }.uppercase()
}
}

actual fun isWasm(): Boolean = false
4 changes: 3 additions & 1 deletion composeApp/src/iosMain/kotlin/Login.ios.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ actual fun md5Hash(input: String): String {
}
}
return result.joinToString("") { it.toInt().toString(16).padStart(2, '0') }.uppercase()
}
}

actual fun isWasm(): Boolean = false
6 changes: 6 additions & 0 deletions composeApp/src/wasmJsMain/kotlin/Copy.wasmJs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
actual fun copyToClipboard(text: String) {
writeToClipboard(text)
}

@JsFun("text => navigator.clipboard.writeText(text)")
external fun writeToClipboard(text: String)
10 changes: 10 additions & 0 deletions composeApp/src/wasmJsMain/kotlin/Crypto.wasmJs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import dev.whyoleg.cryptography.CryptographyProvider
import dev.whyoleg.cryptography.providers.webcrypto.WebCrypto

actual suspend fun provider() = CryptographyProvider.WebCrypto

actual fun ownEncrypt(string: String): Pair<String, String> = Pair(string, "")

actual fun ownDecrypt(encryptedText: String, encodedIv: String): String = encryptedText

actual fun generateKey() = Unit
11 changes: 11 additions & 0 deletions composeApp/src/wasmJsMain/kotlin/Download.wasmJs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import kotlinx.browser.document
import org.w3c.dom.HTMLAnchorElement

actual fun downloadToLocal(url: String, fileName: String) {
val anchorElement = document.createElement("a") as HTMLAnchorElement
anchorElement.href = url
anchorElement.download = fileName
document.body?.appendChild(anchorElement)
anchorElement.click()
document.body?.removeChild(anchorElement)
}
12 changes: 12 additions & 0 deletions composeApp/src/wasmJsMain/kotlin/Login.wasmJs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import io.ktor.client.HttpClient
import io.ktor.client.engine.js.Js

actual fun httpClientPlatform(): HttpClient {
return HttpClient(Js)
}

actual fun md5Hash(input: String): String {
TODO("Not yet implemented")
}

actual fun isWasm(): Boolean = true
13 changes: 13 additions & 0 deletions composeApp/src/wasmJsMain/kotlin/Preferences.wasmJs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import kotlinx.browser.window

actual fun perfSet(key: String, value: String) {
window.localStorage.setItem(key, value)
}

actual fun perfGet(key: String): String? {
return window.localStorage.getItem(key)
}

actual fun perfRemove(key: String) {
window.localStorage.removeItem(key)
}
6 changes: 6 additions & 0 deletions composeApp/src/wasmJsMain/kotlin/Theme.wasmJs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme

actual fun platformDarkColor(): ColorScheme = darkColorScheme()
actual fun platformLightColor(): ColorScheme = lightColorScheme()
4 changes: 4 additions & 0 deletions composeApp/src/wasmJsMain/kotlin/Toast.wasmJs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
actual fun useToast(): Boolean = false
actual fun isSupportMiuiStrongToast(): Boolean = false
actual fun showToast(message: String, duration: Long) {}
actual fun showExtToast(message: String, duration: Long) {}
66 changes: 66 additions & 0 deletions composeApp/src/wasmJsMain/kotlin/main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.platform.Font
import androidx.compose.ui.window.CanvasBasedWindow
import io.ktor.client.fetch.Response
import kotlinx.browser.window
import kotlinx.coroutines.await
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Int8Array
import kotlin.wasm.unsafe.UnsafeWasmMemoryApi
import kotlin.wasm.unsafe.withScopedMemoryAllocator

private const val MiSanVF = "./MiSansVF.woff2"

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
CanvasBasedWindow(canvasElementId = "Updater") {
val fontFamilyResolver = LocalFontFamilyResolver.current
val fontsLoaded = remember { mutableStateOf(false) }

if (fontsLoaded.value) {
App()
}

LaunchedEffect(Unit) {
val miSanVFBytes = loadRes(MiSanVF).toByteArray()
val fontFamily = FontFamily(Font("MiSans VF", miSanVFBytes))
fontFamilyResolver.preload(fontFamily)
fontsLoaded.value = true
}
}
}

suspend fun loadRes(url: String): ArrayBuffer {
return window.fetch(url).await<Response>().arrayBuffer().await()
}

fun ArrayBuffer.toByteArray(): ByteArray {
val source = Int8Array(this, 0, byteLength)
return jsInt8ArrayToKotlinByteArray(source)
}

@JsFun(
""" (src, size, dstAddr) => {
const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size);
mem8.set(src);
}
"""
)
external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int)

internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray {
val size = x.length

@OptIn(UnsafeWasmMemoryApi::class)
return withScopedMemoryAllocator { allocator ->
val memBuffer = allocator.allocate(size)
val dstAddress = memBuffer.address.toInt()
jsExportInt8ArrayToWasm(x, size, dstAddress)
ByteArray(size) { i -> (memBuffer + i).loadByte() }
}
}
Binary file not shown.
Binary file added composeApp/src/wasmJsMain/resources/favicon.ico
Binary file not shown.
17 changes: 17 additions & 0 deletions composeApp/src/wasmJsMain/resources/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Updater</title>
<link rel="icon" href="./favicon.ico">
<link rel="shortcut icon" href="./favicon.ico">
<script type="application/javascript" src="skiko.js"></script>
<script type="application/javascript" src="Updater.js"></script>
</head>

<body>
<canvas id="Updater"></canvas>
</body>

</html>
4 changes: 4 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ org.gradle.caching=true
android.nonTransitiveRClass=true
android.useAndroidX=true

# Kotlin/WASM
org.jetbrains.compose.experimental.wasm.enabled=true

# MPP
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.mpp.enableCInteropCommonization=true
Expand All @@ -18,6 +21,7 @@ kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
kotlin.incremental.useClasspathSnapshot=true

# Native
kotlin.native.ignoreDisabledTargets=true
kotlin.native.useEmbeddableCompilerJar=true
kotlin.native.binary.memoryModel=experimental

Expand Down
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ compose-plugin = "1.7.0-alpha01"
cryptography = "0.3.1"
hiddenapibypass = "4.3"
image-loader = "1.8.2"
kotlin = "2.0.0"
kotlin = "2.0.20-Beta2-48"
kotlinx-serialization-json = "1.7.1"
ktor-client = "3.0.0-beta-2"

Expand All @@ -17,9 +17,11 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
cryptography-core = { module = "dev.whyoleg.cryptography:cryptography-core", version.ref = "cryptography" }
cryptography-provider-jdk = { module = "dev.whyoleg.cryptography:cryptography-provider-jdk", version.ref = "cryptography" }
cryptography-provider-apple = { module = "dev.whyoleg.cryptography:cryptography-provider-apple", version.ref = "cryptography" }
cryptography-provider-webcrypto = { module = "dev.whyoleg.cryptography:cryptography-provider-webcrypto", version.ref = "cryptography" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-client" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor-client" }
ktor-client-ios = { module = "io.ktor:ktor-client-ios", version.ref = "ktor-client" }
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor-client" }

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
Expand Down
Loading

0 comments on commit c603624

Please sign in to comment.