full reborn
This commit is contained in:
.github/workflows
LICENSEREADME.mdbuild.gradlebusiness_cases/post_creating
client
common
src
commonMain
kotlin
dev
inmo
postssystem
business_cases
post_creating
main
server
client
build.gradle
src
commonMain
kotlin
dev
inmo
jsMain
kotlin
dev
resources
jvmMain
kotlin
dev
inmo
postssystem
client
main
core
api
src
commonMain
kotlin
dev
inmo
postssystem
core
commonTest
kotlin
dev
inmo
postssystem
core
jvmMain
kotlin
dev
inmo
postssystem
core
content
api
business
content_adapters
binary
main
exposed
build.gradlegradle.properties
src
jvmMain
kotlin
dev
inmo
postssystem
core
exposed
jvmTest
kotlin
dev
inmo
postssystem
ktor
client
common
src
commonMain
kotlin
dev
inmo
postssystem
main
server
features
auth
client
common
server
common
client
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
common
jvmMain
kotlin
dev
inmo
postssystem
features
common
common
main
server
files
client
build.gradle
src
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
jvmMain
kotlin
dev
inmo
postssystem
features
main
server
roles
client
common
manager
client
common
server
server
status
template
client
common
server
users
client
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
users
jvmMain
kotlin
dev
inmo
postssystem
features
users
common
main
server
gradle/wrapper
gradlewgradlew.batmimes_generator
mppAndroidProject.gradlemppJavaProject.gradlemppJsProject.gradlemppProjectWithSerialization.gradlepubconf.kpsbpublish.gradlepublish.kpsbpublishing
api
src
commonMain
kotlin
main
exposed
ktor
client
src
commonMain
kotlin
com
insanusmokrassar
postssystem
main
common
src
commonMain
kotlin
com
insanusmokrassar
main
server
server
settings.gradle
41
client/build.gradle
Normal file
41
client/build.gradle
Normal file
@ -0,0 +1,41 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
js(IR) {
|
||||
binaries.executable()
|
||||
}
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.client")
|
||||
api project(":postssystem.features.status.client")
|
||||
api project(":postssystem.features.files.client")
|
||||
api project(":postssystem.features.users.client")
|
||||
api project(":postssystem.features.auth.client")
|
||||
api project(":postssystem.features.roles.client")
|
||||
api project(":postssystem.features.roles.manager.client")
|
||||
api "dev.inmo:micro_utils.fsm.common:$microutils_version"
|
||||
api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version"
|
||||
api "dev.inmo:micro_utils.crypto:$microutils_version"
|
||||
}
|
||||
}
|
||||
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "io.ktor:ktor-client-apache:$ktor_version"
|
||||
}
|
||||
}
|
||||
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-html:$kotlinx_html_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) })
|
||||
}
|
||||
}
|
105
client/src/commonMain/kotlin/dev/inmo/postssystem/client/DI.kt
Normal file
105
client/src/commonMain/kotlin/dev/inmo/postssystem/client/DI.kt
Normal 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()) }
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
}
|
18
client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/AuthSettings.kt
Normal file
18
client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/AuthSettings.kt
Normal 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?
|
||||
}
|
118
client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/DefaultAuthSettings.kt
Normal file
118
client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/DefaultAuthSettings.kt
Normal 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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
92
client/src/jsMain/kotlin/dev/inmo/postssystem/client/JSDI.kt
Normal file
92
client/src/jsMain/kotlin/dev/inmo/postssystem/client/JSDI.kt
Normal file
@ -0,0 +1,92 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.client.fsm.ui.*
|
||||
import dev.inmo.postssystem.client.ui.*
|
||||
import dev.inmo.postssystem.client.ui.fsm.*
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMStateSerializer
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIModel
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIViewModel
|
||||
import dev.inmo.postssystem.features.auth.common.AuthTokenInfo
|
||||
import dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.localStorage
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.serializer
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.context.loadKoinModules
|
||||
import org.koin.dsl.module
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
val defaultTypedSerializer = TypedSerializer<Any>(
|
||||
"AuthTokenInfo" to AuthTokenInfo.serializer(),
|
||||
"String" to String.serializer(),
|
||||
"Int" to Int.serializer(),
|
||||
"Long" to Long.serializer(),
|
||||
"Short" to Short.serializer(),
|
||||
"Byte" to Byte.serializer(),
|
||||
"Float" to Float.serializer(),
|
||||
"Double" to Double.serializer(),
|
||||
"UIFSMState" to UIFSMStateSerializer
|
||||
)
|
||||
|
||||
fun baseKoin(): Koin {
|
||||
val anyToString: suspend Any.() -> String = {
|
||||
defaultSerialFormat.encodeToString(
|
||||
defaultTypedSerializer,
|
||||
this
|
||||
)
|
||||
}
|
||||
return baseKoin(
|
||||
CoroutineScope(
|
||||
Dispatchers.Default +
|
||||
ContextSafelyExceptionHandler { it.printStackTrace() } +
|
||||
CoroutineExceptionHandler { _, it -> it.printStackTrace() }
|
||||
),
|
||||
{
|
||||
CookiesKeyValueRepo.withMapper<String, Any, String, String>(
|
||||
{ this },
|
||||
{
|
||||
runCatching {
|
||||
anyToString()
|
||||
}.getOrElse {
|
||||
if (it is NoSuchElementException) {
|
||||
val name = this::class.simpleName!!
|
||||
defaultTypedSerializer.include(name, serializer())
|
||||
anyToString()
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
},
|
||||
{ this },
|
||||
{
|
||||
defaultSerialFormat.decodeFromString(
|
||||
defaultTypedSerializer,
|
||||
this
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
OneStateUIFSMStatesRepo(get(), localStorage)
|
||||
}
|
||||
) {
|
||||
first.apply {
|
||||
second.apply {
|
||||
loadKoinModules(
|
||||
module {
|
||||
factory { document.getElementById("main") as HTMLElement }
|
||||
|
||||
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
|
||||
factory { AuthUIViewModel(get()) }
|
||||
factory { AuthView(get(), get(UIScopeQualifier)) }
|
||||
}
|
||||
)
|
||||
strictlyOn<AuthUIFSMState>(get<AuthView>())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
client/src/jsMain/kotlin/dev/inmo/postssystem/client/Main.kt
Normal file
14
client/src/jsMain/kotlin/dev/inmo/postssystem/client/Main.kt
Normal file
@ -0,0 +1,14 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMQualifier
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import kotlinx.browser.window
|
||||
|
||||
fun main() {
|
||||
window.addEventListener("load", {
|
||||
val koin = baseKoin()
|
||||
val uiStatesMachine = koin.get<StatesMachine<UIFSMState>>(UIFSMQualifier)
|
||||
uiStatesMachine.start(koin.get())
|
||||
})
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.*
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
||||
import kotlinx.serialization.StringFormat
|
||||
import org.w3c.dom.*
|
||||
|
||||
class OneStateUIFSMStatesRepo(
|
||||
private val serialFormat: StringFormat,
|
||||
private val storage: Storage,
|
||||
private val initialState: UIFSMState = DefaultAuthUIFSMState
|
||||
) : DefaultStatesManagerRepo<UIFSMState> {
|
||||
private val String.storageKey
|
||||
get() = "${FSMStateSettingsFieldPrefix}$this"
|
||||
private val String.UIFSMState
|
||||
get() = runCatching {
|
||||
serialFormat.decodeFromString(UIFSMStateSerializer, this)
|
||||
}.onFailure { it.printStackTrace() }.getOrNull()
|
||||
|
||||
init {
|
||||
if (states().isEmpty()) {
|
||||
setState(initialState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setState(state: UIFSMState) {
|
||||
storage[state.context.storageKey] = serialFormat.encodeToString(UIFSMStateSerializer, state)
|
||||
}
|
||||
|
||||
override suspend fun getContextState(context: Any): UIFSMState? {
|
||||
return when (context) {
|
||||
is String -> storage[context.storageKey] ?.UIFSMState ?: return DefaultAuthUIFSMState
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun contains(context: Any): Boolean = when (context) {
|
||||
is String -> storage.get(context) ?.UIFSMState != null
|
||||
else -> super.contains(context)
|
||||
}
|
||||
|
||||
private fun states(): List<UIFSMState> = storage.iterator().asSequence().mapNotNull { (k, v) ->
|
||||
if (k.startsWith(FSMStateSettingsFieldPrefix)) {
|
||||
v.UIFSMState
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toList()
|
||||
|
||||
override suspend fun getStates(): List<UIFSMState> = states()
|
||||
|
||||
override suspend fun removeState(state: UIFSMState) {
|
||||
storage.removeItem((state.context as? String) ?.storageKey ?: return)
|
||||
}
|
||||
|
||||
override suspend fun set(state: UIFSMState) {
|
||||
setState(state)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FSMStateSettingsFieldPrefix = "UIFSMState_"
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
||||
import dev.inmo.micro_utils.pagination.utils.paginate
|
||||
import dev.inmo.micro_utils.pagination.utils.reverse
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import kotlinx.browser.localStorage
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.w3c.dom.get
|
||||
import org.w3c.dom.set
|
||||
|
||||
object CookiesKeyValueRepo : KeyValueRepo<String, String> {
|
||||
private val _onNewValue = MutableSharedFlow<Pair<String, String>>()
|
||||
private val _onValueRemoved = MutableSharedFlow<String>()
|
||||
override val onNewValue: Flow<Pair<String, String>> = _onNewValue.asSharedFlow()
|
||||
override val onValueRemoved: Flow<String> = _onValueRemoved.asSharedFlow()
|
||||
|
||||
override suspend fun contains(key: String): Boolean = localStorage.iterator().asSequence().any { it.first == key }
|
||||
|
||||
override suspend fun count(): Long = localStorage.length.toLong()
|
||||
|
||||
override suspend fun get(k: String): String? = localStorage[k]
|
||||
|
||||
override suspend fun keys(
|
||||
v: String,
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<String> = localStorage.iterator().asSequence().mapNotNull {
|
||||
if (it.second == v) it.first else null
|
||||
}.toList().let {
|
||||
it.paginate(
|
||||
if (reversed) {
|
||||
pagination.reverse(it.count())
|
||||
} else {
|
||||
pagination
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun keys(
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<String> = localStorage.iterator().asSequence().map { it.first }.toList().let {
|
||||
it.paginate(
|
||||
if (reversed) {
|
||||
pagination.reverse(it.count())
|
||||
} else {
|
||||
pagination
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun values(
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<String> = localStorage.iterator().asSequence().map { it.second }.toList().let {
|
||||
it.paginate(
|
||||
if (reversed) {
|
||||
pagination.reverse(it.count())
|
||||
} else {
|
||||
pagination
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun set(toSet: Map<String, String>) {
|
||||
toSet.forEach { (k, v) ->
|
||||
localStorage[k] = v
|
||||
_onNewValue.emit(k to v)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unset(toUnset: List<String>) {
|
||||
toUnset.forEach {
|
||||
localStorage[it] ?.let { _ ->
|
||||
localStorage.removeItem(it)
|
||||
_onValueRemoved.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import org.w3c.dom.Storage
|
||||
import org.w3c.dom.get
|
||||
|
||||
class StorageIterator(private val storage: Storage) : Iterator<Pair<String, String>> {
|
||||
private var index = 0
|
||||
|
||||
override fun hasNext(): Boolean = index < storage.length
|
||||
|
||||
override fun next(): Pair<String, String> {
|
||||
val k = storage.key(index) ?: error("Key for index $index was not found")
|
||||
val v = storage[k] ?: error("Key for index $index was not found")
|
||||
index++
|
||||
return k to v
|
||||
}
|
||||
}
|
||||
|
||||
operator fun Storage.iterator() = StorageIterator(this)
|
@ -0,0 +1,120 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.*
|
||||
import dev.inmo.postssystem.client.utils.HTMLViewContainer
|
||||
import dev.inmo.postssystem.features.auth.client.ui.*
|
||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.dom.clear
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.dom.append
|
||||
import kotlinx.html.js.form
|
||||
import kotlinx.html.js.onClickFunction
|
||||
import org.w3c.dom.*
|
||||
|
||||
class AuthView(
|
||||
private val viewModel: AuthUIViewModel,
|
||||
private val uiScope: CoroutineScope
|
||||
) : JSView<AuthUIFSMState>() {
|
||||
private val usernameInput
|
||||
get() = document.getElementById("authUsername") as? HTMLInputElement
|
||||
private val passwordInput
|
||||
get() = document.getElementById("authPassword") as? HTMLInputElement
|
||||
private val authButton
|
||||
get() = document.getElementById("authButton")
|
||||
private val errorBadge
|
||||
get() = document.getElementById("errorBadge") as? HTMLElement
|
||||
private val progressBarDiv
|
||||
get() = document.getElementById("progressBar") as? HTMLDivElement
|
||||
|
||||
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
|
||||
htmlElement: HTMLElement,
|
||||
container: HTMLViewContainer,
|
||||
state: AuthUIFSMState
|
||||
): UIFSMState? {
|
||||
val completion = CompletableDeferred<UIFSMState?>()
|
||||
htmlElement.clear()
|
||||
|
||||
htmlElement.append {
|
||||
form(classes = "vertical_container") {
|
||||
div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
|
||||
input(type = InputType.text, classes = "mdl-textfield__input") {
|
||||
id = "authUsername"
|
||||
}
|
||||
label(classes = "mdl-textfield__label") {
|
||||
+"Имя пользователя"
|
||||
}
|
||||
}
|
||||
div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
|
||||
input(type = InputType.password, classes = "mdl-textfield__input") {
|
||||
id = "authPassword"
|
||||
}
|
||||
label(classes = "mdl-textfield__label") {
|
||||
+"Пароль"
|
||||
}
|
||||
}
|
||||
div(classes = "mdl-progress mdl-js-progress") {
|
||||
id = "progressBar"
|
||||
}
|
||||
span(classes = "material-icons mdl-badge mdl-badge--overlap gone") {
|
||||
id = "errorBadge"
|
||||
attributes["data-badge"] = "!"
|
||||
}
|
||||
button(classes = "mdl-button mdl-js-button mdl-button--raised") {
|
||||
+"Авторизоваться"
|
||||
id = "authButton"
|
||||
onClickFunction = {
|
||||
it.preventDefault()
|
||||
val serverUrl = document.location ?.run { "$hostname:$port" }
|
||||
val username = usernameInput ?.value
|
||||
val password = passwordInput ?.value
|
||||
if (serverUrl != null && username != null && password != null) {
|
||||
uiScope.launchSafelyWithoutExceptions { viewModel.initAuth(serverUrl, username, password) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
|
||||
when (it) {
|
||||
is InitAuthUIState -> {
|
||||
usernameInput ?.removeAttribute("disabled")
|
||||
passwordInput ?.removeAttribute("disabled")
|
||||
authButton ?.removeAttribute("disabled")
|
||||
errorBadge ?.apply {
|
||||
when (it.showError) {
|
||||
ServerUnavailableAuthUIError -> {
|
||||
classList.remove("gone")
|
||||
innerText = "Сервер недоступен"
|
||||
}
|
||||
AuthIncorrectAuthUIError -> {
|
||||
classList.remove("gone")
|
||||
innerText = "Данные некорректны"
|
||||
}
|
||||
null -> classList.add("gone")
|
||||
}
|
||||
}
|
||||
progressBarDiv ?.classList ?.add("gone")
|
||||
}
|
||||
LoadingAuthUIState -> {
|
||||
usernameInput ?.setAttribute("disabled", "")
|
||||
passwordInput ?.setAttribute("disabled", "")
|
||||
authButton ?.setAttribute("disabled", "")
|
||||
errorBadge ?.classList ?.add("gone")
|
||||
progressBarDiv ?.classList ?.remove("gone")
|
||||
}
|
||||
AuthorizedAuthUIState -> {
|
||||
htmlElement.clear()
|
||||
completion.complete(state.from)
|
||||
}
|
||||
}
|
||||
}
|
||||
return completion.await().also {
|
||||
viewJob.cancel()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.Element
|
||||
|
||||
val mainContainer: Element
|
||||
get() = document.getElementById("main")!!
|
@ -0,0 +1,21 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMHandler
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
|
||||
import dev.inmo.postssystem.client.utils.HTMLViewContainer
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
abstract class JSView<T : UIFSMState> : UIFSMHandler<T> {
|
||||
open suspend fun StatesMachine<in UIFSMState>.safeHandleState(
|
||||
htmlElement: HTMLElement,
|
||||
container: HTMLViewContainer,
|
||||
state: T
|
||||
): UIFSMState? = null
|
||||
|
||||
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(state: T): UIFSMState? {
|
||||
return HTMLViewContainer.from(state.context) ?.let {
|
||||
safeHandleState(it.htmlElement ?: return null, it, state)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui.defaults
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.html.TagConsumer
|
||||
import kotlinx.html.js.button
|
||||
import kotlinx.html.js.onClickFunction
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
fun TagConsumer<HTMLElement>.addBackButton(
|
||||
completableDeferred: CompletableDeferred<UIFSMState>,
|
||||
stateToBack: UIFSMState
|
||||
) {
|
||||
button {
|
||||
+"Назад"
|
||||
onClickFunction = {
|
||||
completableDeferred.complete(stateToBack)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.dom.append
|
||||
import kotlinx.html.js.*
|
||||
import org.w3c.dom.*
|
||||
|
||||
object DialogHelper {
|
||||
fun createOneFieldDialog(
|
||||
title: String,
|
||||
hint: String,
|
||||
doneButtonText: String,
|
||||
closeButtonText: String,
|
||||
onClose: () -> Unit,
|
||||
onSubmit: (String) -> Unit
|
||||
): HTMLDialogElement {
|
||||
lateinit var dialogElement: HTMLDialogElement
|
||||
(document.getElementsByTagName("body").item(0) as? HTMLBodyElement) ?.append {
|
||||
dialogElement = dialog("mdl-dialog") {
|
||||
h4("mdl-dialog__title") {
|
||||
+title
|
||||
}
|
||||
val id = "form_${uuid4()}_text"
|
||||
div(classes = "mdl-dialog__content") {
|
||||
form("#") {
|
||||
div("mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
|
||||
input(InputType.text, classes = "mdl-textfield__input") {
|
||||
this.id = id
|
||||
}
|
||||
label(classes = "mdl-textfield__label") {
|
||||
+hint
|
||||
attributes["for"] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div(classes = "mdl-dialog__actions mdl-dialog__actions--full-width") {
|
||||
button(classes = "mdl-button", type = ButtonType.button) {
|
||||
+doneButtonText
|
||||
onClickFunction = {
|
||||
it.preventDefault()
|
||||
|
||||
val input = document.getElementById(id) as? HTMLInputElement
|
||||
input ?.value ?.let {
|
||||
onSubmit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
button(classes = "mdl-button", type = ButtonType.button) {
|
||||
+closeButtonText
|
||||
onClickFunction = {
|
||||
it.preventDefault()
|
||||
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dialogElement
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.FullFileInfo
|
||||
import dev.inmo.micro_utils.common.toArrayBuffer
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
import org.w3c.dom.url.URL
|
||||
import org.w3c.files.Blob
|
||||
|
||||
fun triggerDownloadFile(fullFileInfo: FullFileInfo) {
|
||||
val hiddenElement = document.createElement("a") as HTMLAnchorElement
|
||||
|
||||
val url = URL.createObjectURL(Blob(arrayOf(fullFileInfo.byteArrayAllocator().toArrayBuffer())))
|
||||
hiddenElement.href = url
|
||||
hiddenElement.target = "_blank"
|
||||
hiddenElement.download = fullFileInfo.name.name
|
||||
hiddenElement.click()
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.dom.clear
|
||||
import kotlinx.html.dom.append
|
||||
import kotlinx.html.id
|
||||
import kotlinx.html.js.*
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.events.Event
|
||||
|
||||
object HTMLViewsConstants {
|
||||
val mainContainer: MainHTMLViewContainer = MainHTMLViewContainer
|
||||
val processesContainer: DrawerHTMLViewContainer = DrawerHTMLViewContainer
|
||||
val toolsContainerId: ToolsHTMLViewContainer = ToolsHTMLViewContainer
|
||||
}
|
||||
|
||||
sealed interface HTMLViewContainer {
|
||||
val htmlElement: HTMLElement?
|
||||
get() = document.getElementById(id) as? HTMLElement
|
||||
val id: String
|
||||
|
||||
fun setIsLoading() {
|
||||
htmlElement ?.apply {
|
||||
clear()
|
||||
append {
|
||||
div("mdl-spinner mdl-js-spinner is-active")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(elementId: String) = when (elementId) {
|
||||
MainHTMLViewContainer.id -> MainHTMLViewContainer
|
||||
DrawerHTMLViewContainer.id -> DrawerHTMLViewContainer
|
||||
ToolsHTMLViewContainer.id -> ToolsHTMLViewContainer
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object MainHTMLViewContainer : HTMLViewContainer {
|
||||
override val id: String
|
||||
get() = "main"
|
||||
}
|
||||
|
||||
object DrawerHTMLViewContainer : HTMLViewContainer {
|
||||
data class DrawerAddButtonInfo(
|
||||
val text: String,
|
||||
val onAddButtonClick: (Event) -> Unit,
|
||||
)
|
||||
|
||||
override val id: String
|
||||
get() = "drawer"
|
||||
|
||||
private val titleElement: HTMLElement?
|
||||
get() = (document.getElementById("drawerTitle") ?:let {
|
||||
htmlElement ?.append {
|
||||
span("mdl-layout-title") {
|
||||
id = "drawerTitle"
|
||||
}
|
||||
} ?.first()
|
||||
}) as? HTMLElement
|
||||
var title: String?
|
||||
get() = titleElement ?.textContent
|
||||
set(value) {
|
||||
if (value == null) {
|
||||
titleElement ?.classList ?.add("gone")
|
||||
} else {
|
||||
val element = titleElement ?: return
|
||||
element.textContent = value
|
||||
element.classList.remove("gone")
|
||||
}
|
||||
}
|
||||
private val contentElement
|
||||
get() = (document.getElementById("drawerContent") ?:let {
|
||||
htmlElement ?.append {
|
||||
nav("mdl-navigation") {
|
||||
id = "drawerContent"
|
||||
}
|
||||
} ?.first()
|
||||
}) as? HTMLElement
|
||||
|
||||
fun <T> setListContent(
|
||||
title: String?,
|
||||
data: Iterable<T>,
|
||||
getText: (T) -> String?,
|
||||
addButtonInfo: DrawerAddButtonInfo? = null,
|
||||
onClick: (T) -> Unit
|
||||
) {
|
||||
this.title = title
|
||||
val contentElement = contentElement ?: return
|
||||
|
||||
contentElement.clear()
|
||||
contentElement.append {
|
||||
fun hideDrawer() {
|
||||
// Emulate clicking for hiding of drawer
|
||||
(document.getElementsByClassName("mdl-layout__obfuscator").item(0) as? HTMLElement) ?.click()
|
||||
}
|
||||
data.forEach {
|
||||
val elementTitle = getText(it) ?: return@forEach
|
||||
div("mdl-navigation__link") {
|
||||
+elementTitle
|
||||
onClickFunction = { _ ->
|
||||
onClick(it)
|
||||
hideDrawer()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (addButtonInfo != null) {
|
||||
button(classes = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent") {
|
||||
+addButtonInfo.text
|
||||
onClickFunction = {
|
||||
addButtonInfo.onAddButtonClick(it)
|
||||
hideDrawer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setIsLoading() {
|
||||
contentElement ?.apply {
|
||||
clear()
|
||||
append {
|
||||
div("mdl-spinner mdl-js-spinner is-active")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ToolsHTMLViewContainer : HTMLViewContainer {
|
||||
override val id: String
|
||||
get() = "tools"
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.FullFileInfo
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
|
||||
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.files.FileReader
|
||||
import org.w3c.files.get
|
||||
|
||||
fun uploadFileCallbackForHTMLInputChange(
|
||||
output: MutableStateFlow<FullFileInfo?>,
|
||||
scope: CoroutineScope
|
||||
): (Event) -> Unit = {
|
||||
(it.target as? HTMLInputElement) ?.apply {
|
||||
files ?.also { files ->
|
||||
files[0] ?.also { file ->
|
||||
scope.launch {
|
||||
val reader: FileReader = FileReader()
|
||||
|
||||
reader.onload = {
|
||||
val bytes = ((it.target.asDynamic()).result as ArrayBuffer).toByteArray()
|
||||
output.value = FullFileInfo(
|
||||
FileName(file.name),
|
||||
findBuiltinMimeType(file.type) ?: KnownMimeTypes.Any,
|
||||
bytes.asAllocator
|
||||
)
|
||||
Unit
|
||||
}
|
||||
|
||||
reader.readAsArrayBuffer(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
client/src/jsMain/resources/index.html
Normal file
34
client/src/jsMain/resources/index.html
Normal 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>
|
10
client/src/jsMain/resources/js/material.min.js
vendored
Normal file
10
client/src/jsMain/resources/js/material.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
client/src/jsMain/resources/styles/containers.css
Normal file
5
client/src/jsMain/resources/styles/containers.css
Normal file
@ -0,0 +1,5 @@
|
||||
.vertical_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
8
client/src/jsMain/resources/styles/material.min.css
vendored
Normal file
8
client/src/jsMain/resources/styles/material.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
client/src/jsMain/resources/styles/visibility.css
Normal file
3
client/src/jsMain/resources/styles/visibility.css
Normal file
@ -0,0 +1,3 @@
|
||||
.gone {
|
||||
display: none;
|
||||
}
|
35
client/src/jvmMain/kotlin/dev/inmo/postssystem/client/CMD.kt
Normal file
35
client/src/jvmMain/kotlin/dev/inmo/postssystem/client/CMD.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
1
client/src/main/AndroidManifest.xml
Normal file
1
client/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.client"/>
|
Reference in New Issue
Block a user