diff --git a/client/build.gradle b/client/build.gradle index 4437d3e0..876879ab 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -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 } } } diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/AuthView.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/AuthView.kt index 71580870..ff80d2b4 100644 --- a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/AuthView.kt +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/AuthView.kt @@ -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() { - 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.safeHandleState( htmlElement: HTMLElement, @@ -38,42 +32,36 @@ class AuthView( val completion = CompletableDeferred() 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(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() } } diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/LinkCompositionToJob.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/LinkCompositionToJob.kt new file mode 100644 index 00000000..9e8a9522 --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/LinkCompositionToJob.kt @@ -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 renderComposableAndLinkToContext( + root: TElement, + monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, + content: @Composable DOMScope.() -> Unit +): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { + linkWithContext( + currentCoroutineContext() + ) +} diff --git a/features/content/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/common/Content.kt b/features/content/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/common/Content.kt index 254c83a0..7ed3a946 100644 --- a/features/content/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/common/Content.kt +++ b/features/content/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/common/Content.kt @@ -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 diff --git a/features/content/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/server/PartDataContentDownloading.kt b/features/content/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/server/PartDataContentDownloading.kt deleted file mode 100644 index aeeefaed..00000000 --- a/features/content/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/server/PartDataContentDownloading.kt +++ /dev/null @@ -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 */ - } -} diff --git a/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/WriteDistFilesStorage.kt b/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/WriteDistFilesStorage.kt index a52aa52b..1848cc82 100644 --- a/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/WriteDistFilesStorage.kt +++ b/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/WriteDistFilesStorage.kt @@ -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( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..0f241f20 --- /dev/null +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/settings.gradle b/settings.gradle index 305d5817..6a1f98c8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -70,3 +70,5 @@ includes.each { originalName -> project.projectDir = new File(projectDirectory) println(project) } + +enableFeaturePreview("VERSION_CATALOGS") diff --git a/targets/telegram/publication/server/src/jvmMain/kotlin/dev/inmo/postssystem/targets/telegram/publication/server/PublicationTargetTelegram.kt b/targets/telegram/publication/server/src/jvmMain/kotlin/dev/inmo/postssystem/targets/telegram/publication/server/PublicationTargetTelegram.kt index 91285c23..3fb7dfb6 100644 --- a/targets/telegram/publication/server/src/jvmMain/kotlin/dev/inmo/postssystem/targets/telegram/publication/server/PublicationTargetTelegram.kt +++ b/targets/telegram/publication/server/src/jvmMain/kotlin/dev/inmo/postssystem/targets/telegram/publication/server/PublicationTargetTelegram.kt @@ -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,