diff --git a/app/src/main/java/pub/hackers/android/data/local/PreferencesManager.kt b/app/src/main/java/pub/hackers/android/data/local/PreferencesManager.kt index 14254caa..8804b5c5 100644 --- a/app/src/main/java/pub/hackers/android/data/local/PreferencesManager.kt +++ b/app/src/main/java/pub/hackers/android/data/local/PreferencesManager.kt @@ -29,6 +29,7 @@ class PreferencesManager @Inject constructor( private val FONT_SIZE_MULTIPLIER = intPreferencesKey("font_size_multiplier") // stored as percentage (100 = 1.0x) private val RECENT_SEARCHES = stringPreferencesKey("recent_searches") private val THEME_MODE = stringPreferencesKey("theme_mode") + private val SHOW_LANGUAGE_FILTER = booleanPreferencesKey("show_language_filter") private const val MAX_RECENT_SEARCHES = 10 } @@ -74,6 +75,16 @@ class PreferencesManager @Inject constructor( } } + val showLanguageFilter: Flow = context.preferencesDataStore.data.map { prefs -> + prefs[SHOW_LANGUAGE_FILTER] ?: false + } + + suspend fun setShowLanguageFilter(value: Boolean) { + context.preferencesDataStore.edit { prefs -> + prefs[SHOW_LANGUAGE_FILTER] = value + } + } + suspend fun setConfirmBeforeDelete(value: Boolean) { context.preferencesDataStore.edit { prefs -> prefs[CONFIRM_BEFORE_DELETE] = value diff --git a/app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt b/app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt index c46bb95a..8ba4cff6 100644 --- a/app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt +++ b/app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt @@ -182,6 +182,7 @@ fun HackersPubApp( val isLoggedInState by viewModel.isLoggedIn.collectAsState(initial = null as Boolean?) val fontSizePercent by viewModel.preferencesManager.fontSizePercent.collectAsState(initial = 100) + val showLanguageFilter by viewModel.preferencesManager.showLanguageFilter.collectAsState(initial = false) val hasUnread by viewModel.hasUnread.collectAsState() var selectedHomeFeed by remember { mutableStateOf(HomeFeed.TIMELINE) } @@ -364,6 +365,7 @@ fun HackersPubApp( .collectAsState() TimelineScreen( tabRetapped = tabRetapped, + showLanguageFilter = showLanguageFilter, selectedHomeFeed = selectedHomeFeed, onHomeFeedSelected = ::navigateHomeFeed, onPostClick = { postId -> @@ -419,6 +421,7 @@ fun HackersPubApp( composable(Screen.Explore.route) { ExploreScreen( + showLanguageFilter = showLanguageFilter, onPostClick = { postId -> navController.navigate(DetailScreen.PostDetail.createRoute(postId)) }, diff --git a/app/src/main/java/pub/hackers/android/ui/screens/explore/ExploreScreen.kt b/app/src/main/java/pub/hackers/android/ui/screens/explore/ExploreScreen.kt index 00f10692..bf9d5abc 100644 --- a/app/src/main/java/pub/hackers/android/ui/screens/explore/ExploreScreen.kt +++ b/app/src/main/java/pub/hackers/android/ui/screens/explore/ExploreScreen.kt @@ -59,6 +59,7 @@ fun ExploreScreen( onEditClick: (String) -> Unit = {}, onSignInClick: () -> Unit, isLoggedIn: Boolean, + showLanguageFilter: Boolean = false, viewModel: ExploreViewModel = hiltViewModel() ) { val items = viewModel.posts.collectAsLazyPagingItems() @@ -72,6 +73,11 @@ fun ExploreScreen( val colors = LocalAppColors.current val typography = LocalAppTypography.current + // A language selected before the filter row was hidden would otherwise keep filtering invisibly. + LaunchedEffect(showLanguageFilter) { + if (!showLanguageFilter) viewModel.selectLanguage(null) + } + // Scroll to top when tab changes. LaunchedEffect(selectedTab) { listState.scrollToItem(0) @@ -162,13 +168,15 @@ fun ExploreScreen( modifier = Modifier.padding(horizontal = 16.dp) ) - LanguageFilterRow( - languages = uiState.suggestedFilterLanguages, - selectedLanguage = uiState.selectedLanguage, - onLanguageSelected = viewModel::selectLanguage, - ) + if (showLanguageFilter) { + LanguageFilterRow( + languages = uiState.suggestedFilterLanguages, + selectedLanguage = uiState.selectedLanguage, + onLanguageSelected = viewModel::selectLanguage, + ) - HorizontalDivider(color = colors.divider) + HorizontalDivider(color = colors.divider) + } Box(modifier = Modifier.fillMaxSize()) { val refresh = items.loadState.refresh diff --git a/app/src/main/java/pub/hackers/android/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/pub/hackers/android/ui/screens/settings/SettingsScreen.kt index ed1067d6..9861b5df 100644 --- a/app/src/main/java/pub/hackers/android/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/pub/hackers/android/ui/screens/settings/SettingsScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Fingerprint import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Login import androidx.compose.material.icons.filled.Palette import androidx.compose.material3.AlertDialog @@ -37,6 +38,7 @@ import androidx.compose.material3.RadioButtonDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -390,6 +392,39 @@ fun SettingsScreen( HorizontalDivider(color = colors.divider, thickness = 1.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { viewModel.setShowLanguageFilter(!uiState.showLanguageFilter) } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Language, + contentDescription = null, + tint = colors.textSecondary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_show_language_filter), + style = typography.bodyLarge, + color = colors.textPrimary + ) + Text( + text = stringResource(R.string.settings_show_language_filter_description), + style = typography.bodyMedium, + color = colors.textSecondary + ) + } + Switch( + checked = uiState.showLanguageFilter, + onCheckedChange = viewModel::setShowLanguageFilter + ) + } + + HorizontalDivider(color = colors.divider, thickness = 1.dp) + Row( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/pub/hackers/android/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/pub/hackers/android/ui/screens/settings/SettingsViewModel.kt index d6b93aa7..93ac4725 100644 --- a/app/src/main/java/pub/hackers/android/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/pub/hackers/android/ui/screens/settings/SettingsViewModel.kt @@ -45,6 +45,7 @@ data class SettingsUiState( val useInAppBrowser: Boolean = true, val fontSizePercent: Int = 100, val themeMode: ThemeMode = ThemeMode.SYSTEM, + val showLanguageFilter: Boolean = false, val passkeys: List = emptyList(), val accountId: String? = null, val isLoadingPasskeys: Boolean = false, @@ -112,6 +113,7 @@ class SettingsViewModel @Inject constructor( val inAppBrowser = preferencesManager.useInAppBrowser.first() val fontSize = preferencesManager.fontSizePercent.first() val theme = preferencesManager.themeMode.first() + val showLanguageFilter = preferencesManager.showLanguageFilter.first() _uiState.update { it.copy( @@ -120,7 +122,8 @@ class SettingsViewModel @Inject constructor( timelineMaxLength = maxLength, useInAppBrowser = inAppBrowser, fontSizePercent = fontSize, - themeMode = theme + themeMode = theme, + showLanguageFilter = showLanguageFilter ) } } @@ -156,6 +159,11 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { preferencesManager.setThemeMode(value) } } + fun setShowLanguageFilter(value: Boolean) { + _uiState.update { it.copy(showLanguageFilter = value) } + viewModelScope.launch { preferencesManager.setShowLanguageFilter(value) } + } + fun signOut() { viewModelScope.launch { fcmTokenManager.unregisterCurrentToken() diff --git a/app/src/main/java/pub/hackers/android/ui/screens/timeline/TimelineScreen.kt b/app/src/main/java/pub/hackers/android/ui/screens/timeline/TimelineScreen.kt index cafb6b89..1008013d 100644 --- a/app/src/main/java/pub/hackers/android/ui/screens/timeline/TimelineScreen.kt +++ b/app/src/main/java/pub/hackers/android/ui/screens/timeline/TimelineScreen.kt @@ -73,6 +73,7 @@ fun TimelineScreen( selectedHomeFeed: HomeFeed = HomeFeed.TIMELINE, onHomeFeedSelected: (HomeFeed) -> Unit = {}, tabRetapped: Long = 0L, + showLanguageFilter: Boolean = false, userAvatarUrl: String? = null, viewModel: TimelineViewModel = hiltViewModel() ) { @@ -85,6 +86,11 @@ fun TimelineScreen( val bookmarkRemovedMessage = stringResource(R.string.bookmark_removed) val colors = LocalAppColors.current + // A language selected before the filter row was hidden would otherwise keep filtering invisibly. + LaunchedEffect(showLanguageFilter) { + if (!showLanguageFilter) viewModel.selectLanguage(null) + } + // Refresh draft count when screen becomes visible (e.g., returning from Drafts) LaunchedEffect(Unit) { viewModel.loadDraftCount() @@ -233,12 +239,14 @@ fun TimelineScreen( .fillMaxSize() .padding(paddingValues) ) { - LanguageFilterRow( - languages = uiState.suggestedFilterLanguages, - selectedLanguage = uiState.selectedLanguage, - onLanguageSelected = viewModel::selectLanguage, - ) - HorizontalDivider(color = colors.divider) + if (showLanguageFilter) { + LanguageFilterRow( + languages = uiState.suggestedFilterLanguages, + selectedLanguage = uiState.selectedLanguage, + onLanguageSelected = viewModel::selectLanguage, + ) + HorizontalDivider(color = colors.divider) + } Box(modifier = Modifier.weight(1f)) { val refresh = items.loadState.refresh diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c978daec..1f9112f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -208,6 +208,8 @@ Post preview length Unlimited Theme + Show language filter + Show language chips above the Home and Explore feeds Follow system Light Dark