mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-22 17:52:06 +00:00
ref(cloud): ui adjustments
This commit is contained in:
parent
914f4147e9
commit
996fd7f949
7 changed files with 160 additions and 67 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue