simpliest version of content upload
This commit is contained in:
parent
d15ace3c49
commit
a06324568e
@ -18,7 +18,13 @@ object UIFSMStateSerializer : KSerializer<UIFSMState> by TypedSerializer(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AuthUIFSMState(
|
data class AuthUIFSMState(
|
||||||
override val from: UIFSMState? = null,
|
override val from: UIFSMState? = CreatePostUIFSMState(),
|
||||||
override val context: String = "main"
|
override val context: String = "main"
|
||||||
) : UIFSMState
|
) : UIFSMState
|
||||||
val DefaultAuthUIFSMState = AuthUIFSMState()
|
val DefaultAuthUIFSMState = AuthUIFSMState()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreatePostUIFSMState(
|
||||||
|
override val from: UIFSMState? = null,
|
||||||
|
override val context: String = "main"
|
||||||
|
) : UIFSMState
|
||||||
|
@ -13,8 +13,8 @@ import dev.inmo.micro_utils.fsm.common.StatesMachine
|
|||||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||||
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
||||||
import dev.inmo.postssystem.client.settings.auth.AuthSettings
|
import dev.inmo.postssystem.client.settings.auth.AuthSettings
|
||||||
import kotlinx.browser.document
|
import dev.inmo.postssystem.services.posts.client.ui.create.*
|
||||||
import kotlinx.browser.localStorage
|
import kotlinx.browser.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.builtins.serializer
|
import kotlinx.serialization.builtins.serializer
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
@ -77,7 +77,7 @@ fun baseKoin(): Koin {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
JSUIFSMStatesRepo(get(), localStorage)
|
JSUIFSMStatesRepo(window.history)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
first.apply {
|
first.apply {
|
||||||
@ -89,6 +89,10 @@ fun baseKoin(): Koin {
|
|||||||
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
|
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
|
||||||
factory { AuthUIViewModel(get()) }
|
factory { AuthUIViewModel(get()) }
|
||||||
factory { AuthView(get(), get(UIScopeQualifier)) }
|
factory { AuthView(get(), get(UIScopeQualifier)) }
|
||||||
|
|
||||||
|
factory<PostCreateUIModel> { DefaultPostCreateUIModel(get()) }
|
||||||
|
factory { PostCreateUIViewModel(get()) }
|
||||||
|
factory { PostCreateView(get(), get(UIScopeQualifier)) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
strictlyOn<AuthUIFSMState>(get<AuthView>())
|
strictlyOn<AuthUIFSMState>(get<AuthView>())
|
||||||
@ -135,6 +139,7 @@ fun baseKoin(): Koin {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
registerHandler(PostCreateView::class)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,62 +2,78 @@ package dev.inmo.postssystem.client
|
|||||||
|
|
||||||
import dev.inmo.postssystem.client.ui.fsm.*
|
import dev.inmo.postssystem.client.ui.fsm.*
|
||||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
||||||
|
import kotlinx.browser.window
|
||||||
import kotlinx.serialization.StringFormat
|
import kotlinx.serialization.StringFormat
|
||||||
import org.w3c.dom.*
|
import org.w3c.dom.*
|
||||||
|
import org.w3c.dom.url.URL
|
||||||
|
|
||||||
|
private fun History.refreshHistory(
|
||||||
|
states: Iterable<UIFSMState>,
|
||||||
|
) {
|
||||||
|
val currentUrl = window.location.pathname
|
||||||
|
val params = states.mapNotNull<UIFSMState, Pair<String, String>> {
|
||||||
|
when (it) {
|
||||||
|
is AuthUIFSMState -> null
|
||||||
|
is CreatePostUIFSMState -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushState(
|
||||||
|
null,
|
||||||
|
"Posts System",
|
||||||
|
"$currentUrl${params.joinToString("&", "?") { "${it.first}=${it.second}" }}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun takeStates(initialState: UIFSMState): List<UIFSMState> {
|
||||||
|
val params = (URL(window.location.href)).searchParams
|
||||||
|
val additionalStates = listOfNotNull<UIFSMState>()
|
||||||
|
|
||||||
|
return additionalStates + listOfNotNull(
|
||||||
|
if (additionalStates.isEmpty()) {
|
||||||
|
initialState
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class JSUIFSMStatesRepo(
|
class JSUIFSMStatesRepo(
|
||||||
private val serialFormat: StringFormat,
|
private val history: History,
|
||||||
private val storage: Storage,
|
|
||||||
private val initialState: UIFSMState = DefaultAuthUIFSMState
|
private val initialState: UIFSMState = DefaultAuthUIFSMState
|
||||||
) : DefaultStatesManagerRepo<UIFSMState> {
|
) : DefaultStatesManagerRepo<UIFSMState> {
|
||||||
private val String.storageKey
|
private val statesMap = mutableMapOf<String, UIFSMState>()
|
||||||
get() = "${FSMStateSettingsFieldPrefix}$this"
|
|
||||||
private val String.UIFSMState
|
|
||||||
get() = runCatching {
|
|
||||||
serialFormat.decodeFromString(UIFSMStateSerializer, this)
|
|
||||||
}.onFailure { it.printStackTrace() }.getOrNull()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (states().isEmpty()) {
|
val states = takeStates(initialState)
|
||||||
setState(initialState)
|
states.forEach {
|
||||||
|
statesMap[it.context] = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setState(state: UIFSMState) {
|
|
||||||
storage[state.context.storageKey] = serialFormat.encodeToString(UIFSMStateSerializer, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getContextState(context: Any): UIFSMState? {
|
override suspend fun getContextState(context: Any): UIFSMState? {
|
||||||
return when (context) {
|
return when (context) {
|
||||||
is String -> storage[context.storageKey] ?.UIFSMState ?: return DefaultAuthUIFSMState
|
is String -> statesMap[context]
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun contains(context: Any): Boolean = when (context) {
|
override suspend fun contains(context: Any): Boolean = when (context) {
|
||||||
is String -> storage.get(context) ?.UIFSMState != null
|
is String -> statesMap.contains(context)
|
||||||
else -> super.contains(context)
|
else -> super.contains(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun states(): List<UIFSMState> = storage.iterator().asSequence().mapNotNull { (k, v) ->
|
override suspend fun getStates(): List<UIFSMState> = statesMap.values.toList()
|
||||||
if (k.startsWith(FSMStateSettingsFieldPrefix)) {
|
|
||||||
v.UIFSMState
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
|
|
||||||
override suspend fun getStates(): List<UIFSMState> = states()
|
|
||||||
|
|
||||||
override suspend fun removeState(state: UIFSMState) {
|
override suspend fun removeState(state: UIFSMState) {
|
||||||
storage.removeItem((state.context as? String) ?.storageKey ?: return)
|
statesMap.remove((state.context as? String) ?: return)
|
||||||
|
history.refreshHistory(statesMap.values)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun set(state: UIFSMState) {
|
override suspend fun set(state: UIFSMState) {
|
||||||
setState(state)
|
console.log(state)
|
||||||
}
|
statesMap[state.context] = state
|
||||||
|
history.refreshHistory(statesMap.values)
|
||||||
companion object {
|
|
||||||
private const val FSMStateSettingsFieldPrefix = "UIFSMState_"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
package dev.inmo.postssystem.client.fsm.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import dev.inmo.jsuikit.elements.DefaultButton
|
||||||
|
import dev.inmo.jsuikit.elements.DropArea
|
||||||
|
import dev.inmo.jsuikit.utils.InputAttrs
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.common.filename
|
||||||
|
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.common.common.*
|
||||||
|
import dev.inmo.postssystem.features.content.common.BinaryContent
|
||||||
|
import dev.inmo.postssystem.features.content.text.common.TextContent
|
||||||
|
import dev.inmo.postssystem.services.posts.client.ui.create.PostCreateUIModel
|
||||||
|
import dev.inmo.postssystem.services.posts.client.ui.create.PostCreateUIViewModel
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import org.jetbrains.compose.web.dom.*
|
||||||
|
import org.w3c.dom.HTMLElement
|
||||||
|
import org.w3c.files.get
|
||||||
|
|
||||||
|
class PostCreateView(
|
||||||
|
private val createPostCreateUIModel: PostCreateUIViewModel,
|
||||||
|
private val uiScope: CoroutineScope
|
||||||
|
) : JSView<CreatePostUIFSMState>() {
|
||||||
|
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
|
||||||
|
htmlElement: HTMLElement,
|
||||||
|
state: CreatePostUIFSMState
|
||||||
|
): UIFSMState? {
|
||||||
|
val result = CompletableDeferred<UIFSMState?>()
|
||||||
|
|
||||||
|
val textContent = mutableStateOf("")
|
||||||
|
val fileState = mutableStateOf<MPPFile?>(null)
|
||||||
|
|
||||||
|
renderComposableAndLinkToContext(htmlElement) {
|
||||||
|
Form {
|
||||||
|
TextArea(textContent.value) {
|
||||||
|
onInput { textContent.value = it.value }
|
||||||
|
}
|
||||||
|
DropArea (
|
||||||
|
inputAttrs = InputAttrs {
|
||||||
|
onInput {
|
||||||
|
it.target.files ?.get(0) ?.let {
|
||||||
|
fileState.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DefaultButton(
|
||||||
|
"Upload",
|
||||||
|
disabled = fileState.value == null || textContent.value.isBlank()
|
||||||
|
) {
|
||||||
|
it.preventDefault()
|
||||||
|
fileState.value ?.let { file ->
|
||||||
|
uiScope.launchSafelyWithoutExceptions {
|
||||||
|
createPostCreateUIModel.create(
|
||||||
|
listOf(
|
||||||
|
TextContent(
|
||||||
|
textContent.value
|
||||||
|
),
|
||||||
|
BinaryContent(
|
||||||
|
file.filename,
|
||||||
|
file.mimeType,
|
||||||
|
file.inputProvider()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.await()
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,16 @@
|
|||||||
package dev.inmo.postssystem.features.common.common
|
package dev.inmo.postssystem.features.common.common
|
||||||
|
|
||||||
import dev.inmo.micro_utils.common.MPPFile
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.common.bytes
|
||||||
import io.ktor.utils.io.core.ByteReadPacket
|
import io.ktor.utils.io.core.ByteReadPacket
|
||||||
import io.ktor.utils.io.core.Input
|
import io.ktor.utils.io.core.Input
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable(SimpleInputProviderSerializer::class)
|
@Serializable(SimpleInputProviderSerializer::class)
|
||||||
actual class FileBasedInputProvider internal constructor(
|
actual class FileBasedInputProvider internal constructor(
|
||||||
actual val file: MPPFile,
|
actual val file: MPPFile
|
||||||
private val bytes: ByteArray
|
|
||||||
) : SimpleInputProvider {
|
) : SimpleInputProvider {
|
||||||
override fun invoke(): Input = ByteReadPacket(bytes)
|
override fun invoke(): Input = error("Files inputs must not be used directly")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MPPFile.inputProvider() = FileBasedInputProvider(this)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package dev.inmo.postssystem.features.content.common
|
package dev.inmo.postssystem.features.content.common
|
||||||
|
|
||||||
import dev.inmo.micro_utils.common.FileName
|
import dev.inmo.micro_utils.common.FileName
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
import dev.inmo.micro_utils.mime_types.MimeType
|
import dev.inmo.micro_utils.mime_types.MimeType
|
||||||
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
|
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
|
||||||
import kotlinx.serialization.PolymorphicSerializer
|
import kotlinx.serialization.PolymorphicSerializer
|
||||||
|
@ -12,16 +12,7 @@ class ClientStaticRoutingConfiguration(
|
|||||||
private val clientStatic: String?
|
private val clientStatic: String?
|
||||||
) : ApplicationRoutingConfigurator.Element {
|
) : ApplicationRoutingConfigurator.Element {
|
||||||
override fun Route.invoke() {
|
override fun Route.invoke() {
|
||||||
clientStatic ?.let {
|
resources("web")
|
||||||
static("client") {
|
defaultResource("/web/index.html")
|
||||||
resources(it)
|
|
||||||
get {
|
|
||||||
call.respondRedirect("client/index.html")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get {
|
|
||||||
call.respondRedirect("client")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@ import dev.inmo.postssystem.features.content.binary.server.BinaryServerContentSt
|
|||||||
import dev.inmo.postssystem.features.content.common.*
|
import dev.inmo.postssystem.features.content.common.*
|
||||||
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
|
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
|
||||||
import dev.inmo.postssystem.features.content.server.ServerContentStorageAggregator
|
import dev.inmo.postssystem.features.content.server.ServerContentStorageAggregator
|
||||||
|
import dev.inmo.postssystem.features.content.server.ServerContentStorageWrapper
|
||||||
|
import dev.inmo.postssystem.features.content.text.common.TextContent
|
||||||
import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator
|
import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator
|
||||||
import dev.inmo.postssystem.features.content.text.server.TextServerContentStorage
|
import dev.inmo.postssystem.features.content.text.server.TextServerContentStorage
|
||||||
import dev.inmo.postssystem.features.posts.server.ExposedServerPostsStorage
|
import dev.inmo.postssystem.features.posts.server.ExposedServerPostsStorage
|
||||||
@ -55,6 +57,7 @@ import org.koin.core.module.Module
|
|||||||
import org.koin.core.parameter.ParametersHolder
|
import org.koin.core.parameter.ParametersHolder
|
||||||
import org.koin.core.qualifier.StringQualifier
|
import org.koin.core.qualifier.StringQualifier
|
||||||
import org.koin.dsl.*
|
import org.koin.dsl.*
|
||||||
|
import org.w3c.dom.Text
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private fun Route.print() {
|
private fun Route.print() {
|
||||||
@ -169,8 +172,18 @@ fun getDIModule(
|
|||||||
WriteDistFilesStorage(get(binaryFilesFolderQualifier), get(binaryFilesMetasKeyValueRepoQualifier))
|
WriteDistFilesStorage(get(binaryFilesFolderQualifier), get(binaryFilesMetasKeyValueRepoQualifier))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
singleWithRandomQualifier { BinaryServerContentStorage(get(binaryStorageFilesQualifier)) }
|
singleWithRandomQualifier {
|
||||||
singleWithRandomQualifier { TextServerContentStorage(get()) }
|
ServerContentStorageWrapper(
|
||||||
|
BinaryServerContentStorage(get(binaryStorageFilesQualifier)),
|
||||||
|
BinaryContent::class
|
||||||
|
)
|
||||||
|
}
|
||||||
|
singleWithRandomQualifier {
|
||||||
|
ServerContentStorageWrapper(
|
||||||
|
TextServerContentStorage(get()),
|
||||||
|
TextContent::class
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
single<ServerContentStorage<Content>> { ServerContentStorageAggregator(getAll(), get()) }
|
single<ServerContentStorage<Content>> { ServerContentStorageAggregator(getAll(), get()) }
|
||||||
|
|
||||||
@ -213,7 +226,7 @@ fun getDIModule(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
singleWithBinds { AuthenticationRoutingConfigurator(get(), get(), get()) }
|
singleWithBinds { AuthenticationRoutingConfigurator(get(), get(), get()) }
|
||||||
singleWithBinds { NotFoundStatusPageRedirectToIndex("/client") }
|
singleWithBinds { NotFoundStatusPageRedirectToIndex("/") }
|
||||||
|
|
||||||
if (config.debugMode) {
|
if (config.debugMode) {
|
||||||
single<ApplicationRoutingConfigurator.Element>(StringQualifier("Tracer")) { ApplicationRoutingConfigurator.Element {(this as Routing).trace { application.log.trace(it.buildText()) } } }
|
single<ApplicationRoutingConfigurator.Element>(StringQualifier("Tracer")) { ApplicationRoutingConfigurator.Element {(this as Routing).trace { application.log.trace(it.buildText()) } } }
|
||||||
|
File diff suppressed because one or more lines are too long
@ -48,7 +48,7 @@ class ClientWritePostsService(
|
|||||||
removeRoute
|
removeRoute
|
||||||
)
|
)
|
||||||
private val tempUploadFullPath = buildStandardUrl(
|
private val tempUploadFullPath = buildStandardUrl(
|
||||||
baseUrl,
|
root,
|
||||||
postsCreateTempPathPart
|
postsCreateTempPathPart
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -60,6 +60,7 @@ class ClientWritePostsService(
|
|||||||
tempUploadFullPath,
|
tempUploadFullPath,
|
||||||
provider.file
|
provider.file
|
||||||
)
|
)
|
||||||
|
println(fileId)
|
||||||
it.copy(inputProvider = TempFileIdentifierInputProvider(fileId))
|
it.copy(inputProvider = TempFileIdentifierInputProvider(fileId))
|
||||||
}
|
}
|
||||||
is TempFileIdentifierInputProvider -> it
|
is TempFileIdentifierInputProvider -> it
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package dev.inmo.postssystem.services.posts.client.ui.create
|
||||||
|
|
||||||
|
import dev.inmo.postssystem.features.common.common.*
|
||||||
|
import dev.inmo.postssystem.features.content.common.Content
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
class PostCreateUIViewModel(
|
||||||
|
private val model: PostCreateUIModel
|
||||||
|
) : UIViewModel<PostCreateUIState> {
|
||||||
|
override val currentState: StateFlow<PostCreateUIState>
|
||||||
|
get() = model.currentState
|
||||||
|
suspend fun create(content: List<Content>) {
|
||||||
|
model.create(content)
|
||||||
|
}
|
||||||
|
}
|
@ -3,9 +3,8 @@ package dev.inmo.postssystem.services.posts.client
|
|||||||
import dev.inmo.micro_utils.common.MPPFile
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||||
import dev.inmo.postssystem.features.files.common.FileId
|
import dev.inmo.postssystem.features.files.common.FileId
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.GlobalScope.coroutineContext
|
import kotlinx.coroutines.GlobalScope.coroutineContext
|
||||||
import kotlinx.coroutines.job
|
|
||||||
import org.w3c.xhr.*
|
import org.w3c.xhr.*
|
||||||
|
|
||||||
internal actual suspend fun UnifiedRequester.tempUpload(
|
internal actual suspend fun UnifiedRequester.tempUpload(
|
||||||
@ -39,7 +38,7 @@ internal actual suspend fun UnifiedRequester.tempUpload(
|
|||||||
request.open("POST", fullTempUploadDraftPath, true)
|
request.open("POST", fullTempUploadDraftPath, true)
|
||||||
request.send(formData)
|
request.send(formData)
|
||||||
|
|
||||||
coroutineContext.job.invokeOnCompletion {
|
currentCoroutineContext().job.invokeOnCompletion {
|
||||||
runCatching {
|
runCatching {
|
||||||
request.abort()
|
request.abort()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user