feat: implement empty state handling for streams with descriptive messages

This commit is contained in:
tapframe 2026-03-11 22:00:12 +05:30
parent 0a70df9e2d
commit eaba1f9b19
3 changed files with 73 additions and 10 deletions

View file

@ -36,11 +36,19 @@ data class AddonStreamGroup(
val error: String? = null,
)
enum class StreamsEmptyStateReason {
NoAddonsInstalled,
NoCompatibleAddons,
NoStreamsFound,
StreamFetchFailed,
}
data class StreamsUiState(
val groups: List<AddonStreamGroup> = emptyList(),
val activeAddonIds: Set<String> = emptySet(),
val selectedFilter: String? = null,
val isAnyLoading: Boolean = false,
val emptyStateReason: StreamsEmptyStateReason? = null,
) {
val filteredGroups: List<AddonStreamGroup>
get() = if (selectedFilter == null) groups

View file

@ -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<AddonStreamGroup>.toEmptyStateReason(anyLoading: Boolean): StreamsEmptyStateReason? {
if (anyLoading || any { it.streams.isNotEmpty() }) {
return null
}
return if (isNotEmpty() && all { !it.error.isNullOrBlank() }) {
StreamsEmptyStateReason.StreamFetchFailed
} else {
StreamsEmptyStateReason.NoStreamsFound
}
}

View file

@ -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,