perf: decouple home startup from continue watching sync
This commit is contained in:
parent
0c2bb40707
commit
6c2acafec1
5 changed files with 101 additions and 25 deletions
|
|
@ -74,6 +74,7 @@ class WatchProgressRepositoryImpl @Inject constructor(
|
|||
private val syncScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var syncJob: Job? = null
|
||||
private var watchedItemsSyncJob: Job? = null
|
||||
private var lastRemoteRefreshAtMs: Long = 0L
|
||||
var isSyncingFromRemote = false
|
||||
var hasCompletedInitialPull = false
|
||||
var hasCompletedInitialWatchedItemsPull = false
|
||||
|
|
@ -98,6 +99,44 @@ class WatchProgressRepositoryImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun refreshRemoteProgressIfStale(
|
||||
maxAgeMs: Long,
|
||||
force: Boolean = false
|
||||
): Long? {
|
||||
if (isSyncingFromRemote) return null
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val useTraktProgress = shouldUseTraktProgress()
|
||||
if (!force && hasCompletedInitialPull && lastRemoteRefreshAtMs > 0L) {
|
||||
val ageMs = now - lastRemoteRefreshAtMs
|
||||
if (ageMs in 0 until maxAgeMs) {
|
||||
return lastRemoteRefreshAtMs
|
||||
}
|
||||
}
|
||||
|
||||
if (useTraktProgress) {
|
||||
traktProgressService.refreshNow()
|
||||
lastRemoteRefreshAtMs = now
|
||||
return lastRemoteRefreshAtMs
|
||||
}
|
||||
|
||||
if (!authManager.isAuthenticated) return lastRemoteRefreshAtMs.takeIf { it > 0L }
|
||||
|
||||
isSyncingFromRemote = true
|
||||
return try {
|
||||
val remoteEntries = watchProgressSyncService.pullFromRemote().getOrElse { throw it }
|
||||
watchProgressPreferences.mergeRemoteEntries(remoteEntries.toMap())
|
||||
hasCompletedInitialPull = true
|
||||
lastRemoteRefreshAtMs = System.currentTimeMillis()
|
||||
lastRemoteRefreshAtMs
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "refreshRemoteProgressIfStale failed", error)
|
||||
lastRemoteRefreshAtMs.takeIf { it > 0L }
|
||||
} finally {
|
||||
isSyncingFromRemote = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerWatchedItemsSync() {
|
||||
if (isSyncingFromRemote) return
|
||||
if (!hasCompletedInitialWatchedItemsPull) return
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.ButtonDefaults
|
||||
|
|
@ -50,7 +53,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import com.nuvio.tv.R
|
||||
import com.nuvio.tv.data.local.StartupAuthNotice
|
||||
import com.nuvio.tv.ui.theme.NuvioColors
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private data class HomePosterOptionsTarget(
|
||||
|
|
@ -58,8 +60,6 @@ private data class HomePosterOptionsTarget(
|
|||
val addonBaseUrl: String
|
||||
)
|
||||
|
||||
private const val HOME_STARTUP_CW_GATE_TIMEOUT_MS = 5_000L
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
|
|
@ -86,11 +86,10 @@ fun HomeScreen(
|
|||
val effectiveAutoplayEnabled by viewModel.effectiveAutoplayEnabled.collectAsStateWithLifecycle(
|
||||
initialValue = false
|
||||
)
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val hasCatalogContent = uiState.catalogRows.any { it.items.isNotEmpty() }
|
||||
var hasEnteredCatalogContent by rememberSaveable { mutableStateOf(false) }
|
||||
var showHomeContentWithAnimation by rememberSaveable { mutableStateOf(false) }
|
||||
var hasReleasedStartupCwGate by rememberSaveable { mutableStateOf(false) }
|
||||
var startupCwGateTimedOut by rememberSaveable { mutableStateOf(false) }
|
||||
var hasShownInitialHomeContent by rememberSaveable { mutableStateOf(false) }
|
||||
var posterOptionsTarget by remember { mutableStateOf<HomePosterOptionsTarget?>(null) }
|
||||
|
||||
|
|
@ -112,15 +111,18 @@ fun HomeScreen(
|
|||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(HOME_STARTUP_CW_GATE_TIMEOUT_MS)
|
||||
startupCwGateTimedOut = true
|
||||
viewModel.refreshContinueWatchingIfStale()
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.continueWatchingItems.isNotEmpty(), startupCwGateTimedOut) {
|
||||
if (!hasReleasedStartupCwGate &&
|
||||
(uiState.continueWatchingItems.isNotEmpty() || startupCwGateTimedOut)
|
||||
) {
|
||||
hasReleasedStartupCwGate = true
|
||||
androidx.compose.runtime.DisposableEffect(lifecycleOwner, viewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
viewModel.refreshContinueWatchingIfStale()
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,9 +197,7 @@ fun HomeScreen(
|
|||
}
|
||||
|
||||
else -> {
|
||||
val shouldShowLoadingGate =
|
||||
!hasReleasedStartupCwGate ||
|
||||
(!hasEnteredCatalogContent && !hasCatalogContent)
|
||||
val shouldShowLoadingGate = !hasEnteredCatalogContent && !hasCatalogContent
|
||||
LaunchedEffect(shouldShowLoadingGate) {
|
||||
if (shouldShowLoadingGate) {
|
||||
showHomeContentWithAnimation = false
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import com.nuvio.tv.domain.model.WatchProgress
|
|||
data class HomeUiState(
|
||||
val catalogRows: List<CatalogRow> = emptyList(),
|
||||
val continueWatchingItems: List<ContinueWatchingItem> = emptyList(),
|
||||
val continueWatchingResolved: Boolean = false,
|
||||
val continueWatchingLastRemoteSyncAtMs: Long? = null,
|
||||
val isLoading: Boolean = true,
|
||||
val error: String? = null,
|
||||
val selectedItemId: String? = null,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.nuvio.tv.data.local.StartupAuthNotice
|
|||
import com.nuvio.tv.data.local.TmdbSettingsDataStore
|
||||
import com.nuvio.tv.data.local.TraktSettingsDataStore
|
||||
import com.nuvio.tv.data.local.WatchedItemsPreferences
|
||||
import com.nuvio.tv.data.repository.WatchProgressRepositoryImpl
|
||||
import com.nuvio.tv.data.trailer.TrailerService
|
||||
import com.nuvio.tv.domain.model.Addon
|
||||
import com.nuvio.tv.domain.model.CatalogDescriptor
|
||||
|
|
@ -27,7 +28,6 @@ import com.nuvio.tv.domain.repository.AddonRepository
|
|||
import com.nuvio.tv.domain.repository.CatalogRepository
|
||||
import com.nuvio.tv.domain.repository.LibraryRepository
|
||||
import com.nuvio.tv.domain.repository.MetaRepository
|
||||
import com.nuvio.tv.domain.repository.WatchProgressRepository
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -49,7 +49,7 @@ class HomeViewModel @Inject constructor(
|
|||
@ApplicationContext internal val appContext: Context,
|
||||
internal val addonRepository: AddonRepository,
|
||||
internal val catalogRepository: CatalogRepository,
|
||||
internal val watchProgressRepository: WatchProgressRepository,
|
||||
internal val watchProgressRepository: WatchProgressRepositoryImpl,
|
||||
internal val libraryRepository: LibraryRepository,
|
||||
internal val metaRepository: MetaRepository,
|
||||
internal val layoutPreferenceDataStore: LayoutPreferenceDataStore,
|
||||
|
|
@ -74,6 +74,7 @@ class HomeViewModel @Inject constructor(
|
|||
internal const val EXTERNAL_META_PREFETCH_FOCUS_DEBOUNCE_MS = 220L
|
||||
internal const val EXTERNAL_META_PREFETCH_ADJACENT_DEBOUNCE_MS = 120L
|
||||
internal const val MAX_POSTER_STATUS_OBSERVERS = 24
|
||||
internal const val CONTINUE_WATCHING_REMOTE_REFRESH_STALE_MS = 60_000L
|
||||
}
|
||||
|
||||
internal val _uiState = MutableStateFlow(HomeUiState())
|
||||
|
|
@ -273,6 +274,24 @@ class HomeViewModel @Inject constructor(
|
|||
loadContinueWatchingPipeline()
|
||||
}
|
||||
|
||||
fun refreshContinueWatchingIfStale(force: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
val refreshedAt = watchProgressRepository.refreshRemoteProgressIfStale(
|
||||
maxAgeMs = CONTINUE_WATCHING_REMOTE_REFRESH_STALE_MS,
|
||||
force = force
|
||||
)
|
||||
if (refreshedAt != null) {
|
||||
_uiState.update { state ->
|
||||
if (state.continueWatchingLastRemoteSyncAtMs == refreshedAt) {
|
||||
state
|
||||
} else {
|
||||
state.copy(continueWatchingLastRemoteSyncAtMs = refreshedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeContinueWatching(
|
||||
contentId: String,
|
||||
season: Int? = null,
|
||||
|
|
|
|||
|
|
@ -200,10 +200,14 @@ internal fun HomeViewModel.loadContinueWatchingPipeline() {
|
|||
if (inProgressOnly.isNotEmpty()) {
|
||||
val initialItems = inProgressOnly.map { it as ContinueWatchingItem }
|
||||
_uiState.update { state ->
|
||||
if (state.continueWatchingItems == initialItems) {
|
||||
val nextState = state.copy(
|
||||
continueWatchingItems = initialItems,
|
||||
continueWatchingResolved = true
|
||||
)
|
||||
if (state == nextState) {
|
||||
state
|
||||
} else {
|
||||
state.copy(continueWatchingItems = initialItems)
|
||||
nextState
|
||||
}
|
||||
}
|
||||
debug.recordInitialRendered(
|
||||
|
|
@ -233,10 +237,14 @@ internal fun HomeViewModel.loadContinueWatchingPipeline() {
|
|||
nextUpItems = partialNextUpItems
|
||||
)
|
||||
_uiState.update { state ->
|
||||
if (state.continueWatchingItems == partialItems) {
|
||||
val nextState = state.copy(
|
||||
continueWatchingItems = partialItems,
|
||||
continueWatchingResolved = true
|
||||
)
|
||||
if (state == nextState) {
|
||||
state
|
||||
} else {
|
||||
state.copy(continueWatchingItems = partialItems)
|
||||
nextState
|
||||
}
|
||||
}
|
||||
debug.recordPartialRendered(
|
||||
|
|
@ -259,10 +267,14 @@ internal fun HomeViewModel.loadContinueWatchingPipeline() {
|
|||
)
|
||||
|
||||
_uiState.update { state ->
|
||||
if (state.continueWatchingItems == normalItems) {
|
||||
val nextState = state.copy(
|
||||
continueWatchingItems = normalItems,
|
||||
continueWatchingResolved = true
|
||||
)
|
||||
if (state == nextState) {
|
||||
state
|
||||
} else {
|
||||
state.copy(continueWatchingItems = normalItems)
|
||||
nextState
|
||||
}
|
||||
}
|
||||
debug.recordLightweightRendered(
|
||||
|
|
@ -578,10 +590,14 @@ private suspend fun HomeViewModel.enrichVisibleContinueWatchingItems(
|
|||
if (enrichedItems == finalItems) return@coroutineScope false
|
||||
|
||||
_uiState.update { state ->
|
||||
if (state.continueWatchingItems == enrichedItems) {
|
||||
val nextState = state.copy(
|
||||
continueWatchingItems = enrichedItems,
|
||||
continueWatchingResolved = true
|
||||
)
|
||||
if (state == nextState) {
|
||||
state
|
||||
} else {
|
||||
state.copy(continueWatchingItems = enrichedItems)
|
||||
nextState
|
||||
}
|
||||
}
|
||||
persistLocalContinueWatchingMetadata(
|
||||
|
|
|
|||
Loading…
Reference in a new issue