include compose
This commit is contained in:
parent
4f96d2ce76
commit
0716035f0b
@ -2,6 +2,7 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
|
alias(libs.plugins.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
@ -29,6 +30,8 @@ kotlin {
|
|||||||
api "dev.inmo:micro_utils.fsm.common:$microutils_version"
|
api "dev.inmo:micro_utils.fsm.common:$microutils_version"
|
||||||
api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version"
|
api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version"
|
||||||
api "dev.inmo:micro_utils.crypto:$microutils_version"
|
api "dev.inmo:micro_utils.crypto:$microutils_version"
|
||||||
|
|
||||||
|
implementation compose.runtime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +43,8 @@ kotlin {
|
|||||||
|
|
||||||
jsMain {
|
jsMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-html:$kotlinx_html_version"
|
implementation compose.web.core
|
||||||
|
implementation libs.jsuikit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,28 @@
|
|||||||
package dev.inmo.postssystem.client.fsm.ui
|
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.ui.fsm.*
|
||||||
import dev.inmo.postssystem.client.utils.HTMLViewContainer
|
import dev.inmo.postssystem.client.utils.HTMLViewContainer
|
||||||
import dev.inmo.postssystem.features.auth.client.ui.*
|
import dev.inmo.postssystem.features.auth.client.ui.*
|
||||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||||
|
import dev.inmo.postssystem.client.utils.renderComposableAndLinkToContext
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.dom.clear
|
import kotlinx.dom.clear
|
||||||
import kotlinx.html.*
|
import org.jetbrains.compose.web.attributes.InputType
|
||||||
import kotlinx.html.dom.append
|
import org.jetbrains.compose.web.dom.Form
|
||||||
import kotlinx.html.js.form
|
|
||||||
import kotlinx.html.js.onClickFunction
|
|
||||||
import org.w3c.dom.*
|
import org.w3c.dom.*
|
||||||
|
|
||||||
class AuthView(
|
class AuthView(
|
||||||
private val viewModel: AuthUIViewModel,
|
private val viewModel: AuthUIViewModel,
|
||||||
private val uiScope: CoroutineScope
|
private val uiScope: CoroutineScope
|
||||||
) : JSView<AuthUIFSMState>() {
|
) : 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(
|
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
|
||||||
htmlElement: HTMLElement,
|
htmlElement: HTMLElement,
|
||||||
@ -38,42 +32,36 @@ class AuthView(
|
|||||||
val completion = CompletableDeferred<UIFSMState?>()
|
val completion = CompletableDeferred<UIFSMState?>()
|
||||||
htmlElement.clear()
|
htmlElement.clear()
|
||||||
|
|
||||||
htmlElement.append {
|
val usernameState = mutableStateOf("")
|
||||||
form(classes = "vertical_container") {
|
val passwordState = mutableStateOf("")
|
||||||
div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
|
val usernameDisabled = mutableStateOf(true)
|
||||||
input(type = InputType.text, classes = "mdl-textfield__input") {
|
val passwordDisabled = mutableStateOf(true)
|
||||||
id = "authUsername"
|
val authBtnDisabled = remember {
|
||||||
}
|
usernameState.value.isNotBlank() && passwordState.value.isNotBlank()
|
||||||
label(classes = "mdl-textfield__label") {
|
}
|
||||||
+"Имя пользователя"
|
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") {
|
DefaultButton("Authorise", UIKitButton.Type.Primary, disabled = authBtnDisabled) {
|
||||||
id = "authPassword"
|
val serverUrl = document.location ?.run { "$hostname:$port" }
|
||||||
}
|
if (serverUrl != null) {
|
||||||
label(classes = "mdl-textfield__label") {
|
uiScope.launchSafelyWithoutExceptions { viewModel.initAuth(serverUrl, usernameState.value, passwordState.value) }
|
||||||
+"Пароль"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,38 +70,27 @@ class AuthView(
|
|||||||
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
|
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is InitAuthUIState -> {
|
is InitAuthUIState -> {
|
||||||
usernameInput ?.removeAttribute("disabled")
|
usernameDisabled.value = false
|
||||||
passwordInput ?.removeAttribute("disabled")
|
passwordDisabled.value = false
|
||||||
authButton ?.removeAttribute("disabled")
|
|
||||||
errorBadge ?.apply {
|
errorText.value = when (it.showError) {
|
||||||
when (it.showError) {
|
ServerUnavailableAuthUIError -> "Сервер недоступен"
|
||||||
ServerUnavailableAuthUIError -> {
|
AuthIncorrectAuthUIError -> "Данные некорректны"
|
||||||
classList.remove("gone")
|
null -> null
|
||||||
innerText = "Сервер недоступен"
|
|
||||||
}
|
|
||||||
AuthIncorrectAuthUIError -> {
|
|
||||||
classList.remove("gone")
|
|
||||||
innerText = "Данные некорректны"
|
|
||||||
}
|
|
||||||
null -> classList.add("gone")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
progressBarDiv ?.classList ?.add("gone")
|
|
||||||
}
|
}
|
||||||
LoadingAuthUIState -> {
|
LoadingAuthUIState -> {
|
||||||
usernameInput ?.setAttribute("disabled", "")
|
usernameDisabled.value = true
|
||||||
passwordInput ?.setAttribute("disabled", "")
|
passwordDisabled.value = true
|
||||||
authButton ?.setAttribute("disabled", "")
|
errorText.value = null
|
||||||
errorBadge ?.classList ?.add("gone")
|
|
||||||
progressBarDiv ?.classList ?.remove("gone")
|
|
||||||
}
|
}
|
||||||
AuthorizedAuthUIState -> {
|
AuthorizedAuthUIState -> {
|
||||||
htmlElement.clear()
|
|
||||||
completion.complete(state.from)
|
completion.complete(state.from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return completion.await().also {
|
return completion.await().also {
|
||||||
|
composition.dispose()
|
||||||
viewJob.cancel()
|
viewJob.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
@ -17,12 +17,7 @@ value class ContentId(val string: String)
|
|||||||
* @see ContentSerializersModuleConfigurator.Element
|
* @see ContentSerializersModuleConfigurator.Element
|
||||||
* @see ContentSerializersModuleConfigurator
|
* @see ContentSerializersModuleConfigurator
|
||||||
*/
|
*/
|
||||||
sealed interface Content
|
interface Content
|
||||||
|
|
||||||
/**
|
|
||||||
* This type of content represents simple content which is easy to serialize/deserialize and to use
|
|
||||||
*/
|
|
||||||
interface SimpleContent : Content
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This type represents some binary data which can be sent with multipart and deserialized from it
|
* This type represents some binary data which can be sent with multipart and deserialized from it
|
||||||
|
@ -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 */
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,6 +3,8 @@ package dev.inmo.postssystem.features.files.common
|
|||||||
import com.benasher44.uuid.uuid4
|
import com.benasher44.uuid.uuid4
|
||||||
import dev.inmo.postssystem.features.files.common.storage.WriteFilesStorage
|
import dev.inmo.postssystem.features.files.common.storage.WriteFilesStorage
|
||||||
import dev.inmo.micro_utils.repos.*
|
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 kotlinx.coroutines.flow.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -36,7 +38,7 @@ class WriteDistFilesStorage(
|
|||||||
file = newId.file
|
file = newId.file
|
||||||
} while (file.exists())
|
} while (file.exists())
|
||||||
metasKeyValueRepo.set(newId, it.toMetaFileInfo())
|
metasKeyValueRepo.set(newId, it.toMetaFileInfo())
|
||||||
file.writeBytes(it.inputProvider())
|
it.inputProvider().copyTo(file.outputStream().asOutput())
|
||||||
FullFileInfoStorageWrapper(newId, it)
|
FullFileInfoStorageWrapper(newId, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,13 +54,15 @@ class WriteDistFilesStorage(
|
|||||||
override suspend fun update(
|
override suspend fun update(
|
||||||
id: FileId,
|
id: FileId,
|
||||||
value: FullFileInfo
|
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())
|
val result = FullFileInfoStorageWrapper(id, value.copy())
|
||||||
|
|
||||||
metasKeyValueRepo.set(id, value.toMetaFileInfo())
|
metasKeyValueRepo.set(id, value.toMetaFileInfo())
|
||||||
_updatedObjectsFlow.emit(result)
|
_updatedObjectsFlow.emit(result)
|
||||||
|
|
||||||
result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(
|
override suspend fun update(
|
||||||
|
26
gradle/libs.versions.toml
Normal file
26
gradle/libs.versions.toml
Normal 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" }
|
@ -70,3 +70,5 @@ includes.each { originalName ->
|
|||||||
project.projectDir = new File(projectDirectory)
|
project.projectDir = new File(projectDirectory)
|
||||||
println(project)
|
println(project)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableFeaturePreview("VERSION_CATALOGS")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package dev.inmo.postssystem.targets.telegram.publication.server
|
package dev.inmo.postssystem.targets.telegram.publication.server
|
||||||
|
|
||||||
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
|
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.content.text.common.TextContent
|
||||||
import dev.inmo.postssystem.features.publication.server.PublicationPost
|
import dev.inmo.postssystem.features.publication.server.PublicationPost
|
||||||
import dev.inmo.postssystem.features.publication.server.PublicationTarget
|
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.requests.send.media.*
|
||||||
import dev.inmo.tgbotapi.types.ChatId
|
import dev.inmo.tgbotapi.types.ChatId
|
||||||
import dev.inmo.tgbotapi.utils.StorageFile
|
import dev.inmo.tgbotapi.utils.StorageFile
|
||||||
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
|
import io.ktor.utils.io.core.readBytes
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
class PublicationTargetTelegram(
|
class PublicationTargetTelegram(
|
||||||
@ -22,9 +24,9 @@ class PublicationTargetTelegram(
|
|||||||
post.content.mapNotNull {
|
post.content.mapNotNull {
|
||||||
val content = it.content
|
val content = it.content
|
||||||
when (content) {
|
when (content) {
|
||||||
is DefaultBinaryContent -> {
|
is BinaryContent -> {
|
||||||
val storageFile by lazy {
|
val storageFile by lazy {
|
||||||
StorageFile(content.filename.name, content.bytesAllocator()).asMultipartFile()
|
StorageFile(content.filename.name, content.inputProvider().readBytes()).asMultipartFile()
|
||||||
}
|
}
|
||||||
when (content.mimeType) {
|
when (content.mimeType) {
|
||||||
is KnownMimeTypes.Image.Jpeg,
|
is KnownMimeTypes.Image.Jpeg,
|
||||||
|
Loading…
Reference in New Issue
Block a user