From 95d0c9ebf872470cb535130b3a165a132917c258 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:27:56 +0530 Subject: [PATCH 1/6] feat: add offline network indicator and popup on download screen (#651) --- app/src/main/java/be/scri/App.kt | 37 +++++++++++++++++-- .../java/be/scri/helpers/NetworkMonitor.kt | 29 +++++++++++++++ .../ConjugateDataDownloadViewModel.kt | 35 ++++++++++++++++-- .../screens/download/DataDownloadViewModel.kt | 35 ++++++++++++++++-- 4 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/be/scri/helpers/NetworkMonitor.kt diff --git a/app/src/main/java/be/scri/App.kt b/app/src/main/java/be/scri/App.kt index 7b5430be..3e6bce35 100644 --- a/app/src/main/java/be/scri/App.kt +++ b/app/src/main/java/be/scri/App.kt @@ -133,12 +133,18 @@ fun ScribeApp( cancelCheckForNewData = conjugateDownloadViewModel::cancelCheckForNewData, ) + val downloadToastMessage by downloadViewModel.toastMessage.collectAsState() + val conjugateToastMessage by conjugateDownloadViewModel.toastMessage.collectAsState() + val activeToastMessage = downloadToastMessage ?: conjugateToastMessage + val isOfflineErrorVisible = activeToastMessage != null + val screens = remember(context) { BottomBarScreen.getScreens() } ScribeTheme( useDarkTheme = isDarkTheme, isIncreaseTextSize = isIncreaseTextSize, ) { - Scaffold( + Box(modifier = Modifier.fillMaxSize()) { + Scaffold( bottomBar = { ScribeBottomBar( onItemClick = { @@ -425,10 +431,33 @@ fun ScribeApp( modifier = Modifier.padding(innerPadding), ) } + } // Ends NavHost + } // Ends Scaffold content + + androidx.compose.animation.AnimatedVisibility( + visible = isOfflineErrorVisible, + enter = androidx.compose.animation.slideInVertically(initialOffsetY = { it }) + androidx.compose.animation.fadeIn(), + exit = androidx.compose.animation.slideOutVertically(targetOffsetY = { it }) + androidx.compose.animation.fadeOut(), + modifier = Modifier + .align(androidx.compose.ui.Alignment.BottomCenter) + .fillMaxWidth() + .padding(bottom = 120.dp, start = 16.dp, end = 16.dp) + ) { + androidx.compose.material3.Surface( + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + color = androidx.compose.ui.graphics.Color.White, + shadowElevation = 8.dp + ) { + androidx.compose.material3.Text( + text = activeToastMessage ?: "", + color = androidx.compose.ui.graphics.Color(0xFFE68A00), + modifier = Modifier.padding(16.dp) + ) + } } - } - } -} + } // Ends Box + } // Ends ScribeTheme +} // Ends App /** * Handles the back press behavior within the pager. diff --git a/app/src/main/java/be/scri/helpers/NetworkMonitor.kt b/app/src/main/java/be/scri/helpers/NetworkMonitor.kt new file mode 100644 index 00000000..606c2de7 --- /dev/null +++ b/app/src/main/java/be/scri/helpers/NetworkMonitor.kt @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package be.scri.helpers + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities + +/** + * Utility singleton to check network connectivity status. + */ +object NetworkMonitor { + + /** + * Checks if the device is currently connected to the internet. + * + * @param context The application context. + * @return true if connected via WiFi, Cellular, or Ethernet, false otherwise. + */ + fun isOnline(context: Context): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + ?: return false + + val activeNetwork = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false + + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } +} diff --git a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt index 637e3d43..f463d0e9 100644 --- a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt @@ -14,6 +14,7 @@ import be.scri.R import be.scri.data.remote.ConjugateDynamicDbHelper import be.scri.data.remote.RetrofitClient import be.scri.helpers.LanguageMappingConstants +import be.scri.helpers.NetworkMonitor import be.scri.helpers.StringUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -39,6 +40,9 @@ class ConjugateDataDownloadViewModel( private val _checkUpdateState = MutableStateFlow(CheckUpdateState.Idle) val checkUpdateState = _checkUpdateState.asStateFlow() + private val _toastMessage = MutableStateFlow(null) + val toastMessage = _toastMessage.asStateFlow() + private var checkUpdateJob: Job? = null /** @@ -98,6 +102,11 @@ class ConjugateDataDownloadViewModel( key: String, forceDownload: Boolean = false, ) { + if (!NetworkMonitor.isOnline(getApplication())) { + showToast("No internet connection. Please connect and try again.") + return + } + val currentState = downloadStates[key] ?: DownloadState.Ready val displayLang = key.replaceFirstChar { it.uppercase() } if (forceDownload) { @@ -113,7 +122,7 @@ class ConjugateDataDownloadViewModel( R.string.i18n_app_download_menu_ui_conjugate_data_already_up_to_date, ) val msg = StringUtils.formatStringWithParams(template, displayLang) - Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() + showToast(msg) return } } @@ -156,7 +165,7 @@ class ConjugateDataDownloadViewModel( R.string.i18n_app_download_menu_ui_conjugate_data_download_success, ) val msg = StringUtils.formatStringWithParams(template, displayLang) - Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() + showToast(msg) } } else { // Already up to date: Skip the DB work. @@ -166,7 +175,7 @@ class ConjugateDataDownloadViewModel( getApplication().getString( R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date, ) - Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() + showToast(msg) } } } catch (e: IOException) { @@ -209,6 +218,11 @@ class ConjugateDataDownloadViewModel( * Handles the "All languages" download action by initiating downloads for all languages that are not already completed or downloading. */ fun handleDownloadAllLanguages() { + if (!NetworkMonitor.isOnline(getApplication())) { + showToast("No internet connection. Please connect and try again.") + return + } + val toDownload = downloadStates.keys.filter { key -> downloadStates[key] != DownloadState.Completed && downloadStates[key] != DownloadState.Downloading @@ -272,6 +286,11 @@ class ConjugateDataDownloadViewModel( * Checks for new data updates for all completed languages. */ fun checkForNewData() { + if (!NetworkMonitor.isOnline(getApplication())) { + showToast("No internet connection. Please connect and try again.") + return + } + checkUpdateJob?.cancel() val keysToCheck = downloadStates.keys.filter { downloadStates[it] == DownloadState.Completed } @@ -314,7 +333,7 @@ class ConjugateDataDownloadViewModel( withContext(Dispatchers.Main) { // Reset status so user can retry. downloadStates[key] = DownloadState.Ready - Toast.makeText(getApplication(), message, Toast.LENGTH_LONG).show() + showToast(message) } } @@ -326,4 +345,12 @@ class ConjugateDataDownloadViewModel( downloadJobs.values.forEach { it.cancel() } downloadJobs.clear() } + + private fun showToast(msg: String) { + viewModelScope.launch { + _toastMessage.value = msg + kotlinx.coroutines.delay(3000) + _toastMessage.value = null + } + } } diff --git a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt index 36a057e8..1c68b705 100644 --- a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt @@ -14,6 +14,7 @@ import be.scri.R import be.scri.data.remote.DynamicDbHelper import be.scri.data.remote.RetrofitClient import be.scri.helpers.LanguageMappingConstants +import be.scri.helpers.NetworkMonitor import be.scri.helpers.StringUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -39,6 +40,9 @@ class DataDownloadViewModel( private val _checkUpdateState = MutableStateFlow(CheckUpdateState.Idle) val checkUpdateState = _checkUpdateState.asStateFlow() + private val _toastMessage = MutableStateFlow(null) + val toastMessage = _toastMessage.asStateFlow() + private var checkUpdateJob: Job? = null /** @@ -98,6 +102,11 @@ class DataDownloadViewModel( key: String, forceDownload: Boolean = false, ) { + if (!NetworkMonitor.isOnline(getApplication())) { + showToast("No internet connection. Please connect and try again.") + return + } + val currentState = downloadStates[key] ?: DownloadState.Ready val displayLang = key.replaceFirstChar { it.uppercase() } if (forceDownload) { @@ -113,7 +122,7 @@ class DataDownloadViewModel( R.string.i18n_app_download_menu_ui_download_data_already_up_to_date, ) val msg = StringUtils.formatStringWithParams(template, displayLang) - Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() + showToast(msg) return } } @@ -156,7 +165,7 @@ class DataDownloadViewModel( R.string.i18n_app_download_menu_ui_download_data_download_success, ) val msg = StringUtils.formatStringWithParams(template, displayLang) - Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() + showToast(msg) } } else { // Already up to date: Skip the DB work. @@ -166,7 +175,7 @@ class DataDownloadViewModel( getApplication().getString( R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date, ) - Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() + showToast(msg) } } } catch (e: IOException) { @@ -209,6 +218,11 @@ class DataDownloadViewModel( * Handles the "All languages" download action by initiating downloads for all languages that are not already completed or downloading. */ fun handleDownloadAllLanguages() { + if (!NetworkMonitor.isOnline(getApplication())) { + showToast("No internet connection. Please connect and try again.") + return + } + val toDownload = downloadStates.keys.filter { key -> downloadStates[key] != DownloadState.Completed && downloadStates[key] != DownloadState.Downloading @@ -272,6 +286,11 @@ class DataDownloadViewModel( * Checks for new data updates for all completed languages. */ fun checkForNewData() { + if (!NetworkMonitor.isOnline(getApplication())) { + showToast("No internet connection. Please connect and try again.") + return + } + checkUpdateJob?.cancel() val keysToCheck = downloadStates.keys.filter { downloadStates[it] == DownloadState.Completed } @@ -314,7 +333,7 @@ class DataDownloadViewModel( withContext(Dispatchers.Main) { // Reset status so user can retry. downloadStates[key] = DownloadState.Ready - Toast.makeText(getApplication(), message, Toast.LENGTH_LONG).show() + showToast(message) } } @@ -326,6 +345,14 @@ class DataDownloadViewModel( downloadJobs.values.forEach { it.cancel() } downloadJobs.clear() } + + private fun showToast(msg: String) { + viewModelScope.launch { + _toastMessage.value = msg + kotlinx.coroutines.delay(3000) + _toastMessage.value = null + } + } } /** From db52fcf40f68254c3d0a738c0d33b55f4257f83d Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:04:51 +0530 Subject: [PATCH 2/6] style: autoformat kotlin code to fix ktlint CI checks --- app/src/main/java/be/scri/App.kt | 547 +++++++++--------- .../java/be/scri/helpers/NetworkMonitor.kt | 12 +- .../ConjugateDataDownloadViewModel.kt | 1 - .../screens/download/DataDownloadViewModel.kt | 1 - 4 files changed, 282 insertions(+), 279 deletions(-) diff --git a/app/src/main/java/be/scri/App.kt b/app/src/main/java/be/scri/App.kt index 3e6bce35..a7acc758 100644 --- a/app/src/main/java/be/scri/App.kt +++ b/app/src/main/java/be/scri/App.kt @@ -145,317 +145,322 @@ fun ScribeApp( ) { Box(modifier = Modifier.fillMaxSize()) { Scaffold( - bottomBar = { - ScribeBottomBar( - onItemClick = { - coroutineScope.launch { - if (navBackStackEntry?.destination?.route != "pager") { - navController.popBackStack() + bottomBar = { + ScribeBottomBar( + onItemClick = { + coroutineScope.launch { + if (navBackStackEntry?.destination?.route != "pager") { + navController.popBackStack() + } + pagerState.animateScrollToPage(it) } - pagerState.animateScrollToPage(it) - } - }, - pagerState = pagerState, - modifier = Modifier, - screens = screens, - ) - }, - modifier = modifier.fillMaxSize(), - ) { innerPadding -> - NavHost( - navController = navController, - startDestination = "pager", - ) { - composable("pager") { - HorizontalPager( - state = pagerState, - beyondViewportPageCount = screens.size, - modifier = Modifier.padding(innerPadding), - ) { page -> - when (screens[page]) { - is BottomBarScreen.Installation -> { - Box( - modifier = Modifier.fillMaxSize(), - ) { - InstallationScreen( - isDark = isDarkTheme, - context = context, - onNavigateToDownloadData = { - navController.navigate("download_data") - }, - onTutorialClick = { - navController.navigate("tutorial") + }, + pagerState = pagerState, + modifier = Modifier, + screens = screens, + ) + }, + modifier = modifier.fillMaxSize(), + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = "pager", + ) { + composable("pager") { + HorizontalPager( + state = pagerState, + beyondViewportPageCount = screens.size, + modifier = Modifier.padding(innerPadding), + ) { page -> + when (screens[page]) { + is BottomBarScreen.Installation -> { + Box( + modifier = Modifier.fillMaxSize(), + ) { + InstallationScreen( + isDark = isDarkTheme, + context = context, + onNavigateToDownloadData = { + navController.navigate("download_data") + }, + onTutorialClick = { + navController.navigate("tutorial") - coroutineScope.launch { - kotlinx.coroutines.delay(400) - val aboutIndex = screens.indexOfFirst { it is BottomBarScreen.About } - if (aboutIndex != -1) { - pagerState.scrollToPage(aboutIndex) + coroutineScope.launch { + kotlinx.coroutines.delay(400) + val aboutIndex = screens.indexOfFirst { it is BottomBarScreen.About } + if (aboutIndex != -1) { + pagerState.scrollToPage(aboutIndex) + } } - } - }, - ) - HintDialog( - pagerState = pagerState, - currentPageIndex = page, - sharedPrefsKey = "hint_shown_main", - hintMessageResId = R.string.i18n_app_installation_app_hint_tooltip, - isHintChanged = isHintChanged[page] == true, - onDismiss = { onDismiss(it) }, - modifier = - Modifier - .fillMaxWidth() - .padding(8.dp), - ) + }, + ) + HintDialog( + pagerState = pagerState, + currentPageIndex = page, + sharedPrefsKey = "hint_shown_main", + hintMessageResId = R.string.i18n_app_installation_app_hint_tooltip, + isHintChanged = isHintChanged[page] == true, + onDismiss = { onDismiss(it) }, + modifier = + Modifier + .fillMaxWidth() + .padding(8.dp), + ) + } + HandleBackPress(pagerState, coroutineScope) } - HandleBackPress(pagerState, coroutineScope) - } - is BottomBarScreen.Conjugate -> { - Box( - modifier = Modifier.fillMaxSize(), - ) { - ConjugateScreen( - onNavigateToDownloadData = { - navController.navigate("conjugate_download_data") - }, - onNavigateToConjugationSelection = { verb, languageAlias -> - navController.navigate( - "${Screen.ConjugationSelection.route}/$verb/$languageAlias", - ) - }, - ) - HintDialog( - pagerState = pagerState, - currentPageIndex = page, - sharedPrefsKey = "hint_shown_conjugate", - hintMessageResId = R.string.i18n_app_conjugate_app_hint_tooltip, - isHintChanged = isHintChanged[page] == true, - onDismiss = { onDismiss(it) }, - modifier = - Modifier - .fillMaxWidth() - .padding(8.dp), - ) + is BottomBarScreen.Conjugate -> { + Box( + modifier = Modifier.fillMaxSize(), + ) { + ConjugateScreen( + onNavigateToDownloadData = { + navController.navigate("conjugate_download_data") + }, + onNavigateToConjugationSelection = { verb, languageAlias -> + navController.navigate( + "${Screen.ConjugationSelection.route}/$verb/$languageAlias", + ) + }, + ) + HintDialog( + pagerState = pagerState, + currentPageIndex = page, + sharedPrefsKey = "hint_shown_conjugate", + hintMessageResId = R.string.i18n_app_conjugate_app_hint_tooltip, + isHintChanged = isHintChanged[page] == true, + onDismiss = { onDismiss(it) }, + modifier = + Modifier + .fillMaxWidth() + .padding(8.dp), + ) + } + HandleBackPress(pagerState, coroutineScope) } - HandleBackPress(pagerState, coroutineScope) - } - is BottomBarScreen.Settings -> { - Box( - modifier = Modifier.fillMaxSize(), - ) { - SettingsScreen( - onDarkModeChange = { isDarkMode -> - onDarkModeChange(isDarkMode) - }, - onIncreaseTextSizeChange = { increaseTextSize -> - onIncreaseTextSizeChange(increaseTextSize) - }, - onLanguageSettingsClick = { language -> - navController.navigate( - "${Screen.LanguageSettings.route}/$language", - ) - }, - context = context, - ) - HintDialog( - pagerState = pagerState, - currentPageIndex = page, - sharedPrefsKey = "hint_shown_settings", - hintMessageResId = - if (FlavorProvider.get() == AppFlavor.CONJUGATE) { - R.string.i18n_app_settings_conjugate_app_hint_tooltip - } else { - R.string.i18n_app_settings_keyboard_app_hint_tooltip + is BottomBarScreen.Settings -> { + Box( + modifier = Modifier.fillMaxSize(), + ) { + SettingsScreen( + onDarkModeChange = { isDarkMode -> + onDarkModeChange(isDarkMode) }, - isHintChanged = isHintChanged[page] == true, - onDismiss = { onDismiss(it) }, - modifier = Modifier.padding(8.dp), - ) + onIncreaseTextSizeChange = { increaseTextSize -> + onIncreaseTextSizeChange(increaseTextSize) + }, + onLanguageSettingsClick = { language -> + navController.navigate( + "${Screen.LanguageSettings.route}/$language", + ) + }, + context = context, + ) + HintDialog( + pagerState = pagerState, + currentPageIndex = page, + sharedPrefsKey = "hint_shown_settings", + hintMessageResId = + if (FlavorProvider.get() == AppFlavor.CONJUGATE) { + R.string.i18n_app_settings_conjugate_app_hint_tooltip + } else { + R.string.i18n_app_settings_keyboard_app_hint_tooltip + }, + isHintChanged = isHintChanged[page] == true, + onDismiss = { onDismiss(it) }, + modifier = Modifier.padding(8.dp), + ) + } + HandleBackPress(pagerState, coroutineScope) } - HandleBackPress(pagerState, coroutineScope) - } - is BottomBarScreen.About -> { - Box( - modifier = Modifier.fillMaxSize(), - ) { - AboutScreen( - onPrivacyPolicyClick = { - navController.navigate(Screen.PrivacyPolicy.route) - }, - onThirdPartyLicensesClick = { - navController.navigate(Screen.ThirdParty.route) - }, - onWikiClick = { - navController.navigate(Screen.WikimediaScribe.route) - }, - onTutorialClick = { - navController.navigate("tutorial") - }, - resetHints = { resetHints() }, - context = context, - ) - HintDialog( - pagerState = pagerState, - currentPageIndex = page, - sharedPrefsKey = "hint_shown_about", - hintMessageResId = R.string.i18n_app_about_app_hint_tooltip, - isHintChanged = isHintChanged[page] == true, - onDismiss = { onDismiss(it) }, - modifier = Modifier.padding(8.dp), - ) + is BottomBarScreen.About -> { + Box( + modifier = Modifier.fillMaxSize(), + ) { + AboutScreen( + onPrivacyPolicyClick = { + navController.navigate(Screen.PrivacyPolicy.route) + }, + onThirdPartyLicensesClick = { + navController.navigate(Screen.ThirdParty.route) + }, + onWikiClick = { + navController.navigate(Screen.WikimediaScribe.route) + }, + onTutorialClick = { + navController.navigate("tutorial") + }, + resetHints = { resetHints() }, + context = context, + ) + HintDialog( + pagerState = pagerState, + currentPageIndex = page, + sharedPrefsKey = "hint_shown_about", + hintMessageResId = R.string.i18n_app_about_app_hint_tooltip, + isHintChanged = isHintChanged[page] == true, + onDismiss = { onDismiss(it) }, + modifier = Modifier.padding(8.dp), + ) + } + HandleBackPress(pagerState, coroutineScope) } - HandleBackPress(pagerState, coroutineScope) } } } - } - - composable("tutorial") { - TutorialNavigator( - onTutorialExit = { - navController.popBackStack() - }, - modifier = Modifier.padding(innerPadding), - ) - } - composable("download_data") { - DownloadDataScreen( - onBackNavigation = { - navController.popBackStack() - }, - onNavigateToTranslation = { language -> - navController.navigate( - "translation_language_detail/$language", - ) - }, - isDarkTheme = isDarkTheme, - downloadActions = downloadActions, - checkUpdateActions = checkUpdateActions, - modifier = Modifier.padding(innerPadding), - ) - } - - composable("conjugate_download_data") { - ConjugateDownloadDataScreen( - onBackNavigation = { - navController.popBackStack() - }, - isDarkTheme = isDarkTheme, - downloadActions = conjugateDownloadActions, - checkUpdateActions = conjugateCheckUpdateActions, - modifier = Modifier.padding(innerPadding), - ) - } + composable("tutorial") { + TutorialNavigator( + onTutorialExit = { + navController.popBackStack() + }, + modifier = Modifier.padding(innerPadding), + ) + } - composable("${Screen.ConjugationSelection.route}/{verb}/{languageAlias}") { backStackEntry -> - val verb = backStackEntry.arguments?.getString("verb") ?: "" - val languageAlias = backStackEntry.arguments?.getString("languageAlias") ?: "" - ConjugationSelectionScreen( - verb = verb, - languageAlias = languageAlias, - onBackNavigation = { navController.popBackStack() }, - modifier = Modifier.padding(innerPadding), - ) - } + composable("download_data") { + DownloadDataScreen( + onBackNavigation = { + navController.popBackStack() + }, + onNavigateToTranslation = { language -> + navController.navigate( + "translation_language_detail/$language", + ) + }, + isDarkTheme = isDarkTheme, + downloadActions = downloadActions, + checkUpdateActions = checkUpdateActions, + modifier = Modifier.padding(innerPadding), + ) + } - composable("${Screen.LanguageSettings.route}/{languageName}") { - val language = it.arguments?.getString("languageName") - if (language != null) { - LanguageSettingsScreen( - language = language, + composable("conjugate_download_data") { + ConjugateDownloadDataScreen( onBackNavigation = { navController.popBackStack() }, + isDarkTheme = isDarkTheme, + downloadActions = conjugateDownloadActions, + checkUpdateActions = conjugateCheckUpdateActions, + modifier = Modifier.padding(innerPadding), + ) + } + + composable("${Screen.ConjugationSelection.route}/{verb}/{languageAlias}") { backStackEntry -> + val verb = backStackEntry.arguments?.getString("verb") ?: "" + val languageAlias = backStackEntry.arguments?.getString("languageAlias") ?: "" + ConjugationSelectionScreen( + verb = verb, + languageAlias = languageAlias, + onBackNavigation = { navController.popBackStack() }, modifier = Modifier.padding(innerPadding), - onTranslationLanguageSelect = { - navController.navigate("translation_language_detail/$language") + ) + } + + composable("${Screen.LanguageSettings.route}/{languageName}") { + val language = it.arguments?.getString("languageName") + if (language != null) { + LanguageSettingsScreen( + language = language, + onBackNavigation = { + navController.popBackStack() + }, + modifier = Modifier.padding(innerPadding), + onTranslationLanguageSelect = { + navController.navigate("translation_language_detail/$language") + }, + onCurrencySelect = { + val currentSymbol = PreferencesHelper.getDefaultCurrencySymbol(context, language) + navController.navigate("currency_symbol_detail/$currentSymbol/$language") + }, + ) + } + } + + composable("translation_language_detail" + "/{languageName}") { backStackEntry -> + val language = backStackEntry.arguments?.getString("languageName") ?: "" + SelectTranslationSourceLanguageScreen( + onBackNavigation = { + navController.popBackStack() }, - onCurrencySelect = { - val currentSymbol = PreferencesHelper.getDefaultCurrencySymbol(context, language) - navController.navigate("currency_symbol_detail/$currentSymbol/$language") + onNavigateToDownloadData = { + navController.navigate("download_data") }, + onDownloadAction = onDownloadAction, + modifier = Modifier.padding(innerPadding), + currentLanguage = language, ) } - } - - composable("translation_language_detail" + "/{languageName}") { backStackEntry -> - val language = backStackEntry.arguments?.getString("languageName") ?: "" - SelectTranslationSourceLanguageScreen( - onBackNavigation = { - navController.popBackStack() - }, - onNavigateToDownloadData = { - navController.navigate("download_data") - }, - onDownloadAction = onDownloadAction, - modifier = Modifier.padding(innerPadding), - currentLanguage = language, - ) - } - composable("currency_symbol_detail/{symbolName}/{languageName}") { backStackEntry -> - val language = backStackEntry.arguments?.getString("languageName") ?: "" - DefaultCurrencySymbolScreen( - currentLanguage = language, - onBackNavigation = { - navController.popBackStack() - }, - modifier = Modifier.padding(innerPadding), - ) - } + composable("currency_symbol_detail/{symbolName}/{languageName}") { backStackEntry -> + val language = backStackEntry.arguments?.getString("languageName") ?: "" + DefaultCurrencySymbolScreen( + currentLanguage = language, + onBackNavigation = { + navController.popBackStack() + }, + modifier = Modifier.padding(innerPadding), + ) + } - composable(Screen.WikimediaScribe.route) { - WikimediaScreen( - onBackNavigation = { - navController.popBackStack() - }, - modifier = Modifier.padding(innerPadding), - ) - } + composable(Screen.WikimediaScribe.route) { + WikimediaScreen( + onBackNavigation = { + navController.popBackStack() + }, + modifier = Modifier.padding(innerPadding), + ) + } - composable(Screen.PrivacyPolicy.route) { - PrivacyPolicyScreen( - onBackNavigation = { - navController.popBackStack() - }, - modifier = Modifier.padding(innerPadding), - ) - } + composable(Screen.PrivacyPolicy.route) { + PrivacyPolicyScreen( + onBackNavigation = { + navController.popBackStack() + }, + modifier = Modifier.padding(innerPadding), + ) + } - composable(Screen.ThirdParty.route) { - ThirdPartyScreen( - onBackNavigation = { - navController.popBackStack() - }, - modifier = Modifier.padding(innerPadding), - ) - } - } // Ends NavHost - } // Ends Scaffold content + composable(Screen.ThirdParty.route) { + ThirdPartyScreen( + onBackNavigation = { + navController.popBackStack() + }, + modifier = Modifier.padding(innerPadding), + ) + } + } // Ends NavHost + } // Ends Scaffold content androidx.compose.animation.AnimatedVisibility( visible = isOfflineErrorVisible, enter = androidx.compose.animation.slideInVertically(initialOffsetY = { it }) + androidx.compose.animation.fadeIn(), exit = androidx.compose.animation.slideOutVertically(targetOffsetY = { it }) + androidx.compose.animation.fadeOut(), - modifier = Modifier - .align(androidx.compose.ui.Alignment.BottomCenter) - .fillMaxWidth() - .padding(bottom = 120.dp, start = 16.dp, end = 16.dp) + modifier = + Modifier + .align(androidx.compose.ui.Alignment.BottomCenter) + .fillMaxWidth() + .padding(bottom = 120.dp, start = 16.dp, end = 16.dp), ) { androidx.compose.material3.Surface( - shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + shape = + androidx.compose.foundation.shape + .RoundedCornerShape(12.dp), color = androidx.compose.ui.graphics.Color.White, - shadowElevation = 8.dp + shadowElevation = 8.dp, ) { androidx.compose.material3.Text( text = activeToastMessage ?: "", - color = androidx.compose.ui.graphics.Color(0xFFE68A00), - modifier = Modifier.padding(16.dp) + color = + androidx.compose.ui.graphics + .Color(0xFFE68A00), + modifier = Modifier.padding(16.dp), ) } } - } // Ends Box + } // Ends Box } // Ends ScribeTheme } // Ends App diff --git a/app/src/main/java/be/scri/helpers/NetworkMonitor.kt b/app/src/main/java/be/scri/helpers/NetworkMonitor.kt index 606c2de7..ca7ca6d7 100644 --- a/app/src/main/java/be/scri/helpers/NetworkMonitor.kt +++ b/app/src/main/java/be/scri/helpers/NetworkMonitor.kt @@ -9,7 +9,6 @@ import android.net.NetworkCapabilities * Utility singleton to check network connectivity status. */ object NetworkMonitor { - /** * Checks if the device is currently connected to the internet. * @@ -17,13 +16,14 @@ object NetworkMonitor { * @return true if connected via WiFi, Cellular, or Ethernet, false otherwise. */ fun isOnline(context: Context): Boolean { - val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager - ?: return false - + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + ?: return false + val activeNetwork = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false - + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } } diff --git a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt index f463d0e9..e30c9bfd 100644 --- a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt @@ -6,7 +6,6 @@ import android.app.Application import android.content.Context import android.database.sqlite.SQLiteException import android.util.Log -import android.widget.Toast import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt index 1c68b705..110750cb 100644 --- a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt @@ -6,7 +6,6 @@ import android.app.Application import android.content.Context import android.database.sqlite.SQLiteException import android.util.Log -import android.widget.Toast import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope From b8ae81a5a33588f34573b6b406a3a6d84e9a1e61 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:21:09 +0530 Subject: [PATCH 3/6] fix: resolve ktlint modifier rule and detekt TooManyFunctions --- app/src/main/java/be/scri/App.kt | 4 ++-- .../ui/screens/download/ConjugateDataDownloadViewModel.kt | 1 + .../java/be/scri/ui/screens/download/DataDownloadViewModel.kt | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/be/scri/App.kt b/app/src/main/java/be/scri/App.kt index a7acc758..7e9b2081 100644 --- a/app/src/main/java/be/scri/App.kt +++ b/app/src/main/java/be/scri/App.kt @@ -143,7 +143,7 @@ fun ScribeApp( useDarkTheme = isDarkTheme, isIncreaseTextSize = isIncreaseTextSize, ) { - Box(modifier = Modifier.fillMaxSize()) { + Box(modifier = modifier.fillMaxSize()) { Scaffold( bottomBar = { ScribeBottomBar( @@ -160,7 +160,7 @@ fun ScribeApp( screens = screens, ) }, - modifier = modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { innerPadding -> NavHost( navController = navController, diff --git a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt index e30c9bfd..77e73af0 100644 --- a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt @@ -29,6 +29,7 @@ import java.io.IOException import java.time.LocalDate /** ViewModel to manage conjugate data download states and actions. */ +@Suppress("TooManyFunctions") class ConjugateDataDownloadViewModel( application: Application, ) : AndroidViewModel(application) { diff --git a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt index 110750cb..54c824ec 100644 --- a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt @@ -29,6 +29,7 @@ import java.io.IOException import java.time.LocalDate /** ViewModel to manage data download states and actions. */ +@Suppress("TooManyFunctions") class DataDownloadViewModel( application: Application, ) : AndroidViewModel(application) { From 24355489788ced5e4b8f04c5cf126bde649e03bf Mon Sep 17 00:00:00 2001 From: angrezichatterbox Date: Sun, 12 Jul 2026 21:11:01 +0530 Subject: [PATCH 4/6] feat: change strings --- app/src/main/assets/i18n | 2 +- .../ui/screens/download/ConjugateDataDownloadViewModel.kt | 6 +++--- .../be/scri/ui/screens/download/DataDownloadViewModel.kt | 6 +++--- app/src/main/res/values/string.xml | 5 +---- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/main/assets/i18n b/app/src/main/assets/i18n index 58b1dd61..fe9fa190 160000 --- a/app/src/main/assets/i18n +++ b/app/src/main/assets/i18n @@ -1 +1 @@ -Subproject commit 58b1dd6190e0e43d9892980b52150449861838ab +Subproject commit fe9fa1901a12a14d71fec283b0afe5e702398a28 diff --git a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt index 77e73af0..9e596f91 100644 --- a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt @@ -103,7 +103,7 @@ class ConjugateDataDownloadViewModel( forceDownload: Boolean = false, ) { if (!NetworkMonitor.isOnline(getApplication())) { - showToast("No internet connection. Please connect and try again.") + showToast(getApplication().getString(R.string.i18n_app_download_error_no_network)) return } @@ -219,7 +219,7 @@ class ConjugateDataDownloadViewModel( */ fun handleDownloadAllLanguages() { if (!NetworkMonitor.isOnline(getApplication())) { - showToast("No internet connection. Please connect and try again.") + showToast(getApplication().getString(R.string.i18n_app_download_error_no_network)) return } @@ -287,7 +287,7 @@ class ConjugateDataDownloadViewModel( */ fun checkForNewData() { if (!NetworkMonitor.isOnline(getApplication())) { - showToast("No internet connection. Please connect and try again.") + showToast(getApplication().getString(R.string.i18n_app_download_error_no_network)) return } diff --git a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt index 54c824ec..6978a481 100644 --- a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt @@ -103,7 +103,7 @@ class DataDownloadViewModel( forceDownload: Boolean = false, ) { if (!NetworkMonitor.isOnline(getApplication())) { - showToast("No internet connection. Please connect and try again.") + showToast(getApplication().getString(R.string.i18n_app_download_error_no_network)) return } @@ -219,7 +219,7 @@ class DataDownloadViewModel( */ fun handleDownloadAllLanguages() { if (!NetworkMonitor.isOnline(getApplication())) { - showToast("No internet connection. Please connect and try again.") + showToast(getApplication().getString(R.string.i18n_app_download_error_no_network)) return } @@ -287,7 +287,7 @@ class DataDownloadViewModel( */ fun checkForNewData() { if (!NetworkMonitor.isOnline(getApplication())) { - showToast("No internet connection. Please connect and try again.") + showToast(getApplication().getString(R.string.i18n_app_download_error_no_network)) return } diff --git a/app/src/main/res/values/string.xml b/app/src/main/res/values/string.xml index b9ab4db4..b1f24461 100644 --- a/app/src/main/res/values/string.xml +++ b/app/src/main/res/values/string.xml @@ -80,6 +80,7 @@ Network error: {error} Server error: {code} Download timed out + No internet connection. Please connect and try again. Add new data to Scribe Conjugate. Download verb data Download data to start conjugating! @@ -149,8 +150,6 @@ Have the device play a sound when keys are pressed. Vibrate on key press Have the device vibrate when keys are pressed. - Clipboard key on keyboard - Include a key to access the clipboard on the main keyboard. Default currency symbol Symbol for the 123 keys Select which currency symbol appears on the number keys. @@ -159,8 +158,6 @@ Select a keyboard layout that suits your typing preference and language needs. Disable accent characters Remove accented letter keys on the primary keyboard layout. - Floating key on keyboard - Include a key to float the keyboard UI on the main keyboard. Period and comma on ABC Include period and comma keys on the main keyboard for convenient typing. Layout From b9c9f4f42b67cd1774d094295a3b5e55506e1048 Mon Sep 17 00:00:00 2001 From: angrezichatterbox Date: Sun, 12 Jul 2026 21:18:52 +0530 Subject: [PATCH 5/6] chore: remove clipboard key --- .../java/be/scri/ui/screens/LanguageSettingsScreen.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt b/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt index 887dcfe7..ca0b8152 100644 --- a/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt @@ -393,14 +393,7 @@ private fun getLayoutListData( onToggle = onTogglePeriodAndComma, ), ) - list.add( - ScribeItem.SwitchItem( - title = R.string.i18n_app_settings_keyboard_layout_clipboard_on_keyboard, - desc = R.string.i18n_app_settings_keyboard_layout_clipboard_on_keyboard_description, - state = toggleClipboardKeyOnKeyboard, - onToggle = onToggleClipboardKeyOnKeyboard, - ), - ) + list.add( ScribeItem.ClickableItem( title = R.string.i18n_app_settings_keyboard_layout_default_currency, From 042e8d34b975f07fb2c69e3d79460169c99981b4 Mon Sep 17 00:00:00 2001 From: angrezichatterbox Date: Sun, 12 Jul 2026 21:26:36 +0530 Subject: [PATCH 6/6] feat: remove clipboard logic --- .../be/scri/services/GeneralKeyboardIME.kt | 2 -- .../main/java/be/scri/helpers/KeyboardBase.kt | 2 -- .../java/be/scri/helpers/PreferencesHelper.kt | 26 ------------------- .../scri/ui/screens/LanguageSettingsScreen.kt | 18 ------------- 4 files changed, 48 deletions(-) diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index 35a26bb3..5eed7d68 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -581,8 +581,6 @@ abstract class GeneralKeyboardIME( return isActionSearch || isUriType || hasSearchHint } - override fun isClipboardKeyEnabled(): Boolean = PreferencesHelper.getIsClipboardKeyEnabled(this, language) - private fun loadLanguageData() { val languageAlias = getLanguageAlias(language) dataContract = dbManagers.getLanguageContract(languageAlias) diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt index 06fbe0b7..f5c89fdf 100644 --- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt +++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt @@ -37,8 +37,6 @@ class KeyboardBase { val keyboardLetters: Int fun isSearchBar(): Boolean - - fun isClipboardKeyEnabled(): Boolean } /** Horizontal gap default for all rows */ diff --git a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt index 88284f49..b449b320 100644 --- a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt +++ b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt @@ -26,7 +26,6 @@ object PreferencesHelper { private const val DEFAULT_CURRENCY = "default_currency" private const val HOLD_FOR_ALT_KEYS = "hold_for_alt_keys" private const val INCREASE_TEXT_SIZE = "increase_text_size" - private const val CLIPBOARD_KEY_ON_KEYBOARD = "clipboard_key_on_keyboard" /** * Sets the translation source language for a given language. @@ -535,31 +534,6 @@ object PreferencesHelper { return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(HOLD_FOR_ALT_KEYS, language), true) } - /** - * Sets the preference for showing or hiding the clipboard key on the keyboard. - */ - fun setClipboardKeyPreference( - context: Context, - language: String, - enabled: Boolean, - ) { - val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) - sharedPref.edit { - putBoolean(getLanguageSpecificPreferenceKey(CLIPBOARD_KEY_ON_KEYBOARD, language), enabled) - } - } - - /** - * Retrieves whether the clipboard key is enabled on the keyboard for a given language. - */ - fun getIsClipboardKeyEnabled( - context: Context, - language: String, - ): Boolean { - val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) - return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(CLIPBOARD_KEY_ON_KEYBOARD, language), true) - } - /** * Resets the application hints, marking them as not shown in the shared preferences. * diff --git a/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt b/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt index ca0b8152..82d55761 100644 --- a/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt @@ -109,13 +109,6 @@ fun LanguageSettingsScreen( ) } - val clipboardKeyOnKeyboardState = - remember { - mutableStateOf( - PreferencesHelper.getIsClipboardKeyEnabled(context, language), - ) - } - val wordByWordDeletionState = remember { mutableStateOf( @@ -161,15 +154,6 @@ fun LanguageSettingsScreen( shouldDisableAccentCharacter, ) }, - toggleClipboardKeyOnKeyboard = clipboardKeyOnKeyboardState.value, - onToggleClipboardKeyOnKeyboard = { isEnabled -> - clipboardKeyOnKeyboardState.value = isEnabled - PreferencesHelper.setClipboardKeyPreference( - context, - language, - isEnabled, - ) - }, onCurrencySelect = onCurrencySelect, ), ) @@ -366,8 +350,6 @@ private fun getLayoutListData( onTogglePeriodAndComma: (Boolean) -> Unit, toggleDisableAccentCharacter: Boolean, onToggleDisableAccentCharacter: (Boolean) -> Unit, - toggleClipboardKeyOnKeyboard: Boolean, - onToggleClipboardKeyOnKeyboard: (Boolean) -> Unit, onCurrencySelect: () -> Unit, ): List { val list: MutableList = mutableListOf()