almost complete rework of ui up to module ui

This commit is contained in:
2022-03-23 23:21:02 +06:00
parent 72578f6b58
commit cb5de073fb
62 changed files with 499 additions and 471 deletions
client
features
auth
common
content
binary
client
src
jsMain
kotlin
dev
inmo
postssystem
features
client
src
commonMain
kotlin
dev
inmo
postssystem
features
content
text
client
src
jsMain
kotlin
dev
inmo
postssystem
features
files
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
files
roles
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
roles
manager
common
src
commonMain
kotlin
dev
inmo
postssystem
features
roles
manager
users
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
users
gradle
publicators/simple/client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
publicators
simple
services/posts/client
build.gradle
src
commonMain
jsMain
kotlin
dev
inmo
postssystem
services
targets/telegram/loader/client/src/commonMain/kotlin/dev/inmo/postssystem/targets/telegram/loader/client

@ -32,9 +32,6 @@ kotlin {
api project(":postssystem.features.content.binary.client")
api project(":postssystem.services.posts.client")
api libs.microutils.fsm.common
api libs.microutils.fsm.repos.common
api libs.microutils.crypto
implementation compose.runtime

@ -1,12 +0,0 @@
package dev.inmo.postssystem.client
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
import dev.inmo.micro_utils.repos.KeyValueRepo
class DBDropper(
private val repo: KeyValueRepo<String, Any>
) {
suspend operator fun invoke() {
repo.unset(repo.getAllByWithNextPaging { keys(it) })
}
}

@ -1,148 +0,0 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.postssystem.features.auth.client.installClientAuthenticator
import dev.inmo.postssystem.features.auth.common.*
import dev.inmo.postssystem.features.files.client.ClientReadFilesStorage
import dev.inmo.postssystem.features.files.common.storage.ReadFilesStorage
import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.roles.common.RolesStorage
import dev.inmo.postssystem.features.roles.client.ClientRolesStorage
import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRoleSerializer
import dev.inmo.postssystem.features.users.client.UsersStorageKtorClient
import dev.inmo.postssystem.features.users.common.ReadUsersStorage
import dev.inmo.postssystem.features.users.common.User
import dev.inmo.micro_utils.common.Either
import dev.inmo.micro_utils.coroutines.LinkedSupervisorScope
import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.postssystem.client.settings.DefaultSettings
import dev.inmo.postssystem.client.settings.Settings
import dev.inmo.postssystem.client.settings.auth.AuthSettings
import dev.inmo.postssystem.client.settings.auth.DefaultAuthSettings
import dev.inmo.postssystem.features.common.common.*
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.status.client.StatusFeatureClient
import dev.inmo.postssystem.publicators.simple.client.SimplePublicatorService
import dev.inmo.postssystem.publicators.simple.client.SimplePublicatorServiceClient
import dev.inmo.postssystem.services.posts.client.ClientPostsService
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.StringFormat
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import org.koin.core.Koin
import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.core.qualifier.*
import org.koin.core.scope.Scope
import org.koin.dsl.binds
import org.koin.dsl.module
val UIScopeQualifier = StringQualifier("CoroutineScopeUI")
val SettingsQualifier = StringQualifier("Settings")
val RolesQualifier = StringQualifier("Roles")
private val FSMHandlersBuilderQualifier = StringQualifier("FSMHandlersBuilder")
val defaultSerialFormat = Json {
ignoreUnknownKeys = true
}
/**
* Entrypoint for getting [org.koin.core.Koin] DI for the client
*
* @param repoFactory Factory for creating of [DefaultStatesManagerRepo] for [dev.inmo.postssystem.client.ui.fsm.UIFSM]
*/
fun baseKoin(
defaultScope: CoroutineScope,
settingsFactory: Scope.() -> KeyValueRepo<String, Any>,
repoFactory: Scope.() -> DefaultStatesManagerRepo<UIFSMState>,
handlersSetter: Pair<Scope, FSMBuilder<UIFSMState>>.() -> Unit
): Koin = startKoin {
modules(
module {
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator }
singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
single { SerializersModuleConfigurator(getAll()) }
single {
Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } }
}
}
single<StringFormat> { get<Json>() }
single(SettingsQualifier) { settingsFactory() }
single { DBDropper(get(SettingsQualifier)) }
single(FSMHandlersBuilderQualifier) { handlersSetter }
single { repoFactory() }
single { defaultScope }
single(UIScopeQualifier) { get<CoroutineScope>().LinkedSupervisorScope(Dispatchers.Main) }
single<StatesMachine<UIFSMState>>(UIFSMQualifier) { UIFSM(get()) { (this@single to this@UIFSM).apply(get(
FSMHandlersBuilderQualifier
)) } }
single<AuthSettings> { DefaultAuthSettings(get(SettingsQualifier), get(), koin, get()) }
single<Settings> { DefaultSettings(get()) }
AdditionalModules.modules.forEach {
it.apply { load() }
}
}
)
}.koin.apply {
loadModules(
listOf(
module { single<Koin> { this@apply } }
)
)
RolesManagerRoleSerializer // Just to activate it in JS client
}
fun getAuthorizedFeaturesDIModule(
serverUrl: String,
initialAuthKey: Either<AuthKey, AuthTokenInfo>,
onAuthKeyUpdated: suspend (AuthTokenInfo) -> Unit,
onUserRetrieved: suspend (User?) -> Unit,
onAuthKeyInvalidated: suspend () -> Unit
): Module {
val serverUrlQualifier = StringQualifier("serverUrl")
val credsQualifier = StringQualifier("creds")
return module {
single(createdAtStart = true) {
HttpClient {
installClientAuthenticator(serverUrl, get(), get(credsQualifier), onAuthKeyUpdated, onUserRetrieved, onAuthKeyInvalidated)
}
}
single(credsQualifier) { initialAuthKey }
single(serverUrlQualifier) { serverUrl }
single<BinaryFormat> {
Cbor {
serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } }
}
}
single { UnifiedRequester(get(), get()) }
single { StatusFeatureClient(get(serverUrlQualifier), get()) }
single<ReadFilesStorage> { ClientReadFilesStorage(get(serverUrlQualifier), get(), get()) }
single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) }
single<RolesStorage<Role>> { ClientRolesStorage(get(serverUrlQualifier), get(), Role.serializer()) }
single<PostsService> { ClientPostsService(get(serverUrlQualifier), get()) } binds arrayOf(
ReadPostsService::class,
WritePostsService::class
)
single<SimplePublicatorService> { SimplePublicatorServiceClient(get(serverUrlQualifier), get()) }
}
}

@ -1,8 +0,0 @@
package dev.inmo.postssystem.client.settings
import dev.inmo.postssystem.client.settings.auth.AuthSettings
data class DefaultSettings(
override val authSettings: AuthSettings
) : Settings

@ -1,12 +0,0 @@
package dev.inmo.postssystem.client.settings
import dev.inmo.postssystem.client.settings.auth.AuthSettings
import kotlinx.coroutines.flow.StateFlow
import org.koin.core.module.Module
interface Settings {
val authSettings: AuthSettings
val authorizedDIModule: StateFlow<Module?>
get() = authSettings.authorizedDIModule
}

@ -1,18 +0,0 @@
package dev.inmo.postssystem.client.settings.auth
import dev.inmo.postssystem.features.auth.client.ui.AuthUIError
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.users.common.User
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.StateFlow
import org.koin.core.module.Module
interface AuthSettings {
val authorizedDIModule: StateFlow<Module?>
val user: StateFlow<User?>
val userRoles: StateFlow<List<Role>>
val loadingJob: Job
suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError?
}

@ -1,118 +0,0 @@
package dev.inmo.postssystem.client.settings.auth
import dev.inmo.postssystem.client.DBDropper
import dev.inmo.postssystem.client.getAuthorizedFeaturesDIModule
import dev.inmo.postssystem.features.auth.client.AuthUnavailableException
import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.postssystem.features.auth.common.*
import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.roles.common.RolesStorage
import dev.inmo.postssystem.features.status.client.StatusFeatureClient
import dev.inmo.postssystem.features.users.common.User
import dev.inmo.micro_utils.common.Either
import dev.inmo.micro_utils.common.either
import dev.inmo.micro_utils.coroutines.plus
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koin.core.Koin
import org.koin.core.module.Module
data class DefaultAuthSettings(
private val repo: KeyValueRepo<String, Any>,
private val scope: CoroutineScope,
private val koin: Koin,
private val dbDropper: DBDropper
) : AuthSettings {
private val _authorizedDIModule = MutableStateFlow<Module?>(null)
override val authorizedDIModule: StateFlow<Module?> = _authorizedDIModule.asStateFlow()
private val _user = MutableStateFlow<User?>(null)
override val user: StateFlow<User?> = _user.asStateFlow()
private val _userRoles = MutableStateFlow<List<Role>>(emptyList())
override val userRoles: StateFlow<List<Role>> = _userRoles.asStateFlow()
private suspend fun getCurrentServerURL() = repo.get(SERVER_URL_FIELD) as? String
private suspend fun getCurrentUsername() = repo.get(USERNAME_FIELD) as? String
private suspend fun getCurrentToken() = repo.get(TOKEN_FIELD) as? AuthTokenInfo
override val loadingJob: Job = scope.launch {
val serverUrl = getCurrentServerURL() ?: return@launch
val token = getCurrentToken() ?: return@launch
updateModule(serverUrl, token.either())
}
val rolesUpdatingJob = (user + authorizedDIModule).subscribeSafelyWithoutExceptions(scope) {
val user = user.value
if (user == null || authorizedDIModule.value == null) {
_userRoles.value = emptyList()
} else {
_userRoles.value = koin.get<RolesStorage<Role>>().getRoles(user.id)
}
println(user)
println(userRoles.value)
}
override suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError? {
return runCatching {
if (getCurrentServerURL() != serverUrl || getCurrentUsername() != creds.username.string) {
dbDropper()
}
repo.set(SERVER_URL_FIELD, serverUrl)
repo.set(USERNAME_FIELD, creds.username.string)
repo.unset(TOKEN_FIELD)
updateModule(serverUrl, creds.either())
}.onFailure {
it.printStackTrace()
}.getOrThrow()
}
private suspend fun updateModule(
serverUrl: String,
initialAuthKey: Either<AuthKey, AuthTokenInfo>,
): AuthUIError? {
val currentModule = authorizedDIModule.value
val newModule = getAuthorizedFeaturesDIModule(
serverUrl,
initialAuthKey,
{
repo.set(TOKEN_FIELD, it)
},
{
_user.value = it
}
) {
repo.unset(SERVER_URL_FIELD, USERNAME_FIELD, TOKEN_FIELD)
_authorizedDIModule.value = null
_user.value = null
throw AuthUnavailableException
}
currentModule ?.let { koin.unloadModules(listOf(currentModule)) }
koin.loadModules(listOf(newModule))
val statusFeature = koin.get<StatusFeatureClient>()
val serverAvailable = statusFeature.checkServerStatus()
val authCorrect = serverAvailable && runCatching {
statusFeature.checkServerStatusWithAuth()
}.getOrElse { false }
if (!serverAvailable && !authCorrect) {
koin.unloadModules(listOf(newModule))
currentModule ?.let { koin.loadModules(listOf(currentModule)) }
}
return when {
!serverAvailable -> ServerUnavailableAuthUIError
!authCorrect -> AuthIncorrectAuthUIError
else -> {
_authorizedDIModule.value = newModule
null
}
}
}
companion object {
private const val SERVER_URL_FIELD = "AuthServerURL"
private const val USERNAME_FIELD = "AuthUsername"
private const val TOKEN_FIELD = "AuthToken"
}
}

@ -1,36 +0,0 @@
package dev.inmo.postssystem.client.ui
import dev.inmo.postssystem.client.settings.auth.AuthSettings
import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.common.common.AbstractUIModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class DefaultAuthUIModel(
private val scope: CoroutineScope,
private val authSettings: AuthSettings
) : AbstractUIModel<AuthUIState>(LoadingAuthUIState), AuthUIModel {
init {
scope.launch {
_currentState.value = LoadingAuthUIState
authSettings.loadingJob.join()
if (authSettings.authorizedDIModule.value == null) {
_currentState.value = DefaultInitAuthUIState
} else {
_currentState.value = AuthorizedAuthUIState
}
}
}
override suspend fun initAuth(serverUrl: String, creds: AuthCreds) {
_currentState.value = LoadingAuthUIState
val authError = authSettings.auth(serverUrl, creds)
if (authError == null) {
_currentState.value = AuthorizedAuthUIState
} else {
_currentState.value = InitAuthUIState(authError)
}
}
}

@ -1,17 +0,0 @@
package dev.inmo.postssystem.client.ui.fsm
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import org.koin.core.qualifier.StringQualifier
val UIFSMQualifier = StringQualifier("UIFSM")
fun UIFSM(
repo: DefaultStatesManagerRepo<UIFSMState>,
handlersSetter: FSMBuilder<UIFSMState>.() -> Unit
) = buildFSM<UIFSMState> {
statesManager = DefaultStatesManager(repo)
handlersSetter()
}

@ -1,27 +0,0 @@
package dev.inmo.postssystem.client.ui.fsm
import dev.inmo.postssystem.features.auth.client.AuthUnavailableException
import dev.inmo.micro_utils.fsm.common.*
interface UIFSMHandler<T : UIFSMState> : StatesHandler<T, UIFSMState> {
suspend fun StatesMachine<in UIFSMState>.safeHandleState(state: T): UIFSMState?
override suspend fun StatesMachine<in UIFSMState>.handleState(state: T): UIFSMState? {
return runCatching {
safeHandleState(state).also(::println)
}.getOrElse {
errorToNextStep(state, it) ?.let { return it } ?: throw it
}.also(::println)
}
suspend fun errorToNextStep(
currentState: T,
e: Throwable
): UIFSMState? = when (e) {
is AuthUnavailableException -> if (currentState is AuthUIFSMState) {
currentState
} else {
AuthUIFSMState(currentState)
}
else -> null
}
}

@ -1,30 +0,0 @@
package dev.inmo.postssystem.client.ui.fsm
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
import kotlinx.serialization.*
@Serializable(UIFSMStateSerializer::class)
sealed interface UIFSMState : State {
val from: UIFSMState?
get() = null
override val context: String
get() = "main"
}
object UIFSMStateSerializer : KSerializer<UIFSMState> by TypedSerializer(
"auth" to AuthUIFSMState.serializer(),
)
@Serializable
data class AuthUIFSMState(
override val from: UIFSMState? = CreatePostUIFSMState(),
override val context: String = "main"
) : UIFSMState
val DefaultAuthUIFSMState = AuthUIFSMState()
@Serializable
data class CreatePostUIFSMState(
override val from: UIFSMState? = null,
override val context: String = "main"
) : UIFSMState

@ -1,23 +1,23 @@
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.fsm.common.CheckableHandlerHolder
import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
import dev.inmo.postssystem.client.settings.auth.AuthSettings
import dev.inmo.postssystem.features.auth.client.settings.AuthSettings
import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.postssystem.features.common.common.baseKoin
import dev.inmo.postssystem.features.common.common.getAllDistinct
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMHandler
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMStateSerializer
import dev.inmo.postssystem.services.posts.client.ui.create.*
import kotlinx.browser.*
import kotlinx.coroutines.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import org.koin.core.Koin
import org.koin.core.context.loadKoinModules
@ -38,6 +38,9 @@ val defaultTypedSerializer = TypedSerializer<Any>(
"Double" to Double.serializer(),
"UIFSMState" to UIFSMStateSerializer
)
val defaultSerialFormat = Json {
ignoreUnknownKeys = true
}
fun baseKoin(): Koin {
val anyToString: suspend Any.() -> String = {
@ -81,66 +84,19 @@ fun baseKoin(): Koin {
JSUIFSMStatesRepo(window.history)
}
) {
val scope = first
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)) }
factory<PostCreateUIModel> { DefaultPostCreateUIModel(get(), get()) }
factory { PostCreateUIViewModel(get()) }
factory { PostCreateView(get(), getAllDistinct(), get(UIScopeQualifier)) }
}
)
strictlyOn<AuthUIFSMState>(get<AuthView>())
// Костыль, в JS на момент Пн дек 6 14:19:29 +06 2021 если использовать strictlyOn генерируются
// некорректные безымянные классы (у них отсутствует метод handleState)
class DefaultStateHandlerWrapper<T : UIFSMState>(
private val klass: KClass<out UIFSMHandler<T>>,
private val stateKlass: KClass<T>,
private val qualifier: Qualifier? = null,
private val parameters: ((T) -> ParametersHolder)? = null
) : CheckableHandlerHolder<UIFSMState, UIFSMState> {
override suspend fun StatesMachine<in UIFSMState>.handleState(state: UIFSMState): UIFSMState? {
@Suppress("UNCHECKED_CAST", "NAME_SHADOWING")
val state = state as T
return runCatching {
val authSettings = get<AuthSettings>()
authSettings.loadingJob.join()
if (authSettings.authorizedDIModule.value == null) {
error("Can't perform state $state: Auth module was not initialized")
} else {
get<UIFSMHandler<T>>(klass, qualifier, parameters ?.let { { it(state) } }).run {
handleState(state)
}
}
}.getOrElse { e ->
e.printStackTrace()
AuthUIFSMState(state)
}
getAllDistinct<UIFSMHandler.Registrator>().forEach {
with(it) {
include()
}
override suspend fun checkHandleable(state: UIFSMState): Boolean = stateKlass.isInstance(state)
}
inline fun <reified T : UIFSMState> registerHandler(
klass: KClass<out UIFSMHandler<T>>,
qualifier: Qualifier? = null,
parameters: ((T) -> ParametersHolder)? = null
) = add(
DefaultStateHandlerWrapper<T>(
klass,
T::class,
qualifier,
parameters
)
)
registerHandler(PostCreateView::class)
}
}
}

@ -1,7 +1,7 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
import kotlinx.browser.window
import kotlinx.serialization.StringFormat
import org.w3c.dom.*

@ -1,14 +1,13 @@
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 dev.inmo.postssystem.features.common.common.DefaultQualifiers
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
import kotlinx.browser.window
fun main() {
window.addEventListener("load", {
val koin = baseKoin()
val uiStatesMachine = koin.get<StatesMachine<UIFSMState>>(UIFSMQualifier)
val uiStatesMachine = koin.get<StatesMachine<UIFSMState>>(DefaultQualifiers.UIFSMQualifier)
uiStatesMachine.start(koin.get())
})
}

@ -1,109 +0,0 @@
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.*
import dev.inmo.jsuikit.utils.Attrs
import dev.inmo.postssystem.client.ui.fsm.*
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.*
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.*
class AuthView(
private val viewModel: AuthUIViewModel,
private val uiScope: CoroutineScope
) : JSView<AuthUIFSMState>() {
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement,
state: AuthUIFSMState
): UIFSMState? {
val completion = CompletableDeferred<UIFSMState?>()
val usernameState = mutableStateOf("")
val passwordState = mutableStateOf("")
val disabled = mutableStateOf(true)
val errorText = mutableStateOf<String?>(null)
val root = htmlElement.appendElement("div") {}
val composition = renderComposableAndLinkToContext(root) {
val authBtnDisabled = usernameState.value.isBlank() || passwordState.value.isBlank()
Flex(
UIKitFlex.Alignment.Horizontal.Center
) {
Card(
Attrs(UIKitText.Alignment.Horizontal.Center),
bodyAttrs = Attrs(UIKitWidth.Fixed.Medium),
) {
CardTitle { Text("Log in") }
if (errorText.value != null) {
CardBadge(Attrs(UIKitLabel.Error)) {
Text(errorText.value.toString())
}
}
TextField(
InputType.Text,
usernameState,
disabled,
"Username",
)
TextField(
InputType.Password,
passwordState,
disabled,
"Password"
)
DefaultButton("Authorise", UIKitButton.Type.Primary, UIKitMargin.Small, disabled = authBtnDisabled) {
it.nativeEvent.preventDefault()
val serverUrl = document.location ?.run { "$hostname:$port" }
if (serverUrl != null) {
uiScope.launchSafelyWithoutExceptions { viewModel.initAuth(serverUrl, usernameState.value, passwordState.value) }
}
}
}
}
}
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
when (it) {
is InitAuthUIState -> {
disabled.value = false
errorText.value = when (it.showError) {
ServerUnavailableAuthUIError -> "Server unavailable"
AuthIncorrectAuthUIError -> {
passwordState.value = ""
"Username or password is incorrect"
}
null -> null
}
}
LoadingAuthUIState -> {
disabled.value = true
errorText.value = null
}
AuthorizedAuthUIState -> {
completion.complete(state.from)
}
}
}
return completion.await().also {
composition.dispose()
viewJob.cancel()
}
}
}

@ -1,7 +0,0 @@
package dev.inmo.postssystem.client.fsm.ui
import kotlinx.browser.document
import org.w3c.dom.Element
val mainContainer: Element
get() = document.getElementById("main")!!

@ -1,18 +0,0 @@
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.micro_utils.fsm.common.StatesMachine
import kotlinx.browser.document
import org.w3c.dom.HTMLElement
abstract class JSView<T : UIFSMState> : UIFSMHandler<T> {
open suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement,
state: T
): UIFSMState? = null
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(state: T): UIFSMState? {
return safeHandleState(document.getElementById(state.context) as? HTMLElement ?: return null, state)
}
}

@ -1,82 +0,0 @@
package dev.inmo.postssystem.client.fsm.ui
import androidx.compose.runtime.*
import dev.inmo.jsuikit.elements.*
import dev.inmo.jsuikit.modifiers.*
import dev.inmo.jsuikit.utils.Attrs
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.postssystem.client.ui.fsm.CreatePostUIFSMState
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import dev.inmo.postssystem.client.utils.renderComposableAndLinkToContext
import dev.inmo.postssystem.features.content.client.ContentClientProvider
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.services.posts.client.ui.create.PostCreateUIViewModel
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.HTMLElement
class PostCreateView(
private val createPostCreateUIModel: PostCreateUIViewModel,
private val contentClientProviders: List<ContentClientProvider>,
private val uiScope: CoroutineScope
) : JSView<CreatePostUIFSMState>() {
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement,
state: CreatePostUIFSMState
): UIFSMState? {
val result = CompletableDeferred<UIFSMState?>()
val contentProvidersList = mutableStateListOf<Pair<ContentClientProvider, MutableState<Content?>>>()
renderComposableAndLinkToContext(htmlElement) {
Flex(
UIKitFlex.Alignment.Horizontal.Center
) {
Div ({ include(UIKitWidth.Fixed.XLarge) }) {
contentProvidersList.forEachIndexed { i, (provider, state) ->
Flex(UIKitWidth.Expand) {
provider.renderNewInstance(state)
DefaultButton("Remove") {
contentProvidersList.removeAt(i)
}
}
}
Label("Add content", Attrs.empty())
Dropdown {
DefaultNav {
contentClientProviders.forEach {
NavItemElement(
attributesCustomizer = {
onClick { _ ->
val newContentState = mutableStateOf<Content?>(null)
contentProvidersList.add(it to newContentState)
}
}
) {
Text(it.contentTypeNameForUser())
}
}
}
}
DefaultButton(
"Upload",
disabled = contentProvidersList.isEmpty()
) {
it.preventDefault()
uiScope.launchSafelyWithoutExceptions {
createPostCreateUIModel.create(
contentProvidersList.mapNotNull { it.second.value }
)
}
}
}
}
}
return result.await()
}
}

@ -1,19 +0,0 @@
package dev.inmo.postssystem.client.utils
import dev.inmo.postssystem.features.files.common.FullFileInfo
import dev.inmo.micro_utils.common.toArrayBuffer
import io.ktor.utils.io.core.readBytes
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.inputProvider().readBytes().toArrayBuffer())))
hiddenElement.href = url
hiddenElement.target = "_blank"
hiddenElement.download = fullFileInfo.name.name
hiddenElement.click()
}

@ -1,25 +0,0 @@
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()
)
}

@ -1,77 +0,0 @@
package dev.inmo.postssystem.client.utils
import androidx.compose.runtime.MutableState
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 dev.inmo.postssystem.features.common.common.BytesBasedInputProvider
import io.ktor.utils.io.core.ByteReadPacket
import kotlinx.coroutines.flow.MutableStateFlow
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(
onSet: (FullFileInfo) -> Unit
): (Event) -> Unit = {
(it.target as? HTMLInputElement) ?.apply {
files ?.also { files ->
files[0] ?.also { file ->
val reader: FileReader = FileReader()
reader.onload = {
val bytes = ((it.target.asDynamic()).result as ArrayBuffer).toByteArray()
onSet(
FullFileInfo(
FileName(file.name),
findBuiltinMimeType(file.type) ?: KnownMimeTypes.Any,
BytesBasedInputProvider(bytes)
)
)
}
reader.readAsArrayBuffer(file)
}
}
}
}
fun fileCallbackForHTMLInputChange(
onSet: (MPPFile) -> Unit
): (Event) -> Unit = {
(it.target as? HTMLInputElement) ?.apply {
files ?.also { files ->
files[0] ?.also { file ->
onSet(file)
}
}
}
}
fun uploadFileCallbackForHTMLInputChange(
output: MutableState<FullFileInfo?>
): (Event) -> Unit = uploadFileCallbackForHTMLInputChange {
output.value = it
}
fun uploadFileCallbackForHTMLInputChange(
output: MutableStateFlow<FullFileInfo?>
): (Event) -> Unit = uploadFileCallbackForHTMLInputChange {
output.value = it
}
fun fileCallbackForHTMLInputChange(
output: MutableState<MPPFile?>
): (Event) -> Unit = fileCallbackForHTMLInputChange {
output.value = it
}
fun fileCallbackForHTMLInputChange(
output: MutableStateFlow<MPPFile?>
): (Event) -> Unit = fileCallbackForHTMLInputChange {
output.value = it
}