Add map coordinates to clipboard feature.
This commit is contained in:
parent
9dbfb9c3a0
commit
62b41e664d
10 changed files with 119 additions and 48 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 } }
|
||||||
|
|
@ -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)
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue