mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 08:39:24 +00:00
fix: fix collection keys dupliation causing sigsegv
This commit is contained in:
parent
4ba92f3926
commit
c68d996881
6 changed files with 134 additions and 11 deletions
|
|
@ -56,7 +56,11 @@ object CollectionRepository {
|
|||
val parsed = json.parseToJsonElement(payload)
|
||||
rawCollectionsJson = parsed
|
||||
val decoded = json.decodeFromString<List<Collection>>(payload)
|
||||
_collections.value = CollectionMobileSettingsRepository.applyToCollections(decoded)
|
||||
val normalized = normalizeCollections(decoded, source = "local storage")
|
||||
_collections.value = CollectionMobileSettingsRepository.applyToCollections(normalized)
|
||||
if (normalized.size != decoded.size) {
|
||||
persist(sync = false)
|
||||
}
|
||||
}.onFailure { e ->
|
||||
log.e(e) { "Failed to load collections from storage" }
|
||||
}
|
||||
|
|
@ -79,14 +83,15 @@ object CollectionRepository {
|
|||
|
||||
fun addCollection(collection: Collection) {
|
||||
ensureLoaded()
|
||||
_collections.value = _collections.value + CollectionMobileSettingsRepository.applyToCollection(collection)
|
||||
val decorated = CollectionMobileSettingsRepository.applyToCollection(collection)
|
||||
_collections.value = _collections.value.upsertCollectionById(decorated)
|
||||
persist()
|
||||
}
|
||||
|
||||
fun updateCollection(collection: Collection) {
|
||||
ensureLoaded()
|
||||
val decorated = CollectionMobileSettingsRepository.applyToCollection(collection)
|
||||
_collections.value = _collections.value.map {
|
||||
_collections.value = _collections.value.deduplicatedById().map {
|
||||
if (it.id == collection.id) decorated else it
|
||||
}
|
||||
persist()
|
||||
|
|
@ -100,7 +105,8 @@ object CollectionRepository {
|
|||
|
||||
fun setCollections(collections: List<Collection>) {
|
||||
ensureLoaded()
|
||||
_collections.value = CollectionMobileSettingsRepository.applyToCollections(collections)
|
||||
val normalized = normalizeCollections(collections, source = "setCollections")
|
||||
_collections.value = CollectionMobileSettingsRepository.applyToCollections(normalized)
|
||||
persist()
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +141,7 @@ object CollectionRepository {
|
|||
throw IllegalArgumentException(validation.error.orEmpty())
|
||||
}
|
||||
rawCollectionsJson = json.parseToJsonElement(jsonString)
|
||||
val imported = json.decodeFromString<List<Collection>>(jsonString)
|
||||
val imported = json.decodeFromString<List<Collection>>(jsonString).deduplicatedById()
|
||||
_collections.value = CollectionMobileSettingsRepository.applyToCollections(imported)
|
||||
persist()
|
||||
imported
|
||||
|
|
@ -197,13 +203,26 @@ object CollectionRepository {
|
|||
|
||||
internal fun applyFromRemote(collections: List<Collection>, rawJson: JsonElement) {
|
||||
rawCollectionsJson = rawJson
|
||||
_collections.value = CollectionMobileSettingsRepository.applyToCollections(collections)
|
||||
val normalized = normalizeCollections(collections, source = "remote sync")
|
||||
_collections.value = CollectionMobileSettingsRepository.applyToCollections(normalized)
|
||||
persist(sync = false)
|
||||
}
|
||||
|
||||
internal fun onMobileSettingsChanged() {
|
||||
if (!hasLoaded) return
|
||||
_collections.value = CollectionMobileSettingsRepository.applyToCollections(_collections.value)
|
||||
_collections.value = CollectionMobileSettingsRepository.applyToCollections(
|
||||
_collections.value.deduplicatedById(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun normalizeCollections(collections: List<Collection>, source: String): List<Collection> {
|
||||
val normalized = collections.deduplicatedById()
|
||||
if (normalized.size != collections.size) {
|
||||
log.w {
|
||||
"Dropped ${collections.size - normalized.size} duplicate collection IDs from $source"
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
private fun ensureLoaded() {
|
||||
|
|
@ -227,6 +246,26 @@ object CollectionRepository {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun List<Collection>.deduplicatedById(): List<Collection> {
|
||||
if (size < 2) return this
|
||||
|
||||
val collectionsById = linkedMapOf<String, Collection>()
|
||||
forEach { collection ->
|
||||
collectionsById[collection.id] = collection
|
||||
}
|
||||
return if (collectionsById.size == size) this else collectionsById.values.toList()
|
||||
}
|
||||
|
||||
internal fun List<Collection>.upsertCollectionById(collection: Collection): List<Collection> {
|
||||
val normalized = deduplicatedById()
|
||||
val existingIndex = normalized.indexOfFirst { existing -> existing.id == collection.id }
|
||||
if (existingIndex == -1) return normalized + collection
|
||||
|
||||
return normalized.toMutableList().apply {
|
||||
this[existingIndex] = collection
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed interface CollectionImportModelError {
|
||||
data class BlankCollectionId(val collectionIndex: Int) : CollectionImportModelError
|
||||
data class DuplicateCollectionId(val collectionId: String) : CollectionImportModelError
|
||||
|
|
|
|||
|
|
@ -588,8 +588,13 @@ internal data class CollectionCatalogDefinition(
|
|||
val isPinnedToTop: Boolean,
|
||||
)
|
||||
|
||||
internal fun visibleCollectionsWithUniqueIds(collections: List<Collection>): List<Collection> =
|
||||
collections
|
||||
.filter { collection -> collection.folders.isNotEmpty() }
|
||||
.distinctBy(Collection::id)
|
||||
|
||||
internal fun buildCollectionDefinitions(collections: List<Collection>): List<CollectionCatalogDefinition> =
|
||||
collections.filter { it.folders.isNotEmpty() }.map { collection ->
|
||||
visibleCollectionsWithUniqueIds(collections).map { collection ->
|
||||
CollectionCatalogDefinition(
|
||||
key = "collection_${collection.id}",
|
||||
collectionId = collection.id,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
|
|||
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
||||
import com.nuvio.app.core.ui.rememberHeroStretchState
|
||||
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||
import com.nuvio.app.core.ui.withDuplicateSafeLazyKeys
|
||||
import com.nuvio.app.features.addons.AddonRepository
|
||||
import com.nuvio.app.features.addons.enabledAddons
|
||||
import com.nuvio.app.features.cloud.CloudLibraryContentType
|
||||
|
|
@ -774,6 +775,9 @@ fun HomeScreen(
|
|||
val enabledHomeItems = remember(homeSettingsUiState.items) {
|
||||
homeSettingsUiState.items.filter { it.enabled }
|
||||
}
|
||||
val keyedEnabledHomeItems = remember(enabledHomeItems) {
|
||||
enabledHomeItems.withDuplicateSafeLazyKeys(HomeCatalogSettingsItem::key)
|
||||
}
|
||||
val visibleSeriesPosterTargets = remember(enabledHomeItems, sectionsMap) {
|
||||
enabledHomeItems
|
||||
.filterNot { it.isCollection }
|
||||
|
|
@ -970,11 +974,12 @@ fun HomeScreen(
|
|||
}
|
||||
}
|
||||
|
||||
enabledHomeItems.forEach { settingsItem ->
|
||||
keyedEnabledHomeItems.forEach { keyedSettingsItem ->
|
||||
val settingsItem = keyedSettingsItem.value
|
||||
if (settingsItem.isCollection) {
|
||||
val collection = collectionsMap[settingsItem.key]
|
||||
if (collection != null) {
|
||||
item(key = settingsItem.key) {
|
||||
item(key = keyedSettingsItem.lazyKey) {
|
||||
HomeCollectionRowSection(
|
||||
collection = collection,
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
|
|
@ -987,7 +992,7 @@ fun HomeScreen(
|
|||
} else {
|
||||
val section = sectionsMap[settingsItem.key]
|
||||
if (section != null && section.items.isNotEmpty()) {
|
||||
item(key = settingsItem.key) {
|
||||
item(key = keyedSettingsItem.lazyKey) {
|
||||
HomeCatalogRowSection(
|
||||
section = section,
|
||||
entries = section.items.take(HOME_CATALOG_PREVIEW_LIMIT),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DuplicateSafeLazyKeysTest {
|
||||
@Test
|
||||
fun `duplicate lazy keys receive stable occurrence suffixes`() {
|
||||
val result = listOf("duplicate", "other", "duplicate")
|
||||
.withDuplicateSafeLazyKeys { value -> value }
|
||||
|
||||
assertEquals(listOf("duplicate#0", "other", "duplicate#1"), result.map { it.lazyKey })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.nuvio.app.features.collection
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CollectionRepositoryTest {
|
||||
@Test
|
||||
fun `deduplication keeps stable order and latest collection value`() {
|
||||
val collections = listOf(
|
||||
Collection(id = "duplicate", title = "Old value"),
|
||||
Collection(id = "other", title = "Other"),
|
||||
Collection(id = "duplicate", title = "Latest value"),
|
||||
)
|
||||
|
||||
val result = collections.deduplicatedById()
|
||||
|
||||
assertEquals(listOf("duplicate", "other"), result.map(Collection::id))
|
||||
assertEquals("Latest value", result.first().title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `upsert replaces duplicate collection without changing its position`() {
|
||||
val collections = listOf(
|
||||
Collection(id = "duplicate", title = "Old value"),
|
||||
Collection(id = "other", title = "Other"),
|
||||
Collection(id = "duplicate", title = "Stale duplicate"),
|
||||
)
|
||||
|
||||
val result = collections.upsertCollectionById(
|
||||
Collection(id = "duplicate", title = "Replacement"),
|
||||
)
|
||||
|
||||
assertEquals(listOf("duplicate", "other"), result.map(Collection::id))
|
||||
assertEquals("Replacement", result.first().title)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.nuvio.app.features.home
|
||||
|
||||
import com.nuvio.app.features.collection.Collection
|
||||
import com.nuvio.app.features.collection.CollectionFolder
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class HomeCollectionDefinitionsTest {
|
||||
@Test
|
||||
fun `visible home collections ignore duplicate IDs and empty collections`() {
|
||||
val visibleFolder = CollectionFolder(id = "folder", title = "Folder")
|
||||
val collections = listOf(
|
||||
Collection(id = "duplicate", title = "First", folders = listOf(visibleFolder)),
|
||||
Collection(id = "empty", title = "Empty"),
|
||||
Collection(id = "duplicate", title = "Second", folders = listOf(visibleFolder)),
|
||||
Collection(id = "other", title = "Other", folders = listOf(visibleFolder)),
|
||||
)
|
||||
|
||||
val result = visibleCollectionsWithUniqueIds(collections)
|
||||
|
||||
assertEquals(listOf("duplicate", "other"), result.map(Collection::id))
|
||||
assertEquals("First", result.first().title)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue