Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import com.localstream.app.domain.model.TmdbMetadata
import com.localstream.app.domain.model.VideoItem
import com.localstream.app.ui.library.LibraryUiState
import com.localstream.app.ui.library.LibraryViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

Expand Down Expand Up @@ -43,10 +46,12 @@ data class HomeUiState(
*/
class HomeViewModel(
libraryUiState: StateFlow<LibraryUiState>,
computationDispatcher: CoroutineDispatcher = Dispatchers.Default,
) : ViewModel() {

val uiState: StateFlow<HomeUiState> = libraryUiState
.map { deriveHomeUiState(it) }
.flowOn(computationDispatcher)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MS),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,14 @@ data class LibraryUiState(
* (Phase 3), l'enrichissement TMDB (Phase 5) et observe l'état de visionnage
* (Phase 4). Partagé entre les écrans via le scope de l'activité.
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
class LibraryViewModel(
private val videoRepository: VideoRepository,
private val watchStateRepository: WatchStateRepository,
private val tmdbRepository: TmdbRepository,
private val settingsRepository: SettingsRepository,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val computationDispatcher: CoroutineDispatcher = Dispatchers.Default,
private val metadataChunkSize: Int = DEFAULT_METADATA_CHUNK_SIZE,
) : ViewModel() {

Expand Down Expand Up @@ -119,7 +120,9 @@ class LibraryViewModel(
// Cache Room d'abord : les affiches s'affichent immédiatement.
val cached = loadCachedMetadata(grouped)
if (cached.isNotEmpty()) {
_uiState.update { it.copy(metadata = it.metadata + cached).withDerived() }
withContext(computationDispatcher) {
_uiState.update { it.withMetadataUpdate(cached) }
}
}

if (hasKey) {
Expand All @@ -134,10 +137,12 @@ class LibraryViewModel(
}
}

private fun publishVideos(grouped: List<VideoItem>) {
private suspend fun publishVideos(grouped: List<VideoItem>) {
val durations = videoRepository.getRawVideos().associate { it.name to it.duration }
_uiState.update {
it.copy(videos = grouped, videoDurations = durations).withDerived()
withContext(computationDispatcher) {
_uiState.update {
it.copy(videos = grouped, videoDurations = durations).withDerived()
}
}
}

Expand Down Expand Up @@ -198,7 +203,9 @@ class LibraryViewModel(
}.filterNotNull()
if (results.isNotEmpty()) {
val additions = results.associate { it.queryKey to it }
_uiState.update { it.copy(metadata = it.metadata + additions).withDerived() }
withContext(computationDispatcher) {
_uiState.update { it.withMetadataUpdate(additions) }
}
}
}
}
Expand Down Expand Up @@ -308,6 +315,44 @@ class LibraryViewModel(
)
}

/**
* Met à jour les métadonnées sans ré-exécuter filterAndSort si le tri et les filtres
* actifs ne dépendent pas des métadonnées (évite 20 tris complets lors du streaming TMDB).
*/
private fun LibraryUiState.withMetadataUpdate(additions: Map<String, TmdbMetadata>): LibraryUiState {
val newMetadata = metadata + additions
val needsFilterSort = filterGenre != null || sortBy == SortBy.DATE
val newFilteredSorted = if (needsFilterSort) {
val newReleaseDates = newMetadata.mapNotNull { (key, meta) ->
meta.releaseDate?.takeIf { it.isNotBlank() }?.let { key to it }
}.toMap()
val newVideoGenres = newMetadata.mapValues { it.value.genreIds }
VideoFilterSorter.filterAndSortVideos(
videos,
FilterSortOptions(
sortBy = sortBy,
filterGenre = filterGenre,
filterResolution = filterResolution,
releaseDates = newReleaseDates,
videoGenres = newVideoGenres,
videoDurations = videoDurations,
watchedVideos = watched,
),
)
} else {
filteredSorted
}
return copy(
metadata = newMetadata,
filteredSorted = newFilteredSorted,
searchResults = if (needsFilterSort) {
VideoUiSelectors.filterByQuery(newFilteredSorted, _searchQuery.value)
} else {
searchResults
},
)
}

companion object {
const val DEFAULT_METADATA_CHUNK_SIZE = 8
const val WIFI_METADATA_CHUNK_SIZE = 16
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
Expand Down Expand Up @@ -83,6 +85,7 @@ class LibraryViewModelTest {
tmdbRepository = tmdbRepository,
settingsRepository = SettingsRepository(),
ioDispatcher = testDispatcher,
computationDispatcher = testDispatcher,
)
}

Expand Down Expand Up @@ -180,6 +183,21 @@ class LibraryViewModelTest {
assertEquals(listOf("Avatar.2009.2160p.mkv"), playbackDao.deletedNames)
}

@Test
fun `HomeViewModel derive les rows sur le computationDispatcher sans jank`() = runTest(testDispatcher) {
val homeVm = com.localstream.app.ui.home.HomeViewModel(
libraryUiState = viewModel.uiState,
computationDispatcher = testDispatcher,
)
backgroundScope.launch { homeVm.uiState.collect() }
viewModel.refreshLibrary()
advanceUntilIdle()

val homeState = homeVm.uiState.value
assertTrue(homeState.hasContent)
assertEquals(3, homeState.alphabetical.size)
}

// -------- Fakes --------

private class FakeScanner(
Expand Down
Loading