almost complete rework of ui up to module ui
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user