Add map coordinates to clipboard feature.

This commit is contained in:
Thomas Andres Gomez 2023-08-17 15:33:09 +02:00
parent 9dbfb9c3a0
commit 62b41e664d
10 changed files with 119 additions and 48 deletions

View file

@ -22,7 +22,7 @@ class MarqueeParser @Inject constructor() {
item is List<*> -> { item is List<*> -> {
val map = item.getOrNull(structure.map) as? String val map = item.getOrNull(structure.map) as? String
val name = item.getOrNull(structure.name) as? String val name = item.getOrNull(structure.name) as? String?
val x = (item.getOrNull(structure.x) as? String) val x = (item.getOrNull(structure.x) as? String)
?.replace(oldValue = ",", newValue = ".") ?.replace(oldValue = ",", newValue = ".")
?.toFloatOrNull() ?.toFloatOrNull()
@ -31,15 +31,15 @@ class MarqueeParser @Inject constructor() {
?.toFloatOrNull() ?.toFloatOrNull()
val description = item.getOrNull(structure.description) as? String? val description = item.getOrNull(structure.description) as? String?
if (map != null && name != null) { if (map != null) {
Location.Marquee( Location.Marquee(
map = map, map = map,
name = name, name = name?.takeIf { it.isNotBlank() },
position = when { position = when {
x != null && y != null -> Offset(x, y) x != null && y != null -> Offset(x, y)
else -> Offset.Unspecified else -> Offset.Unspecified
}, },
description = description, description = description?.takeIf { it.isNotBlank() },
) )
} else { } else {
null null

View file

@ -15,7 +15,7 @@ data class Location(
@Stable @Stable
data class Marquee( data class Marquee(
val map: String, val map: String,
val name: String, val name: String?,
val position: Offset, val position: Offset,
val description: String?, val description: String?,
) )

View file

@ -56,11 +56,14 @@ class LocationRepository @Inject constructor(
companion object { companion object {
private const val TAG = "LocationRepository" private const val TAG = "LocationRepository"
private const val ROOT = "https://docs.google.com/spreadsheets"
const val SHEET_URL = "$ROOT/d/${Sheet.ID}/edit#gid=${Sheet.MARQUEE_GID}"
} }
private object Sheet { private object Sheet {
const val ID = "1gbWaqXChz8pDJ-O3-Q8a_0wbDAYGlVA2voL7nEJUSn0" const val ID = "1gbWaqXChz8pDJ-O3-Q8a_0wbDAYGlVA2voL7nEJUSn0"
const val MAP = "carte" const val MAP = "carte"
const val MARQUEE = "marqueur" const val MARQUEE = "marqueur"
const val MARQUEE_GID = "1581979177"
} }
} }

View file

@ -30,6 +30,7 @@ import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@ -53,7 +54,9 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@ -94,8 +97,10 @@ data class AnnotatedLocationDetailUio(
fun LocationDetail( fun LocationDetail(
viewModel: LocationDetailViewModel = hiltViewModel() viewModel: LocationDetailViewModel = hiltViewModel()
) { ) {
val uriHandler = LocalUriHandler.current
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val snack = LocalSnack.current val snack = LocalSnack.current
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scroll = rememberScrollState() val scroll = rememberScrollState()
@ -136,11 +141,20 @@ fun LocationDetail(
onMapTap = { onMapTap = {
snackJob.value?.cancel() snackJob.value?.cancel()
snackJob.value = scope.launch { snackJob.value = scope.launch {
snack.showSnackbar( val result = snack.showSnackbar(
message = "x:${it.x}, y:${it.y}", message = "x:${it.x}, y:${it.y}",
actionLabel = ok, actionLabel = ok,
duration = SnackbarDuration.Indefinite, duration = SnackbarDuration.Indefinite,
) )
if (result == SnackbarResult.ActionPerformed) {
viewModel.clip(
label = context.getString(R.string.map_label),
coordinate = it,
)
uriHandler.openUri(
uri = viewModel.sheetUri,
)
}
mapHighlight.value = Offset.Unspecified mapHighlight.value = Offset.Unspecified
} }
mapHighlight.value = it mapHighlight.value = it
@ -162,7 +176,7 @@ fun LocationDetail(
if (newScale <= fantasy.maxScale) { if (newScale <= fantasy.maxScale) {
val oldScale = fantasy.scale val oldScale = fantasy.scale
val oldOffset = fantasy.offset val oldOffset = fantasy.offset
fantasy.scale(newScale) fantasy.scale(scale = newScale)
fantasy.pan( fantasy.pan(
offset = Offset( offset = Offset(
x = oldOffset.x * newScale / oldScale, x = oldOffset.x * newScale / oldScale,
@ -176,7 +190,7 @@ fun LocationDetail(
if (newScale >= fantasy.minScale) { if (newScale >= fantasy.minScale) {
val oldScale = fantasy.scale val oldScale = fantasy.scale
val oldOffset = fantasy.offset val oldOffset = fantasy.offset
fantasy.scale(newScale) fantasy.scale(scale = newScale)
fantasy.pan( fantasy.pan(
offset = Offset( offset = Offset(
x = oldOffset.x * newScale / oldScale, x = oldOffset.x * newScale / oldScale,
@ -259,12 +273,6 @@ private fun LocationContent(
modifier = Modifier.verticalScroll(state = scrollState), modifier = Modifier.verticalScroll(state = scrollState),
) { ) {
Surface( Surface(
modifier = Modifier
.fillMaxWidth()
.heightIn(
min = this@constraint.maxHeight / 2,
max = this@constraint.maxHeight * 2 / 3,
),
tonalElevation = 2.dp, tonalElevation = 2.dp,
) { ) {
Box( Box(
@ -272,8 +280,12 @@ private fun LocationContent(
) { ) {
FantasyMap( FantasyMap(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.Center) .fillMaxWidth()
.offset { IntOffset(x = 0, y = scrollState.value / 2) }, .heightIn(
min = this@constraint.maxHeight / 2,
max = this@constraint.maxHeight * 2 / 3,
)
.offset(scrollState = scrollState),
state = fantasyMapState, state = fantasyMapState,
model = { item.value.map }, model = { item.value.map },
previewPlaceholder = R.drawable.im_brulkhai, previewPlaceholder = R.drawable.im_brulkhai,
@ -483,6 +495,11 @@ private fun rememberScrollConnection(
} }
} }
@Stable
private fun Modifier.offset(scrollState: ScrollState) = this.offset {
IntOffset(x = 0, y = scrollState.value / 2)
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)

View file

@ -1,19 +1,31 @@
package com.pixelized.rplexicon.ui.screens.location.detail package com.pixelized.rplexicon.ui.screens.location.detail
import android.app.Application
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Offset
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.repository.LocationRepository import com.pixelized.rplexicon.repository.LocationRepository
import com.pixelized.rplexicon.ui.navigation.screens.locationDetailArgument import com.pixelized.rplexicon.ui.navigation.screens.locationDetailArgument
import com.pixelized.rplexicon.utilitary.cells
import com.pixelized.rplexicon.utilitary.line
import com.pixelized.rplexicon.utilitary.table
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class LocationDetailViewModel @Inject constructor( class LocationDetailViewModel @Inject constructor(
application: Application,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
repository: LocationRepository, repository: LocationRepository,
) : ViewModel() { ) : AndroidViewModel(application) {
private val clipboard = application.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val sheetUri = LocationRepository.SHEET_URL
val location: State<LocationDetailUio> val location: State<LocationDetailUio>
init { init {
@ -34,4 +46,23 @@ class LocationDetailViewModel @Inject constructor(
) )
) )
} }
fun clip(
label: String,
coordinate: Offset,
) {
val clip = ClipData.newHtmlText(
label,
"${coordinate.x}, ${coordinate.y}",
table {
line {
cells(
coordinate.x.toString().replace(".", ","),
coordinate.y.toString().replace(".", ","),
)
}
}
)
clipboard.setPrimaryClip(clip)
}
} }

View file

@ -26,14 +26,14 @@ import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable @Stable
data class MarqueeUio( data class MarqueeUio(
val name: String, val name: String?,
val position: Offset, val position: Offset,
val description: String?, val description: String?,
) )
@Stable @Stable
data class AnnotatedMarqueeUio( data class AnnotatedMarqueeUio(
val name: AnnotatedString, val name: AnnotatedString?,
val position: Offset, val position: Offset,
val description: AnnotatedString?, val description: AnnotatedString?,
) )
@ -43,9 +43,13 @@ fun MarqueeUio.annotate(
typography: LexiconTypography typography: LexiconTypography
): AnnotatedMarqueeUio { ): AnnotatedMarqueeUio {
return AnnotatedMarqueeUio( return AnnotatedMarqueeUio(
name = name.annotateWithDropCap(style = typography.bodyDropCapSpan), name = name?.annotateWithDropCap(
style = typography.bodyDropCapSpan,
),
position = position, position = position,
description = description?.annotateWithDropCap(style = typography.bodyDropCapSpan), description = description?.annotateWithDropCap(
style = typography.bodyDropCapSpan,
),
) )
} }
@ -58,21 +62,23 @@ fun MarqueeItem(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = 8.dp) verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) { ) {
Row( marquee.name?.let {
horizontalArrangement = Arrangement.spacedBy(8.dp), Row(
) { horizontalArrangement = Arrangement.spacedBy(8.dp),
Text( ) {
modifier = Modifier.alignByBaseline(), Text(
text = LOS_HOLLOW, modifier = Modifier.alignByBaseline(),
) text = LOS_HOLLOW,
Text( )
modifier = Modifier.alignByBaseline(), Text(
style = MaterialTheme.typography.titleMedium, modifier = Modifier.alignByBaseline(),
textAlign = TextAlign.Center, style = MaterialTheme.typography.titleMedium,
overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center,
maxLines = 3, overflow = TextOverflow.Ellipsis,
text = marquee.name, maxLines = 3,
) text = marquee.name,
)
}
} }
marquee.description?.let { marquee.description?.let {
Text( Text(

View file

@ -0,0 +1,9 @@
package com.pixelized.rplexicon.utilitary
inline fun table(crossinline block: () -> String): String = "<table>${block()}</table>"
inline fun line(crossinline block: () -> String): String = "<tr>${block()}</tr>"
inline fun cell(crossinline block: () -> String): String = "<td>${block()}</td>"
fun cells(vararg values: String): String = values.joinToString(separator = "") { cell { it } }

View file

@ -1,19 +1,27 @@
package com.pixelized.rplexicon.utilitary.extentions package com.pixelized.rplexicon.utilitary.extentions
import android.net.Uri import android.net.Uri
import android.view.accessibility.AccessibilityNodeInfo
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.core.net.toUri import androidx.core.net.toUri
private val dropCapRegex = Regex(
pattern = "(?:^|\n\n)([A-Z])"
)
val String.ARG: String get() = "$this={$this}" val String.ARG: String get() = "$this={$this}"
@Stable
val String?.highlightRegex: Regex? val String?.highlightRegex: Regex?
get() = this?.takeIf { it.isNotEmpty() }?.let { get() = this?.takeIf { it.isNotEmpty() }?.let {
Regex(pattern = Regex.escape(it), option = RegexOption.IGNORE_CASE) Regex(
pattern = Regex.escape(it),
option = RegexOption.IGNORE_CASE,
)
} }
@Stable
val String?.finderRegex: Regex? val String?.finderRegex: Regex?
get() = this?.takeIf { it.isNotEmpty() }?.let { get() = this?.takeIf { it.isNotEmpty() }?.let {
Regex( Regex(
@ -28,18 +36,13 @@ fun String?.toUriOrNull(): Uri? = try {
null null
} }
private val dropCapRegex = Regex("(?:\n\n)(.)")
@Stable @Stable
fun String.annotateWithDropCap( fun String.annotateWithDropCap(
style: SpanStyle, style: SpanStyle,
) = AnnotatedString( ) = AnnotatedString(
text = this, text = this,
spanStyles = listOf( spanStyles = dropCapRegex.annotatedSpan(
AnnotatedString.Range( input = this,
item = style, spanStyle = style,
start = 0, )
end = Integer.min(1, this.length),
),
) + dropCapRegex.annotatedSpan(input = this, spanStyle = style)
) )

View file

@ -54,4 +54,5 @@
<string name="quest_detail_group_rewars">Récompense de groupe :</string> <string name="quest_detail_group_rewars">Récompense de groupe :</string>
<string name="map_title">Carte</string> <string name="map_title">Carte</string>
<string name="map_label">Coordonnées</string>
</resources> </resources>

View file

@ -54,4 +54,5 @@
<string name="quest_detail_group_rewars">Group reward:</string> <string name="quest_detail_group_rewars">Group reward:</string>
<string name="map_title">Map</string> <string name="map_title">Map</string>
<string name="map_label">Coordinates</string>
</resources> </resources>