include compose

This commit is contained in:
InsanusMokrassar 2022-01-22 20:19:50 +06:00
parent 4f96d2ce76
commit 0716035f0b
9 changed files with 119 additions and 106 deletions

View File

@ -2,6 +2,7 @@ plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.compose)
}
apply from: "$mppProjectWithSerializationPresetPath"
@ -29,6 +30,8 @@ kotlin {
api "dev.inmo:micro_utils.fsm.common:$microutils_version"
api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version"
api "dev.inmo:micro_utils.crypto:$microutils_version"
implementation compose.runtime
}
}
@ -40,7 +43,8 @@ kotlin {
jsMain {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-html:$kotlinx_html_version"
implementation compose.web.core
implementation libs.jsuikit
}
}
}

View File

@ -1,34 +1,28 @@
package dev.inmo.postssystem.client.fsm.ui
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import dev.inmo.jsuikit.elements.*
import dev.inmo.jsuikit.modifiers.UIKitButton
import dev.inmo.jsuikit.modifiers.UIKitMargin
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.postssystem.client.utils.HTMLViewContainer
import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.postssystem.client.utils.renderComposableAndLinkToContext
import kotlinx.browser.document
import kotlinx.coroutines.*
import kotlinx.dom.clear
import kotlinx.html.*
import kotlinx.html.dom.append
import kotlinx.html.js.form
import kotlinx.html.js.onClickFunction
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.dom.Form
import org.w3c.dom.*
class AuthView(
private val viewModel: AuthUIViewModel,
private val uiScope: CoroutineScope
) : JSView<AuthUIFSMState>() {
private val usernameInput
get() = document.getElementById("authUsername") as? HTMLInputElement
private val passwordInput
get() = document.getElementById("authPassword") as? HTMLInputElement
private val authButton
get() = document.getElementById("authButton")
private val errorBadge
get() = document.getElementById("errorBadge") as? HTMLElement
private val progressBarDiv
get() = document.getElementById("progressBar") as? HTMLDivElement
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement,
@ -38,42 +32,36 @@ class AuthView(
val completion = CompletableDeferred<UIFSMState?>()
htmlElement.clear()
htmlElement.append {
form(classes = "vertical_container") {
div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
input(type = InputType.text, classes = "mdl-textfield__input") {
id = "authUsername"
}
label(classes = "mdl-textfield__label") {
+"Имя пользователя"
}
val usernameState = mutableStateOf("")
val passwordState = mutableStateOf("")
val usernameDisabled = mutableStateOf(true)
val passwordDisabled = mutableStateOf(true)
val authBtnDisabled = remember {
usernameState.value.isNotBlank() && passwordState.value.isNotBlank()
}
val errorText = mutableStateOf<String?>(null)
val composition = renderComposableAndLinkToContext(htmlElement) {
Form {
TextField(
InputType.Text,
usernameState,
disabledState = usernameDisabled
)
TextField(
InputType.Password,
passwordState,
disabledState = passwordDisabled
)
if (errorText.value != null) {
Label.Error.draw(errorText.value.toString(), UIKitMargin.Small.Bottom)
}
div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
input(type = InputType.password, classes = "mdl-textfield__input") {
id = "authPassword"
}
label(classes = "mdl-textfield__label") {
+"Пароль"
}
}
div(classes = "mdl-progress mdl-js-progress") {
id = "progressBar"
}
span(classes = "material-icons mdl-badge mdl-badge--overlap gone") {
id = "errorBadge"
attributes["data-badge"] = "!"
}
button(classes = "mdl-button mdl-js-button mdl-button--raised") {
+"Авторизоваться"
id = "authButton"
onClickFunction = {
it.preventDefault()
val serverUrl = document.location ?.run { "$hostname:$port" }
val username = usernameInput ?.value
val password = passwordInput ?.value
if (serverUrl != null && username != null && password != null) {
uiScope.launchSafelyWithoutExceptions { viewModel.initAuth(serverUrl, username, password) }
}
DefaultButton("Authorise", UIKitButton.Type.Primary, disabled = authBtnDisabled) {
val serverUrl = document.location ?.run { "$hostname:$port" }
if (serverUrl != null) {
uiScope.launchSafelyWithoutExceptions { viewModel.initAuth(serverUrl, usernameState.value, passwordState.value) }
}
}
}
@ -82,38 +70,27 @@ class AuthView(
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
when (it) {
is InitAuthUIState -> {
usernameInput ?.removeAttribute("disabled")
passwordInput ?.removeAttribute("disabled")
authButton ?.removeAttribute("disabled")
errorBadge ?.apply {
when (it.showError) {
ServerUnavailableAuthUIError -> {
classList.remove("gone")
innerText = "Сервер недоступен"
}
AuthIncorrectAuthUIError -> {
classList.remove("gone")
innerText = "Данные некорректны"
}
null -> classList.add("gone")
}
usernameDisabled.value = false
passwordDisabled.value = false
errorText.value = when (it.showError) {
ServerUnavailableAuthUIError -> "Сервер недоступен"
AuthIncorrectAuthUIError -> "Данные некорректны"
null -> null
}
progressBarDiv ?.classList ?.add("gone")
}
LoadingAuthUIState -> {
usernameInput ?.setAttribute("disabled", "")
passwordInput ?.setAttribute("disabled", "")
authButton ?.setAttribute("disabled", "")
errorBadge ?.classList ?.add("gone")
progressBarDiv ?.classList ?.remove("gone")
usernameDisabled.value = true
passwordDisabled.value = true
errorText.value = null
}
AuthorizedAuthUIState -> {
htmlElement.clear()
completion.complete(state.from)
}
}
}
return completion.await().also {
composition.dispose()
viewJob.cancel()
}
}

View File

@ -0,0 +1,25 @@
package dev.inmo.postssystem.client.utils
import androidx.compose.runtime.*
import kotlinx.coroutines.*
import org.jetbrains.compose.web.dom.DOMScope
import org.w3c.dom.Element
import kotlin.coroutines.CoroutineContext
fun Composition.linkWithJob(job: Job) {
job.invokeOnCompletion {
this@linkWithJob.dispose()
}
}
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
suspend fun <TElement : Element> renderComposableAndLinkToContext(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithContext(
currentCoroutineContext()
)
}

View File

@ -17,12 +17,7 @@ value class ContentId(val string: String)
* @see ContentSerializersModuleConfigurator.Element
* @see ContentSerializersModuleConfigurator
*/
sealed interface Content
/**
* This type of content represents simple content which is easy to serialize/deserialize and to use
*/
interface SimpleContent : Content
interface Content
/**
* This type represents some binary data which can be sent with multipart and deserialized from it

View File

@ -1,22 +0,0 @@
package dev.inmo.postssystem.features.content.server
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.common.FileName
import io.ktor.http.content.PartData
import java.io.File
suspend fun PartData.loadContent() {
when (this) {
is PartData.FormItem -> TODO()
is PartData.FileItem -> {
val fileName = FileName(originalFileName ?: return null)
val downloadTo = File.createTempFile(
uuid4().toString(),
fileName.extension
)
headers["data"]
return
}
is PartData.BinaryItem -> return null/* Currently impossible state */
}
}

View File

@ -3,6 +3,8 @@ package dev.inmo.postssystem.features.files.common
import com.benasher44.uuid.uuid4
import dev.inmo.postssystem.features.files.common.storage.WriteFilesStorage
import dev.inmo.micro_utils.repos.*
import io.ktor.utils.io.core.copyTo
import io.ktor.utils.io.streams.asOutput
import kotlinx.coroutines.flow.*
import java.io.File
@ -36,7 +38,7 @@ class WriteDistFilesStorage(
file = newId.file
} while (file.exists())
metasKeyValueRepo.set(newId, it.toMetaFileInfo())
file.writeBytes(it.inputProvider())
it.inputProvider().copyTo(file.outputStream().asOutput())
FullFileInfoStorageWrapper(newId, it)
}
@ -52,13 +54,15 @@ class WriteDistFilesStorage(
override suspend fun update(
id: FileId,
value: FullFileInfo
): FullFileInfoStorageWrapper? = id.file.takeIf { it.exists() } ?.writeBytes(value.inputProvider()) ?.let {
): FullFileInfoStorageWrapper? {
val file = id.file.takeIf { it.exists() } ?: return null
value.inputProvider().copyTo(file.outputStream().asOutput())
val result = FullFileInfoStorageWrapper(id, value.copy())
metasKeyValueRepo.set(id, value.toMetaFileInfo())
_updatedObjectsFlow.emit(result)
result
return result
}
override suspend fun update(

26
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,26 @@
[versions]
jsuikit = "0.0.15"
compose = "1.0.1"
microutils = "0.9.4"
[libraries]
jsuikit = { module = "dev.inmo:kjsuikit", version.ref = "jsuikit" }
microutils-common = { module = "dev.inmo:micro_utils.common", version.ref = "microutils" }
microutils-pagination-common = { module = "dev.inmo:micro_utils.pagination.common", version.ref = "microutils" }
microutils-fsm-common = { module = "dev.inmo:micro_utils.fsm.common", version.ref = "microutils" }
microutils-fsm-repos-common = { module = "dev.inmo:micro_utils.fsm.common", version.ref = "microutils" }
microutils-crypto = { module = "dev.inmo:micro_utils.crypto", version.ref = "microutils" }
microutils-repos-common = { module = "dev.inmo:micro_utils.repos.common", version.ref = "microutils" }
microutils-repos-ktor-client = { module = "dev.inmo:micro_utils.repos.ktor.client", version.ref = "microutils" }
microutils-repos-ktor-server = { module = "dev.inmo:micro_utils.repos.ktor.server", version.ref = "microutils" }
microutils-repos-exposed = { module = "dev.inmo:micro_utils.repos.exposed", version.ref = "microutils" }
microutils-mimetypes = { module = "dev.inmo:micro_utils.mime_types", version.ref = "microutils" }
microutils-coroutines = { module = "dev.inmo:micro_utils.coroutines", version.ref = "microutils" }
microutils-serialization-typedserializer = { module = "dev.inmo:micro_utils.serialization.typed_serializer", version.ref = "microutils" }
[plugins]
compose = { id = "org.jetbrains.compose", version.ref = "compose" }

View File

@ -70,3 +70,5 @@ includes.each { originalName ->
project.projectDir = new File(projectDirectory)
println(project)
}
enableFeaturePreview("VERSION_CATALOGS")

View File

@ -1,7 +1,7 @@
package dev.inmo.postssystem.targets.telegram.publication.server
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
import dev.inmo.postssystem.features.content.binary.common.DefaultBinaryContent
import dev.inmo.postssystem.features.content.common.BinaryContent
import dev.inmo.postssystem.features.content.text.common.TextContent
import dev.inmo.postssystem.features.publication.server.PublicationPost
import dev.inmo.postssystem.features.publication.server.PublicationTarget
@ -12,6 +12,8 @@ import dev.inmo.tgbotapi.requests.send.SendTextMessage
import dev.inmo.tgbotapi.requests.send.media.*
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.utils.StorageFile
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.core.readBytes
import kotlinx.coroutines.delay
class PublicationTargetTelegram(
@ -22,9 +24,9 @@ class PublicationTargetTelegram(
post.content.mapNotNull {
val content = it.content
when (content) {
is DefaultBinaryContent -> {
is BinaryContent -> {
val storageFile by lazy {
StorageFile(content.filename.name, content.bytesAllocator()).asMultipartFile()
StorageFile(content.filename.name, content.inputProvider().readBytes()).asMultipartFile()
}
when (content.mimeType) {
is KnownMimeTypes.Image.Jpeg,