Skip to content

Commit

Permalink
add redirect type and migrate to content type request
Browse files Browse the repository at this point in the history
  • Loading branch information
avan1235 committed Mar 16, 2024
1 parent 730f6b1 commit e3ea871
Show file tree
Hide file tree
Showing 16 changed files with 297 additions and 114 deletions.
3 changes: 2 additions & 1 deletion composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ kotlin {
implementation(projects.shared)

implementation(libs.ktor.client.core)
implementation(libs.ktor.client.resources)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.cbor)

implementation(libs.procyk.compose.qrcode)
implementation(libs.procyk.compose.calendar)
Expand Down
10 changes: 8 additions & 2 deletions composeApp/src/commonMain/kotlin/in/procyk/shin/HttpClient.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package `in`.procyk.shin

import ShinCbor
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.resources.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.cbor.*
import kotlinx.serialization.ExperimentalSerializationApi

@OptIn(ExperimentalSerializationApi::class)
internal fun createHttpClient(): HttpClient = HttpClient {
install(Resources)
install(ContentNegotiation) {
cbor(ShinCbor)
}
defaultRequest {
host = ComposeAppConfig.CLIENT_HOST
url {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,26 @@ package `in`.procyk.shin.component
import Option
import Option.None
import Option.Some
import RedirectType
import SHORTEN_PATH
import ShinCbor
import Shorten
import ShortenExpiring
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import `in`.procyk.shin.createHttpClient
import `in`.procyk.shin.model.ShortenedProtocol
import io.ktor.client.*
import io.ktor.client.plugins.resources.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.*
import kotlinx.datetime.Clock.System.now
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToByteArray
import toLocalDate
import toNullable


interface ShinComponent : Component {
Expand All @@ -30,6 +33,10 @@ interface ShinComponent : Component {

val expirationDateVisible: Value<Boolean>

val redirectType: Value<RedirectType>

val redirectTypeVisible: Value<Boolean>

val url: Value<String>

val shortenedUrl: Value<Option<String>>
Expand All @@ -42,6 +49,10 @@ interface ShinComponent : Component {

fun onExpirationDateVisibleChange(visible: Boolean)

fun onRedirectTypeChange(redirectType: RedirectType)

fun onRedirectTypeVisibleChange(visible: Boolean)

fun onUrlChange(url: String)

fun onProtocolChange(protocol: ShortenedProtocol)
Expand All @@ -67,6 +78,12 @@ class ShinComponentImpl(
private val _expirationDateVisible = MutableStateFlow(false)
override val expirationDateVisible: Value<Boolean> = _expirationDateVisible.asValue()

private val _redirectType = MutableStateFlow(RedirectType.Default)
override val redirectType: Value<RedirectType> = _redirectType.asValue()

private val _redirectTypeVisible = MutableStateFlow(false)
override val redirectTypeVisible: Value<Boolean> = _redirectTypeVisible.asValue()

private val _url = MutableStateFlow("")
override val url: Value<String> = _url.asValue()

Expand Down Expand Up @@ -99,6 +116,14 @@ class ShinComponentImpl(
_expirationDateVisible.update { visible }
}

override fun onRedirectTypeChange(redirectType: RedirectType) {
_redirectType.update { redirectType }
}

override fun onRedirectTypeVisibleChange(visible: Boolean) {
_redirectTypeVisible.update { visible }
}

override fun onUrlChange(url: String) {
val (updatedUrl, updatedProtocol) = ShortenedProtocol.simplifyInputUrl(url)
updatedProtocol?.let { protocol -> _protocol.update { protocol } }
Expand All @@ -120,6 +145,7 @@ class ShinComponentImpl(
url = _url.value,
shortenedProtocol = _protocol.value,
expirationDate = _expirationDate.value.takeIfExtraElementsVisibleAnd(expirationDateVisible),
redirectType = _redirectType.value.takeIfExtraElementsVisibleAnd(redirectTypeVisible),
onResponse = { response ->
val some = Some(response)
_shortenedUrl.update { some }
Expand All @@ -133,18 +159,21 @@ class ShinComponentImpl(
takeIf { _extraElementsVisible.value && value.value }
}

@OptIn(ExperimentalSerializationApi::class)
private suspend fun HttpClient.requestShortenedUrl(
url: String,
shortenedProtocol: ShortenedProtocol,
expirationDate: LocalDate?,
redirectType: RedirectType?,
onResponse: (String) -> Unit,
onError: (String) -> Unit,
) {
try {
val expirationAt = expirationDate?.plus(1, DateTimeUnit.DAY)?.atStartOfDayIn(TimeZone.currentSystemDefault())
val response = when (expirationAt) {
null -> post<_>(Shorten(shortenedProtocol.buildUrl(url)))
else -> post<_>(ShortenExpiring(shortenedProtocol.buildUrl(url), expirationAt))
val shorten = Shorten(shortenedProtocol.buildUrl(url), expirationAt, redirectType)
val response = post(SHORTEN_PATH) {
contentType(ContentType.Application.Cbor)
setBody(ShinCbor.encodeToByteArray(shorten))
}
response
.takeIf { it.status == HttpStatusCode.OK }
Expand Down
62 changes: 62 additions & 0 deletions composeApp/src/commonMain/kotlin/in/procyk/shin/ui/EnumChooser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package `in`.procyk.shin.ui

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import applyIf

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun <T> EnumChooser(
label: String,
entries: Iterable<T>,
value: T,
onValueChange: (T) -> Unit,
presentableName: T.() -> String,
fillMaxWidth: Boolean = true,
defaultWidth: Dp = 128.dp,
) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it },
) {
OutlinedTextField(
readOnly = true,
value = value.presentableName(),
onValueChange = {},
label = { Text(label) },
modifier = Modifier
.menuAnchor()
.height(64.dp)
.applyIf(fillMaxWidth) { fillMaxWidth() }
.width(defaultWidth),
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
},
singleLine = true,
shape = RoundedCornerShape(12.dp),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
entries.forEach { protocol ->
DropdownMenuItem(
text = {
Text(protocol.presentableName())
},
onClick = {
onValueChange(protocol)
expanded = false
})
}
}
}
}
138 changes: 70 additions & 68 deletions composeApp/src/commonMain/kotlin/in/procyk/shin/ui/ShortenRequest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package `in`.procyk.shin.ui

import RedirectType
import androidx.compose.animation.*
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.*
Expand All @@ -22,6 +23,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import applyIf
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.value.Value
import `in`.procyk.compose.calendar.SelectableCalendar
import `in`.procyk.compose.calendar.rememberSelectableCalendarState
import `in`.procyk.compose.calendar.year.YearMonth
Expand Down Expand Up @@ -91,26 +93,17 @@ private fun ShortenRequestExtraElements(
)
}
VerticalAnimatedVisibility(extraElementsVisible) {
Column(
modifier = Modifier
.padding(top = 12.dp)
.sizeIn(maxWidth = 280.dp),
verticalArrangement = Arrangement.spacedBy(4.dp, alignment = Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
) {
val expirationDate by component.expirationDate.subscribeAsState()
val expirationDateVisible by component.expirationDateVisible.subscribeAsState()
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically
HideableSettingsColumn(
name = "Expiration Date",
visible = component.expirationDateVisible,
modifier = Modifier.sizeIn(maxWidth = 250.dp),
onVisibleChange = component::onExpirationDateVisibleChange,
) {
Text("Expiration Date", fontSize = 16.sp)
Switch(
checked = expirationDateVisible,
onCheckedChange = component::onExpirationDateVisibleChange
)
}
VerticalAnimatedVisibility(expirationDateVisible) {
val expirationDate by component.expirationDate.subscribeAsState()
val calendarState = rememberSelectableCalendarState(
initialMonth = YearMonth.now(),
minMonth = YearMonth.now(),
Expand All @@ -119,6 +112,58 @@ private fun ShortenRequestExtraElements(
)
SelectableCalendar(calendarState = calendarState)
}
HideableSettingsColumn(
name = "Redirect Type",
visible = component.redirectTypeVisible,
modifier = Modifier.width(width = 250.dp),
onVisibleChange = component::onRedirectTypeVisibleChange,
) {
val redirectType by component.redirectType.subscribeAsState()
EnumChooser(
label = "Redirect Type",
entries = RedirectType.entries,
value = redirectType,
onValueChange = component::onRedirectTypeChange,
presentableName = RedirectType::presentableName,
)
}
}
}
}

private inline val RedirectType.presentableName: String
get() = when (this) {
RedirectType.MovedPermanently -> "301 (Moved Permanently)"
RedirectType.Found -> "302 (Moved Temporarily)"
}

@Composable
private inline fun HideableSettingsColumn(
name: String,
visible: Value<Boolean>,
noinline onVisibleChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
crossinline content: @Composable() AnimatedVisibilityScope.() -> Unit,
) {
Column(
modifier = modifier
.padding(top = 12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp, alignment = Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val contentVisible by visible.subscribeAsState()
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically
) {
Text(name, fontSize = 16.sp)
Switch(
checked = contentVisible,
onCheckedChange = onVisibleChange,
)
}
VerticalAnimatedVisibility(contentVisible) {
content()
}
}
}
Expand All @@ -132,10 +177,13 @@ private fun ShortenRequestElements(
val url by component.url.subscribeAsState()
val protocol by component.protocol.subscribeAsState()

ProtocolChooser(
protocol = protocol,
onChange = component::onProtocolChange,
fillMaxWidth = fillMaxWidth
EnumChooser(
label = "Protocol",
entries = ShortenedProtocol.entries,
value = protocol,
onValueChange = component::onProtocolChange,
presentableName = ShortenedProtocol::presentableName,
fillMaxWidth = fillMaxWidth,
)
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
Expand Down Expand Up @@ -175,52 +223,6 @@ private fun ShortenRequestElements(
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ProtocolChooser(
protocol: ShortenedProtocol,
onChange: (ShortenedProtocol) -> Unit,
fillMaxWidth: Boolean,
) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it },
) {
OutlinedTextField(
readOnly = true,
value = protocol.presentableName,
onValueChange = {},
label = { Text("Protocol") },
modifier = Modifier
.menuAnchor()
.height(64.dp)
.applyIf(fillMaxWidth) { fillMaxWidth() }
.width(128.dp),
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
},
singleLine = true,
shape = RoundedCornerShape(12.dp),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
ShortenedProtocol.entries.forEach { protocol ->
DropdownMenuItem(
text = {
Text(protocol.presentableName)
},
onClick = {
onChange(protocol)
expanded = false
})
}
}
}
}

@Composable
private inline fun VerticalAnimatedVisibility(
visible: Boolean,
Expand Down
Loading

0 comments on commit e3ea871

Please sign in to comment.