ref(cloud): ui adjustments

This commit is contained in:
tapframe 2026-05-21 21:29:39 +05:30
parent 914f4147e9
commit 996fd7f949
7 changed files with 160 additions and 67 deletions

View file

@ -84,30 +84,7 @@ private class TorboxDebridProviderApi(
val normalized = deviceCode.trim()
if (normalized.isBlank()) return DebridDeviceAuthorizationTokenResult.Failed(null)
val response = TorboxApiClient.redeemDeviceAuthorization(deviceCode = normalized)
val envelope = response.body
val accessToken = envelope
?.takeIf { response.isSuccessful && it.success != false }
?.data
?.accessToken
?.takeIf { it.isNotBlank() }
if (accessToken != null) {
return DebridDeviceAuthorizationTokenResult.Authorized(accessToken)
}
val message = listOfNotNull(envelope?.error, envelope?.detail, response.rawBody)
.joinToString(" ")
.lowercase()
return when {
message.contains("pending") || message.contains("not authorized") ->
DebridDeviceAuthorizationTokenResult.Pending
message.contains("expired") ->
DebridDeviceAuthorizationTokenResult.Expired
response.status == 404 || response.status == 409 || response.status == 425 ->
DebridDeviceAuthorizationTokenResult.Pending
response.status == 410 ->
DebridDeviceAuthorizationTokenResult.Expired
else ->
DebridDeviceAuthorizationTokenResult.Failed(envelope?.detail ?: envelope?.error)
}
return torboxDeviceAuthorizationTokenResult(response)
}
override suspend fun resolveClientStream(
@ -320,6 +297,39 @@ internal fun premiumizeDeviceAuthorizationFromResponse(
)
}
internal fun torboxDeviceAuthorizationTokenResult(
response: DebridApiResponse<TorboxEnvelopeDto<TorboxDeviceTokenDto>>,
): DebridDeviceAuthorizationTokenResult {
val envelope = response.body
val accessToken = envelope
?.takeIf { response.isSuccessful && it.success != false }
?.data
?.accessToken
?.takeIf { it.isNotBlank() }
if (accessToken != null) {
return DebridDeviceAuthorizationTokenResult.Authorized(accessToken)
}
val message = listOfNotNull(envelope?.error, envelope?.detail, response.rawBody)
.joinToString(" ")
.lowercase()
return when {
message.contains("pending") ||
message.contains("not authorized") ||
message.contains("not been used") ||
message.contains("not used yet") ||
message.contains("scan the code") ->
DebridDeviceAuthorizationTokenResult.Pending
message.contains("expired") ->
DebridDeviceAuthorizationTokenResult.Expired
response.status == 404 || response.status == 409 || response.status == 425 ->
DebridDeviceAuthorizationTokenResult.Pending
response.status == 410 ->
DebridDeviceAuthorizationTokenResult.Expired
else ->
DebridDeviceAuthorizationTokenResult.Failed(envelope?.detail ?: envelope?.error)
}
}
internal fun premiumizeDeviceAuthorizationTokenResult(
response: DebridApiResponse<PremiumizeDeviceTokenDto>,
): DebridDeviceAuthorizationTokenResult {

View file

@ -100,7 +100,10 @@ fun HomeScreen(
val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
val homeUiState by HomeRepository.uiState.collectAsStateWithLifecycle()
val homeSettingsUiState by HomeCatalogSettingsRepository.uiState.collectAsStateWithLifecycle()
val homeSettingsUiState by remember {
HomeCatalogSettingsRepository.snapshot()
HomeCatalogSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val homeListState = rememberLazyListState()
val collections by CollectionRepository.collections.collectAsStateWithLifecycle()
val continueWatchingPreferences by ContinueWatchingPreferencesRepository.uiState.collectAsStateWithLifecycle()
@ -612,7 +615,10 @@ fun HomeScreen(
}
}
items(3) {
HomeSkeletonRow(modifier = Modifier.padding(horizontal = 16.dp))
HomeSkeletonRow(
modifier = Modifier.padding(horizontal = 16.dp),
showHeaderAccent = !homeSettingsUiState.hideCatalogUnderline,
)
}
}

View file

@ -181,7 +181,10 @@ fun HomeSkeletonHero(
}
@Composable
fun HomeSkeletonRow(modifier: Modifier = Modifier) {
fun HomeSkeletonRow(
modifier: Modifier = Modifier,
showHeaderAccent: Boolean = true,
) {
val brush = rememberHomeSkeletonBrush()
val posterCardStyle = rememberPosterCardStyleUiState()
val skeletonWidth = if (posterCardStyle.catalogLandscapeModeEnabled) {
@ -207,15 +210,17 @@ fun HomeSkeletonRow(modifier: Modifier = Modifier) {
.clip(RoundedCornerShape(6.dp))
.background(brush),
)
// Accent bar
Box(
modifier = Modifier
.width(60.dp)
.height(4.dp)
.clip(RoundedCornerShape(999.dp))
.background(brush),
)
Spacer(modifier = Modifier.height(2.dp))
if (showHeaderAccent) {
// Accent bar
Box(
modifier = Modifier
.width(60.dp)
.height(4.dp)
.clip(RoundedCornerShape(999.dp))
.background(brush),
)
Spacer(modifier = Modifier.height(2.dp))
}
// Poster row
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),

View file

@ -72,6 +72,7 @@ import com.nuvio.app.features.cloud.CloudLibraryItemType
import com.nuvio.app.features.cloud.CloudLibraryRepository
import com.nuvio.app.features.cloud.CloudLibraryUiState
import com.nuvio.app.features.debrid.DebridSettingsRepository
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.components.HomeEmptyStateCard
import com.nuvio.app.features.home.components.HomePosterCard
import com.nuvio.app.features.home.components.HomeSkeletonRow
@ -107,6 +108,10 @@ fun LibraryScreen(
WatchedRepository.ensureLoaded()
WatchedRepository.uiState
}.collectAsStateWithLifecycle()
val homeCatalogSettingsUiState by remember {
HomeCatalogSettingsRepository.snapshot()
HomeCatalogSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
var observedOfflineState by remember { mutableStateOf(false) }
var sourceModeName by rememberSaveable { mutableStateOf(LibraryViewMode.Saved.name) }
@ -230,7 +235,10 @@ fun LibraryScreen(
when {
!uiState.isLoaded || (uiState.isLoading && uiState.sections.isEmpty()) -> {
items(3) {
HomeSkeletonRow(modifier = Modifier.padding(horizontal = 16.dp))
HomeSkeletonRow(
modifier = Modifier.padding(horizontal = 16.dp),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
)
}
}
@ -288,6 +296,7 @@ fun LibraryScreen(
librarySections(
sections = uiState.sections,
watchedKeys = watchedUiState.watchedKeys,
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
onPosterClick = onPosterClick,
onSectionViewAllClick = onSectionViewAllClick,
onPosterLongClick = onPosterLongClick,
@ -423,7 +432,7 @@ private fun LazyListScope.cloudLibrarySkeletonItems() {
modifier = Modifier.padding(horizontal = 16.dp),
)
}
items(4) {
items(3) {
CloudLibrarySkeletonRow()
}
}
@ -856,24 +865,17 @@ private fun CloudLibrarySkeletonToolbar(
modifier: Modifier = Modifier,
) {
val brush = rememberCloudLibrarySkeletonBrush()
Column(
Row(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
CloudSkeletonBlock(brush = brush, width = 92.dp, height = 34.dp, cornerRadius = 12.dp)
CloudSkeletonBlock(brush = brush, width = 78.dp, height = 34.dp, cornerRadius = 12.dp)
CloudSkeletonBlock(
brush = brush,
modifier = Modifier.weight(1f),
height = 34.dp,
cornerRadius = 12.dp,
)
CloudSkeletonBlock(brush = brush, width = 40.dp, height = 40.dp, cornerRadius = 20.dp)
CloudSkeletonBlock(brush = brush, width = 112.dp, height = 36.dp, cornerRadius = 12.dp)
CloudSkeletonBlock(brush = brush, width = 92.dp, height = 36.dp, cornerRadius = 12.dp)
}
}
}
@ -903,35 +905,29 @@ private fun CloudLibrarySkeletonRow(
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(7.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
CloudSkeletonBlock(
brush = brush,
modifier = Modifier.fillMaxWidth(0.74f),
height = 17.dp,
height = 18.dp,
cornerRadius = 6.dp,
)
CloudSkeletonBlock(
brush = brush,
modifier = Modifier.fillMaxWidth(),
height = 12.dp,
modifier = Modifier.fillMaxWidth(0.9f),
height = 14.dp,
cornerRadius = 6.dp,
)
CloudSkeletonBlock(
brush = brush,
modifier = Modifier.fillMaxWidth(0.58f),
modifier = Modifier.fillMaxWidth(0.52f),
height = 12.dp,
cornerRadius = 6.dp,
)
}
CloudSkeletonBlock(brush = brush, width = 32.dp, height = 32.dp, cornerRadius = 16.dp)
CloudSkeletonBlock(brush = brush, width = 48.dp, height = 48.dp, cornerRadius = 24.dp)
}
CloudSkeletonBlock(
brush = brush,
modifier = Modifier.fillMaxWidth(0.44f),
height = 4.dp,
cornerRadius = 999.dp,
)
}
}
}
@ -987,6 +983,7 @@ private enum class LibraryViewMode {
private fun LazyListScope.librarySections(
sections: List<LibrarySection>,
watchedKeys: Set<String>,
showHeaderAccent: Boolean,
onPosterClick: ((LibraryItem) -> Unit)?,
onSectionViewAllClick: ((LibrarySection) -> Unit)?,
onPosterLongClick: ((LibraryItem, LibrarySection) -> Unit)?,
@ -1001,6 +998,7 @@ private fun LazyListScope.librarySections(
entries = previewItems,
headerHorizontalPadding = 16.dp,
rowContentPadding = PaddingValues(horizontal = 16.dp),
showHeaderAccent = showHeaderAccent,
onViewAllClick = if (section.items.size > LIBRARY_SECTION_PREVIEW_LIMIT) {
onSectionViewAllClick?.let { { it(section) } }
} else {

View file

@ -104,7 +104,10 @@ fun SearchScreen(
val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
val uiState by SearchRepository.uiState.collectAsStateWithLifecycle()
val discoverUiState by SearchRepository.discoverUiState.collectAsStateWithLifecycle()
val homeCatalogSettingsUiState by HomeCatalogSettingsRepository.uiState.collectAsStateWithLifecycle()
val homeCatalogSettingsUiState by remember {
HomeCatalogSettingsRepository.snapshot()
HomeCatalogSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val recentSearches by SearchHistoryRepository.uiState.collectAsStateWithLifecycle()
val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle()
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
@ -305,13 +308,19 @@ fun SearchScreen(
when {
isWaitingForSearch -> {
items(2) {
HomeSkeletonRow(modifier = Modifier.padding(horizontal = homeSectionPadding))
HomeSkeletonRow(
modifier = Modifier.padding(horizontal = homeSectionPadding),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
)
}
}
uiState.isLoading && uiState.sections.isEmpty() -> {
items(2) {
HomeSkeletonRow(modifier = Modifier.padding(horizontal = homeSectionPadding))
HomeSkeletonRow(
modifier = Modifier.padding(horizontal = homeSectionPadding),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
)
}
}
@ -351,7 +360,10 @@ fun SearchScreen(
}
if (uiState.isLoading) {
item(key = "search_loading_more") {
HomeSkeletonRow(modifier = Modifier.padding(horizontal = homeSectionPadding))
HomeSkeletonRow(
modifier = Modifier.padding(horizontal = homeSectionPadding),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
)
}
}
}

View file

@ -123,12 +123,15 @@ import nuvio.composeapp.generated.resources.settings_debrid_section_providers
import nuvio.composeapp.generated.resources.settings_debrid_section_title
import org.jetbrains.compose.resources.stringResource
private const val CLOUD_SERVICES_FAQ_URL = "https://nuvioapp.space/faq#common-cloud-library-and-cloud-services"
internal fun LazyListScope.debridSettingsContent(
isTablet: Boolean,
settings: DebridSettings,
) {
item {
var showResolverProviderDialog by rememberSaveable { mutableStateOf(false) }
val uriHandler = LocalUriHandler.current
val resolverProviders = settings.resolverServices.map { it.provider }
val activeResolverProvider = settings.activeResolverCredential?.provider
SettingsSection(
@ -141,6 +144,15 @@ internal fun LazyListScope.debridSettingsContent(
text = stringResource(Res.string.settings_debrid_experimental_notice),
)
SettingsGroupDivider(isTablet = isTablet)
DebridPreferenceRow(
isTablet = isTablet,
title = "Learn more",
description = "Cloud Library, connected accounts, and playable-link preparation.",
value = "Open",
enabled = true,
onClick = { runCatching { uriHandler.openUri(CLOUD_SERVICES_FAQ_URL) } },
)
SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = stringResource(Res.string.settings_debrid_cloud_library),
description = stringResource(Res.string.settings_debrid_cloud_library_description),

View file

@ -0,0 +1,50 @@
package com.nuvio.app.features.debrid
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class TorboxDeviceAuthTest {
@Test
fun `maps unused device code response to pending`() {
val response = DebridApiResponse(
status = 400,
body = TorboxEnvelopeDto<TorboxDeviceTokenDto>(
success = false,
detail = "This device code has not been used yet. Please wait for the user to scan the code.",
),
rawBody = "",
)
assertEquals(
DebridDeviceAuthorizationTokenResult.Pending,
torboxDeviceAuthorizationTokenResult(response),
)
}
@Test
fun `maps authorized and expired Torbox device states`() {
assertTrue(
torboxDeviceAuthorizationTokenResult(
DebridApiResponse(
status = 200,
body = TorboxEnvelopeDto(
success = true,
data = TorboxDeviceTokenDto(accessToken = "tb-token", tokenType = "Bearer"),
),
rawBody = "",
),
) is DebridDeviceAuthorizationTokenResult.Authorized,
)
assertEquals(
DebridDeviceAuthorizationTokenResult.Expired,
torboxDeviceAuthorizationTokenResult(
DebridApiResponse(
status = 410,
body = TorboxEnvelopeDto(success = false, detail = "Device code expired."),
rawBody = "",
),
),
)
}
}