Category management - Maps

This commit is contained in:
Thomas Andres Gomez 2023-11-15 16:21:19 +01:00
parent fa7fcbeae6
commit 012e8844cb
7 changed files with 158 additions and 36 deletions

View file

@ -6,6 +6,7 @@ import androidx.compose.ui.geometry.Offset
data class Location(
val id: String,
val name: String,
val category: String?,
val uri: Uri?,
val description: String?,
val child: List<Pair<Offset, Location>> = emptyList(),

View file

@ -19,6 +19,7 @@ class LocationParser @Inject constructor(
Location(
id = localMap.name,
name = localMap.name,
category = localMap.category,
uri = localMap.uri,
description = localMap.description,
child = emptyList(),

View file

@ -15,13 +15,12 @@ class MapParser @Inject constructor() {
sheet.forEachDataLine(columns = COLUMNS) {
val name = it.parse(column = NAME)
val uri = it.parseUri(column = URI)
val description = it.parse(column = DESCRIPTION)
if (name != null) {
val map = MapDto(
name = name,
uri = uri,
description = description,
category = it.parse(column = CATEGORY),
uri = it.parseUri(column = URI),
description = it.parse(column = DESCRIPTION),
)
maps.add(map)
}
@ -32,14 +31,16 @@ class MapParser @Inject constructor() {
data class MapDto(
val name: String,
val category: String?,
val uri: Uri?,
val description: String?,
)
companion object {
private val NAME = column("Nom")
private val CATEGORY = column("Catégorie")
private val URI = column("Carte")
private val DESCRIPTION = column("Description")
private val COLUMNS get() = listOf(NAME, URI, DESCRIPTION)
private val COLUMNS get() = listOf(NAME, CATEGORY, URI, DESCRIPTION)
}
}

View file

@ -0,0 +1,26 @@
package com.pixelized.rplexicon.ui.screens.location.list
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@Stable
data class LocationCategoryUio(
val title: String,
)
@Composable
fun LocationCategory(
modifier: Modifier = Modifier,
item: LocationCategoryUio,
) {
Text(
modifier = modifier,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Light,
text = item.title,
)
}

View file

@ -0,0 +1,9 @@
package com.pixelized.rplexicon.ui.screens.location.list
import androidx.compose.runtime.Stable
@Stable
data class LocationGroupUio(
val category: LocationCategoryUio?,
val maps: List<LocationItemUio>,
)

View file

@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
@ -23,6 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.composable.error.HandleFetchError
@ -76,7 +78,7 @@ private fun LocationContent(
lazyColumnState: LazyListState,
refreshState: PullRefreshState,
refreshing: State<Boolean>,
items: State<List<LocationItemUio>>,
items: State<List<LocationGroupUio>>,
onItem: (LocationItemUio) -> Unit,
) {
Box(
@ -111,17 +113,31 @@ private fun LocationContent(
state = lazyColumnState,
contentPadding = MaterialTheme.lexicon.dimens.itemListPadding,
) {
items(
items = items.value,
key = { it.id },
contentType = { "Location" },
) {
LocationItem(
modifier = Modifier
.clickable { onItem(it) }
.cell(),
item = it,
)
items.value.forEachIndexed { index, entry ->
entry.category?.let {
item(
contentType = { "Header" },
) {
LocationCategory(
modifier = Modifier
.padding(top = if (index == 0) 0.dp else 16.dp)
.padding(horizontal = 16.dp),
item = it,
)
}
}
items(
items = entry.maps,
key = { it.id },
contentType = { "Location" },
) {
LocationItem(
modifier = Modifier
.clickable { onItem(it) }
.cell(),
item = it,
)
}
}
}
}
@ -152,9 +168,14 @@ private fun QuestListPreview() {
items = remember {
mutableStateOf(
listOf(
LocationItemUio.preview(id = "0", title = "Daggerfall"),
LocationItemUio.preview(id = "1", title = "Athkatla"),
)
LocationGroupUio(
category = null,
maps = listOf(
LocationItemUio.preview(id = "0", title = "Daggerfall"),
LocationItemUio.preview(id = "1", title = "Athkatla"),
),
)
),
)
},
onItem = { },

View file

@ -1,29 +1,42 @@
package com.pixelized.rplexicon.ui.screens.location.list
import android.app.Application
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.data.repository.lexicon.CategoryOrderRepository
import com.pixelized.rplexicon.data.repository.lexicon.LocationRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.ui.screens.quest.list.QuestListViewModel
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.context
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class LocationViewModel @Inject constructor(
private val repository: LocationRepository,
) : ViewModel() {
private val orderRepository: CategoryOrderRepository,
application: Application
) : AndroidViewModel(application) {
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
private val _items = mutableStateOf<List<LocationItemUio>>(emptyList())
val items: State<List<LocationItemUio>> get() = _items
private val _items = mutableStateOf<List<LocationGroupUio>>(emptyList())
val items: State<List<LocationGroupUio>> get() = _items
private val _error = MutableSharedFlow<FetchErrorUio>()
val error: SharedFlow<FetchErrorUio> get() = _error
@ -31,14 +44,36 @@ class LocationViewModel @Inject constructor(
init {
viewModelScope.launch {
launch {
repository.data.collect { items ->
_items.value = items.map { item ->
LocationItemUio(
id = item.name,
title = item.name,
)
orderRepository.data.combine(repository.data) { _, location -> location }
.collect { items ->
_items.value = items
.sortedBy { it.name }
.groupBy(
keySelector = { entry ->
LocationCategoryUio(
title = entry.category
?: context.getString(R.string.default_category_other)
)
},
valueTransform = { entry ->
LocationItemUio(
id = entry.name,
title = entry.name,
)
}
)
.map { entry ->
LocationGroupUio(
category = entry.key,
maps = entry.value,
)
}
.sortedBy { item ->
item.category?.title?.let {
orderRepository.findMapOrder(quest = it)
}
}
}
}
}
launch {
update(force = false)
@ -46,8 +81,22 @@ class LocationViewModel @Inject constructor(
}
}
suspend fun update(force: Boolean) {
_isLoading.value = true
suspend fun update(force: Boolean) = coroutineScope {
withContext(Dispatchers.Main) {
_isLoading.value = true
}
withContext(Dispatchers.IO) {
awaitAll(
async { fetchLocation(force = force) },
async { fetchCategoryOrder(force = force) },
)
}
withContext(Dispatchers.Main) {
_isLoading.value = false
}
}
private suspend fun fetchLocation(force: Boolean) {
try {
if (force || repository.lastSuccessFullUpdate.shouldUpdate()) {
repository.fetchMap()
@ -63,9 +112,23 @@ class LocationViewModel @Inject constructor(
Log.e(TAG, exception.message, exception)
_error.emit(FetchErrorUio.Default)
}
// clean the laoding state
finally {
_isLoading.value = false
}
private suspend fun fetchCategoryOrder(force: Boolean) {
try {
if (force || orderRepository.lastSuccessFullUpdate.shouldUpdate()) {
orderRepository.fetchCategoryOrder()
}
}
// the data sheet structure is not as expected
catch (exception: IncompatibleSheetStructure) {
Log.e(QuestListViewModel.TAG, exception.message, exception)
_error.emit(FetchErrorUio.Structure(type = FetchErrorUio.Structure.Type.CATEGORY_ORDER))
}
// default exception
catch (exception: Exception) {
Log.e(QuestListViewModel.TAG, exception.message, exception)
_error.emit(FetchErrorUio.Default)
}
}