full reborn

This commit is contained in:
2021-11-24 13:52:27 +06:00
parent 0ac6b0a4df
commit 6a6a197041
246 changed files with 4327 additions and 6952 deletions

View File

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

View File

@@ -0,0 +1,105 @@
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.ClientFilesStorage
import dev.inmo.postssystem.features.files.common.storage.FilesStorage
import dev.inmo.postssystem.features.roles.common.UserRole
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
import dev.inmo.postssystem.features.roles.client.ClientUsersRolesStorage
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.ktor.common.standardKtorSerialFormat
import dev.inmo.micro_utils.repos.KeyValueRepo
import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.StringFormat
import kotlinx.serialization.json.Json
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.module
val UIScopeQualifier = StringQualifier("CoroutineScopeUI")
val SettingsQualifier = StringQualifier("Settings")
val UserRolesQualifier = StringQualifier("UserRoles")
private val DBDropperQualifier = StringQualifier("DBDropper")
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 {
single<StringFormat> { defaultSerialFormat }
single(SettingsQualifier) { settingsFactory() }
single<DBDropper>(DBDropperQualifier) { 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
)) } }
}
)
}.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> { standardKtorSerialFormat }
single { UnifiedRequester(get(), get()) }
single<FilesStorage> { ClientFilesStorage(get(serverUrlQualifier), get(), get()) }
single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) }
single<UsersRolesStorage<UserRole>> { ClientUsersRolesStorage(get(serverUrlQualifier), get(), UserRole.serializer()) }
}
}

View File

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

View File

@@ -0,0 +1,12 @@
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
}

View File

@@ -0,0 +1,18 @@
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.UserRole
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<UserRole>>
val loadingJob: Job
suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError?
}

View File

@@ -0,0 +1,118 @@
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.UserRole
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
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<UserRole>>(emptyList())
override val userRoles: StateFlow<List<UserRole>> = _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<UsersRolesStorage<UserRole>>().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"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
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
}
}

View File

@@ -0,0 +1,24 @@
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? = null,
override val context: String = "main"
) : UIFSMState
val DefaultAuthUIFSMState = AuthUIFSMState()

View File

@@ -0,0 +1,92 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.fsm.ui.*
import dev.inmo.postssystem.client.ui.*
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.postssystem.client.ui.fsm.UIFSMStateSerializer
import dev.inmo.postssystem.features.auth.client.ui.AuthUIModel
import dev.inmo.postssystem.features.auth.client.ui.AuthUIViewModel
import dev.inmo.postssystem.features.auth.common.AuthTokenInfo
import dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
import kotlinx.browser.document
import kotlinx.browser.localStorage
import kotlinx.coroutines.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.serializer
import org.koin.core.Koin
import org.koin.core.context.loadKoinModules
import org.koin.dsl.module
import org.w3c.dom.HTMLElement
val defaultTypedSerializer = TypedSerializer<Any>(
"AuthTokenInfo" to AuthTokenInfo.serializer(),
"String" to String.serializer(),
"Int" to Int.serializer(),
"Long" to Long.serializer(),
"Short" to Short.serializer(),
"Byte" to Byte.serializer(),
"Float" to Float.serializer(),
"Double" to Double.serializer(),
"UIFSMState" to UIFSMStateSerializer
)
fun baseKoin(): Koin {
val anyToString: suspend Any.() -> String = {
defaultSerialFormat.encodeToString(
defaultTypedSerializer,
this
)
}
return baseKoin(
CoroutineScope(
Dispatchers.Default +
ContextSafelyExceptionHandler { it.printStackTrace() } +
CoroutineExceptionHandler { _, it -> it.printStackTrace() }
),
{
CookiesKeyValueRepo.withMapper<String, Any, String, String>(
{ this },
{
runCatching {
anyToString()
}.getOrElse {
if (it is NoSuchElementException) {
val name = this::class.simpleName!!
defaultTypedSerializer.include(name, serializer())
anyToString()
} else {
throw it
}
}
},
{ this },
{
defaultSerialFormat.decodeFromString(
defaultTypedSerializer,
this
)
}
)
},
{
OneStateUIFSMStatesRepo(get(), localStorage)
}
) {
first.apply {
second.apply {
loadKoinModules(
module {
factory { document.getElementById("main") as HTMLElement }
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
factory { AuthUIViewModel(get()) }
factory { AuthView(get(), get(UIScopeQualifier)) }
}
)
strictlyOn<AuthUIFSMState>(get<AuthView>())
}
}
}
}

View File

@@ -0,0 +1,14 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.ui.fsm.UIFSMQualifier
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import dev.inmo.micro_utils.fsm.common.StatesMachine
import kotlinx.browser.window
fun main() {
window.addEventListener("load", {
val koin = baseKoin()
val uiStatesMachine = koin.get<StatesMachine<UIFSMState>>(UIFSMQualifier)
uiStatesMachine.start(koin.get())
})
}

View File

@@ -0,0 +1,63 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import kotlinx.serialization.StringFormat
import org.w3c.dom.*
class OneStateUIFSMStatesRepo(
private val serialFormat: StringFormat,
private val storage: Storage,
private val initialState: UIFSMState = DefaultAuthUIFSMState
) : DefaultStatesManagerRepo<UIFSMState> {
private val String.storageKey
get() = "${FSMStateSettingsFieldPrefix}$this"
private val String.UIFSMState
get() = runCatching {
serialFormat.decodeFromString(UIFSMStateSerializer, this)
}.onFailure { it.printStackTrace() }.getOrNull()
init {
if (states().isEmpty()) {
setState(initialState)
}
}
private fun setState(state: UIFSMState) {
storage[state.context.storageKey] = serialFormat.encodeToString(UIFSMStateSerializer, state)
}
override suspend fun getContextState(context: Any): UIFSMState? {
return when (context) {
is String -> storage[context.storageKey] ?.UIFSMState ?: return DefaultAuthUIFSMState
else -> null
}
}
override suspend fun contains(context: Any): Boolean = when (context) {
is String -> storage.get(context) ?.UIFSMState != null
else -> super.contains(context)
}
private fun states(): List<UIFSMState> = storage.iterator().asSequence().mapNotNull { (k, v) ->
if (k.startsWith(FSMStateSettingsFieldPrefix)) {
v.UIFSMState
} else {
null
}
}.toList()
override suspend fun getStates(): List<UIFSMState> = states()
override suspend fun removeState(state: UIFSMState) {
storage.removeItem((state.context as? String) ?.storageKey ?: return)
}
override suspend fun set(state: UIFSMState) {
setState(state)
}
companion object {
private const val FSMStateSettingsFieldPrefix = "UIFSMState_"
}
}

View File

@@ -0,0 +1,82 @@
package dev.inmo.postssystem.client
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.pagination.utils.reverse
import dev.inmo.micro_utils.repos.KeyValueRepo
import kotlinx.browser.localStorage
import kotlinx.coroutines.flow.*
import org.w3c.dom.get
import org.w3c.dom.set
object CookiesKeyValueRepo : KeyValueRepo<String, String> {
private val _onNewValue = MutableSharedFlow<Pair<String, String>>()
private val _onValueRemoved = MutableSharedFlow<String>()
override val onNewValue: Flow<Pair<String, String>> = _onNewValue.asSharedFlow()
override val onValueRemoved: Flow<String> = _onValueRemoved.asSharedFlow()
override suspend fun contains(key: String): Boolean = localStorage.iterator().asSequence().any { it.first == key }
override suspend fun count(): Long = localStorage.length.toLong()
override suspend fun get(k: String): String? = localStorage[k]
override suspend fun keys(
v: String,
pagination: Pagination,
reversed: Boolean
): PaginationResult<String> = localStorage.iterator().asSequence().mapNotNull {
if (it.second == v) it.first else null
}.toList().let {
it.paginate(
if (reversed) {
pagination.reverse(it.count())
} else {
pagination
}
)
}
override suspend fun keys(
pagination: Pagination,
reversed: Boolean
): PaginationResult<String> = localStorage.iterator().asSequence().map { it.first }.toList().let {
it.paginate(
if (reversed) {
pagination.reverse(it.count())
} else {
pagination
}
)
}
override suspend fun values(
pagination: Pagination,
reversed: Boolean
): PaginationResult<String> = localStorage.iterator().asSequence().map { it.second }.toList().let {
it.paginate(
if (reversed) {
pagination.reverse(it.count())
} else {
pagination
}
)
}
override suspend fun set(toSet: Map<String, String>) {
toSet.forEach { (k, v) ->
localStorage[k] = v
_onNewValue.emit(k to v)
}
}
override suspend fun unset(toUnset: List<String>) {
toUnset.forEach {
localStorage[it] ?.let { _ ->
localStorage.removeItem(it)
_onValueRemoved.emit(it)
}
}
}
}

View File

@@ -0,0 +1,19 @@
package dev.inmo.postssystem.client
import org.w3c.dom.Storage
import org.w3c.dom.get
class StorageIterator(private val storage: Storage) : Iterator<Pair<String, String>> {
private var index = 0
override fun hasNext(): Boolean = index < storage.length
override fun next(): Pair<String, String> {
val k = storage.key(index) ?: error("Key for index $index was not found")
val v = storage[k] ?: error("Key for index $index was not found")
index++
return k to v
}
}
operator fun Storage.iterator() = StorageIterator(this)

View File

@@ -0,0 +1,120 @@
package dev.inmo.postssystem.client.fsm.ui
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.postssystem.client.utils.HTMLViewContainer
import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.fsm.common.StatesMachine
import kotlinx.browser.document
import kotlinx.coroutines.*
import kotlinx.dom.clear
import kotlinx.html.*
import kotlinx.html.dom.append
import kotlinx.html.js.form
import kotlinx.html.js.onClickFunction
import org.w3c.dom.*
class AuthView(
private val viewModel: AuthUIViewModel,
private val uiScope: CoroutineScope
) : JSView<AuthUIFSMState>() {
private val usernameInput
get() = document.getElementById("authUsername") as? HTMLInputElement
private val passwordInput
get() = document.getElementById("authPassword") as? HTMLInputElement
private val authButton
get() = document.getElementById("authButton")
private val errorBadge
get() = document.getElementById("errorBadge") as? HTMLElement
private val progressBarDiv
get() = document.getElementById("progressBar") as? HTMLDivElement
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement,
container: HTMLViewContainer,
state: AuthUIFSMState
): UIFSMState? {
val completion = CompletableDeferred<UIFSMState?>()
htmlElement.clear()
htmlElement.append {
form(classes = "vertical_container") {
div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
input(type = InputType.text, classes = "mdl-textfield__input") {
id = "authUsername"
}
label(classes = "mdl-textfield__label") {
+"Имя пользователя"
}
}
div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
input(type = InputType.password, classes = "mdl-textfield__input") {
id = "authPassword"
}
label(classes = "mdl-textfield__label") {
+"Пароль"
}
}
div(classes = "mdl-progress mdl-js-progress") {
id = "progressBar"
}
span(classes = "material-icons mdl-badge mdl-badge--overlap gone") {
id = "errorBadge"
attributes["data-badge"] = "!"
}
button(classes = "mdl-button mdl-js-button mdl-button--raised") {
+"Авторизоваться"
id = "authButton"
onClickFunction = {
it.preventDefault()
val serverUrl = document.location ?.run { "$hostname:$port" }
val username = usernameInput ?.value
val password = passwordInput ?.value
if (serverUrl != null && username != null && password != null) {
uiScope.launchSafelyWithoutExceptions { viewModel.initAuth(serverUrl, username, password) }
}
}
}
}
}
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
when (it) {
is InitAuthUIState -> {
usernameInput ?.removeAttribute("disabled")
passwordInput ?.removeAttribute("disabled")
authButton ?.removeAttribute("disabled")
errorBadge ?.apply {
when (it.showError) {
ServerUnavailableAuthUIError -> {
classList.remove("gone")
innerText = "Сервер недоступен"
}
AuthIncorrectAuthUIError -> {
classList.remove("gone")
innerText = "Данные некорректны"
}
null -> classList.add("gone")
}
}
progressBarDiv ?.classList ?.add("gone")
}
LoadingAuthUIState -> {
usernameInput ?.setAttribute("disabled", "")
passwordInput ?.setAttribute("disabled", "")
authButton ?.setAttribute("disabled", "")
errorBadge ?.classList ?.add("gone")
progressBarDiv ?.classList ?.remove("gone")
}
AuthorizedAuthUIState -> {
htmlElement.clear()
completion.complete(state.from)
}
}
}
return completion.await().also {
viewJob.cancel()
}
}
}

View File

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

View File

@@ -0,0 +1,21 @@
package dev.inmo.postssystem.client.fsm.ui
import dev.inmo.postssystem.client.ui.fsm.UIFSMHandler
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import dev.inmo.postssystem.client.utils.HTMLViewContainer
import dev.inmo.micro_utils.fsm.common.StatesMachine
import org.w3c.dom.HTMLElement
abstract class JSView<T : UIFSMState> : UIFSMHandler<T> {
open suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement,
container: HTMLViewContainer,
state: T
): UIFSMState? = null
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(state: T): UIFSMState? {
return HTMLViewContainer.from(state.context) ?.let {
safeHandleState(it.htmlElement ?: return null, it, state)
}
}
}

View File

@@ -0,0 +1,20 @@
package dev.inmo.postssystem.client.fsm.ui.defaults
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import kotlinx.coroutines.CompletableDeferred
import kotlinx.html.TagConsumer
import kotlinx.html.js.button
import kotlinx.html.js.onClickFunction
import org.w3c.dom.HTMLElement
fun TagConsumer<HTMLElement>.addBackButton(
completableDeferred: CompletableDeferred<UIFSMState>,
stateToBack: UIFSMState
) {
button {
+"Назад"
onClickFunction = {
completableDeferred.complete(stateToBack)
}
}
}

View File

@@ -0,0 +1,64 @@
package dev.inmo.postssystem.client.utils
import com.benasher44.uuid.uuid4
import kotlinx.browser.document
import kotlinx.html.*
import kotlinx.html.dom.append
import kotlinx.html.js.*
import org.w3c.dom.*
object DialogHelper {
fun createOneFieldDialog(
title: String,
hint: String,
doneButtonText: String,
closeButtonText: String,
onClose: () -> Unit,
onSubmit: (String) -> Unit
): HTMLDialogElement {
lateinit var dialogElement: HTMLDialogElement
(document.getElementsByTagName("body").item(0) as? HTMLBodyElement) ?.append {
dialogElement = dialog("mdl-dialog") {
h4("mdl-dialog__title") {
+title
}
val id = "form_${uuid4()}_text"
div(classes = "mdl-dialog__content") {
form("#") {
div("mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
input(InputType.text, classes = "mdl-textfield__input") {
this.id = id
}
label(classes = "mdl-textfield__label") {
+hint
attributes["for"] = id
}
}
}
}
div(classes = "mdl-dialog__actions mdl-dialog__actions--full-width") {
button(classes = "mdl-button", type = ButtonType.button) {
+doneButtonText
onClickFunction = {
it.preventDefault()
val input = document.getElementById(id) as? HTMLInputElement
input ?.value ?.let {
onSubmit(it)
}
}
}
button(classes = "mdl-button", type = ButtonType.button) {
+closeButtonText
onClickFunction = {
it.preventDefault()
onClose()
}
}
}
}
}
return dialogElement
}
}

View File

@@ -0,0 +1,18 @@
package dev.inmo.postssystem.client.utils
import dev.inmo.postssystem.features.files.common.FullFileInfo
import dev.inmo.micro_utils.common.toArrayBuffer
import kotlinx.browser.document
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.url.URL
import org.w3c.files.Blob
fun triggerDownloadFile(fullFileInfo: FullFileInfo) {
val hiddenElement = document.createElement("a") as HTMLAnchorElement
val url = URL.createObjectURL(Blob(arrayOf(fullFileInfo.byteArrayAllocator().toArrayBuffer())))
hiddenElement.href = url
hiddenElement.target = "_blank"
hiddenElement.download = fullFileInfo.name.name
hiddenElement.click()
}

View File

@@ -0,0 +1,134 @@
package dev.inmo.postssystem.client.utils
import kotlinx.browser.document
import kotlinx.dom.clear
import kotlinx.html.dom.append
import kotlinx.html.id
import kotlinx.html.js.*
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.Event
object HTMLViewsConstants {
val mainContainer: MainHTMLViewContainer = MainHTMLViewContainer
val processesContainer: DrawerHTMLViewContainer = DrawerHTMLViewContainer
val toolsContainerId: ToolsHTMLViewContainer = ToolsHTMLViewContainer
}
sealed interface HTMLViewContainer {
val htmlElement: HTMLElement?
get() = document.getElementById(id) as? HTMLElement
val id: String
fun setIsLoading() {
htmlElement ?.apply {
clear()
append {
div("mdl-spinner mdl-js-spinner is-active")
}
}
}
companion object {
fun from(elementId: String) = when (elementId) {
MainHTMLViewContainer.id -> MainHTMLViewContainer
DrawerHTMLViewContainer.id -> DrawerHTMLViewContainer
ToolsHTMLViewContainer.id -> ToolsHTMLViewContainer
else -> null
}
}
}
object MainHTMLViewContainer : HTMLViewContainer {
override val id: String
get() = "main"
}
object DrawerHTMLViewContainer : HTMLViewContainer {
data class DrawerAddButtonInfo(
val text: String,
val onAddButtonClick: (Event) -> Unit,
)
override val id: String
get() = "drawer"
private val titleElement: HTMLElement?
get() = (document.getElementById("drawerTitle") ?:let {
htmlElement ?.append {
span("mdl-layout-title") {
id = "drawerTitle"
}
} ?.first()
}) as? HTMLElement
var title: String?
get() = titleElement ?.textContent
set(value) {
if (value == null) {
titleElement ?.classList ?.add("gone")
} else {
val element = titleElement ?: return
element.textContent = value
element.classList.remove("gone")
}
}
private val contentElement
get() = (document.getElementById("drawerContent") ?:let {
htmlElement ?.append {
nav("mdl-navigation") {
id = "drawerContent"
}
} ?.first()
}) as? HTMLElement
fun <T> setListContent(
title: String?,
data: Iterable<T>,
getText: (T) -> String?,
addButtonInfo: DrawerAddButtonInfo? = null,
onClick: (T) -> Unit
) {
this.title = title
val contentElement = contentElement ?: return
contentElement.clear()
contentElement.append {
fun hideDrawer() {
// Emulate clicking for hiding of drawer
(document.getElementsByClassName("mdl-layout__obfuscator").item(0) as? HTMLElement) ?.click()
}
data.forEach {
val elementTitle = getText(it) ?: return@forEach
div("mdl-navigation__link") {
+elementTitle
onClickFunction = { _ ->
onClick(it)
hideDrawer()
}
}
}
if (addButtonInfo != null) {
button(classes = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent") {
+addButtonInfo.text
onClickFunction = {
addButtonInfo.onAddButtonClick(it)
hideDrawer()
}
}
}
}
}
override fun setIsLoading() {
contentElement ?.apply {
clear()
append {
div("mdl-spinner mdl-js-spinner is-active")
}
}
}
}
object ToolsHTMLViewContainer : HTMLViewContainer {
override val id: String
get() = "tools"
}

View File

@@ -0,0 +1,41 @@
package dev.inmo.postssystem.client.utils
import dev.inmo.postssystem.features.files.common.FullFileInfo
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
import org.w3c.files.FileReader
import org.w3c.files.get
fun uploadFileCallbackForHTMLInputChange(
output: MutableStateFlow<FullFileInfo?>,
scope: CoroutineScope
): (Event) -> Unit = {
(it.target as? HTMLInputElement) ?.apply {
files ?.also { files ->
files[0] ?.also { file ->
scope.launch {
val reader: FileReader = FileReader()
reader.onload = {
val bytes = ((it.target.asDynamic()).result as ArrayBuffer).toByteArray()
output.value = FullFileInfo(
FileName(file.name),
findBuiltinMimeType(file.type) ?: KnownMimeTypes.Any,
bytes.asAllocator
)
Unit
}
reader.readAsArrayBuffer(file)
}
}
}
}
}

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PostsSystem</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" type="text/css">
<link rel="stylesheet" href="styles/material.min.css" type="text/css">
<link rel="stylesheet" href="styles/containers.css" type="text/css">
<link rel="stylesheet" href="styles/visibility.css" type="text/css">
</head>
<body>
<!-- Always shows a header, even in smaller screens. -->
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
<header class="mdl-layout__header">
<div class="mdl-layout__header-row">
<!-- Title -->
<span class="mdl-layout-title">Posts System</span>
<!-- Add spacer, to align navigation to the right -->
<div class="mdl-layout-spacer"></div>
<!-- Navigation. We hide it in small screens. -->
<nav id="tools" class="mdl-navigation mdl-layout--large-screen-only"></nav>
</div>
</header>
<main class="mdl-layout__content">
<div id="main" class="page-content"></div>
</main>
</div>
<script type="application/javascript" defer src="js/material.min.js"></script>
<script type="application/javascript" src="postssystem.client.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
.vertical_container {
display: flex;
flex-direction: column;
align-items: center;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
.gone {
display: none;
}

View File

@@ -0,0 +1,35 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.features.users.common.ReadUsersStorage
import dev.inmo.micro_utils.repos.pagination.getAll
import org.koin.core.context.startKoin
fun readLine(suggestionText: String): String {
while (true) {
println(suggestionText)
readLine() ?.let { return it }
}
}
suspend fun main(args: Array<String>) {
val serverUrl = readLine("Server url:")
val login = readLine("Username:")
val password = readLine("Password:")
val koin = startKoin {
// modules(getAuthorizedFeaturesDIModule(serverUrl, AuthCreds(Username(login), password)))
}.koin
while (true) {
val chosen = readLine(
"""
Choose action:
1. Show server users
""".trimIndent()
).toIntOrNull()
when (chosen) {
1 -> println(koin.get<ReadUsersStorage>().getAll { getByPagination(it) })
else -> println("Sorry, I didn't understand")
}
}
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.postssystem.client"/>