almost complete rework of ui up to module ui
This commit is contained in:
client
features
auth
client
common
client
content
binary
client
src
jsMain
kotlin
dev
inmo
postssystem
features
content
binary
client
src
commonMain
kotlin
dev
inmo
postssystem
features
content
client
text
client
src
jsMain
kotlin
dev
inmo
postssystem
features
content
text
files
client
roles
client
manager
common
src
commonMain
kotlin
dev
inmo
postssystem
features
roles
manager
common
users
client
gradle
publicators/simple/client
services/posts/client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
services
jsMain
kotlin
dev
inmo
postssystem
services
posts
client
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user