From ed1eeb68c9e42e5511dc30d47ffd4dd6dab79dc4 Mon Sep 17 00:00:00 2001 From: Ajay Prem Shankar Date: Sat, 25 Apr 2026 11:44:40 +0530 Subject: [PATCH 1/2] Added default view setting in appearance --- .../readeckapp/domain/model/DefaultFilter.kt | 8 ++ .../readeckapp/io/prefs/SettingsDataStore.kt | 3 + .../io/prefs/SettingsDataStoreImpl.kt | 14 ++ .../readeckapp/ui/list/BookmarkListScreen.kt | 13 +- .../ui/list/BookmarkListViewModel.kt | 22 +++- .../ui/settings/DefaultFilterDialog.kt | 124 ++++++++++++++++++ .../ui/settings/UiSettingsScreen.kt | 30 +++++ .../ui/settings/UiSettingsViewModel.kt | 66 +++++++++- app/src/main/res/values/strings.xml | 6 + .../ui/list/BookmarkListViewModelTest.kt | 2 + 10 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/de/readeckapp/domain/model/DefaultFilter.kt create mode 100644 app/src/main/java/de/readeckapp/ui/settings/DefaultFilterDialog.kt diff --git a/app/src/main/java/de/readeckapp/domain/model/DefaultFilter.kt b/app/src/main/java/de/readeckapp/domain/model/DefaultFilter.kt new file mode 100644 index 0000000..532ba47 --- /dev/null +++ b/app/src/main/java/de/readeckapp/domain/model/DefaultFilter.kt @@ -0,0 +1,8 @@ +package de.readeckapp.domain.model + +enum class DefaultFilter { + ALL, + UNREAD, + ARCHIVED, + FAVORITES +} diff --git a/app/src/main/java/de/readeckapp/io/prefs/SettingsDataStore.kt b/app/src/main/java/de/readeckapp/io/prefs/SettingsDataStore.kt index c90d7c1..260772c 100644 --- a/app/src/main/java/de/readeckapp/io/prefs/SettingsDataStore.kt +++ b/app/src/main/java/de/readeckapp/io/prefs/SettingsDataStore.kt @@ -1,6 +1,7 @@ package de.readeckapp.io.prefs import de.readeckapp.domain.model.AutoSyncTimeframe +import de.readeckapp.domain.model.DefaultFilter import de.readeckapp.domain.model.Theme import kotlinx.coroutines.flow.StateFlow import kotlinx.datetime.Instant @@ -37,4 +38,6 @@ interface SettingsDataStore { suspend fun getTheme(): Theme suspend fun getZoomFactor(): Int suspend fun saveZoomFactor(zoomFactor: Int) + suspend fun getDefaultFilter(): DefaultFilter + suspend fun saveDefaultFilter(defaultFilter: DefaultFilter) } diff --git a/app/src/main/java/de/readeckapp/io/prefs/SettingsDataStoreImpl.kt b/app/src/main/java/de/readeckapp/io/prefs/SettingsDataStoreImpl.kt index 1609ff5..12fc805 100644 --- a/app/src/main/java/de/readeckapp/io/prefs/SettingsDataStoreImpl.kt +++ b/app/src/main/java/de/readeckapp/io/prefs/SettingsDataStoreImpl.kt @@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import dagger.hilt.android.qualifiers.ApplicationContext import de.readeckapp.domain.model.AutoSyncTimeframe +import de.readeckapp.domain.model.DefaultFilter import de.readeckapp.domain.model.Theme import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -36,6 +37,7 @@ class SettingsDataStoreImpl @Inject constructor(@ApplicationContext private val private val KEY_ZOOM_FACTOR = intPreferencesKey("zoom_factor") private val KEY_SYNC_READ_PROGRESS = booleanPreferencesKey("sync_read_progress") private val KEY_SCROLL_TO_PROGRESS = booleanPreferencesKey("scroll_to_progress") + private val KEY_DEFAULT_FILTER = stringPreferencesKey("default_filter") override fun saveUsername(username: String) { Timber.d("saveUsername") @@ -164,6 +166,18 @@ class SettingsDataStoreImpl @Inject constructor(@ApplicationContext private val } } + override suspend fun getDefaultFilter(): DefaultFilter { + return encryptedSharedPreferences.getString(KEY_DEFAULT_FILTER.name, DefaultFilter.ALL.name)?.let { + DefaultFilter.valueOf(it) + } ?: DefaultFilter.ALL + } + + override suspend fun saveDefaultFilter(defaultFilter: DefaultFilter) { + encryptedSharedPreferences.edit { + putString(KEY_DEFAULT_FILTER.name, defaultFilter.name) + } + } + override val tokenFlow = getStringFlow(KEY_TOKEN.name, null) override val usernameFlow = getStringFlow(KEY_USERNAME.name, null) override val urlFlow = getStringFlow(KEY_URL.name, null) diff --git a/app/src/main/java/de/readeckapp/ui/list/BookmarkListScreen.kt b/app/src/main/java/de/readeckapp/ui/list/BookmarkListScreen.kt index 2336ab9..0c33043 100644 --- a/app/src/main/java/de/readeckapp/ui/list/BookmarkListScreen.kt +++ b/app/src/main/java/de/readeckapp/ui/list/BookmarkListScreen.kt @@ -332,7 +332,18 @@ fun BookmarkListScreen(navHostController: NavHostController) { snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( - title = { Text(stringResource(id = R.string.bookmarks)) }, + title = { + val titleRes = when { + filterState.value.unread == true -> R.string.unread + filterState.value.archived == true -> R.string.archive + filterState.value.favorite == true -> R.string.favorites + filterState.value.type == Bookmark.Type.Article -> R.string.articles + filterState.value.type == Bookmark.Type.Video -> R.string.videos + filterState.value.type == Bookmark.Type.Picture -> R.string.pictures + else -> R.string.bookmarks + } + Text(stringResource(id = titleRes)) + }, navigationIcon = { IconButton( onClick = { scope.launch { drawerState.open() } } diff --git a/app/src/main/java/de/readeckapp/ui/list/BookmarkListViewModel.kt b/app/src/main/java/de/readeckapp/ui/list/BookmarkListViewModel.kt index 6b7bcdc..3bf0ca2 100644 --- a/app/src/main/java/de/readeckapp/ui/list/BookmarkListViewModel.kt +++ b/app/src/main/java/de/readeckapp/ui/list/BookmarkListViewModel.kt @@ -14,6 +14,7 @@ import de.readeckapp.domain.BookmarkRepository import de.readeckapp.domain.model.Bookmark import de.readeckapp.domain.model.BookmarkCounts import de.readeckapp.domain.model.BookmarkListItem +import de.readeckapp.domain.model.DefaultFilter import de.readeckapp.domain.usecase.UpdateBookmarkUseCase import de.readeckapp.io.prefs.SettingsDataStore import de.readeckapp.util.extractUrlAndTitle @@ -102,6 +103,14 @@ class BookmarkListViewModel @Inject constructor( } viewModelScope.launch(loadBookmarkExceptionHandler) { + _filterState.value = settingsDataStore.getDefaultFilter().toFilterState() + + // Check if the initial sync has been performed + if (!settingsDataStore.isInitialSyncPerformed()) { + Timber.d("loadBookmarks") + loadBookmarks() // Start incremental sync when the ViewModel is created + } + filterState.collectLatest { filterState -> bookmarkRepository.observeBookmarkListItems( type = filterState.type, @@ -117,12 +126,6 @@ class BookmarkListViewModel @Inject constructor( } } } - - // Check if the initial sync has been performed - if (!settingsDataStore.isInitialSyncPerformed()) { - Timber.d("loadBookmarks") - loadBookmarks() // Start incremental sync when the ViewModel is created - } } } @@ -383,3 +386,10 @@ class BookmarkListViewModel @Inject constructor( data class Error(val message: String) : UpdateBookmarkState() } } + +private fun DefaultFilter.toFilterState(): BookmarkListViewModel.FilterState = when (this) { + DefaultFilter.ALL -> BookmarkListViewModel.FilterState() + DefaultFilter.UNREAD -> BookmarkListViewModel.FilterState(unread = true) + DefaultFilter.ARCHIVED -> BookmarkListViewModel.FilterState(archived = true) + DefaultFilter.FAVORITES -> BookmarkListViewModel.FilterState(favorite = true) +} diff --git a/app/src/main/java/de/readeckapp/ui/settings/DefaultFilterDialog.kt b/app/src/main/java/de/readeckapp/ui/settings/DefaultFilterDialog.kt new file mode 100644 index 0000000..2902f39 --- /dev/null +++ b/app/src/main/java/de/readeckapp/ui/settings/DefaultFilterDialog.kt @@ -0,0 +1,124 @@ +package de.readeckapp.ui.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import de.readeckapp.R +import de.readeckapp.domain.model.DefaultFilter +import de.readeckapp.ui.theme.Typography + +@Composable +fun DefaultFilterDialog( + defaultFilterOptions: List, + onDismissRequest: () -> Unit, + onElementSelected: (selected: DefaultFilter) -> Unit, +) { + Dialog( + onDismissRequest = onDismissRequest + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .sizeIn( + minWidth = 280.dp, + maxWidth = 560.dp + ), + shape = RoundedCornerShape(28.dp), + ) { + Column( + modifier = Modifier.padding(24.dp) + ) { + Text( + text = stringResource(R.string.ui_settings_default_filter_dialog_support_text), + style = Typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + HorizontalDivider() + defaultFilterOptions.forEach { option -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .selectable( + selected = option.selected, + onClick = { onElementSelected(option.filter) }, + role = Role.RadioButton + ), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = option.selected, + onClick = null + ) + Text( + text = stringResource(option.label), + modifier = Modifier.padding(start = 16.dp) + ) + } + } + HorizontalDivider() + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + ) { + TextButton( + modifier = Modifier.padding(top = 24.dp), + onClick = onDismissRequest + ) { + Text(text = stringResource(R.string.ok)) + } + } + } + } + } +} + +@Preview +@Composable +fun DefaultFilterDialogPreview() { + DefaultFilterDialog( + defaultFilterOptions = listOf( + DefaultFilterOption( + filter = DefaultFilter.ALL, + label = R.string.default_filter_all, + selected = false + ), + DefaultFilterOption( + filter = DefaultFilter.UNREAD, + label = R.string.default_filter_unread, + selected = true + ), + DefaultFilterOption( + filter = DefaultFilter.ARCHIVED, + label = R.string.default_filter_archived, + selected = false + ), + DefaultFilterOption( + filter = DefaultFilter.FAVORITES, + label = R.string.default_filter_favorites, + selected = false + ), + ), + onDismissRequest = {}, + onElementSelected = {} + ) +} diff --git a/app/src/main/java/de/readeckapp/ui/settings/UiSettingsScreen.kt b/app/src/main/java/de/readeckapp/ui/settings/UiSettingsScreen.kt index 95830f2..0932c85 100644 --- a/app/src/main/java/de/readeckapp/ui/settings/UiSettingsScreen.kt +++ b/app/src/main/java/de/readeckapp/ui/settings/UiSettingsScreen.kt @@ -32,6 +32,7 @@ import androidx.navigation.NavHostController import com.google.accompanist.permissions.ExperimentalPermissionsApi import de.readeckapp.R import de.readeckapp.domain.model.AutoSyncTimeframe +import de.readeckapp.domain.model.DefaultFilter import de.readeckapp.domain.model.Theme import de.readeckapp.ui.theme.Typography @@ -45,6 +46,7 @@ fun UiSettingsScreen( val navigationEvent = viewModel.navigationEvent.collectAsState() val onClickBack: () -> Unit = { viewModel.onClickBack() } val onClickTheme: () -> Unit = { viewModel.onClickTheme() } + val onClickDefaultFilter: () -> Unit = { viewModel.onClickDefaultFilter() } val onScrollToProgressToggle: (Boolean) -> Unit = { viewModel.onScrollToProgressToggle(it) } val snackbarHostState = remember { SnackbarHostState() } @@ -67,11 +69,20 @@ fun UiSettingsScreen( ) } + if (settingsUiState.showDefaultFilterDialog) { + DefaultFilterDialog( + defaultFilterOptions = settingsUiState.defaultFilterOptions, + onDismissRequest = { viewModel.onDismissDefaultFilterDialog() }, + onElementSelected = { viewModel.onDefaultFilterSelected(it) } + ) + } + UiSettingsView( modifier = Modifier, snackbarHostState = snackbarHostState, onClickBack = onClickBack, onClickTheme = onClickTheme, + onClickDefaultFilter = onClickDefaultFilter, onScrollToProgressToggle = onScrollToProgressToggle, settingsUiState = settingsUiState ) @@ -85,6 +96,7 @@ fun UiSettingsView( snackbarHostState: SnackbarHostState, settingsUiState: UiSettingsUiState, onClickTheme: () -> Unit, + onClickDefaultFilter: () -> Unit, onScrollToProgressToggle: (Boolean) -> Unit, onClickBack: () -> Unit, ) { @@ -129,6 +141,19 @@ fun UiSettingsView( ) } } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable(enabled = true, onClick = onClickDefaultFilter) + ) { + Column(modifier = Modifier.weight(1f)) { + Text(stringResource(R.string.ui_settings_default_filter_title)) + Text( + text = stringResource(settingsUiState.defaultFilterLabel), + style = Typography.bodySmall + ) + } + } Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -158,12 +183,17 @@ fun UiSettingsScreenViewPreview() { showDialog = false, themeLabel = Theme.SYSTEM.toLabelResource(), scrollToProgressEnabled = true, + defaultFilter = DefaultFilter.ALL, + defaultFilterLabel = DefaultFilter.ALL.toLabelResource(), + defaultFilterOptions = listOf(), + showDefaultFilterDialog = false, ) UiSettingsView( modifier = Modifier, snackbarHostState = SnackbarHostState(), onClickBack = {}, onClickTheme = {}, + onClickDefaultFilter = {}, settingsUiState = settingsUiState, onScrollToProgressToggle = {}, ) diff --git a/app/src/main/java/de/readeckapp/ui/settings/UiSettingsViewModel.kt b/app/src/main/java/de/readeckapp/ui/settings/UiSettingsViewModel.kt index 1fe619b..2d467ba 100644 --- a/app/src/main/java/de/readeckapp/ui/settings/UiSettingsViewModel.kt +++ b/app/src/main/java/de/readeckapp/ui/settings/UiSettingsViewModel.kt @@ -9,6 +9,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import de.readeckapp.R +import de.readeckapp.domain.model.DefaultFilter import de.readeckapp.domain.model.Theme import de.readeckapp.io.prefs.SettingsDataStore import kotlinx.coroutines.flow.MutableStateFlow @@ -33,22 +34,29 @@ class UiSettingsViewModel @Inject constructor( private val theme = MutableStateFlow(Theme.SYSTEM) private val scrollToProgressEnabled = MutableStateFlow(true) private val showDialog = MutableStateFlow(false) + private val defaultFilter = MutableStateFlow(DefaultFilter.ALL) + private val showDefaultFilterDialog = MutableStateFlow(false) init { viewModelScope.launch { theme.value = settingsDataStore.getTheme() scrollToProgressEnabled.value = settingsDataStore.isScrollToProgressEnabled() + defaultFilter.value = settingsDataStore.getDefaultFilter() } } - val uiState = combine(theme, scrollToProgressEnabled, showDialog) { theme, scrollToProgressEnabled, showDialog -> + val uiState = combine(theme, scrollToProgressEnabled, showDialog, defaultFilter, showDefaultFilterDialog) { theme, scrollToProgressEnabled, showDialog, defaultFilter, showDefaultFilterDialog -> UiSettingsUiState( theme = theme, scrollToProgressEnabled = scrollToProgressEnabled, themeOptions = getThemeOptionList(theme), showDialog = showDialog, themeLabel = theme.toLabelResource(), + defaultFilter = defaultFilter, + defaultFilterLabel = defaultFilter.toLabelResource(), + defaultFilterOptions = getDefaultFilterOptionList(defaultFilter), + showDefaultFilterDialog = showDefaultFilterDialog, ) } .stateIn( @@ -61,6 +69,10 @@ class UiSettingsViewModel @Inject constructor( themeOptions = getThemeOptionList(Theme.SYSTEM), showDialog = false, themeLabel = Theme.SYSTEM.toLabelResource(), + defaultFilter = DefaultFilter.ALL, + defaultFilterLabel = DefaultFilter.ALL.toLabelResource(), + defaultFilterOptions = getDefaultFilterOptionList(DefaultFilter.ALL), + showDefaultFilterDialog = false, ) ) @@ -92,6 +104,19 @@ class UiSettingsViewModel @Inject constructor( _navigationEvent.update { NavigationEvent.NavigateBack } } + fun onClickDefaultFilter() { + showDefaultFilterDialog.value = true + } + + fun onDismissDefaultFilterDialog() { + showDefaultFilterDialog.value = false + } + + fun onDefaultFilterSelected(selected: DefaultFilter) { + Timber.d("onDefaultFilterSelected [selected=$selected]") + updateDefaultFilter(selected) + } + sealed class NavigationEvent { data object NavigateBack : NavigationEvent() } @@ -106,12 +131,29 @@ class UiSettingsViewModel @Inject constructor( } } + private fun getDefaultFilterOptionList(selected: DefaultFilter): List { + return DefaultFilter.entries.map { + DefaultFilterOption( + filter = it, + label = it.toLabelResource(), + selected = it == selected + ) + } + } + private fun updateTheme(value: Theme) { viewModelScope.launch { settingsDataStore.saveTheme(value) theme.value = settingsDataStore.getTheme() } } + + private fun updateDefaultFilter(value: DefaultFilter) { + viewModelScope.launch { + settingsDataStore.saveDefaultFilter(value) + defaultFilter.value = settingsDataStore.getDefaultFilter() + } + } } @Immutable @@ -122,6 +164,11 @@ data class UiSettingsUiState( val showDialog: Boolean, @StringRes val themeLabel: Int, + val defaultFilter: DefaultFilter, + @StringRes + val defaultFilterLabel: Int, + val defaultFilterOptions: List, + val showDefaultFilterDialog: Boolean, ) data class ThemeOption( @@ -131,6 +178,13 @@ data class ThemeOption( val selected: Boolean ) +data class DefaultFilterOption( + val filter: DefaultFilter, + @StringRes + val label: Int, + val selected: Boolean +) + @StringRes fun Theme.toLabelResource(): Int { return when (this) { @@ -140,3 +194,13 @@ fun Theme.toLabelResource(): Int { Theme.SYSTEM -> R.string.theme_system } } + +@StringRes +fun DefaultFilter.toLabelResource(): Int { + return when (this) { + DefaultFilter.ALL -> R.string.default_filter_all + DefaultFilter.UNREAD -> R.string.default_filter_unread + DefaultFilter.ARCHIVED -> R.string.default_filter_archived + DefaultFilter.FAVORITES -> R.string.default_filter_favorites + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a24da8..a438e9c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,6 +67,12 @@ other{} Which theme should be used? Scroll to reading progress When opening a bookmark, automatically scroll to your last reading position. + Default view + Which view should be shown on startup? + All + Unread + Archive + Favorites Sync reading progress When leaving a bookmark, sync your reading progress to the server. diff --git a/app/src/test/java/de/readeckapp/ui/list/BookmarkListViewModelTest.kt b/app/src/test/java/de/readeckapp/ui/list/BookmarkListViewModelTest.kt index 60734fd..49f5e0e 100644 --- a/app/src/test/java/de/readeckapp/ui/list/BookmarkListViewModelTest.kt +++ b/app/src/test/java/de/readeckapp/ui/list/BookmarkListViewModelTest.kt @@ -9,6 +9,7 @@ import de.readeckapp.domain.BookmarkRepository import de.readeckapp.domain.model.Bookmark import de.readeckapp.domain.model.BookmarkCounts import de.readeckapp.domain.model.BookmarkListItem +import de.readeckapp.domain.model.DefaultFilter import de.readeckapp.domain.usecase.UpdateBookmarkUseCase import de.readeckapp.io.prefs.SettingsDataStore import io.mockk.coEvery @@ -64,6 +65,7 @@ class BookmarkListViewModelTest { // Default Mocking Behavior coEvery { settingsDataStore.isInitialSyncPerformed() } returns true // Assume sync is done + coEvery { settingsDataStore.getDefaultFilter() } returns DefaultFilter.ALL every { bookmarkRepository.observeBookmarkListItems(any(), any(), any(), any(), any()) } returns flowOf( emptyList() ) // No bookmarks initially From d2fcde9b8a05594738546cf4cb065971189433ec Mon Sep 17 00:00:00 2001 From: Ajay Prem Shankar Date: Sat, 2 May 2026 22:37:34 +0530 Subject: [PATCH 2/2] fixed review comments --- .../ui/settings/DefaultFilterDialog.kt | 124 ------------------ .../{ThemeDialog.kt => SingleChoiceDialog.kt} | 47 ++++--- .../ui/settings/UiSettingsScreen.kt | 10 +- .../ui/settings/UiSettingsViewModel.kt | 30 ++--- .../ui/list/BookmarkListViewModelTest.kt | 91 +++---------- 5 files changed, 57 insertions(+), 245 deletions(-) delete mode 100644 app/src/main/java/de/readeckapp/ui/settings/DefaultFilterDialog.kt rename app/src/main/java/de/readeckapp/ui/settings/{ThemeDialog.kt => SingleChoiceDialog.kt} (79%) diff --git a/app/src/main/java/de/readeckapp/ui/settings/DefaultFilterDialog.kt b/app/src/main/java/de/readeckapp/ui/settings/DefaultFilterDialog.kt deleted file mode 100644 index 2902f39..0000000 --- a/app/src/main/java/de/readeckapp/ui/settings/DefaultFilterDialog.kt +++ /dev/null @@ -1,124 +0,0 @@ -package de.readeckapp.ui.settings - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import de.readeckapp.R -import de.readeckapp.domain.model.DefaultFilter -import de.readeckapp.ui.theme.Typography - -@Composable -fun DefaultFilterDialog( - defaultFilterOptions: List, - onDismissRequest: () -> Unit, - onElementSelected: (selected: DefaultFilter) -> Unit, -) { - Dialog( - onDismissRequest = onDismissRequest - ) { - Card( - modifier = Modifier - .fillMaxWidth() - .sizeIn( - minWidth = 280.dp, - maxWidth = 560.dp - ), - shape = RoundedCornerShape(28.dp), - ) { - Column( - modifier = Modifier.padding(24.dp) - ) { - Text( - text = stringResource(R.string.ui_settings_default_filter_dialog_support_text), - style = Typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) - ) - HorizontalDivider() - defaultFilterOptions.forEach { option -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .selectable( - selected = option.selected, - onClick = { onElementSelected(option.filter) }, - role = Role.RadioButton - ), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = option.selected, - onClick = null - ) - Text( - text = stringResource(option.label), - modifier = Modifier.padding(start = 16.dp) - ) - } - } - HorizontalDivider() - Row( - horizontalArrangement = Arrangement.End, - modifier = Modifier.fillMaxWidth() - ) { - TextButton( - modifier = Modifier.padding(top = 24.dp), - onClick = onDismissRequest - ) { - Text(text = stringResource(R.string.ok)) - } - } - } - } - } -} - -@Preview -@Composable -fun DefaultFilterDialogPreview() { - DefaultFilterDialog( - defaultFilterOptions = listOf( - DefaultFilterOption( - filter = DefaultFilter.ALL, - label = R.string.default_filter_all, - selected = false - ), - DefaultFilterOption( - filter = DefaultFilter.UNREAD, - label = R.string.default_filter_unread, - selected = true - ), - DefaultFilterOption( - filter = DefaultFilter.ARCHIVED, - label = R.string.default_filter_archived, - selected = false - ), - DefaultFilterOption( - filter = DefaultFilter.FAVORITES, - label = R.string.default_filter_favorites, - selected = false - ), - ), - onDismissRequest = {}, - onElementSelected = {} - ) -} diff --git a/app/src/main/java/de/readeckapp/ui/settings/ThemeDialog.kt b/app/src/main/java/de/readeckapp/ui/settings/SingleChoiceDialog.kt similarity index 79% rename from app/src/main/java/de/readeckapp/ui/settings/ThemeDialog.kt rename to app/src/main/java/de/readeckapp/ui/settings/SingleChoiceDialog.kt index 3f02c04..0661763 100644 --- a/app/src/main/java/de/readeckapp/ui/settings/ThemeDialog.kt +++ b/app/src/main/java/de/readeckapp/ui/settings/SingleChoiceDialog.kt @@ -1,5 +1,6 @@ package de.readeckapp.ui.settings +import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -23,14 +24,21 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import de.readeckapp.R -import de.readeckapp.domain.model.Theme import de.readeckapp.ui.theme.Typography +data class SelectableOption( + val value: T, + @StringRes + val label: Int, + val selected: Boolean +) + @Composable -fun ThemeDialog( - themeOptions: List, +fun SingleChoiceDialog( + @StringRes supportText: Int, + options: List>, onDismissRequest: () -> Unit, - onElementSelected: (selected: Theme) -> Unit, + onElementSelected: (selected: T) -> Unit, ) { Dialog( onDismissRequest = onDismissRequest @@ -48,19 +56,19 @@ fun ThemeDialog( modifier = Modifier.padding(24.dp) ) { Text( - text = stringResource(R.string.ui_settings_theme_dialog_support_text), + text = stringResource(supportText), style = Typography.bodyMedium, modifier = Modifier.padding(bottom = 16.dp) ) HorizontalDivider() - themeOptions.forEach { option -> + options.forEach { option -> Row( modifier = Modifier .fillMaxWidth() .height(56.dp) .selectable( selected = option.selected, - onClick = { onElementSelected(option.theme) }, + onClick = { onElementSelected(option.value) }, role = Role.RadioButton ), verticalAlignment = Alignment.CenterVertically @@ -94,24 +102,13 @@ fun ThemeDialog( @Preview @Composable -fun ThemeDialogPreview() { - ThemeDialog( - themeOptions = listOf( - ThemeOption( - theme = Theme.SYSTEM, - label = R.string.theme_system, - selected = true - ), - ThemeOption( - theme = Theme.LIGHT, - label = R.string.theme_light, - selected = false - ), - ThemeOption( - theme = Theme.DARK, - label = R.string.theme_dark, - selected = false - ), +private fun SingleChoiceDialogPreview() { + SingleChoiceDialog( + supportText = R.string.ui_settings_theme_dialog_support_text, + options = listOf( + SelectableOption(value = "a", label = R.string.theme_system, selected = true), + SelectableOption(value = "b", label = R.string.theme_light, selected = false), + SelectableOption(value = "c", label = R.string.theme_dark, selected = false), ), onDismissRequest = {}, onElementSelected = {} diff --git a/app/src/main/java/de/readeckapp/ui/settings/UiSettingsScreen.kt b/app/src/main/java/de/readeckapp/ui/settings/UiSettingsScreen.kt index 0932c85..bed5bf3 100644 --- a/app/src/main/java/de/readeckapp/ui/settings/UiSettingsScreen.kt +++ b/app/src/main/java/de/readeckapp/ui/settings/UiSettingsScreen.kt @@ -62,16 +62,18 @@ fun UiSettingsScreen( } if (settingsUiState.showDialog) { - ThemeDialog( - themeOptions = settingsUiState.themeOptions, + SingleChoiceDialog( + supportText = R.string.ui_settings_theme_dialog_support_text, + options = settingsUiState.themeOptions, onDismissRequest = { viewModel.onDismissDialog() }, onElementSelected = { viewModel.onThemeSelected(it) } ) } if (settingsUiState.showDefaultFilterDialog) { - DefaultFilterDialog( - defaultFilterOptions = settingsUiState.defaultFilterOptions, + SingleChoiceDialog( + supportText = R.string.ui_settings_default_filter_dialog_support_text, + options = settingsUiState.defaultFilterOptions, onDismissRequest = { viewModel.onDismissDefaultFilterDialog() }, onElementSelected = { viewModel.onDefaultFilterSelected(it) } ) diff --git a/app/src/main/java/de/readeckapp/ui/settings/UiSettingsViewModel.kt b/app/src/main/java/de/readeckapp/ui/settings/UiSettingsViewModel.kt index 2d467ba..63e7672 100644 --- a/app/src/main/java/de/readeckapp/ui/settings/UiSettingsViewModel.kt +++ b/app/src/main/java/de/readeckapp/ui/settings/UiSettingsViewModel.kt @@ -121,20 +121,20 @@ class UiSettingsViewModel @Inject constructor( data object NavigateBack : NavigationEvent() } - private fun getThemeOptionList(selected: Theme): List { + private fun getThemeOptionList(selected: Theme): List> { return Theme.entries.map { - ThemeOption( - theme = it, + SelectableOption( + value = it, label = it.toLabelResource(), selected = it == selected ) } } - private fun getDefaultFilterOptionList(selected: DefaultFilter): List { + private fun getDefaultFilterOptionList(selected: DefaultFilter): List> { return DefaultFilter.entries.map { - DefaultFilterOption( - filter = it, + SelectableOption( + value = it, label = it.toLabelResource(), selected = it == selected ) @@ -160,31 +160,17 @@ class UiSettingsViewModel @Inject constructor( data class UiSettingsUiState( val theme: Theme, val scrollToProgressEnabled: Boolean, - val themeOptions: List, + val themeOptions: List>, val showDialog: Boolean, @StringRes val themeLabel: Int, val defaultFilter: DefaultFilter, @StringRes val defaultFilterLabel: Int, - val defaultFilterOptions: List, + val defaultFilterOptions: List>, val showDefaultFilterDialog: Boolean, ) -data class ThemeOption( - val theme: Theme, - @StringRes - val label: Int, - val selected: Boolean -) - -data class DefaultFilterOption( - val filter: DefaultFilter, - @StringRes - val label: Int, - val selected: Boolean -) - @StringRes fun Theme.toLabelResource(): Int { return when (this) { diff --git a/app/src/test/java/de/readeckapp/ui/list/BookmarkListViewModelTest.kt b/app/src/test/java/de/readeckapp/ui/list/BookmarkListViewModelTest.kt index 49f5e0e..22e093e 100644 --- a/app/src/test/java/de/readeckapp/ui/list/BookmarkListViewModelTest.kt +++ b/app/src/test/java/de/readeckapp/ui/list/BookmarkListViewModelTest.kt @@ -325,19 +325,15 @@ class BookmarkListViewModelTest { settingsDataStore, savedStateHandle ) + advanceUntilIdle() // let init apply default filter before user actions viewModel.onClickArticles() viewModel.onClickUnread() + advanceUntilIdle() - val uiStates = viewModel.uiState.take(2).toList() - val empty = uiStates[0] - val success = uiStates[1] - // Assert initial state - assert(empty is BookmarkListViewModel.UiState.Empty) - // Assert success state assertEquals( BookmarkListViewModel.UiState.Success(expectedBookmarks, null), - success + viewModel.uiState.value ) } @@ -580,18 +576,13 @@ class BookmarkListViewModelTest { savedStateHandle ) - val uiStates = viewModel.uiState.take(2).toList() - val emptyState = uiStates[0] - val successState = uiStates[1] - // Assert initial state - assert(emptyState is BookmarkListViewModel.UiState.Empty) - // Assert success state + advanceUntilIdle() assertEquals( BookmarkListViewModel.UiState.Success( bookmarks, null ), - successState + viewModel.uiState.value ) viewModel.onToggleFavoriteBookmark(bookmarkId, isFavorite) @@ -645,18 +636,13 @@ class BookmarkListViewModelTest { savedStateHandle ) - val uiStates = viewModel.uiState.take(2).toList() - val emptyState = uiStates[0] - val successState = uiStates[1] - // Assert initial state - assert(emptyState is BookmarkListViewModel.UiState.Empty) - // Assert success state + advanceUntilIdle() assertEquals( BookmarkListViewModel.UiState.Success( bookmarks, null ), - successState + viewModel.uiState.value ) viewModel.onToggleFavoriteBookmark(bookmarkId, isFavorite) @@ -710,18 +696,13 @@ class BookmarkListViewModelTest { savedStateHandle ) - val uiStates = viewModel.uiState.take(2).toList() - val emptyState = uiStates[0] - val successState = uiStates[1] - // Assert initial state - assert(emptyState is BookmarkListViewModel.UiState.Empty) - // Assert success state + advanceUntilIdle() assertEquals( BookmarkListViewModel.UiState.Success( bookmarks, null ), - successState + viewModel.uiState.value ) viewModel.onToggleFavoriteBookmark(bookmarkId, isFavorite) @@ -774,18 +755,13 @@ class BookmarkListViewModelTest { savedStateHandle ) - val uiStates = viewModel.uiState.take(2).toList() - val emptyState = uiStates[0] - val successState = uiStates[1] - // Assert initial state - assert(emptyState is BookmarkListViewModel.UiState.Empty) - // Assert success state + advanceUntilIdle() assertEquals( BookmarkListViewModel.UiState.Success( bookmarks, null ), - successState + viewModel.uiState.value ) viewModel.onToggleArchiveBookmark(bookmarkId, isArchived) @@ -839,18 +815,13 @@ class BookmarkListViewModelTest { savedStateHandle ) - val uiStates = viewModel.uiState.take(2).toList() - val emptyState = uiStates[0] - val successState = uiStates[1] - // Assert initial state - assert(emptyState is BookmarkListViewModel.UiState.Empty) - // Assert success state + advanceUntilIdle() assertEquals( BookmarkListViewModel.UiState.Success( bookmarks, null ), - successState + viewModel.uiState.value ) viewModel.onToggleArchiveBookmark(bookmarkId, isArchived) @@ -904,18 +875,13 @@ class BookmarkListViewModelTest { savedStateHandle ) - val uiStates = viewModel.uiState.take(2).toList() - val emptyState = uiStates[0] - val successState = uiStates[1] - // Assert initial state - assert(emptyState is BookmarkListViewModel.UiState.Empty) - // Assert success state + advanceUntilIdle() assertEquals( BookmarkListViewModel.UiState.Success( bookmarks, null ), - successState + viewModel.uiState.value ) viewModel.onToggleArchiveBookmark(bookmarkId, isArchived) @@ -969,18 +935,13 @@ class BookmarkListViewModelTest { savedStateHandle ) - val uiStates = viewModel.uiState.take(2).toList() - val emptyState = uiStates[0] - val successState = uiStates[1] - // Assert initial state - assert(emptyState is BookmarkListViewModel.UiState.Empty) - // Assert success state + advanceUntilIdle() assertEquals( BookmarkListViewModel.UiState.Success( bookmarks, null ), - successState + viewModel.uiState.value ) viewModel.onToggleMarkReadBookmark(bookmarkId, isRead) @@ -1034,18 +995,13 @@ class BookmarkListViewModelTest { savedStateHandle ) - val uiStates = viewModel.uiState.take(2).toList() - val emptyState = uiStates[0] - val successState = uiStates[1] - // Assert initial state - assert(emptyState is BookmarkListViewModel.UiState.Empty) - // Assert success state + advanceUntilIdle() assertEquals( BookmarkListViewModel.UiState.Success( bookmarks, null ), - successState + viewModel.uiState.value ) viewModel.onToggleMarkReadBookmark(bookmarkId, isRead) @@ -1099,18 +1055,13 @@ class BookmarkListViewModelTest { savedStateHandle ) - val uiStates = viewModel.uiState.take(2).toList() - val emptyState = uiStates[0] - val successState = uiStates[1] - // Assert initial state - assert(emptyState is BookmarkListViewModel.UiState.Empty) - // Assert success state + advanceUntilIdle() assertEquals( BookmarkListViewModel.UiState.Success( bookmarks, null ), - successState + viewModel.uiState.value ) viewModel.onToggleMarkReadBookmark(bookmarkId, isRead)