full reborn
This commit is contained in:
92
client/src/jsMain/kotlin/dev/inmo/postssystem/client/JSDI.kt
Normal file
92
client/src/jsMain/kotlin/dev/inmo/postssystem/client/JSDI.kt
Normal file
@@ -0,0 +1,92 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.client.fsm.ui.*
|
||||
import dev.inmo.postssystem.client.ui.*
|
||||
import dev.inmo.postssystem.client.ui.fsm.*
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMStateSerializer
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIModel
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIViewModel
|
||||
import dev.inmo.postssystem.features.auth.common.AuthTokenInfo
|
||||
import dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.localStorage
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.serializer
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.context.loadKoinModules
|
||||
import org.koin.dsl.module
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
val defaultTypedSerializer = TypedSerializer<Any>(
|
||||
"AuthTokenInfo" to AuthTokenInfo.serializer(),
|
||||
"String" to String.serializer(),
|
||||
"Int" to Int.serializer(),
|
||||
"Long" to Long.serializer(),
|
||||
"Short" to Short.serializer(),
|
||||
"Byte" to Byte.serializer(),
|
||||
"Float" to Float.serializer(),
|
||||
"Double" to Double.serializer(),
|
||||
"UIFSMState" to UIFSMStateSerializer
|
||||
)
|
||||
|
||||
fun baseKoin(): Koin {
|
||||
val anyToString: suspend Any.() -> String = {
|
||||
defaultSerialFormat.encodeToString(
|
||||
defaultTypedSerializer,
|
||||
this
|
||||
)
|
||||
}
|
||||
return baseKoin(
|
||||
CoroutineScope(
|
||||
Dispatchers.Default +
|
||||
ContextSafelyExceptionHandler { it.printStackTrace() } +
|
||||
CoroutineExceptionHandler { _, it -> it.printStackTrace() }
|
||||
),
|
||||
{
|
||||
CookiesKeyValueRepo.withMapper<String, Any, String, String>(
|
||||
{ this },
|
||||
{
|
||||
runCatching {
|
||||
anyToString()
|
||||
}.getOrElse {
|
||||
if (it is NoSuchElementException) {
|
||||
val name = this::class.simpleName!!
|
||||
defaultTypedSerializer.include(name, serializer())
|
||||
anyToString()
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
},
|
||||
{ this },
|
||||
{
|
||||
defaultSerialFormat.decodeFromString(
|
||||
defaultTypedSerializer,
|
||||
this
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
OneStateUIFSMStatesRepo(get(), localStorage)
|
||||
}
|
||||
) {
|
||||
first.apply {
|
||||
second.apply {
|
||||
loadKoinModules(
|
||||
module {
|
||||
factory { document.getElementById("main") as HTMLElement }
|
||||
|
||||
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
|
||||
factory { AuthUIViewModel(get()) }
|
||||
factory { AuthView(get(), get(UIScopeQualifier)) }
|
||||
}
|
||||
)
|
||||
strictlyOn<AuthUIFSMState>(get<AuthView>())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
client/src/jsMain/kotlin/dev/inmo/postssystem/client/Main.kt
Normal file
14
client/src/jsMain/kotlin/dev/inmo/postssystem/client/Main.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMQualifier
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import kotlinx.browser.window
|
||||
|
||||
fun main() {
|
||||
window.addEventListener("load", {
|
||||
val koin = baseKoin()
|
||||
val uiStatesMachine = koin.get<StatesMachine<UIFSMState>>(UIFSMQualifier)
|
||||
uiStatesMachine.start(koin.get())
|
||||
})
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.*
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
||||
import kotlinx.serialization.StringFormat
|
||||
import org.w3c.dom.*
|
||||
|
||||
class OneStateUIFSMStatesRepo(
|
||||
private val serialFormat: StringFormat,
|
||||
private val storage: Storage,
|
||||
private val initialState: UIFSMState = DefaultAuthUIFSMState
|
||||
) : DefaultStatesManagerRepo<UIFSMState> {
|
||||
private val String.storageKey
|
||||
get() = "${FSMStateSettingsFieldPrefix}$this"
|
||||
private val String.UIFSMState
|
||||
get() = runCatching {
|
||||
serialFormat.decodeFromString(UIFSMStateSerializer, this)
|
||||
}.onFailure { it.printStackTrace() }.getOrNull()
|
||||
|
||||
init {
|
||||
if (states().isEmpty()) {
|
||||
setState(initialState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setState(state: UIFSMState) {
|
||||
storage[state.context.storageKey] = serialFormat.encodeToString(UIFSMStateSerializer, state)
|
||||
}
|
||||
|
||||
override suspend fun getContextState(context: Any): UIFSMState? {
|
||||
return when (context) {
|
||||
is String -> storage[context.storageKey] ?.UIFSMState ?: return DefaultAuthUIFSMState
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun contains(context: Any): Boolean = when (context) {
|
||||
is String -> storage.get(context) ?.UIFSMState != null
|
||||
else -> super.contains(context)
|
||||
}
|
||||
|
||||
private fun states(): List<UIFSMState> = storage.iterator().asSequence().mapNotNull { (k, v) ->
|
||||
if (k.startsWith(FSMStateSettingsFieldPrefix)) {
|
||||
v.UIFSMState
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toList()
|
||||
|
||||
override suspend fun getStates(): List<UIFSMState> = states()
|
||||
|
||||
override suspend fun removeState(state: UIFSMState) {
|
||||
storage.removeItem((state.context as? String) ?.storageKey ?: return)
|
||||
}
|
||||
|
||||
override suspend fun set(state: UIFSMState) {
|
||||
setState(state)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FSMStateSettingsFieldPrefix = "UIFSMState_"
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
||||
import dev.inmo.micro_utils.pagination.utils.paginate
|
||||
import dev.inmo.micro_utils.pagination.utils.reverse
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import kotlinx.browser.localStorage
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.w3c.dom.get
|
||||
import org.w3c.dom.set
|
||||
|
||||
object CookiesKeyValueRepo : KeyValueRepo<String, String> {
|
||||
private val _onNewValue = MutableSharedFlow<Pair<String, String>>()
|
||||
private val _onValueRemoved = MutableSharedFlow<String>()
|
||||
override val onNewValue: Flow<Pair<String, String>> = _onNewValue.asSharedFlow()
|
||||
override val onValueRemoved: Flow<String> = _onValueRemoved.asSharedFlow()
|
||||
|
||||
override suspend fun contains(key: String): Boolean = localStorage.iterator().asSequence().any { it.first == key }
|
||||
|
||||
override suspend fun count(): Long = localStorage.length.toLong()
|
||||
|
||||
override suspend fun get(k: String): String? = localStorage[k]
|
||||
|
||||
override suspend fun keys(
|
||||
v: String,
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<String> = localStorage.iterator().asSequence().mapNotNull {
|
||||
if (it.second == v) it.first else null
|
||||
}.toList().let {
|
||||
it.paginate(
|
||||
if (reversed) {
|
||||
pagination.reverse(it.count())
|
||||
} else {
|
||||
pagination
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun keys(
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<String> = localStorage.iterator().asSequence().map { it.first }.toList().let {
|
||||
it.paginate(
|
||||
if (reversed) {
|
||||
pagination.reverse(it.count())
|
||||
} else {
|
||||
pagination
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun values(
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<String> = localStorage.iterator().asSequence().map { it.second }.toList().let {
|
||||
it.paginate(
|
||||
if (reversed) {
|
||||
pagination.reverse(it.count())
|
||||
} else {
|
||||
pagination
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun set(toSet: Map<String, String>) {
|
||||
toSet.forEach { (k, v) ->
|
||||
localStorage[k] = v
|
||||
_onNewValue.emit(k to v)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unset(toUnset: List<String>) {
|
||||
toUnset.forEach {
|
||||
localStorage[it] ?.let { _ ->
|
||||
localStorage.removeItem(it)
|
||||
_onValueRemoved.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import org.w3c.dom.Storage
|
||||
import org.w3c.dom.get
|
||||
|
||||
class StorageIterator(private val storage: Storage) : Iterator<Pair<String, String>> {
|
||||
private var index = 0
|
||||
|
||||
override fun hasNext(): Boolean = index < storage.length
|
||||
|
||||
override fun next(): Pair<String, String> {
|
||||
val k = storage.key(index) ?: error("Key for index $index was not found")
|
||||
val v = storage[k] ?: error("Key for index $index was not found")
|
||||
index++
|
||||
return k to v
|
||||
}
|
||||
}
|
||||
|
||||
operator fun Storage.iterator() = StorageIterator(this)
|
@@ -0,0 +1,120 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui
|
||||
|
||||
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 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.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,
|
||||
container: HTMLViewContainer,
|
||||
state: AuthUIFSMState
|
||||
): UIFSMState? {
|
||||
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") {
|
||||
+"Имя пользователя"
|
||||
}
|
||||
}
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
progressBarDiv ?.classList ?.add("gone")
|
||||
}
|
||||
LoadingAuthUIState -> {
|
||||
usernameInput ?.setAttribute("disabled", "")
|
||||
passwordInput ?.setAttribute("disabled", "")
|
||||
authButton ?.setAttribute("disabled", "")
|
||||
errorBadge ?.classList ?.add("gone")
|
||||
progressBarDiv ?.classList ?.remove("gone")
|
||||
}
|
||||
AuthorizedAuthUIState -> {
|
||||
htmlElement.clear()
|
||||
completion.complete(state.from)
|
||||
}
|
||||
}
|
||||
}
|
||||
return completion.await().also {
|
||||
viewJob.cancel()
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.Element
|
||||
|
||||
val mainContainer: Element
|
||||
get() = document.getElementById("main")!!
|
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMHandler
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
|
||||
import dev.inmo.postssystem.client.utils.HTMLViewContainer
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
abstract class JSView<T : UIFSMState> : UIFSMHandler<T> {
|
||||
open suspend fun StatesMachine<in UIFSMState>.safeHandleState(
|
||||
htmlElement: HTMLElement,
|
||||
container: HTMLViewContainer,
|
||||
state: T
|
||||
): UIFSMState? = null
|
||||
|
||||
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(state: T): UIFSMState? {
|
||||
return HTMLViewContainer.from(state.context) ?.let {
|
||||
safeHandleState(it.htmlElement ?: return null, it, state)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui.defaults
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.html.TagConsumer
|
||||
import kotlinx.html.js.button
|
||||
import kotlinx.html.js.onClickFunction
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
fun TagConsumer<HTMLElement>.addBackButton(
|
||||
completableDeferred: CompletableDeferred<UIFSMState>,
|
||||
stateToBack: UIFSMState
|
||||
) {
|
||||
button {
|
||||
+"Назад"
|
||||
onClickFunction = {
|
||||
completableDeferred.complete(stateToBack)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.dom.append
|
||||
import kotlinx.html.js.*
|
||||
import org.w3c.dom.*
|
||||
|
||||
object DialogHelper {
|
||||
fun createOneFieldDialog(
|
||||
title: String,
|
||||
hint: String,
|
||||
doneButtonText: String,
|
||||
closeButtonText: String,
|
||||
onClose: () -> Unit,
|
||||
onSubmit: (String) -> Unit
|
||||
): HTMLDialogElement {
|
||||
lateinit var dialogElement: HTMLDialogElement
|
||||
(document.getElementsByTagName("body").item(0) as? HTMLBodyElement) ?.append {
|
||||
dialogElement = dialog("mdl-dialog") {
|
||||
h4("mdl-dialog__title") {
|
||||
+title
|
||||
}
|
||||
val id = "form_${uuid4()}_text"
|
||||
div(classes = "mdl-dialog__content") {
|
||||
form("#") {
|
||||
div("mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
|
||||
input(InputType.text, classes = "mdl-textfield__input") {
|
||||
this.id = id
|
||||
}
|
||||
label(classes = "mdl-textfield__label") {
|
||||
+hint
|
||||
attributes["for"] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div(classes = "mdl-dialog__actions mdl-dialog__actions--full-width") {
|
||||
button(classes = "mdl-button", type = ButtonType.button) {
|
||||
+doneButtonText
|
||||
onClickFunction = {
|
||||
it.preventDefault()
|
||||
|
||||
val input = document.getElementById(id) as? HTMLInputElement
|
||||
input ?.value ?.let {
|
||||
onSubmit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
button(classes = "mdl-button", type = ButtonType.button) {
|
||||
+closeButtonText
|
||||
onClickFunction = {
|
||||
it.preventDefault()
|
||||
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dialogElement
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.FullFileInfo
|
||||
import dev.inmo.micro_utils.common.toArrayBuffer
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
import org.w3c.dom.url.URL
|
||||
import org.w3c.files.Blob
|
||||
|
||||
fun triggerDownloadFile(fullFileInfo: FullFileInfo) {
|
||||
val hiddenElement = document.createElement("a") as HTMLAnchorElement
|
||||
|
||||
val url = URL.createObjectURL(Blob(arrayOf(fullFileInfo.byteArrayAllocator().toArrayBuffer())))
|
||||
hiddenElement.href = url
|
||||
hiddenElement.target = "_blank"
|
||||
hiddenElement.download = fullFileInfo.name.name
|
||||
hiddenElement.click()
|
||||
}
|
@@ -0,0 +1,134 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.dom.clear
|
||||
import kotlinx.html.dom.append
|
||||
import kotlinx.html.id
|
||||
import kotlinx.html.js.*
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.events.Event
|
||||
|
||||
object HTMLViewsConstants {
|
||||
val mainContainer: MainHTMLViewContainer = MainHTMLViewContainer
|
||||
val processesContainer: DrawerHTMLViewContainer = DrawerHTMLViewContainer
|
||||
val toolsContainerId: ToolsHTMLViewContainer = ToolsHTMLViewContainer
|
||||
}
|
||||
|
||||
sealed interface HTMLViewContainer {
|
||||
val htmlElement: HTMLElement?
|
||||
get() = document.getElementById(id) as? HTMLElement
|
||||
val id: String
|
||||
|
||||
fun setIsLoading() {
|
||||
htmlElement ?.apply {
|
||||
clear()
|
||||
append {
|
||||
div("mdl-spinner mdl-js-spinner is-active")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(elementId: String) = when (elementId) {
|
||||
MainHTMLViewContainer.id -> MainHTMLViewContainer
|
||||
DrawerHTMLViewContainer.id -> DrawerHTMLViewContainer
|
||||
ToolsHTMLViewContainer.id -> ToolsHTMLViewContainer
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object MainHTMLViewContainer : HTMLViewContainer {
|
||||
override val id: String
|
||||
get() = "main"
|
||||
}
|
||||
|
||||
object DrawerHTMLViewContainer : HTMLViewContainer {
|
||||
data class DrawerAddButtonInfo(
|
||||
val text: String,
|
||||
val onAddButtonClick: (Event) -> Unit,
|
||||
)
|
||||
|
||||
override val id: String
|
||||
get() = "drawer"
|
||||
|
||||
private val titleElement: HTMLElement?
|
||||
get() = (document.getElementById("drawerTitle") ?:let {
|
||||
htmlElement ?.append {
|
||||
span("mdl-layout-title") {
|
||||
id = "drawerTitle"
|
||||
}
|
||||
} ?.first()
|
||||
}) as? HTMLElement
|
||||
var title: String?
|
||||
get() = titleElement ?.textContent
|
||||
set(value) {
|
||||
if (value == null) {
|
||||
titleElement ?.classList ?.add("gone")
|
||||
} else {
|
||||
val element = titleElement ?: return
|
||||
element.textContent = value
|
||||
element.classList.remove("gone")
|
||||
}
|
||||
}
|
||||
private val contentElement
|
||||
get() = (document.getElementById("drawerContent") ?:let {
|
||||
htmlElement ?.append {
|
||||
nav("mdl-navigation") {
|
||||
id = "drawerContent"
|
||||
}
|
||||
} ?.first()
|
||||
}) as? HTMLElement
|
||||
|
||||
fun <T> setListContent(
|
||||
title: String?,
|
||||
data: Iterable<T>,
|
||||
getText: (T) -> String?,
|
||||
addButtonInfo: DrawerAddButtonInfo? = null,
|
||||
onClick: (T) -> Unit
|
||||
) {
|
||||
this.title = title
|
||||
val contentElement = contentElement ?: return
|
||||
|
||||
contentElement.clear()
|
||||
contentElement.append {
|
||||
fun hideDrawer() {
|
||||
// Emulate clicking for hiding of drawer
|
||||
(document.getElementsByClassName("mdl-layout__obfuscator").item(0) as? HTMLElement) ?.click()
|
||||
}
|
||||
data.forEach {
|
||||
val elementTitle = getText(it) ?: return@forEach
|
||||
div("mdl-navigation__link") {
|
||||
+elementTitle
|
||||
onClickFunction = { _ ->
|
||||
onClick(it)
|
||||
hideDrawer()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (addButtonInfo != null) {
|
||||
button(classes = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent") {
|
||||
+addButtonInfo.text
|
||||
onClickFunction = {
|
||||
addButtonInfo.onAddButtonClick(it)
|
||||
hideDrawer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setIsLoading() {
|
||||
contentElement ?.apply {
|
||||
clear()
|
||||
append {
|
||||
div("mdl-spinner mdl-js-spinner is-active")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ToolsHTMLViewContainer : HTMLViewContainer {
|
||||
override val id: String
|
||||
get() = "tools"
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.FullFileInfo
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
|
||||
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.files.FileReader
|
||||
import org.w3c.files.get
|
||||
|
||||
fun uploadFileCallbackForHTMLInputChange(
|
||||
output: MutableStateFlow<FullFileInfo?>,
|
||||
scope: CoroutineScope
|
||||
): (Event) -> Unit = {
|
||||
(it.target as? HTMLInputElement) ?.apply {
|
||||
files ?.also { files ->
|
||||
files[0] ?.also { file ->
|
||||
scope.launch {
|
||||
val reader: FileReader = FileReader()
|
||||
|
||||
reader.onload = {
|
||||
val bytes = ((it.target.asDynamic()).result as ArrayBuffer).toByteArray()
|
||||
output.value = FullFileInfo(
|
||||
FileName(file.name),
|
||||
findBuiltinMimeType(file.type) ?: KnownMimeTypes.Any,
|
||||
bytes.asAllocator
|
||||
)
|
||||
Unit
|
||||
}
|
||||
|
||||
reader.readAsArrayBuffer(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user