From 645e28ab30862fa25170ca2d24144ae95ad839f4 Mon Sep 17 00:00:00 2001 From: Purefin Dev Date: Thu, 21 May 2026 19:38:15 +0000 Subject: [PATCH 1/2] feat(settings): add per-library visibility toggles for home screen Add a Home Screen Libraries setting group with per-library toggles to control which libraries appear on the home screen. Libraries toggled off are hidden from both phone and TV home screens but remain accessible via the Libraries tab and navigation drawer. --- .../core/feature/browse/home/AppViewModel.kt | 45 +++++++++++-------- .../settings/HomeLibrarySettingsProvider.kt | 35 +++++++++++++++ .../settings/SettingsGroupProviderModule.kt | 5 +++ 3 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 core/src/main/java/hu/bbara/purefin/core/feature/settings/HomeLibrarySettingsProvider.kt diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/AppViewModel.kt b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/AppViewModel.kt index 94d198f6..e73761e0 100644 --- a/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/AppViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/AppViewModel.kt @@ -9,6 +9,7 @@ import hu.bbara.purefin.core.data.NetworkMonitor import hu.bbara.purefin.core.data.UserSessionRepository import hu.bbara.purefin.core.download.MediaDownloadController import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater +import hu.bbara.purefin.core.settings.SettingsRepository import hu.bbara.purefin.core.model.EpisodeUiModel import hu.bbara.purefin.core.model.LibraryUiModel import hu.bbara.purefin.core.model.MediaUiModel @@ -41,6 +42,7 @@ class AppViewModel @Inject constructor( private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater, private val navigationManager: NavigationManager, private val mediaDownloadManager: MediaDownloadController, + private val settingsRepository: SettingsRepository, networkMonitor: NetworkMonitor, ) : ViewModel() { @@ -147,26 +149,31 @@ class AppViewModel @Inject constructor( homeRepository.latestLibraryContent, localMediaRepository.movies, localMediaRepository.series, - localMediaRepository.episodes - ) { libraryMap, moviesMap, seriesMap, episodesMap -> - libraryMap.mapValues { (_, items) -> - items.mapNotNull { media -> - when (media) { - is Media.MovieMedia -> moviesMap[media.movieId]?.let { - MovieUiModel(movie = it) + localMediaRepository.episodes, + settingsRepository.settings, + ) { libraryMap, moviesMap, seriesMap, episodesMap, settings -> + libraryMap + .filter { (libraryId, _) -> + settings.booleanSettings["home_library_visible_$libraryId"] ?: true + } + .mapValues { (_, items) -> + items.mapNotNull { media -> + when (media) { + is Media.MovieMedia -> moviesMap[media.movieId]?.let { + MovieUiModel(movie = it) + } + is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let { + EpisodeUiModel(episode = it) + } + is Media.SeriesMedia -> seriesMap[media.seriesId]?.let { + SeriesUiModel(series = it) + } + is Media.SeasonMedia -> seriesMap[media.seriesId]?.let { + SeriesUiModel(series = it) + } } - is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let { - EpisodeUiModel(episode = it) - } - is Media.SeriesMedia -> seriesMap[media.seriesId]?.let { - SeriesUiModel(series = it) - } - is Media.SeasonMedia -> seriesMap[media.seriesId]?.let { - SeriesUiModel(series = it) - } - } - }.distinctBy { it.id } - } + }.distinctBy { it.id } + } }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/settings/HomeLibrarySettingsProvider.kt b/core/src/main/java/hu/bbara/purefin/core/feature/settings/HomeLibrarySettingsProvider.kt new file mode 100644 index 00000000..abaf738e --- /dev/null +++ b/core/src/main/java/hu/bbara/purefin/core/feature/settings/HomeLibrarySettingsProvider.kt @@ -0,0 +1,35 @@ +package hu.bbara.purefin.core.feature.settings + +import hu.bbara.purefin.core.data.HomeRepository +import hu.bbara.purefin.core.settings.BooleanSetting +import hu.bbara.purefin.core.settings.SettingGroup +import hu.bbara.purefin.core.settings.SettingsGroupProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class HomeLibrarySettingsProvider @Inject constructor( + private val homeRepository: HomeRepository, +) : SettingsGroupProvider { + + override val settingGroups: Flow> = homeRepository.libraries.map { libraries -> + if (libraries.isEmpty()) return@map emptyList() + + val options = libraries.map { library -> + BooleanSetting( + key = "home_library_visible_${library.id}", + title = library.name, + defaultValue = true + ) + } + + listOf( + SettingGroup( + title = "Home Screen Libraries", + options = options + ) + ) + } +} diff --git a/core/src/main/java/hu/bbara/purefin/core/settings/SettingsGroupProviderModule.kt b/core/src/main/java/hu/bbara/purefin/core/settings/SettingsGroupProviderModule.kt index 75a7078f..475f7492 100644 --- a/core/src/main/java/hu/bbara/purefin/core/settings/SettingsGroupProviderModule.kt +++ b/core/src/main/java/hu/bbara/purefin/core/settings/SettingsGroupProviderModule.kt @@ -5,6 +5,7 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet +import hu.bbara.purefin.core.feature.settings.HomeLibrarySettingsProvider import hu.bbara.purefin.core.feature.settings.LogoutSettingsProvider import hu.bbara.purefin.core.feature.update.AppUpdateController @@ -16,6 +17,10 @@ abstract class SettingsGroupProviderModule { @IntoSet abstract fun bindAppUpdateSettingsProvider(impl: AppUpdateController): SettingsGroupProvider + @Binds + @IntoSet + abstract fun bindHomeLibrarySettingsProvider(impl: HomeLibrarySettingsProvider): SettingsGroupProvider + @Binds @IntoSet abstract fun bindLogoutSettingsProvider(impl: LogoutSettingsProvider): SettingsGroupProvider From 8da759257cc9c28a86a0684f16cc997a78611f6f Mon Sep 17 00:00:00 2001 From: Purefin Dev Date: Thu, 21 May 2026 19:47:58 +0000 Subject: [PATCH 2/2] refactor: replace collectAsState with lifecycle-aware collectAsStateWithLifecycle Migrate all Flow state collection to collectAsStateWithLifecycle for proper lifecycle awareness, ensuring collection stops when the composable is not in the STARTED state. - Added lifecycle-runtime-compose dependency to app and app-tv modules - Replaced all collectAsState() calls with collectAsStateWithLifecycle() - Updated initial parameter to initialValue for the lifecycle-aware variant --- app-tv/build.gradle.kts | 1 + .../java/hu/bbara/purefin/tv/TvActivity.kt | 6 ++--- .../hu/bbara/purefin/ui/screen/TvAppScreen.kt | 10 +++---- .../ui/screen/episode/TvEpisodeScreen.kt | 6 ++--- .../ui/screen/library/TvLibraryScreen.kt | 4 +-- .../purefin/ui/screen/login/TvLoginScreen.kt | 26 +++++++++---------- .../purefin/ui/screen/movie/TvMovieScreen.kt | 4 +-- .../ui/screen/player/TvPlayerScreen.kt | 6 ++--- .../ui/screen/series/TvSeriesScreen.kt | 4 +-- .../ui/screen/settings/TvSettingsScreen.kt | 12 ++++----- app/build.gradle.kts | 1 + .../java/hu/bbara/purefin/PurefinActivity.kt | 4 +-- .../hu/bbara/purefin/ui/screen/AppScreen.kt | 20 +++++++------- .../download/components/DownloadsContent.kt | 6 ++--- .../ui/screen/episode/EpisodeScreen.kt | 6 ++--- .../home/components/search/SearchOverlay.kt | 4 +-- .../ui/screen/library/LibraryScreen.kt | 4 +-- .../purefin/ui/screen/login/LoginScreen.kt | 26 +++++++++---------- .../purefin/ui/screen/movie/MovieScreen.kt | 6 ++--- .../purefin/ui/screen/player/PlayerScreen.kt | 6 ++--- .../purefin/ui/screen/series/SeriesScreen.kt | 10 +++---- .../ui/screen/settings/SettingsScreen.kt | 12 ++++----- gradle/libs.versions.toml | 1 + 23 files changed, 94 insertions(+), 91 deletions(-) diff --git a/app-tv/build.gradle.kts b/app-tv/build.gradle.kts index 654af0e2..38ac9e86 100644 --- a/app-tv/build.gradle.kts +++ b/app-tv/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation(project(":core-ui")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.androidx.activity.compose) diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt index 6aa3af41..f3f6fb7a 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt @@ -18,8 +18,8 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -147,8 +147,8 @@ class TvActivity : ComponentActivity() { updateViewModel: AppUpdateViewModel = hiltViewModel() ) { var sessionLoaded by remember { mutableStateOf(false) } - val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false) - val availableUpdate by updateViewModel.availableUpdate.collectAsState() + val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false) + val availableUpdate by updateViewModel.availableUpdate.collectAsStateWithLifecycle() val context = LocalContext.current LaunchedEffect(Unit) { diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt index 5ce86aa0..d9be787f 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt @@ -7,8 +7,8 @@ import androidx.compose.material.icons.outlined.Movie import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Tv import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel @@ -35,10 +35,10 @@ fun TvAppScreen( viewModel: AppViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { - val libraries by viewModel.libraries.collectAsState() - val continueWatching by viewModel.continueWatching.collectAsState() - val nextUp by viewModel.nextUp.collectAsState() - val latestLibraryContent by viewModel.latestLibraryContent.collectAsState() + val libraries by viewModel.libraries.collectAsStateWithLifecycle() + val continueWatching by viewModel.continueWatching.collectAsStateWithLifecycle() + val nextUp by viewModel.nextUp.collectAsStateWithLifecycle() + val latestLibraryContent by viewModel.latestLibraryContent.collectAsStateWithLifecycle() val navigationManager = LocalNavigationManager.current @Suppress("UNCHECKED_CAST") diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/episode/TvEpisodeScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/episode/TvEpisodeScreen.kt index 743f993a..a69c8ada 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/episode/TvEpisodeScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/episode/TvEpisodeScreen.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.remember import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Modifier @@ -40,8 +40,8 @@ fun TvEpisodeScreen( viewModel.selectEpisode(episode) } - val episode = viewModel.episode.collectAsState() - val seriesTitle = viewModel.seriesTitle.collectAsState() + val episode = viewModel.episode.collectAsStateWithLifecycle() + val seriesTitle = viewModel.seriesTitle.collectAsStateWithLifecycle() val selectedEpisode = episode.value if (selectedEpisode == null) { diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/library/TvLibraryScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/library/TvLibraryScreen.kt index 8f2e1563..3ed6a59c 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/library/TvLibraryScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/library/TvLibraryScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -35,7 +35,7 @@ fun TvLibraryScreen( viewModel.selectLibrary(libraryId = library.id) } - val libraryItems = viewModel.contents.collectAsState() + val libraryItems = viewModel.contents.collectAsStateWithLifecycle() Scaffold( modifier = modifier, diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/login/TvLoginScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/login/TvLoginScreen.kt index 3906d2b3..f2eb2348 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/login/TvLoginScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/login/TvLoginScreen.kt @@ -1,8 +1,8 @@ package hu.bbara.purefin.ui.screen.login import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -16,18 +16,18 @@ fun TvLoginScreen( viewModel: LoginViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { - val serverUrl by viewModel.url.collectAsState() - val phase by viewModel.phase.collectAsState() - val selectedServer by viewModel.selectedServer.collectAsState() - val discoveredServers by viewModel.discoveredServers.collectAsState() - val username by viewModel.username.collectAsState() - val password by viewModel.password.collectAsState() - val isSearching by viewModel.isSearching.collectAsState() - val isLoggingIn by viewModel.isLoggingIn.collectAsState() - val quickConnectAvailable by viewModel.quickConnectAvailable.collectAsState() - val quickConnectCode by viewModel.quickConnectCode.collectAsState() - val isQuickConnecting by viewModel.isQuickConnecting.collectAsState() - val errorMessage by viewModel.errorMessage.collectAsState() + val serverUrl by viewModel.url.collectAsStateWithLifecycle() + val phase by viewModel.phase.collectAsStateWithLifecycle() + val selectedServer by viewModel.selectedServer.collectAsStateWithLifecycle() + val discoveredServers by viewModel.discoveredServers.collectAsStateWithLifecycle() + val username by viewModel.username.collectAsStateWithLifecycle() + val password by viewModel.password.collectAsStateWithLifecycle() + val isSearching by viewModel.isSearching.collectAsStateWithLifecycle() + val isLoggingIn by viewModel.isLoggingIn.collectAsStateWithLifecycle() + val quickConnectAvailable by viewModel.quickConnectAvailable.collectAsStateWithLifecycle() + val quickConnectCode by viewModel.quickConnectCode.collectAsStateWithLifecycle() + val isQuickConnecting by viewModel.isQuickConnecting.collectAsStateWithLifecycle() + val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() val state = remember( diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/movie/TvMovieScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/movie/TvMovieScreen.kt index 933f90b1..5f55aea2 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/movie/TvMovieScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/movie/TvMovieScreen.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.remember import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Modifier @@ -32,7 +32,7 @@ fun TvMovieScreen( viewModel.selectMovie(movie) } - val movieItem = viewModel.movie.collectAsState() + val movieItem = viewModel.movie.collectAsStateWithLifecycle() if (movieItem.value != null) { TvMovieScreenContent( diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/player/TvPlayerScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/player/TvPlayerScreen.kt index 53c3bf36..ed3e2a5f 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/player/TvPlayerScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/player/TvPlayerScreen.kt @@ -29,8 +29,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -85,8 +85,8 @@ fun TvPlayerScreen( viewModel.loadMedia(mediaId) } - val uiState by viewModel.uiState.collectAsState() - val controlsVisible by viewModel.controlsVisible.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val controlsVisible by viewModel.controlsVisible.collectAsStateWithLifecycle() var isPlaylistExpanded by remember { mutableStateOf(false) } var trackPanelType by remember { mutableStateOf(null) } var pendingTrackButtonFocus by remember { mutableStateOf(null) } diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/TvSeriesScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/TvSeriesScreen.kt index 28a33525..e4f84df1 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/TvSeriesScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/TvSeriesScreen.kt @@ -8,8 +8,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -46,7 +46,7 @@ fun TvSeriesScreen( viewModel.selectSeries(series) } - val series = viewModel.series.collectAsState() + val series = viewModel.series.collectAsStateWithLifecycle() val seriesData = series.value if (seriesData != null && seriesData.seasons.isNotEmpty()) { diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/settings/TvSettingsScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/settings/TvSettingsScreen.kt index 3e4b367f..a2f93be6 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/settings/TvSettingsScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/settings/TvSettingsScreen.kt @@ -26,8 +26,8 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -53,7 +53,7 @@ fun TvSettingsScreen( onBack: () -> Unit = viewModel::onBack, modifier: Modifier = Modifier ) { - val settingGroups by viewModel.settingGroups.collectAsState() + val settingGroups by viewModel.settingGroups.collectAsStateWithLifecycle() val context = LocalContext.current LaunchedEffect(viewModel, context) { @@ -117,7 +117,7 @@ private fun TvSettingOptionItem( ) { when (option) { is RangeSetting -> { - val value by viewModel.value(option).collectAsState(initial = null) + val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null) value?.let { TvNumberSettingItem( title = option.title, @@ -129,7 +129,7 @@ private fun TvSettingOptionItem( } is BooleanSetting -> { - val value by viewModel.value(option).collectAsState(initial = null) + val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null) value?.let { TvBooleanSettingItem( title = option.title, @@ -140,7 +140,7 @@ private fun TvSettingOptionItem( } is StringSetting -> { - val value by viewModel.value(option).collectAsState(initial = null) + val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null) value?.let { TvStringSettingItem( title = option.title, @@ -178,7 +178,7 @@ private fun TvDropdownSettingOptionItem( option: DropdownSetting, viewModel: SettingsViewModel ) { - val value by viewModel.value(option).collectAsState(initial = null) + val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null) value?.let { TvDropdownSettingItem( title = option.title, diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e6af5e22..94d9636b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(project(":core-ui")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.androidx.activity.compose) diff --git a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt index ae8085f9..a453b0db 100644 --- a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt +++ b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt @@ -16,8 +16,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -146,7 +146,7 @@ class PurefinActivity : ComponentActivity() { navigationManager: NavigationManager ) { var sessionLoaded by remember { mutableStateOf(false) } - val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false) + val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false) LaunchedEffect(Unit) { userSessionRepository.isLoggedIn.collect { diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt index 4d77fda1..948b5661 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt @@ -11,8 +11,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel @@ -38,15 +38,15 @@ fun AppScreen( updateViewModel: AppUpdateViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { - val libraries by viewModel.libraries.collectAsState() - val libraryContent by viewModel.latestLibraryContent.collectAsState() - val suggestions by viewModel.suggestions.collectAsState() - val continueWatching by viewModel.continueWatching.collectAsState() - val nextUp by viewModel.nextUp.collectAsState() - val isRefreshing by viewModel.isRefreshing.collectAsState() - val isOnline by viewModel.isOnline.collectAsState() - val isCheckingForUpdates by updateViewModel.isCheckingForUpdates.collectAsState() - val availableUpdate by updateViewModel.availableUpdate.collectAsState() + val libraries by viewModel.libraries.collectAsStateWithLifecycle() + val libraryContent by viewModel.latestLibraryContent.collectAsStateWithLifecycle() + val suggestions by viewModel.suggestions.collectAsStateWithLifecycle() + val continueWatching by viewModel.continueWatching.collectAsStateWithLifecycle() + val nextUp by viewModel.nextUp.collectAsStateWithLifecycle() + val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() + val isOnline by viewModel.isOnline.collectAsStateWithLifecycle() + val isCheckingForUpdates by updateViewModel.isCheckingForUpdates.collectAsStateWithLifecycle() + val availableUpdate by updateViewModel.availableUpdate.collectAsStateWithLifecycle() val navigationManager = LocalNavigationManager.current val snackbarHostState = remember { SnackbarHostState() } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadsContent.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadsContent.kt index 69491cad..dd7b20e7 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadsContent.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadsContent.kt @@ -17,7 +17,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -32,8 +32,8 @@ fun DownloadsContent( modifier: Modifier = Modifier, viewModel: DownloadsViewModel = hiltViewModel(), ) { - val downloads = viewModel.downloads.collectAsState(emptyList()) - val activeDownloads = viewModel.activeDownloads.collectAsState() + val downloads = viewModel.downloads.collectAsStateWithLifecycle(initialValue = emptyList()) + val activeDownloads = viewModel.activeDownloads.collectAsStateWithLifecycle() val isEmpty = downloads.value.isEmpty() && activeDownloads.value.isEmpty() diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt index 9bebb4a7..5aea79a5 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt @@ -8,7 +8,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -46,8 +46,8 @@ fun EpisodeScreen( viewModel.selectEpisode(episode) } - val episode = viewModel.episode.collectAsState() - val downloadState = viewModel.downloadState.collectAsState() + val episode = viewModel.episode.collectAsStateWithLifecycle() + val downloadState = viewModel.downloadState.collectAsStateWithLifecycle() val notificationPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/SearchOverlay.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/SearchOverlay.kt index f7b5c1dc..7ad9c2a7 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/SearchOverlay.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/SearchOverlay.kt @@ -24,8 +24,8 @@ import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -55,7 +55,7 @@ fun SearchOverlay( BackHandler(onBack = onDismiss) var query by rememberSaveable { mutableStateOf("") } - val searchResults by searchViewModel.searchResult.collectAsState() + val searchResults by searchViewModel.searchResult.collectAsStateWithLifecycle() val dismissInteractionSource = remember { MutableInteractionSource() } Box( diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt index 27b443c7..298080cd 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt @@ -14,7 +14,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -40,7 +40,7 @@ fun LibraryScreen( viewModel.selectLibrary(libraryId = library.id) } - val libraryItems = viewModel.contents.collectAsState() + val libraryItems = viewModel.contents.collectAsStateWithLifecycle() Scaffold( modifier = modifier.testTag(LibraryScreenTag), diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/login/LoginScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/login/LoginScreen.kt index d6ce3ca3..38ff9115 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/login/LoginScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/login/LoginScreen.kt @@ -1,8 +1,8 @@ package hu.bbara.purefin.ui.screen.login import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -16,18 +16,18 @@ fun LoginScreen( viewModel: LoginViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { - val serverUrl by viewModel.url.collectAsState() - val phase by viewModel.phase.collectAsState() - val selectedServer by viewModel.selectedServer.collectAsState() - val discoveredServers by viewModel.discoveredServers.collectAsState() - val username by viewModel.username.collectAsState() - val password by viewModel.password.collectAsState() - val isSearching by viewModel.isSearching.collectAsState() - val isLoggingIn by viewModel.isLoggingIn.collectAsState() - val quickConnectAvailable by viewModel.quickConnectAvailable.collectAsState() - val quickConnectCode by viewModel.quickConnectCode.collectAsState() - val isQuickConnecting by viewModel.isQuickConnecting.collectAsState() - val errorMessage by viewModel.errorMessage.collectAsState() + val serverUrl by viewModel.url.collectAsStateWithLifecycle() + val phase by viewModel.phase.collectAsStateWithLifecycle() + val selectedServer by viewModel.selectedServer.collectAsStateWithLifecycle() + val discoveredServers by viewModel.discoveredServers.collectAsStateWithLifecycle() + val username by viewModel.username.collectAsStateWithLifecycle() + val password by viewModel.password.collectAsStateWithLifecycle() + val isSearching by viewModel.isSearching.collectAsStateWithLifecycle() + val isLoggingIn by viewModel.isLoggingIn.collectAsStateWithLifecycle() + val quickConnectAvailable by viewModel.quickConnectAvailable.collectAsStateWithLifecycle() + val quickConnectCode by viewModel.quickConnectCode.collectAsStateWithLifecycle() + val isQuickConnecting by viewModel.isQuickConnecting.collectAsStateWithLifecycle() + val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() val state = remember( diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt index c458efe8..b579aced 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt @@ -8,7 +8,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -39,8 +39,8 @@ fun MovieScreen( viewModel.selectMovie(movie) } - val movieItem = viewModel.movie.collectAsState() - val downloadState = viewModel.downloadState.collectAsState() + val movieItem = viewModel.movie.collectAsStateWithLifecycle() + val downloadState = viewModel.downloadState.collectAsStateWithLifecycle() val notificationPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt index a33c0889..63c82641 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt @@ -25,8 +25,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -71,8 +71,8 @@ fun PlayerScreen( viewModel: PlayerViewModel, onBack: () -> Unit ) { - val uiState by viewModel.uiState.collectAsState() - val controlsVisible by viewModel.controlsVisible.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val controlsVisible by viewModel.controlsVisible.collectAsStateWithLifecycle() val context = LocalContext.current val activity = context as? Activity diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt index dcf3997f..9b92e3b9 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt @@ -6,8 +6,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -49,7 +49,7 @@ fun SeriesScreen( viewModel.selectSeries(series) } - val seriesState = viewModel.series.collectAsState() + val seriesState = viewModel.series.collectAsStateWithLifecycle() val seriesData = seriesState.value if (seriesData != null && seriesData.seasons.isNotEmpty()) { @@ -58,9 +58,9 @@ fun SeriesScreen( } SeriesScreenInternal( series = seriesData, - seriesDownloadState = viewModel.seriesDownloadState.collectAsState().value, - seasonDownloadState = viewModel.seasonDownloadState.collectAsState().value, - isSmartDownloadEnabled = viewModel.isSmartDownloadEnabled.collectAsState().value, + seriesDownloadState = viewModel.seriesDownloadState.collectAsStateWithLifecycle().value, + seasonDownloadState = viewModel.seasonDownloadState.collectAsStateWithLifecycle().value, + isSmartDownloadEnabled = viewModel.isSmartDownloadEnabled.collectAsStateWithLifecycle().value, onDownloadOptionSelected = { option, selectedSeason -> when (option) { SeriesDownloadOption.SEASON -> diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt index 43c78096..20091871 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/settings/SettingsScreen.kt @@ -14,8 +14,8 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -42,7 +42,7 @@ fun SettingsScreen( viewModel: SettingsViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { - val settingGroups by viewModel.settingGroups.collectAsState() + val settingGroups by viewModel.settingGroups.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(viewModel) { @@ -107,7 +107,7 @@ private fun SettingOptionItem( ) { when (option) { is RangeSetting -> { - val value by viewModel.value(option).collectAsState(initial = null) + val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null) value?.let { RangeSettingItem( title = option.title, @@ -119,7 +119,7 @@ private fun SettingOptionItem( } is BooleanSetting -> { - val value by viewModel.value(option).collectAsState(initial = null) + val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null) value?.let { BooleanSettingItem( title = option.title, @@ -130,7 +130,7 @@ private fun SettingOptionItem( } is StringSetting -> { - val value by viewModel.value(option).collectAsState(initial = null) + val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null) value?.let { StringSettingItem( title = option.title, @@ -168,7 +168,7 @@ private fun DropdownSettingOptionItem( option: DropdownSetting, viewModel: SettingsViewModel ) { - val value by viewModel.value(option).collectAsState(initial = null) + val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null) value?.let { DropdownSettingItem( title = option.title, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2841c714..0fa87946 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }