mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-16 12:13:47 +00:00
feat: show per-item progress during Nuvio account import
Surfaces which item is currently being imported (index/total/title) under each import step on both mobile and TV auth screens, with a checkmark for completed steps.
This commit is contained in:
parent
c030fd66b2
commit
78569c8f15
8 changed files with 125 additions and 44 deletions
|
|
@ -158,7 +158,7 @@ internal class AndroidAuthEffectHandler(
|
|||
val profile = payload.parseProfile() ?: return failure(effect, "missing_profile")
|
||||
if (payload.string("provider") == "nuvio") {
|
||||
if (profile.nuvioAccessToken.isNullOrBlank()) return failure(effect, "missing_nuvio_token")
|
||||
val imported = nuvioAccountImportCoordinator.sync(profile) {}
|
||||
val imported = nuvioAccountImportCoordinator.sync(profile, onStep = {})
|
||||
pluginRepositoryManager.importNuvioPlugins(imported.plugins)
|
||||
return success(
|
||||
effect,
|
||||
|
|
|
|||
|
|
@ -207,12 +207,19 @@ class AndroidAuthDataSource(
|
|||
email = session.user?.email ?: pendingNuvioEmail,
|
||||
authKey = ""
|
||||
)
|
||||
val imported = nuvioCoordinator.import(baseProfile, session) { step ->
|
||||
NUVIO_STEP_MAP[step]?.let { mapped -> state.update { it.copy(importSteps = it.importSteps + mapped) } }
|
||||
}
|
||||
val imported = nuvioCoordinator.import(
|
||||
baseProfile,
|
||||
session,
|
||||
onStep = { step ->
|
||||
NUVIO_STEP_MAP[step]?.let { mapped -> state.update { it.copy(importSteps = it.importSteps + mapped) } }
|
||||
},
|
||||
onItemProgress = { index, total, title ->
|
||||
state.update { it.copy(importItemIndex = index, importItemTotal = total, importItemTitle = title) }
|
||||
}
|
||||
)
|
||||
pluginRepositoryManager.importNuvioPlugins(imported.plugins)
|
||||
pendingImportedProfile = imported.profile
|
||||
state.update { it.copy(importDone = true) }
|
||||
state.update { it.copy(importDone = true, importItemIndex = null, importItemTotal = null, importItemTitle = null) }
|
||||
} catch (e: Exception) {
|
||||
state.update { it.copy(stage = AuthStage.Nuvio, globalError = AppStrings.t(lang, "auth.error.network")) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@
|
|||
"calendar.no_releases_this_day": "No releases on this day.",
|
||||
"notification.new_episode_released": "New Episode Released",
|
||||
"notification.new_season_released": "New Season Released",
|
||||
"notification.release_now_episode": "%s S%s:E%s Released now",
|
||||
"notification.new_episode_body": "S%s:E%s is now available",
|
||||
"notification.new_episode_body_titled": "S%s:E%s · %s is now available",
|
||||
"notification.new_season_body": "Season %s is now available",
|
||||
"notification.plugins_updated_title": "CloudStream Plugins Updated",
|
||||
"notification.plugins_updated_body": "%s plugin(s) updated: %s",
|
||||
"notification.plugins_update_failed_title": "CloudStream Plugin Update Failed",
|
||||
|
|
@ -88,6 +90,7 @@
|
|||
"auth.nuvio.import.progress": "Watch progress",
|
||||
"auth.nuvio.import.history": "Watch history",
|
||||
"auth.nuvio.import.collections": "Collections",
|
||||
"auth.nuvio.import.item_progress": "[%s/%s] Importing %s…",
|
||||
"login.loading": "Signing in...",
|
||||
"login.qr_content_description": "QR login",
|
||||
"login.instant_title": "Sign In Instantly",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@
|
|||
"calendar.no_releases_this_day": "Bu günde yayın yok.",
|
||||
"notification.new_episode_released": "Yeni Bölüm Yayınlandı",
|
||||
"notification.new_season_released": "Yeni Sezon Yayınlandı",
|
||||
"notification.release_now_episode": "%s S%s:E%s şimdi yayınlandı",
|
||||
"notification.new_episode_body": "S%s:E%s şimdi yayında",
|
||||
"notification.new_episode_body_titled": "S%s:E%s · %s şimdi yayında",
|
||||
"notification.new_season_body": "%s. Sezon şimdi yayında",
|
||||
"notification.plugins_updated_title": "CloudStream Eklentileri Güncellendi",
|
||||
"notification.plugins_updated_body": "%s eklenti güncellendi: %s",
|
||||
"notification.plugins_update_failed_title": "CloudStream Eklenti Güncellemesi Başarısız",
|
||||
|
|
@ -88,6 +90,7 @@
|
|||
"auth.nuvio.import.progress": "İzleme ilerlemesi",
|
||||
"auth.nuvio.import.history": "İzleme geçmişi",
|
||||
"auth.nuvio.import.collections": "Koleksiyonlar",
|
||||
"auth.nuvio.import.item_progress": "[%s/%s] %s aktarılıyor…",
|
||||
"login.loading": "Giriş Yapılıyor...",
|
||||
"login.qr_content_description": "QR ile giriş",
|
||||
"login.instant_title": "Anında Giriş Yap",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,11 @@ class NuvioAccountImportCoordinator(
|
|||
nuvioService.signUp(NuvioCredentials(email, password))
|
||||
}
|
||||
|
||||
suspend fun sync(profile: UserProfile, onStep: (NuvioImportStep) -> Unit): NuvioImportResult {
|
||||
suspend fun sync(
|
||||
profile: UserProfile,
|
||||
onStep: (NuvioImportStep) -> Unit,
|
||||
onItemProgress: (index: Int, total: Int, title: String) -> Unit = { _, _, _ -> }
|
||||
): NuvioImportResult {
|
||||
val refreshedProfile = refreshProfileIfNeeded(profile)
|
||||
val accessToken = refreshedProfile.nuvioAccessToken?.takeIf { it.isNotBlank() }
|
||||
?: throw IllegalStateException("Nuvio is not connected")
|
||||
|
|
@ -72,7 +76,7 @@ class NuvioAccountImportCoordinator(
|
|||
com.fluxa.app.data.remote.NuvioUser(userId, refreshedProfile.nuvioEmail ?: refreshedProfile.email)
|
||||
}
|
||||
)
|
||||
return import(refreshedProfile, session, onStep)
|
||||
return import(refreshedProfile, session, onStep, onItemProgress)
|
||||
}
|
||||
|
||||
private suspend inline fun authenticate(call: suspend () -> retrofit2.Response<NuvioSessionDto>): Result<NuvioSession> {
|
||||
|
|
@ -92,7 +96,8 @@ class NuvioAccountImportCoordinator(
|
|||
suspend fun import(
|
||||
baseProfile: UserProfile,
|
||||
session: NuvioSession,
|
||||
onStep: (NuvioImportStep) -> Unit
|
||||
onStep: (NuvioImportStep) -> Unit,
|
||||
onItemProgress: (index: Int, total: Int, title: String) -> Unit = { _, _, _ -> }
|
||||
): NuvioImportResult {
|
||||
val connectedProfile = baseProfile.copy(
|
||||
email = session.user?.email ?: baseProfile.email,
|
||||
|
|
@ -172,7 +177,7 @@ class NuvioAccountImportCoordinator(
|
|||
|
||||
if (watchProgressDtos != null) {
|
||||
val watchProgressJson = gson.toJsonTree(watchProgressDtos).asJsonArray
|
||||
val addonMetas = fetchAddonMetaNeeds(watchProgressJson, libraryJson, profile.localAddons)
|
||||
val addonMetas = fetchAddonMetaNeeds(watchProgressJson, libraryJson, profile.localAddons, onItemProgress)
|
||||
val mergePlan = NuvioCoreBridge.importMergePlan(
|
||||
libraryJson,
|
||||
addonMetas,
|
||||
|
|
@ -288,16 +293,22 @@ class NuvioAccountImportCoordinator(
|
|||
?: connectedProfile.copy(nuvioProfileIndex = primaryIndex)
|
||||
}
|
||||
|
||||
private suspend fun fetchAddonMetaNeeds(watchProgressJson: JsonArray, libraryJson: JsonArray, localAddons: List<String>?): JsonObject {
|
||||
private suspend fun fetchAddonMetaNeeds(
|
||||
watchProgressJson: JsonArray,
|
||||
libraryJson: JsonArray,
|
||||
localAddons: List<String>?,
|
||||
onItemProgress: (index: Int, total: Int, title: String) -> Unit
|
||||
): JsonObject {
|
||||
val needs = NuvioCoreBridge.progressMetaNeeds(watchProgressJson, libraryJson)
|
||||
val addonMetas = JsonObject()
|
||||
needs.forEach { need ->
|
||||
needs.forEachIndexed { index, need ->
|
||||
val obj = need.asJsonObject
|
||||
val contentId = obj.get("contentId")?.takeUnless { it.isJsonNull }?.asString ?: return@forEach
|
||||
val contentType = obj.get("contentType")?.takeUnless { it.isJsonNull }?.asString ?: return@forEach
|
||||
val contentId = obj.get("contentId")?.takeUnless { it.isJsonNull }?.asString ?: return@forEachIndexed
|
||||
val contentType = obj.get("contentType")?.takeUnless { it.isJsonNull }?.asString ?: return@forEachIndexed
|
||||
val detail = runCatching {
|
||||
addonRepository.getAddonMetaDetail(type = contentType, id = contentId, authKey = "", localAddons = localAddons)
|
||||
}.getOrNull() ?: return@forEach
|
||||
}.getOrNull() ?: return@forEachIndexed
|
||||
onItemProgress(index + 1, needs.size(), detail.name)
|
||||
val metaJson = JsonObject().apply {
|
||||
addProperty("name", detail.name)
|
||||
detail.poster?.let { addProperty("poster", it) }
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ data class AuthUiState(
|
|||
val isSubmitting: Boolean = false,
|
||||
val importSteps: Set<AuthImportStep> = emptySet(),
|
||||
val importDone: Boolean = false,
|
||||
val importItemIndex: Int? = null,
|
||||
val importItemTotal: Int? = null,
|
||||
val importItemTitle: String? = null,
|
||||
val isAuthenticated: Boolean = false
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
|
|
@ -322,20 +323,46 @@ private fun NuvioImportingStage(
|
|||
Spacer(Modifier.height(20.dp))
|
||||
IMPORT_STEP_ORDER.forEach { (step, key) ->
|
||||
val complete = step in state.importSteps
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(vertical = 6.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.background(
|
||||
if (complete) Color.White.copy(alpha = 0.9f) else Color.White.copy(alpha = 0.08f),
|
||||
CircleShape
|
||||
)
|
||||
)
|
||||
Text(AppStrings.t(language, key), color = Color.White.copy(alpha = if (complete) 0.85f else 0.3f))
|
||||
val active = !complete && !state.importDone && (state.importSteps.size == IMPORT_STEP_ORDER.indexOfFirst { it.first == step })
|
||||
Column(modifier = Modifier.padding(vertical = 6.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.background(
|
||||
if (complete) Color.White.copy(alpha = 0.9f) else Color.White.copy(alpha = 0.08f),
|
||||
CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (complete) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(AppStrings.t(language, key), color = Color.White.copy(alpha = if (complete) 0.85f else 0.3f))
|
||||
}
|
||||
if (active && state.importItemTitle != null && state.importItemIndex != null && state.importItemTotal != null) {
|
||||
Text(
|
||||
AppStrings.format(
|
||||
language,
|
||||
"auth.nuvio.import.item_progress",
|
||||
state.importItemIndex,
|
||||
state.importItemTotal,
|
||||
state.importItemTitle
|
||||
),
|
||||
color = Color.White.copy(alpha = 0.5f),
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.padding(start = 28.dp, top = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.importDone) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.compose.material3.OutlinedTextFieldDefaults
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -373,20 +374,46 @@ private fun TvNuvioImportingStage(
|
|||
Spacer(Modifier.height(20.dp))
|
||||
TV_IMPORT_STEP_ORDER.forEach { (step, key) ->
|
||||
val complete = step in state.importSteps
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(vertical = 6.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.background(
|
||||
if (complete) Color.White.copy(alpha = 0.9f) else Color.White.copy(alpha = 0.08f),
|
||||
CircleShape
|
||||
)
|
||||
)
|
||||
Text(AppStrings.t(language, key), color = Color.White.copy(alpha = if (complete) 0.85f else 0.3f))
|
||||
val active = !complete && !state.importDone && (state.importSteps.size == TV_IMPORT_STEP_ORDER.indexOfFirst { it.first == step })
|
||||
Column(modifier = Modifier.padding(vertical = 6.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.background(
|
||||
if (complete) Color.White.copy(alpha = 0.9f) else Color.White.copy(alpha = 0.08f),
|
||||
CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (complete) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(AppStrings.t(language, key), color = Color.White.copy(alpha = if (complete) 0.85f else 0.3f))
|
||||
}
|
||||
if (active && state.importItemTitle != null && state.importItemIndex != null && state.importItemTotal != null) {
|
||||
Text(
|
||||
AppStrings.format(
|
||||
language,
|
||||
"auth.nuvio.import.item_progress",
|
||||
state.importItemIndex,
|
||||
state.importItemTotal,
|
||||
state.importItemTitle
|
||||
),
|
||||
color = Color.White.copy(alpha = 0.5f),
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.padding(start = 28.dp, top = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.importDone) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue