feat: add language selection (#381); closes #331

* chore: font reordering

* chore: second swipe sensitivity

* chore: update models

* chore: update repositories

* chore: update formatting bar

* chore: add dialog

* chore: update comment creation

* chore: update post creation
This commit is contained in:
Diego Beraldin 2023-12-26 21:07:36 +01:00 committed by GitHub
parent c2ef07d4be
commit 7ca04ace88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 604 additions and 328 deletions

View File

@ -5,38 +5,38 @@ import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
enum class UiFontFamily {
Default,
TitilliumWeb,
NotoSans,
CharisSIL,
Comfortaa,
Poppins,
Default,
Comfortaa,
}
fun Int.toUiFontFamily() = when (this) {
7 -> UiFontFamily.Default
6 -> UiFontFamily.TitilliumWeb
5 -> UiFontFamily.Comfortaa
4 -> UiFontFamily.CharisSIL
0 -> UiFontFamily.Poppins
3 -> UiFontFamily.NotoSans
else -> UiFontFamily.Poppins
4 -> UiFontFamily.CharisSIL
5 -> UiFontFamily.Comfortaa
6 -> UiFontFamily.TitilliumWeb
else -> UiFontFamily.Default
}
fun UiFontFamily.toInt() = when (this) {
UiFontFamily.Poppins -> 0
UiFontFamily.Comfortaa -> 5
UiFontFamily.CharisSIL -> 4
UiFontFamily.NotoSans -> 3
UiFontFamily.CharisSIL -> 4
UiFontFamily.Comfortaa -> 5
UiFontFamily.TitilliumWeb -> 6
UiFontFamily.Default -> 7
}
@Composable
fun UiFontFamily.toReadableName() = when (this) {
UiFontFamily.Default -> stringResource(MR.strings.settings_font_family_default)
UiFontFamily.Poppins -> "Poppins"
UiFontFamily.Comfortaa -> "Comfortaa"
UiFontFamily.CharisSIL -> "Charis SIL"
UiFontFamily.NotoSans -> "Noto Sans"
else -> "Titillium Web"
UiFontFamily.CharisSIL -> "Charis SIL"
UiFontFamily.Comfortaa -> "Comfortaa"
UiFontFamily.TitilliumWeb -> "Titillium Web"
UiFontFamily.Default -> stringResource(MR.strings.settings_font_family_default)
}

View File

@ -13,11 +13,11 @@ import dev.icerock.moko.resources.compose.fontFamilyResource
@Composable
fun UiFontFamily.toTypography(): Typography {
val fontFamily = when (this) {
UiFontFamily.CharisSIL -> fontFamilyResource(MR.fonts.CharisSIL.regular)
UiFontFamily.NotoSans -> fontFamilyResource(MR.fonts.NotoSans.regular)
UiFontFamily.Comfortaa -> fontFamilyResource(MR.fonts.Comfortaa.regular)
UiFontFamily.Poppins -> fontFamilyResource(MR.fonts.Poppins.regular)
UiFontFamily.TitilliumWeb -> fontFamilyResource(MR.fonts.TitilliumWeb.regular)
UiFontFamily.NotoSans -> fontFamilyResource(MR.fonts.NotoSans.regular)
UiFontFamily.CharisSIL -> fontFamilyResource(MR.fonts.CharisSIL.regular)
UiFontFamily.Poppins -> fontFamilyResource(MR.fonts.Poppins.regular)
UiFontFamily.Comfortaa -> fontFamilyResource(MR.fonts.Comfortaa.regular)
else -> FontFamily.Default
}
return Typography(

View File

@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
private const val SECOND_ACTION_THRESHOLD = 0.35f
private const val SECOND_ACTION_THRESHOLD = 0.38f
@OptIn(ExperimentalMaterial3Api::class)
@Composable

View File

@ -1,7 +1,13 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.FormatBold
@ -13,292 +19,390 @@ import androidx.compose.material.icons.filled.FormatStrikethrough
import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.filled.InsertLink
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.Dp
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.IconSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.LanguageModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.fontFamilyResource
@Composable
fun TextFormattingBar(
textFieldValue: TextFieldValue,
onTextFieldValueChanged: (TextFieldValue) -> Unit,
onSelectImage: () -> Unit,
modifier: Modifier = Modifier,
onSelectImage: (() -> Unit)? = null,
currentLanguageId: Int? = null,
availableLanguages: List<LanguageModel> = emptyList(),
onSelectLanguage: (() -> Unit)? = null,
) {
Row(
val textPlaceholder = "text here"
val urlPlaceholder = "URL"
LazyRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(Spacing.m),
) {
Icon(
modifier = Modifier.onClick(
onClick = {
val selection = textFieldValue.selection
val newValue = textFieldValue.let {
val newText = buildString {
append(it.text.substring(0, selection.start))
append("**")
if (selection.length == 0) {
append("text here")
} else {
// bold
item {
Icon(
modifier = Modifier.onClick(
onClick = {
val selection = textFieldValue.selection
val newValue = textFieldValue.let {
val newText = buildString {
append(it.text.substring(0, selection.start))
append("**")
if (selection.length == 0) {
append(textPlaceholder)
} else {
append(
it.text.substring(
selection.start,
selection.end
)
)
}
append("**")
append(
it.text.substring(
selection.start,
selection.end
selection.end,
it.text.length
)
)
}
append("**")
append(
it.text.substring(
selection.end,
it.text.length
)
)
}
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + 11)
} else {
TextRange(start = it.selection.start + 2, end = it.selection.end + 2)
}
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatBold,
contentDescription = null,
)
Icon(
modifier = Modifier.onClick(
onClick = {
val selection = textFieldValue.selection
val newValue = textFieldValue.let {
val newText = buildString {
append(it.text.substring(0, selection.start))
append("*")
if (selection.length == 0) {
append("text here")
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + textPlaceholder.length + 2)
} else {
TextRange(
start = it.selection.start + 2,
end = it.selection.end + 2
)
}
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatBold,
contentDescription = null,
)
}
// italic
item {
Icon(
modifier = Modifier.onClick(
onClick = {
val selection = textFieldValue.selection
val newValue = textFieldValue.let {
val newText = buildString {
append(it.text.substring(0, selection.start))
append("*")
if (selection.length == 0) {
append(textPlaceholder)
} else {
append(
it.text.substring(
selection.start,
selection.end
)
)
}
append("*")
append(
it.text.substring(
selection.start,
selection.end
selection.end,
it.text.length
)
)
}
append("*")
append(
it.text.substring(
selection.end,
it.text.length
)
)
}
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + 10)
} else {
TextRange(start = it.selection.start + 1, end = it.selection.end + 1)
}
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatItalic,
contentDescription = null,
)
Icon(
modifier = Modifier.onClick(
onClick = {
val selection = textFieldValue.selection
val newValue = textFieldValue.let {
val newText = buildString {
append(it.text.substring(0, selection.start))
append("~~")
if (selection.length == 0) {
append("text here")
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + textPlaceholder.length + 1)
} else {
TextRange(
start = it.selection.start + 1,
end = it.selection.end + 1
)
}
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatItalic,
contentDescription = null,
)
}
// strikethrough
item {
Icon(
modifier = Modifier.onClick(
onClick = {
val selection = textFieldValue.selection
val newValue = textFieldValue.let {
val newText = buildString {
append(it.text.substring(0, selection.start))
append("~~")
if (selection.length == 0) {
append(textPlaceholder)
} else {
append(
it.text.substring(
selection.start,
selection.end
)
)
}
append("~~")
append(
it.text.substring(
selection.start,
selection.end
selection.end,
it.text.length
)
)
}
append("~~")
append(
it.text.substring(
selection.end,
it.text.length
)
)
}
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + 11)
} else {
TextRange(start = it.selection.start + 2, end = it.selection.end + 2)
}
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatStrikethrough,
contentDescription = null,
)
Icon(
modifier = Modifier.onClick(
onClick = rememberCallback {
onSelectImage()
},
),
imageVector = Icons.Default.Image,
contentDescription = null,
)
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("[")
if (selection.length == 0) {
append("text here")
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + textPlaceholder.length + 2)
} else {
TextRange(
start = it.selection.start + 2,
end = it.selection.end + 2
)
}
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatStrikethrough,
contentDescription = null,
)
}
// image
if (onSelectImage != null) {
item {
Icon(
modifier = Modifier.onClick(
onClick = rememberCallback {
onSelectImage.invoke()
},
),
imageVector = Icons.Default.Image,
contentDescription = null,
)
}
}
// hyperlink
item {
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("[")
if (selection.length == 0) {
append(textPlaceholder)
} else {
append(
it.text.substring(
selection.start,
selection.end
)
)
}
append("](")
append(urlPlaceholder)
append(")")
append(
it.text.substring(
selection.start,
selection.end
selection.end,
it.text.length
)
)
}
append("](URL here)")
append(
it.text.substring(
selection.end,
it.text.length
)
)
}
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + 10)
} else {
TextRange(start = it.selection.start + 1, end = it.selection.end + 1)
}
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.InsertLink,
contentDescription = null,
)
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("`")
if (selection.length == 0) {
append("text here")
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + textPlaceholder.length + 1)
} else {
TextRange(
start = it.selection.start + 1,
end = it.selection.end + 1
)
}
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.InsertLink,
contentDescription = null,
)
}
// inline code
item {
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("`")
if (selection.length == 0) {
append(textPlaceholder)
} else {
append(
it.text.substring(
selection.start,
selection.end
)
)
}
append("`")
append(
it.text.substring(
selection.start,
selection.end
selection.end,
it.text.length
)
)
}
append("`")
append(
it.text.substring(
selection.end,
it.text.length
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + textPlaceholder.length + 1)
} else {
TextRange(
start = it.selection.start + 1,
end = it.selection.end + 1
)
)
}
it.copy(text = newText, selection = newSelection)
}
val newSelection = if (selection.length == 0) {
TextRange(index = selection.start + 10)
} else {
TextRange(start = it.selection.start + 1, end = it.selection.end + 1)
}
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.Code,
contentDescription = null,
)
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("\n> ")
append(
it.text.substring(
selection.end,
it.text.length
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.Code,
contentDescription = null,
)
}
// block quote
item {
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("\n> ")
append(
it.text.substring(
selection.end,
it.text.length
)
)
)
}
val newSelection = TextRange(index = selection.start + 3)
it.copy(text = newText, selection = newSelection)
}
val newSelection = TextRange(index = selection.start + 3)
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatQuote,
contentDescription = null,
)
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("\n- ")
append(
it.text.substring(
selection.end,
it.text.length
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatQuote,
contentDescription = null,
)
}
// bulleted list
item {
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("\n- ")
append(
it.text.substring(
selection.end,
it.text.length
)
)
)
}
val newSelection = TextRange(index = selection.start + 3)
it.copy(text = newText, selection = newSelection)
}
val newSelection = TextRange(index = selection.start + 3)
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatListBulleted,
contentDescription = null,
)
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("\n1. ")
append(
it.text.substring(
selection.end,
it.text.length
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatListBulleted,
contentDescription = null,
)
}
// numbered list
item {
Icon(
modifier = Modifier.onClick(
onClick = {
val newValue = textFieldValue.let {
val selection = it.selection
val newText = buildString {
append(it.text.substring(0, selection.start))
append("\n1. ")
append(
it.text.substring(
selection.end,
it.text.length
)
)
)
}
val newSelection = TextRange(index = selection.start + 4)
it.copy(text = newText, selection = newSelection)
}
val newSelection = TextRange(index = selection.start + 4)
it.copy(text = newText, selection = newSelection)
}
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatListNumbered,
contentDescription = null,
)
onTextFieldValueChanged(newValue)
}),
imageVector = Icons.Default.FormatListNumbered,
contentDescription = null,
)
}
// language selection
if (onSelectLanguage != null) {
item {
Box(
modifier = Modifier
.size(IconSize.m)
.border(
color = MaterialTheme.colorScheme.onBackground,
width = Dp.Hairline,
shape = RoundedCornerShape(CornerSize.m)
)
.clickable(onClick = onSelectLanguage)
.padding(Spacing.xxxs),
contentAlignment = Alignment.Center,
) {
val languageCode = availableLanguages.firstOrNull { l ->
l.id == currentLanguageId
}?.takeIf { l ->
l.id > 0 // undetermied language
}?.code ?: "λ"
Text(
text = languageCode,
style = MaterialTheme.typography.labelSmall,
fontFamily = fontFamilyResource(MR.fonts.TitilliumWeb.regular),
color = MaterialTheme.colorScheme.onBackground,
)
}
}
}
}
}

View File

@ -97,7 +97,7 @@ class FontFamilyBottomSheet(
UiFontFamily.Comfortaa -> fontFamilyResource(MR.fonts.Comfortaa.regular)
UiFontFamily.Poppins -> fontFamilyResource(MR.fonts.Poppins.regular)
UiFontFamily.TitilliumWeb -> fontFamilyResource(MR.fonts.TitilliumWeb.regular)
else -> FontFamily.Default
UiFontFamily.Default -> FontFamily.Default
}
Text(
text = value.toReadableName(),

View File

@ -0,0 +1,112 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.LanguageModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SelectLanguageDialog(
currentLanguageId: Int? = null,
languages: List<LanguageModel> = emptyList(),
onSelect: ((Int?) -> Unit)? = null,
onDismiss: (() -> Unit)? = null,
) {
AlertDialog(
onDismissRequest = { onDismiss?.invoke() },
) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.surface)
.padding(vertical = Spacing.s),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(MR.strings.settings_language),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
Spacer(modifier = Modifier.height(Spacing.xs))
LazyColumn(
modifier = Modifier.fillMaxHeight(0.6f)
) {
items(items = languages, key = { it.id }) { lang ->
LanguageItem(
name = lang.name,
selected = (currentLanguageId ?: 0) == lang.id,
onSelected = rememberCallback {
onSelect?.invoke(lang.id)
},
)
}
}
Spacer(modifier = Modifier.height(Spacing.xxs))
Button(
onClick = {
onDismiss?.invoke()
},
) {
Text(text = stringResource(MR.strings.button_close))
}
}
}
}
@Composable
private fun LanguageItem(
name: String,
modifier: Modifier = Modifier,
selected: Boolean = false,
onSelected: (() -> Unit)? = null,
) {
Row(
modifier = modifier
.fillMaxWidth()
.onClick(
onClick = {
onSelected?.invoke()
},
).padding(
horizontal = Spacing.m,
vertical = Spacing.s,
),
horizontalArrangement = Arrangement.spacedBy(Spacing.s),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = name,
color = MaterialTheme.colorScheme.onBackground,
)
Spacer(modifier = Modifier.weight(1f))
if (selected) {
RadioButton(
selected = true,
onClick = null,
)
}
}
}

View File

@ -26,6 +26,7 @@ data class CommentModel(
val visible: Boolean = true,
@Transient
val loadMoreButtonVisible: Boolean = false,
val languageId: Int = 0,
) : JavaSerializable {
val depth: Int get() = (path.split(".").size - 2).coerceAtLeast(0)
val parentId: String?

View File

@ -0,0 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
data class LanguageModel(
val id: Int,
val name: String = "",
val code: String = "",
)

View File

@ -27,6 +27,7 @@ data class PostModel(
val featuredCommunity: Boolean = false,
val removed: Boolean = false,
val locked: Boolean = false,
val languageId: Int = 0,
) : JavaSerializable
val PostModel.imageUrl: String

View File

@ -48,7 +48,7 @@ interface CommentRepository {
suspend fun downVote(
comment: CommentModel,
auth: String,
downVoted: Boolean
downVoted: Boolean,
): Result<Response<CommentResponse>>
fun asSaved(comment: CommentModel, saved: Boolean): CommentModel
@ -56,19 +56,21 @@ interface CommentRepository {
suspend fun save(
comment: CommentModel,
auth: String,
saved: Boolean
saved: Boolean,
): Result<Response<CommentResponse>>
suspend fun create(
postId: Int,
parentId: Int?,
text: String,
languageId: Int? = null,
auth: String,
)
suspend fun edit(
commentId: Int,
text: String,
languageId: Int? = null,
auth: String,
)

View File

@ -214,12 +214,14 @@ internal class DefaultCommentRepository(
postId: Int,
parentId: Int?,
text: String,
languageId: Int?,
auth: String,
) {
val data = CreateCommentForm(
content = text,
postId = postId,
parentId = parentId,
languageId = languageId,
auth = auth,
)
services.comment.create(authHeader = auth.toAuthHeader(), form = data)
@ -228,11 +230,13 @@ internal class DefaultCommentRepository(
override suspend fun edit(
commentId: Int,
text: String,
languageId: Int?,
auth: String,
) {
val data = EditCommentForm(
content = text,
commentId = commentId,
languageId = languageId,
auth = auth,
)
services.comment.edit(authHeader = auth.toAuthHeader(), form = data)

View File

@ -169,6 +169,7 @@ internal class DefaultPostRepository(
body: String?,
url: String?,
nsfw: Boolean,
languageId: Int?,
auth: String,
) {
val data = CreatePostForm(
@ -177,6 +178,7 @@ internal class DefaultPostRepository(
body = body,
url = url,
nsfw = nsfw,
languageId = languageId,
auth = auth,
)
services.post.create(
@ -191,6 +193,7 @@ internal class DefaultPostRepository(
body: String?,
url: String?,
nsfw: Boolean,
languageId: Int?,
auth: String,
) {
val data = EditPostForm(
@ -199,6 +202,7 @@ internal class DefaultPostRepository(
body = body,
url = url,
nsfw = nsfw,
languageId = languageId,
auth = auth,
)
services.post.edit(

View File

@ -2,6 +2,7 @@ package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.BlockSiteForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.LanguageModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.MetadataModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
@ -57,4 +58,10 @@ internal class DefaultSiteRepository(
)
response.body()?.metadata?.toModel()
}.getOrNull()
override suspend fun getLanguages(auth: String?): List<LanguageModel> = runCatching {
val response = services.site.get(auth = auth)
val dto = response.body()
dto?.allLanguages?.map { it.toModel() }.orEmpty()
}.getOrElse { emptyList() }
}

View File

@ -57,6 +57,7 @@ interface PostRepository {
body: String?,
url: String? = null,
nsfw: Boolean = false,
languageId: Int? = null,
auth: String,
)
@ -66,6 +67,7 @@ interface PostRepository {
body: String?,
url: String? = null,
nsfw: Boolean = false,
languageId: Int? = null,
auth: String,
)

View File

@ -1,5 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.LanguageModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.MetadataModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
@ -11,4 +12,6 @@ interface SiteRepository {
suspend fun block(id: Int, blocked: Boolean, auth: String? = null): Result<Unit>
suspend fun getMetadata(url: String): MetadataModel?
suspend fun getLanguages(auth: String?): List<LanguageModel>
}

View File

@ -7,6 +7,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentSortType
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.Community
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommunityView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.Language
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListingType.All
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListingType.Local
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListingType.Subscribed
@ -48,6 +49,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SubscribedType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentReportModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.LanguageModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.MetadataModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ModlogItem
@ -125,29 +127,16 @@ internal fun PersonAggregates.toModel() = UserScoreModel(
commentScore = commentScore ?: 0,
)
internal fun PostView.toModel() = PostModel(
id = post.id,
originalUrl = post.apId,
title = post.name,
text = post.body.orEmpty(),
internal fun PostView.toModel() = post.toModel().copy(
score = counts.score,
upvotes = counts.upvotes,
downvotes = counts.downvotes,
comments = counts.comments,
thumbnailUrl = post.thumbnailUrl.orEmpty(),
url = post.url,
community = community.toModel(),
creator = creator.toModel(),
saved = saved,
myVote = myVote ?: 0,
publishDate = post.published,
updateDate = post.updated,
nsfw = post.nsfw,
embedVideoUrl = post.embedVideoUrl,
read = read,
featuredCommunity = post.featuredCommunity,
removed = post.removed,
locked = post.locked,
)
internal fun Post.toModel() = PostModel(
@ -163,11 +152,10 @@ internal fun Post.toModel() = PostModel(
featuredCommunity = featuredCommunity,
removed = removed,
locked = locked,
languageId = languageId,
)
internal fun CommentView.toModel() = CommentModel(
id = comment.id,
text = comment.content,
internal fun CommentView.toModel() = comment.toModel().copy(
community = community.toModel(),
creator = creator.toModel(),
score = counts.score,
@ -175,13 +163,7 @@ internal fun CommentView.toModel() = CommentModel(
downvotes = counts.downvotes,
saved = saved,
myVote = myVote ?: 0,
publishDate = comment.published,
updateDate = comment.updated,
postId = comment.postId,
comments = counts.childCount,
path = comment.path,
distinguished = comment.distinguished,
removed = comment.removed,
)
internal fun Comment.toModel() = CommentModel(
@ -193,6 +175,7 @@ internal fun Comment.toModel() = CommentModel(
path = path,
distinguished = distinguished,
removed = removed,
languageId = languageId,
)
internal fun Community.toModel() = CommunityModel(
@ -222,37 +205,17 @@ internal fun CommunityView.toModel() = community.toModel().copy(
internal fun PersonMentionView.toModel() = PersonMentionModel(
id = personMention.id,
read = personMention.read,
post = PostModel(
id = post.id,
originalUrl = post.apId,
title = post.name,
text = post.body.orEmpty(),
post = post.toModel().copy(
score = counts.score,
upvotes = counts.upvotes,
downvotes = counts.downvotes,
thumbnailUrl = post.thumbnailUrl.orEmpty(),
url = post.url,
community = community.toModel(),
creator = creator.toModel(),
saved = saved,
myVote = myVote ?: 0,
publishDate = post.published,
updateDate = post.updated,
nsfw = post.nsfw,
embedVideoUrl = post.embedVideoUrl,
featuredCommunity = post.featuredCommunity,
removed = post.removed,
locked = post.locked,
),
comment = CommentModel(
id = comment.id,
postId = comment.postId,
text = comment.content,
comment = comment.toModel().copy(
community = community.toModel(),
publishDate = comment.published,
updateDate = comment.updated,
distinguished = comment.distinguished,
removed = comment.removed,
),
creator = creator.toModel(),
community = community.toModel(),
@ -267,37 +230,17 @@ internal fun PersonMentionView.toModel() = PersonMentionModel(
internal fun CommentReplyView.toModel() = PersonMentionModel(
id = commentReply.id,
read = commentReply.read,
post = PostModel(
id = post.id,
originalUrl = post.apId,
title = post.name,
text = post.body.orEmpty(),
post = post.toModel().copy(
score = counts.score,
upvotes = counts.upvotes,
downvotes = counts.downvotes,
thumbnailUrl = post.thumbnailUrl.orEmpty(),
url = post.url,
community = community.toModel(),
creator = UserModel(id = post.creatorId),
creator = creator.toModel(),
saved = saved,
myVote = myVote ?: 0,
publishDate = post.published,
updateDate = post.updated,
nsfw = post.nsfw,
embedVideoUrl = post.embedVideoUrl,
featuredCommunity = post.featuredCommunity,
removed = post.removed,
locked = post.locked,
),
comment = CommentModel(
id = comment.id,
postId = comment.postId,
text = comment.content,
comment = comment.toModel().copy(
community = community.toModel(),
publishDate = comment.published,
updateDate = comment.updated,
distinguished = comment.distinguished,
removed = comment.removed,
),
creator = creator.toModel(),
community = community.toModel(),
@ -436,3 +379,9 @@ internal fun ModTransferCommunityView.toDto() = ModlogItem.ModTransferCommunity(
moderator = moderator?.toModel(),
user = moddedPerson.toModel(),
)
internal fun Language.toModel() = LanguageModel(
id = id,
code = code,
name = name,
)

View File

@ -6,6 +6,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.VoteFormat
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CreatePostSection
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.LanguageModel
import dev.icerock.moko.resources.desc.StringDesc
@Stable
@ -30,6 +31,8 @@ interface CreateCommentMviModel :
}
}
data class ChangeLanguage(val value: Int?) : Intent
data class Send(val text: String) : Intent
}
@ -43,6 +46,8 @@ interface CreateCommentMviModel :
val autoLoadImages: Boolean = true,
val currentInstance: String = "",
val currentUser: String = "",
val currentLanguageId: Int? = null,
val availableLanguages: List<LanguageModel> = emptyList(),
)
sealed interface Effect {

View File

@ -63,11 +63,13 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.PostCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.PostCardBody
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.TextFormattingBar
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.RawContentDialog
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SelectLanguageDialog
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs
import com.github.diegoberaldin.raccoonforlemmy.core.utils.gallery.getGalleryHelper
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
@ -116,8 +118,12 @@ class CreateCommentScreen(
val themeRepository = remember { getThemeRepository() }
val contentFontFamily by themeRepository.contentFontFamily.collectAsState()
val typography = contentFontFamily.toTypography()
var selectLanguageDialogOpen by remember { mutableStateOf(false) }
LaunchedEffect(model) {
if (editedComment != null) {
model.reduce(CreateCommentMviModel.Intent.ChangeLanguage(editedComment.languageId))
}
model.effects.onEach { effect ->
when (effect) {
is CreateCommentMviModel.Effect.Failure -> {
@ -248,7 +254,12 @@ class CreateCommentScreen(
},
onSelectImage = {
openImagePicker = true
}
},
currentLanguageId = uiState.currentLanguageId,
availableLanguages = uiState.availableLanguages,
onSelectLanguage = {
selectLanguageDialogOpen = true
},
)
TextField(
modifier = Modifier
@ -426,5 +437,19 @@ class CreateCommentScreen(
}
}
}
if (selectLanguageDialogOpen) {
SelectLanguageDialog(
languages = uiState.availableLanguages,
currentLanguageId = uiState.currentLanguageId,
onSelect = rememberCallbackArgs { langId ->
model.reduce(CreateCommentMviModel.Intent.ChangeLanguage(langId))
selectLanguageDialogOpen = false
},
onDismiss = rememberCallback {
selectLanguageDialogOpen = false
}
)
}
}
}

View File

@ -42,11 +42,13 @@ class CreateCommentViewModel(
if (uiState.value.currentUser.isEmpty()) {
val auth = identityRepository.authToken.value.orEmpty()
val currentUser = siteRepository.getCurrentUser(auth)
val languages = siteRepository.getLanguages(auth)
if (currentUser != null) {
mvi.updateState {
it.copy(
currentUser = currentUser.name,
currentInstance = currentUser.host,
availableLanguages = languages,
)
}
}
@ -65,11 +67,18 @@ class CreateCommentViewModel(
override fun reduce(intent: CreateCommentMviModel.Intent) {
when (intent) {
is CreateCommentMviModel.Intent.ChangeSection -> mvi.updateState {
it.copy(section = intent.value)
is CreateCommentMviModel.Intent.ChangeSection -> {
mvi.updateState { it.copy(section = intent.value) }
}
is CreateCommentMviModel.Intent.ImageSelected -> {
loadImageAndAppendUrlInBody(intent.value)
}
is CreateCommentMviModel.Intent.ChangeLanguage -> {
mvi.updateState { it.copy(currentLanguageId = intent.value) }
}
is CreateCommentMviModel.Intent.ImageSelected -> loadImageAndAppendUrlInBody(intent.value)
is CreateCommentMviModel.Intent.Send -> submit(intent.text)
}
}
@ -85,6 +94,7 @@ class CreateCommentViewModel(
)
}
var valid = true
val languageId = uiState.value.currentLanguageId
if (text.isEmpty()) {
mvi.updateState {
it.copy(
@ -106,12 +116,14 @@ class CreateCommentViewModel(
postId = postId,
parentId = parentId,
text = text,
languageId = languageId,
auth = auth,
)
} else if (editedCommentId != null) {
commentRepository.edit(
commentId = editedCommentId,
text = text,
languageId = languageId,
auth = auth,
)
}

View File

@ -7,6 +7,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.VoteFormat
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CreatePostSection
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.LanguageModel
import dev.icerock.moko.resources.desc.StringDesc
@Stable
@ -50,6 +51,8 @@ interface CreatePostMviModel :
}
data class ChangeSection(val value: CreatePostSection) : Intent
data class ChangeLanguage(val value: Int?) : Intent
data class Send(val body: String) : Intent
}
@ -71,6 +74,8 @@ interface CreatePostMviModel :
val autoLoadImages: Boolean = true,
val currentInstance: String = "",
val currentUser: String = "",
val currentLanguageId: Int? = null,
val availableLanguages: List<LanguageModel> = emptyList(),
)
sealed interface Effect {

View File

@ -66,6 +66,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.Section
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CreatePostSection
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.PostCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.TextFormattingBar
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SelectLanguageDialog
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
@ -141,11 +142,15 @@ class CreatePostScreen(
val themeRepository = remember { getThemeRepository() }
val contentFontFamily by themeRepository.contentFontFamily.collectAsState()
val typography = contentFontFamily.toTypography()
var selectLanguageDialogOpen by remember { mutableStateOf(false) }
LaunchedEffect(model) {
val referencePost = editedPost ?: crossPost
model.reduce(CreatePostMviModel.Intent.SetTitle(referencePost?.title.orEmpty()))
model.reduce(CreatePostMviModel.Intent.SetUrl(referencePost?.url.orEmpty()))
if (editedPost != null) {
model.reduce(CreatePostMviModel.Intent.ChangeLanguage(editedPost.languageId))
}
when {
communityId != null -> model.reduce(
CreatePostMviModel.Intent.SetCommunity(CommunityModel(id = communityId))
@ -439,7 +444,12 @@ class CreatePostScreen(
},
onSelectImage = {
openImagePickerInBody = true
}
},
currentLanguageId = uiState.currentLanguageId,
availableLanguages = uiState.availableLanguages,
onSelectLanguage = {
selectLanguageDialogOpen = true
},
)
TextField(
modifier = Modifier
@ -518,6 +528,20 @@ class CreatePostScreen(
}
}
if (selectLanguageDialogOpen) {
SelectLanguageDialog(
languages = uiState.availableLanguages,
currentLanguageId = uiState.currentLanguageId,
onSelect = rememberCallbackArgs { langId ->
model.reduce(CreatePostMviModel.Intent.ChangeLanguage(langId))
selectLanguageDialogOpen = false
},
onDismiss = rememberCallback {
selectLanguageDialogOpen = false
}
)
}
if (uiState.loading) {
ProgressHud()
}

View File

@ -47,11 +47,13 @@ class CreatePostViewModel(
if (uiState.value.currentUser.isEmpty()) {
val auth = identityRepository.authToken.value.orEmpty()
val currentUser = siteRepository.getCurrentUser(auth)
val languages = siteRepository.getLanguages(auth)
if (currentUser != null) {
mvi.updateState {
it.copy(
currentUser = currentUser.name,
currentInstance = currentUser.host,
availableLanguages = languages,
)
}
}
@ -108,6 +110,10 @@ class CreatePostViewModel(
it.copy(section = intent.value)
}
is CreatePostMviModel.Intent.ChangeLanguage -> mvi.updateState {
it.copy(currentLanguageId = intent.value)
}
is CreatePostMviModel.Intent.Send -> submit(intent.body)
}
}
@ -165,6 +171,7 @@ class CreatePostViewModel(
val title = uiState.value.title
val url = uiState.value.url.takeIf { it.isNotEmpty() }?.trim()
val nsfw = uiState.value.nsfw
val languageId = uiState.value.currentLanguageId
var valid = true
if (title.isEmpty()) {
mvi.updateState {
@ -215,6 +222,7 @@ class CreatePostViewModel(
body = body,
url = url,
nsfw = nsfw,
languageId = languageId,
auth = auth,
)
}
@ -226,6 +234,7 @@ class CreatePostViewModel(
body = body,
url = url,
nsfw = nsfw,
languageId = languageId,
auth = auth,
)
}