Add background image to quest.

This commit is contained in:
Thomas Andres Gomez 2023-08-01 20:52:45 +02:00
parent 331fbb87e5
commit 6a326ad1a8
12 changed files with 321 additions and 158 deletions

View file

@ -1,11 +1,13 @@
package com.pixelized.rplexicon.model
import android.net.Uri
import androidx.compose.runtime.Stable
@Stable
data class Quest(
val id: Int,
val title: String,
val background: Uri?,
val entries: List<QuestEntry>,
)
@ -20,4 +22,5 @@ data class QuestEntry(
val groupReward: String?,
val individualReward: String?,
val description: String,
val background: Uri?,
)

View file

@ -13,6 +13,7 @@ import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import com.pixelized.rplexicon.utilitary.extentions.toUriOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -135,12 +136,6 @@ class LexiconRepository @Inject constructor(
}
}
private fun String?.toUriOrNull(): Uri? = try {
this?.takeIf { it.isNotBlank() }?.toUri()
} catch (_: Exception) {
null
}
private val Map<String, Int>?.name: Int get() = this?.getValue(Sheet.NAME) ?: 0
private val Map<String, Int>?.diminutive: Int get() = this?.getValue(Sheet.DIMINUTIVE) ?: 1
private val Map<String, Int>?.gender: Int get() = this?.getValue(Sheet.GENDER) ?: 2

View file

@ -12,6 +12,7 @@ import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import com.pixelized.rplexicon.utilitary.extentions.toUriOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -82,6 +83,7 @@ class QuestRepository @Inject constructor(
Quest(
id = index,
title = item,
background = questMap[item]?.mapNotNull { it.background }?.randomOrNull(),
entries = questMap[item] ?: emptyList(),
)
}
@ -96,24 +98,26 @@ class QuestRepository @Inject constructor(
): QuestEntry? {
val title = row?.getOrNull(sheetStructure.title) as? String
val subtitle = row?.getOrNull(sheetStructure.subtitle) as? String?
val complete = row?.getOrNull(sheetStructure.complete) as? Boolean? ?: false
val complete = row?.getOrNull(sheetStructure.complete) as? String?
val questGiver = row?.getOrNull(sheetStructure.questGiver) as? String?
val area = row?.getOrNull(sheetStructure.area) as? String?
val groupReward = row?.getOrNull(sheetStructure.groupReward) as? String?
val individualReward = row?.getOrNull(sheetStructure.individualReward) as? String?
val description = row?.getOrNull(sheetStructure.description) as? String
val background = row?.getOrNull(sheetStructure.background) as? String?
return if (title?.isNotEmpty() == true && description?.isNotEmpty() == true) {
QuestEntry(
sheetIndex = sheetIndex,
title = title,
subtitle = subtitle?.takeIf { it.isNotBlank() },
complete = complete,
complete = complete.equals("TRUE", ignoreCase = true),
questGiver = questGiver?.takeIf { it.isNotBlank() },
area = area?.takeIf { it.isNotBlank() },
groupReward = groupReward?.takeIf { it.isNotBlank() },
individualReward = individualReward?.takeIf { it.isNotBlank() },
description = description,
background = background?.toUriOrNull(),
)
} else {
null
@ -128,6 +132,7 @@ class QuestRepository @Inject constructor(
private val Map<String, Int>?.groupReward: Int get() = this?.getValue(Sheet.G_REWARD) ?: 5
private val Map<String, Int>?.individualReward: Int get() = this?.getValue(Sheet.I_REWARD) ?: 6
private val Map<String, Int>?.description: Int get() = this?.getValue(Sheet.DESCRIPTION) ?: 7
private val Map<String, Int>?.background: Int get() = this?.getValue(Sheet.BACKGROUND) ?: 8
private object Sheet {
const val ID = "1sDAay8DjbRYKM39MvEXWs-RuvyxjOFpOfRZLAEWjIUY"
@ -143,6 +148,7 @@ class QuestRepository @Inject constructor(
"Récompense de groupe",
"Récompense individuelle",
"Description",
"fond"
)
val TITLE = COLUMNS[0]
val SUBTITLE = COLUMNS[1]
@ -152,5 +158,6 @@ class QuestRepository @Inject constructor(
val G_REWARD = COLUMNS[5]
val I_REWARD = COLUMNS[6]
val DESCRIPTION = COLUMNS[7]
val BACKGROUND = COLUMNS[8]
}
}

View file

@ -0,0 +1,60 @@
package com.pixelized.rplexicon.ui.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.layout.ContentScale
import com.pixelized.rplexicon.R
import com.skydoves.landscapist.ImageOptions
@Composable
fun BackgroundImage(
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.TopCenter,
contentScale: ContentScale = ContentScale.Crop,
model: () -> Any?,
) {
Box(
modifier = modifier
) {
AsyncImage(
modifier = Modifier.matchParentSize(),
imageOptions = ImageOptions(
alignment = alignment,
contentScale = contentScale,
colorFilter = remember {
ColorFilter.colorMatrix(
ColorMatrix().also { it.setToSaturation(0f) }
)
},
),
imageModel = model,
previewPlaceholder = R.drawable.im_brulkhai,
)
Box(
modifier = Modifier
.matchParentSize()
.background(brush = rememberBackgroundGradient())
)
}
}
@Composable
private fun rememberBackgroundGradient(): Brush {
val colorScheme = MaterialTheme.colorScheme
return remember {
Brush.verticalGradient(
colors = listOf(
colorScheme.surface.copy(alpha = 0.5f),
colorScheme.surface.copy(alpha = 1.0f),
)
)
}
}

View file

@ -0,0 +1,35 @@
package com.pixelized.rplexicon.ui.composable.remember
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
/**
* https://stackoverflow.com/questions/73333935/current-scroll-position-value-in-pixels-in-lazycolumn-jetpack-compose
*/
@Composable
fun rememberCurrentOffset(state: LazyListState): State<Int> {
val position = remember { derivedStateOf { state.firstVisibleItemIndex } }
val itemOffset = remember { derivedStateOf { state.firstVisibleItemScrollOffset } }
val lastPosition = rememberPrevious(position.value)
val lastItemOffset = rememberPrevious(itemOffset.value)
val currentOffset = remember { mutableStateOf(0) }
LaunchedEffect(position.value, itemOffset.value) {
if (lastPosition == null || position.value == 0) {
currentOffset.value = itemOffset.value
} else if (lastPosition == position.value) {
currentOffset.value += (itemOffset.value - (lastItemOffset ?: 0))
} else if (lastPosition > position.value) {
currentOffset.value -= (lastItemOffset ?: 0)
} else { // lastPosition.value < position.value
currentOffset.value += itemOffset.value
}
}
return currentOffset
}

View file

@ -0,0 +1,21 @@
package com.pixelized.rplexicon.ui.composable.remember
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
@Composable
fun <T> rememberPrevious(
current: T,
shouldUpdate: (prev: T?, curr: T) -> Boolean = { a: T?, b: T -> a != b },
): T? {
val ref = rememberRef<T>()
// launched after render, so the current render will have the old value anyway
SideEffect {
if (shouldUpdate(ref.value, current)) {
ref.value = current
}
}
return ref.value
}

View file

@ -0,0 +1,23 @@
package com.pixelized.rplexicon.ui.composable.remember
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
/**
* Returns a dummy MutableState that does not cause render when setting it
*/
@Composable
fun <T> rememberRef(): MutableState<T?> {
// for some reason it always recreated the value with vararg keys,
// leaving out the keys as a parameter for remember for now
return remember {
object : MutableState<T?> {
override var value: T? = null
override fun component1(): T? = value
override fun component2(): (T?) -> Unit = { value = it }
}
}
}

View file

@ -4,7 +4,6 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -35,14 +34,10 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
@ -60,6 +55,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.ui.composable.AsyncImage
import com.pixelized.rplexicon.ui.composable.BackgroundImage
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.composable.stringResource
@ -197,32 +193,13 @@ private fun LexiconDetailContent(
modifier = Modifier.padding(paddingValues = paddingValues),
) {
annotatedItem.portrait.firstOrNull()?.let { uri ->
Box(
BackgroundImage(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(ratio = 1f)
.scrollOffset(scrollState = state) { -it / 2 },
) {
AsyncImage(
modifier = Modifier.matchParentSize(),
imageOptions = ImageOptions(
alignment = Alignment.TopCenter,
contentScale = ContentScale.Crop,
colorFilter = remember {
ColorFilter.colorMatrix(
ColorMatrix().also { it.setToSaturation(0f) }
)
},
),
imageModel = { uri.toString() },
previewPlaceholder = R.drawable.im_brulkhai,
)
Box(
modifier = Modifier
.matchParentSize()
.background(brush = rememberBackgroundGradient())
)
}
model = { uri.toString() },
)
}
Column(
modifier = Modifier
@ -342,19 +319,6 @@ private fun LexiconDetailContent(
}
}
@Composable
private fun rememberBackgroundGradient(): Brush {
val colorScheme = MaterialTheme.colorScheme
return remember {
Brush.verticalGradient(
colors = listOf(
colorScheme.surface.copy(alpha = 0.5f),
colorScheme.surface.copy(alpha = 1.0f),
)
)
}
}
@Composable
private fun rememberPortraitWidth(): Dp {
val configuration = LocalConfiguration.current

View file

@ -1,17 +1,22 @@
package com.pixelized.rplexicon.ui.screens.quest.detail
import android.content.res.Configuration
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -27,10 +32,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@ -39,9 +46,12 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.BackgroundImage
import com.pixelized.rplexicon.ui.composable.remember.rememberCurrentOffset
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@ -50,6 +60,7 @@ import java.lang.Integer.min
@Stable
data class QuestDetailUio(
val id: Int,
val background: Uri?,
val title: String,
val steps: List<QuestStep>,
) {
@ -68,6 +79,7 @@ data class QuestDetailUio(
@Stable
data class AnnotatedQuestDetailUio(
val title: String,
val background: Uri?,
val steps: List<AnnotatedQuestStep>,
) {
@Stable
@ -89,6 +101,7 @@ private fun QuestDetailUio.annotate(): AnnotatedQuestDetailUio {
return remember {
AnnotatedQuestDetailUio(
title = title,
background = background,
steps = annotatedSteps,
)
}
@ -143,6 +156,7 @@ private fun QuestDetailContent(
item: State<QuestDetailUio>,
onBack: () -> Unit,
) {
val state = rememberLazyListState()
val annotatedQuest = item.value.annotate()
Scaffold(
@ -164,128 +178,158 @@ private fun QuestDetailContent(
)
},
content = { padding ->
LazyColumn(
Surface(
modifier = Modifier.padding(padding),
contentPadding = PaddingValues(
top = 40.dp,
bottom = 16.dp,
start = 16.dp,
end = 16.dp
),
verticalArrangement = Arrangement.spacedBy(40.dp),
) {
item {
Column {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displaySmall,
text = annotatedQuest.title,
)
Image(
modifier = Modifier
.height(24.dp)
.graphicsLayer { rotationZ = 180f }
.align(Alignment.CenterHorizontally),
painter = painterResource(id = R.drawable.art_divider_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
}
items(annotatedQuest.steps) { quest ->
Column {
quest.subtitle?.let { subtitle ->
Row(
BackgroundImage(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(ratio = 1f)
.scrollOffset(scrollState = state) { -it / 2 },
model = { annotatedQuest.background },
)
LazyColumn(
state = state,
contentPadding = PaddingValues(
top = 248.dp,
bottom = 16.dp,
start = 16.dp,
end = 16.dp
),
verticalArrangement = Arrangement.spacedBy(0.dp),
) {
item {
Column {
Text(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Image(
modifier = Modifier.graphicsLayer { rotationY = 180f },
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
Text(
modifier = Modifier.padding(all = 8.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
text = subtitle,
)
Image(
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displaySmall,
text = annotatedQuest.title,
)
Image(
modifier = Modifier
.height(24.dp)
.graphicsLayer { rotationZ = 180f }
.align(Alignment.CenterHorizontally),
painter = painterResource(id = R.drawable.art_divider_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
}
items(annotatedQuest.steps) { quest ->
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
quest.subtitle?.let { subtitle ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 64.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Image(
modifier = Modifier.graphicsLayer { rotationY = 180f },
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
Text(
modifier = Modifier.padding(horizontal = 8.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
text = subtitle,
)
Image(
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
}
}
quest.giver?.let {
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Commanditaire",
)
quest.giver?.let {
Column {
Text(
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Commanditaire",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
}
quest.place?.let {
Column {
Text(
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Lieu",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
}
quest.globalReward?.let {
Column {
Text(
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Récompense de groupe",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
}
quest.individualReward?.let {
Column {
Text(
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Récompense individuelle",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
}
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
text = quest.description,
)
}
quest.place?.let {
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Lieu",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
quest.globalReward?.let {
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Récompense de groupe",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
quest.individualReward?.let {
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Récompense individuelle",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
Text(
modifier = Modifier.padding(top = 24.dp),
style = MaterialTheme.typography.bodyMedium,
text = quest.description,
)
}
}
}
},
)
}
@Stable
private fun Modifier.scrollOffset(
scrollState: LazyListState,
block: (Dp) -> Dp
): Modifier = composed {
val scroll = rememberCurrentOffset(scrollState)
val density = LocalDensity.current
this.offset(y = with(density) { block(scroll.value.toDp()) })
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@ -307,6 +351,7 @@ private class QuestDetailPreviewProvider : PreviewParameterProvider<State<QuestD
mutableStateOf(
QuestDetailUio(
id = 0,
background = Uri.parse("https://as1.ftcdn.net/v2/jpg/05/50/22/58/1000_F_550225869_jAkLTRVb7ym7EHJYvDApVXQnpANvRd8O.jpg"),
title = "La chasse aux loups",
steps = listOf(
QuestDetailUio.QuestStep(
@ -333,6 +378,7 @@ private class QuestDetailPreviewProvider : PreviewParameterProvider<State<QuestD
mutableStateOf(
QuestDetailUio(
id = 1,
background = Uri.parse("https://cdnb.artstation.com/p/assets/images/images/008/823/761/large/jon-pintar-adventurers-caravan-jon-pintar.jpg?1515529013"),
title = "Les enfants de la caravanes",
steps = listOf(
QuestDetailUio.QuestStep(
@ -348,5 +394,4 @@ private class QuestDetailPreviewProvider : PreviewParameterProvider<State<QuestD
)
),
)
}
}

View file

@ -23,6 +23,7 @@ class QuestDetailViewModel @Inject constructor(
quest = mutableStateOf(
QuestDetailUio(
id = source.id,
background = source.background,
title = source.title,
steps = source.entries.map { entry ->
QuestDetailUio.QuestStep(

View file

@ -79,7 +79,7 @@ private fun QuestListContent(
modifier: Modifier = Modifier,
lazyColumnState: LazyListState,
paddingValues: PaddingValues = PaddingValues(
top = 6.dp,
top = 8.dp,
bottom = 8.dp + 16.dp + 56.dp + 16.dp,
),
refreshState: PullRefreshState,

View file

@ -1,5 +1,8 @@
package com.pixelized.rplexicon.utilitary.extentions
import android.net.Uri
import androidx.core.net.toUri
val String.ARG: String get() = "$this={$this}"
val String?.highlightRegex: Regex?
@ -15,3 +18,9 @@ val String?.finderRegex: Regex?
)
}
fun String?.toUriOrNull(): Uri? = try {
this?.takeIf { it.isNotBlank() }?.toUri()
} catch (_: Exception) {
null
}