diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt index e2f30c2f..0802e7b5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt @@ -36,11 +36,19 @@ data class AddonStreamGroup( val error: String? = null, ) +enum class StreamsEmptyStateReason { + NoAddonsInstalled, + NoCompatibleAddons, + NoStreamsFound, + StreamFetchFailed, +} + data class StreamsUiState( val groups: List = emptyList(), val activeAddonIds: Set = emptySet(), val selectedFilter: String? = null, val isAnyLoading: Boolean = false, + val emptyStateReason: StreamsEmptyStateReason? = null, ) { val filteredGroups: List get() = if (selectedFilter == null) groups diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt index d49acf7d..e37660e5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt @@ -33,7 +33,16 @@ object StreamsRepository { activeJob?.cancel() _uiState.value = StreamsUiState() - val streamAddons = AddonRepository.uiState.value.addons + val installedAddons = AddonRepository.uiState.value.addons + if (installedAddons.isEmpty()) { + _uiState.value = StreamsUiState( + isAnyLoading = false, + emptyStateReason = StreamsEmptyStateReason.NoAddonsInstalled, + ) + return + } + + val streamAddons = installedAddons .mapNotNull { it.manifest } .filter { manifest -> manifest.resources.any { resource -> @@ -47,7 +56,10 @@ object StreamsRepository { log.d { "Found ${streamAddons.size} addons for stream type=$type id=$videoId" } if (streamAddons.isEmpty()) { - _uiState.value = StreamsUiState(isAnyLoading = false) + _uiState.value = StreamsUiState( + isAnyLoading = false, + emptyStateReason = StreamsEmptyStateReason.NoCompatibleAddons, + ) return } @@ -64,6 +76,7 @@ object StreamsRepository { groups = initialGroups, activeAddonIds = streamAddons.map { it.id }.toSet(), isAnyLoading = true, + emptyStateReason = null, ) activeJob = scope.launch { @@ -111,9 +124,11 @@ object StreamsRepository { val updated = current.groups.map { group -> if (group.addonId == result.addonId) result else group } + val anyLoading = updated.any { it.isLoading } current.copy( groups = updated, - isAnyLoading = updated.any { it.isLoading }, + isAnyLoading = anyLoading, + emptyStateReason = updated.toEmptyStateReason(anyLoading), ) } } @@ -133,3 +148,15 @@ object StreamsRepository { private fun String.encodeForPath(): String = replace("%", "%25").replace(" ", "%20") } + +private fun List.toEmptyStateReason(anyLoading: Boolean): StreamsEmptyStateReason? { + if (anyLoading || any { it.streams.isNotEmpty() }) { + return null + } + + return if (isNotEmpty() && all { !it.error.isNullOrBlank() }) { + StreamsEmptyStateReason.StreamFetchFailed + } else { + StreamsEmptyStateReason.NoStreamsFound + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt index 0a8ab1d6..2b07aa4f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt @@ -417,8 +417,8 @@ private fun StreamList( modifier: Modifier = Modifier, ) { val filteredGroups = uiState.filteredGroups + val hasGroups = filteredGroups.isNotEmpty() val hasAnyStreams = filteredGroups.any { it.streams.isNotEmpty() } - val allLoading = filteredGroups.all { it.isLoading } val anyLoading = filteredGroups.any { it.isLoading } LazyColumn( @@ -430,15 +430,15 @@ private fun StreamList( verticalArrangement = Arrangement.spacedBy(0.dp), ) { when { - allLoading && !hasAnyStreams -> { + hasGroups && anyLoading && !hasAnyStreams -> { item { LoadingStateBlock() } } - !hasAnyStreams && !anyLoading -> { + !hasAnyStreams && !uiState.isAnyLoading -> { item { - EmptyStateBlock() + EmptyStateBlock(reason = uiState.emptyStateReason) } } @@ -667,7 +667,35 @@ private fun LoadingStateBlock(modifier: Modifier = Modifier) { } @Composable -private fun EmptyStateBlock(modifier: Modifier = Modifier) { +private fun EmptyStateBlock( + reason: StreamsEmptyStateReason?, + modifier: Modifier = Modifier, +) { + val title: String + val message: String + + when (reason) { + StreamsEmptyStateReason.NoAddonsInstalled -> { + title = "No addons installed" + message = "Install an addon first to load streams for this title." + } + + StreamsEmptyStateReason.NoCompatibleAddons -> { + title = "No stream addon available" + message = "Your installed addons do not provide streams for this type of title." + } + + StreamsEmptyStateReason.StreamFetchFailed -> { + title = "Could not load streams" + message = "The installed stream addons failed to return a valid stream response." + } + + StreamsEmptyStateReason.NoStreamsFound, null -> { + title = "No streams found" + message = "None of your installed addons returned streams for this title." + } + } + Column( modifier = modifier .fillMaxWidth() @@ -683,7 +711,7 @@ private fun EmptyStateBlock(modifier: Modifier = Modifier) { ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "No streams found", + text = title, style = MaterialTheme.typography.bodyLarge.copy( fontSize = 16.sp, fontWeight = FontWeight.SemiBold, @@ -691,7 +719,7 @@ private fun EmptyStateBlock(modifier: Modifier = Modifier) { color = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( - text = "None of your installed addons returned streams for this title.", + text = message, style = MaterialTheme.typography.bodySmall.copy(fontSize = 14.sp), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), textAlign = TextAlign.Center,