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<*> -> {
 | 
			
		||||
                    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)
 | 
			
		||||
                        ?.replace(oldValue = ",", newValue = ".")
 | 
			
		||||
                        ?.toFloatOrNull()
 | 
			
		||||
| 
						 | 
				
			
			@ -31,15 +31,15 @@ class MarqueeParser @Inject constructor() {
 | 
			
		|||
                        ?.toFloatOrNull()
 | 
			
		||||
                    val description = item.getOrNull(structure.description) as? String?
 | 
			
		||||
 | 
			
		||||
                    if (map != null && name != null) {
 | 
			
		||||
                    if (map != null) {
 | 
			
		||||
                        Location.Marquee(
 | 
			
		||||
                            map = map,
 | 
			
		||||
                            name = name,
 | 
			
		||||
                            name = name?.takeIf { it.isNotBlank() },
 | 
			
		||||
                            position = when {
 | 
			
		||||
                                x != null && y != null -> Offset(x, y)
 | 
			
		||||
                                else -> Offset.Unspecified
 | 
			
		||||
                            },
 | 
			
		||||
                            description = description,
 | 
			
		||||
                            description = description?.takeIf { it.isNotBlank() },
 | 
			
		||||
                        )
 | 
			
		||||
                    } else {
 | 
			
		||||
                        null
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ data class Location(
 | 
			
		|||
    @Stable
 | 
			
		||||
    data class Marquee(
 | 
			
		||||
        val map: String,
 | 
			
		||||
        val name: String,
 | 
			
		||||
        val name: String?,
 | 
			
		||||
        val position: Offset,
 | 
			
		||||
        val description: String?,
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,11 +56,14 @@ class LocationRepository @Inject constructor(
 | 
			
		|||
 | 
			
		||||
    companion object {
 | 
			
		||||
        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 {
 | 
			
		||||
        const val ID = "1gbWaqXChz8pDJ-O3-Q8a_0wbDAYGlVA2voL7nEJUSn0"
 | 
			
		||||
        const val MAP = "carte"
 | 
			
		||||
        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.Scaffold
 | 
			
		||||
import androidx.compose.material3.SnackbarDuration
 | 
			
		||||
import androidx.compose.material3.SnackbarResult
 | 
			
		||||
import androidx.compose.material3.Surface
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
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.layout.ContentScale
 | 
			
		||||
import androidx.compose.ui.layout.onSizeChanged
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.platform.LocalDensity
 | 
			
		||||
import androidx.compose.ui.platform.LocalUriHandler
 | 
			
		||||
import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.AnnotatedString
 | 
			
		||||
| 
						 | 
				
			
			@ -94,8 +97,10 @@ data class AnnotatedLocationDetailUio(
 | 
			
		|||
fun LocationDetail(
 | 
			
		||||
    viewModel: LocationDetailViewModel = hiltViewModel()
 | 
			
		||||
) {
 | 
			
		||||
    val uriHandler = LocalUriHandler.current
 | 
			
		||||
    val screen = LocalScreenNavHost.current
 | 
			
		||||
    val snack = LocalSnack.current
 | 
			
		||||
    val context = LocalContext.current
 | 
			
		||||
 | 
			
		||||
    val scope = rememberCoroutineScope()
 | 
			
		||||
    val scroll = rememberScrollState()
 | 
			
		||||
| 
						 | 
				
			
			@ -136,11 +141,20 @@ fun LocationDetail(
 | 
			
		|||
            onMapTap = {
 | 
			
		||||
                snackJob.value?.cancel()
 | 
			
		||||
                snackJob.value = scope.launch {
 | 
			
		||||
                    snack.showSnackbar(
 | 
			
		||||
                    val result = snack.showSnackbar(
 | 
			
		||||
                        message = "x:${it.x}, y:${it.y}",
 | 
			
		||||
                        actionLabel = ok,
 | 
			
		||||
                        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 = it
 | 
			
		||||
| 
						 | 
				
			
			@ -162,7 +176,7 @@ fun LocationDetail(
 | 
			
		|||
                if (newScale <= fantasy.maxScale) {
 | 
			
		||||
                    val oldScale = fantasy.scale
 | 
			
		||||
                    val oldOffset = fantasy.offset
 | 
			
		||||
                    fantasy.scale(newScale)
 | 
			
		||||
                    fantasy.scale(scale = newScale)
 | 
			
		||||
                    fantasy.pan(
 | 
			
		||||
                        offset = Offset(
 | 
			
		||||
                            x = oldOffset.x * newScale / oldScale,
 | 
			
		||||
| 
						 | 
				
			
			@ -176,7 +190,7 @@ fun LocationDetail(
 | 
			
		|||
                if (newScale >= fantasy.minScale) {
 | 
			
		||||
                    val oldScale = fantasy.scale
 | 
			
		||||
                    val oldOffset = fantasy.offset
 | 
			
		||||
                    fantasy.scale(newScale)
 | 
			
		||||
                    fantasy.scale(scale = newScale)
 | 
			
		||||
                    fantasy.pan(
 | 
			
		||||
                        offset = Offset(
 | 
			
		||||
                            x = oldOffset.x * newScale / oldScale,
 | 
			
		||||
| 
						 | 
				
			
			@ -259,12 +273,6 @@ private fun LocationContent(
 | 
			
		|||
                modifier = Modifier.verticalScroll(state = scrollState),
 | 
			
		||||
            ) {
 | 
			
		||||
                Surface(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .fillMaxWidth()
 | 
			
		||||
                        .heightIn(
 | 
			
		||||
                            min = this@constraint.maxHeight / 2,
 | 
			
		||||
                            max = this@constraint.maxHeight * 2 / 3,
 | 
			
		||||
                        ),
 | 
			
		||||
                    tonalElevation = 2.dp,
 | 
			
		||||
                ) {
 | 
			
		||||
                    Box(
 | 
			
		||||
| 
						 | 
				
			
			@ -272,8 +280,12 @@ private fun LocationContent(
 | 
			
		|||
                    ) {
 | 
			
		||||
                        FantasyMap(
 | 
			
		||||
                            modifier = Modifier
 | 
			
		||||
                                .align(alignment = Alignment.Center)
 | 
			
		||||
                                .offset { IntOffset(x = 0, y = scrollState.value / 2) },
 | 
			
		||||
                                .fillMaxWidth()
 | 
			
		||||
                                .heightIn(
 | 
			
		||||
                                    min = this@constraint.maxHeight / 2,
 | 
			
		||||
                                    max = this@constraint.maxHeight * 2 / 3,
 | 
			
		||||
                                )
 | 
			
		||||
                                .offset(scrollState = scrollState),
 | 
			
		||||
                            state = fantasyMapState,
 | 
			
		||||
                            model = { item.value.map },
 | 
			
		||||
                            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)
 | 
			
		||||
@Composable
 | 
			
		||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,31 @@
 | 
			
		|||
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.mutableStateOf
 | 
			
		||||
import androidx.compose.ui.geometry.Offset
 | 
			
		||||
import androidx.lifecycle.AndroidViewModel
 | 
			
		||||
import androidx.lifecycle.SavedStateHandle
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import com.pixelized.rplexicon.repository.LocationRepository
 | 
			
		||||
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 javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@HiltViewModel
 | 
			
		||||
class LocationDetailViewModel @Inject constructor(
 | 
			
		||||
    application: Application,
 | 
			
		||||
    savedStateHandle: SavedStateHandle,
 | 
			
		||||
    repository: LocationRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
) : AndroidViewModel(application) {
 | 
			
		||||
    private val clipboard = application.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
 | 
			
		||||
 | 
			
		||||
    val sheetUri = LocationRepository.SHEET_URL
 | 
			
		||||
    val location: State<LocationDetailUio>
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
data class MarqueeUio(
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val name: String?,
 | 
			
		||||
    val position: Offset,
 | 
			
		||||
    val description: String?,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class AnnotatedMarqueeUio(
 | 
			
		||||
    val name: AnnotatedString,
 | 
			
		||||
    val name: AnnotatedString?,
 | 
			
		||||
    val position: Offset,
 | 
			
		||||
    val description: AnnotatedString?,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -43,9 +43,13 @@ fun MarqueeUio.annotate(
 | 
			
		|||
    typography: LexiconTypography
 | 
			
		||||
): AnnotatedMarqueeUio {
 | 
			
		||||
    return AnnotatedMarqueeUio(
 | 
			
		||||
        name = name.annotateWithDropCap(style = typography.bodyDropCapSpan),
 | 
			
		||||
        name = name?.annotateWithDropCap(
 | 
			
		||||
            style = typography.bodyDropCapSpan,
 | 
			
		||||
        ),
 | 
			
		||||
        position = position,
 | 
			
		||||
        description = description?.annotateWithDropCap(style = typography.bodyDropCapSpan),
 | 
			
		||||
        description = description?.annotateWithDropCap(
 | 
			
		||||
            style = typography.bodyDropCapSpan,
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -58,21 +62,23 @@ fun MarqueeItem(
 | 
			
		|||
        modifier = modifier,
 | 
			
		||||
        verticalArrangement = Arrangement.spacedBy(space = 8.dp)
 | 
			
		||||
    ) {
 | 
			
		||||
        Row(
 | 
			
		||||
            horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
        ) {
 | 
			
		||||
            Text(
 | 
			
		||||
                modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                text = LOS_HOLLOW,
 | 
			
		||||
            )
 | 
			
		||||
            Text(
 | 
			
		||||
                modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                style = MaterialTheme.typography.titleMedium,
 | 
			
		||||
                textAlign = TextAlign.Center,
 | 
			
		||||
                overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                maxLines = 3,
 | 
			
		||||
                text = marquee.name,
 | 
			
		||||
            )
 | 
			
		||||
        marquee.name?.let {
 | 
			
		||||
            Row(
 | 
			
		||||
                horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
            ) {
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    text = LOS_HOLLOW,
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.titleMedium,
 | 
			
		||||
                    textAlign = TextAlign.Center,
 | 
			
		||||
                    overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                    maxLines = 3,
 | 
			
		||||
                    text = marquee.name,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        marquee.description?.let {
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.view.accessibility.AccessibilityNodeInfo
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.ui.text.AnnotatedString
 | 
			
		||||
import androidx.compose.ui.text.SpanStyle
 | 
			
		||||
import androidx.core.net.toUri
 | 
			
		||||
 | 
			
		||||
private val dropCapRegex = Regex(
 | 
			
		||||
    pattern = "(?:^|\n\n)([A-Z])"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
val String.ARG: String get() = "$this={$this}"
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
val String?.highlightRegex: Regex?
 | 
			
		||||
    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?
 | 
			
		||||
    get() = this?.takeIf { it.isNotEmpty() }?.let {
 | 
			
		||||
        Regex(
 | 
			
		||||
| 
						 | 
				
			
			@ -28,18 +36,13 @@ fun String?.toUriOrNull(): Uri? = try {
 | 
			
		|||
    null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val dropCapRegex = Regex("(?:\n\n)(.)")
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
fun String.annotateWithDropCap(
 | 
			
		||||
    style: SpanStyle,
 | 
			
		||||
) = AnnotatedString(
 | 
			
		||||
    text = this,
 | 
			
		||||
    spanStyles = listOf(
 | 
			
		||||
        AnnotatedString.Range(
 | 
			
		||||
            item = style,
 | 
			
		||||
            start = 0,
 | 
			
		||||
            end = Integer.min(1, this.length),
 | 
			
		||||
        ),
 | 
			
		||||
    ) + dropCapRegex.annotatedSpan(input = this, spanStyle = style)
 | 
			
		||||
    spanStyles = dropCapRegex.annotatedSpan(
 | 
			
		||||
        input = this,
 | 
			
		||||
        spanStyle = style,
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue