diff --git a/README.md b/README.md index 91a5ecce..e7a8ba22 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ This simple app helps you avoid forgetting to consume foods that are about to ex @Maha-Rajan @anuragkanojiya1 @PrakashIrom +@serAKL16lysA ## ❤️ Support diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 037a3c7b..95b28b27 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,14 +26,14 @@ try { android { namespace = "com.lorenzovainigli.foodexpirationdates" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "com.lorenzovainigli.foodexpirationdates" minSdk = 24 - targetSdk = 34 - versionCode = 33 - versionName = "2.4.2" + targetSdk = 35 + versionCode = 35 + versionName = "2.5.1" base.archivesName.set("FoodExpirationDates-$versionName") @@ -138,6 +138,10 @@ dependencies { implementation(libs.androidx.navigation.compose) testImplementation(libs.junit) testImplementation(libs.junit.jupiter) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.robolectric) + testImplementation(libs.mockk) androidTestImplementation(libs.test.core.ktx) androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/Contributor.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/Contributor.kt index 044bfbe9..5277a57a 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/Contributor.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/Contributor.kt @@ -43,4 +43,5 @@ val contributors = listOf( Contributor(name = "Maha Rajan", username = "Maha-Rajan"), Contributor(name = "Anurag Kanojiya", username = "anuragkanojiya1"), Contributor(name = "Prakash Irom", username = "PrakashIrom"), + Contributor(name = "serAKL16lysA", username = "serAKL16lysA"), ) diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/LocaleHelper.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/LocaleHelper.kt new file mode 100644 index 00000000..974ea84b --- /dev/null +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/LocaleHelper.kt @@ -0,0 +1,60 @@ +package com.lorenzovainigli.foodexpirationdates.model + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import com.lorenzovainigli.foodexpirationdates.view.MainActivity +import java.util.Locale +import kotlin.jvm.java + +enum class Language(val code: String, val label: String) { + SYSTEM("system", "System"), + ARABIC("ar", "العربية"), + CHINESE_TRADITIONAL("zh-TW", "繁體中文"), + ENGLISH("en", "English"), + FRENCH("fr", "Français"), + GERMAN("de", "Deutsch"), + HINDI("hi", "हिन्दी"), + INDONESIAN("id", "Bahasa Indonesia"), + ITALIAN("it", "Italiano"), + JAPANESE("ja", "日本語"), + POLISH("pl", "Polski"), + RUSSIAN("ru", "Русский"), + SPANISH("es", "Español"), + TAMIL("ta", "தமிழ்"), + TURKISH("tr", "Türkçe"), + VIETNAMESE("vi", "Tiếng Việt"); + + companion object { + fun fromCode(code: String): Language { + return Language.entries.find { it.code == code } ?: SYSTEM + } + } +} + +object LocaleHelper { + + fun setLocale(context: Context, language: String): Context { + val locale = if (language == Language.SYSTEM.code) { + Locale.getDefault() + } else { + Locale(language) + } + Locale.setDefault(locale) + + val config = Configuration(context.resources.configuration) + config.setLocale(locale) + config.setLayoutDirection(locale) + + return context.createConfigurationContext(config) + } + + fun changeLanguage(context: Context, newLanguage: String) { + setLocale(context, newLanguage) + val intent = Intent(context, MainActivity::class.java) // or current activity + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + (context as Activity).finish() + } +} diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepository.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepository.kt index 900dbce3..1e855065 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepository.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepository.kt @@ -4,6 +4,7 @@ import android.content.Context import android.view.Window import android.view.WindowManager import com.lorenzovainigli.foodexpirationdates.R +import com.lorenzovainigli.foodexpirationdates.model.Language import java.lang.Exception import java.text.DateFormat import java.text.SimpleDateFormat @@ -21,6 +22,8 @@ class PreferencesRepository { const val keyThemeMode = "theme_mode" const val keyTopBarFont = "top_bar_font" const val keyDynamicColors = "dynamic_colors" + const val keyMonochromeIcons = "monochrome_icons" + const val keyLanguage = "language" private val availLocaleDateFormats = arrayOf(DateFormat.SHORT, DateFormat.MEDIUM) private val availOtherDateFormats = arrayOf( @@ -174,8 +177,8 @@ class PreferencesRepository { sharedPrefs: String = sharedPrefsName, ): Boolean { try { - return context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) - .getBoolean(keyDynamicColors, false) + return context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) + .getBoolean(keyDynamicColors, false) } catch (e: Exception){ e.printStackTrace() } @@ -186,9 +189,64 @@ class PreferencesRepository { context: Context, sharedPrefs: String = sharedPrefsName, dynamicColorsEnabled: Boolean + ): Boolean { + try { + context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) + .edit().putBoolean(keyDynamicColors, dynamicColorsEnabled).apply() + return true + } catch (_: Exception){ + return false + } + } + + fun getMonochromeIcons( + context: Context, + sharedPrefs: String = sharedPrefsName, + ): Boolean { + try { + return context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) + .getBoolean(keyMonochromeIcons, true) + } catch (e: Exception){ + e.printStackTrace() + } + return true + } + + fun setMonochromeIcons( + context: Context, + sharedPrefs: String = sharedPrefsName, + monochromeIconsEnabled: Boolean + ): Boolean { + try { + context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) + .edit().putBoolean(keyMonochromeIcons, monochromeIconsEnabled).apply() + return true + } catch (_: Exception){ + return false + } + } + + fun getLanguage( + context: Context, + sharedPrefs: String = sharedPrefsName, + ): String { + try { + return context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) + .getString(keyLanguage, Language.SYSTEM.code) + ?: Language.SYSTEM.code + } catch (e: Exception){ + e.printStackTrace() + } + return Language.SYSTEM.code + } + + fun setLanguage( + context: Context, + sharedPrefs: String = sharedPrefsName, + language: String ) { return context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) - .edit().putBoolean(keyDynamicColors, dynamicColorsEnabled).apply() + .edit().putString(keyLanguage, language).apply() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/worker/CheckExpirationsWorker.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/worker/CheckExpirationsWorker.kt index 386f5070..e2409308 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/worker/CheckExpirationsWorker.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/worker/CheckExpirationsWorker.kt @@ -4,10 +4,13 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import com.lorenzovainigli.foodexpirationdates.BuildConfig import com.lorenzovainigli.foodexpirationdates.R +import com.lorenzovainigli.foodexpirationdates.model.LocaleHelper import com.lorenzovainigli.foodexpirationdates.model.NotificationManager.Companion.CHANNEL_REMINDERS_ID import com.lorenzovainigli.foodexpirationdates.model.entity.computeExpirationDate import com.lorenzovainigli.foodexpirationdates.model.repository.ExpirationDateRepository +import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository import com.lorenzovainigli.foodexpirationdates.showNotification import kotlinx.coroutines.flow.first import java.util.Calendar @@ -57,10 +60,16 @@ class CheckExpirationsWorker @Inject constructor( var message = "" if (sb.toString().length > 2) message = sb.toString().substring(0, sb.toString().length - 2) + "." + val context = if (BuildConfig.DEBUG) { + LocaleHelper.setLocale( + context = applicationContext, + language = PreferencesRepository.getLanguage(applicationContext) + ) + } else applicationContext showNotification( - context = applicationContext, + context = context, channelId = CHANNEL_REMINDERS_ID, - title = applicationContext.getString(R.string.your_food_is_expiring), + title = context.getString(R.string.your_food_is_expiring), message = message ) return Result.success() diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/MainActivity.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/MainActivity.kt index 3bc1e6ad..fe652c64 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/MainActivity.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/MainActivity.kt @@ -1,5 +1,6 @@ package com.lorenzovainigli.foodexpirationdates.view +import android.content.Context import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity @@ -17,9 +18,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.rememberNavController +import com.lorenzovainigli.foodexpirationdates.BuildConfig +import com.lorenzovainigli.foodexpirationdates.model.LocaleHelper import com.lorenzovainigli.foodexpirationdates.model.NotificationManager import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository.Companion.checkAndSetSecureFlags @@ -37,7 +39,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenLoading.value } @@ -105,4 +107,13 @@ class MainActivity : ComponentActivity() { } } + override fun attachBaseContext(newBase: Context) { + if (BuildConfig.DEBUG) { + val locale = PreferencesRepository.getLanguage(newBase) + super.attachBaseContext(LocaleHelper.setLocale(newBase, locale)) + } else { + super.attachBaseContext(newBase) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/FoodCard.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/FoodCard.kt index 5500602d..e85a6917 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/FoodCard.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/FoodCard.kt @@ -64,6 +64,7 @@ fun FoodCard( ) { val context = LocalContext.current val dateFormat = PreferencesRepository.getUserDateFormat(context) + val monochromeIcons = PreferencesRepository.getMonochromeIcons(context) val sdf = SimpleDateFormat(dateFormat, context.resources.configuration.locales[0]) val today = Calendar.getInstance() val twoDaysAgo = Calendar.getInstance().apply { @@ -144,10 +145,12 @@ fun FoodCard( .size(36.dp) .alpha(0.8f) ) { - drawImage( - image = imageBitmap, - colorFilter = ColorFilter.tint(color, BlendMode.Color) - ) + if (monochromeIcons) { + drawImage( + image = imageBitmap, + colorFilter = ColorFilter.tint(color, BlendMode.Color) + ) + } drawImage( image = imageBitmap, blendMode = BlendMode.DstAtop diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/LanguagePickerDialog.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/LanguagePickerDialog.kt new file mode 100644 index 00000000..43bc5786 --- /dev/null +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/LanguagePickerDialog.kt @@ -0,0 +1,121 @@ +package com.lorenzovainigli.foodexpirationdates.view.composable + +import androidx.compose.foundation.clickable +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.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.lorenzovainigli.foodexpirationdates.R +import com.lorenzovainigli.foodexpirationdates.model.Language +import com.lorenzovainigli.foodexpirationdates.model.LocaleHelper +import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository +import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme + +@Composable +fun LanguagePickerDialog( + isDialogOpen: Boolean = true, + onDismiss: () -> Unit = {} +) { + if (isDialogOpen) { + val context = LocalContext.current + val storedLanguage = PreferencesRepository.getLanguage(context) + var selectedLanguage = remember { + mutableStateOf(storedLanguage) + } + Dialog( + onDismissRequest = onDismiss + ) { + Card( + shape = RoundedCornerShape(10.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp + ) + ) { + Column( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) + ) { + Text( + modifier = Modifier.padding(4.dp), + text = stringResource(R.string.select_language), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Column( + modifier = Modifier + .height(480.dp) + .verticalScroll(rememberScrollState()) + ) { + Language.entries.forEach { language -> + Row( + modifier = Modifier.fillMaxWidth(1f).clickable( + onClick = { + selectedLanguage.value = language.code + }), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedLanguage.value == language.code, + onClick = { + selectedLanguage.value = language.code + } + ) + Text( + text = language.label, + color = if (selectedLanguage.value == language.code) + MaterialTheme.colorScheme.primary + else Color.Unspecified + ) + } + } + } + Button( + modifier = Modifier.align(Alignment.End), + onClick = { + LocaleHelper.changeLanguage(context, selectedLanguage.value) + PreferencesRepository.setLanguage( + context, + language = selectedLanguage.value + ) + } + ) { + Text(stringResource(R.string.apply)) + } + } + } + } + } +} + +@PreviewLightDark +@Composable +fun LanguagePickerDialogPreview() { + FoodExpirationDatesTheme { + Surface { + LanguagePickerDialog() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/screen/SettingsScreen.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/screen/SettingsScreen.kt index ee990038..21fb698b 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/screen/SettingsScreen.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/screen/SettingsScreen.kt @@ -5,6 +5,7 @@ import android.os.Build import android.util.Log import android.view.WindowManager import androidx.annotation.RequiresApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button @@ -37,7 +39,9 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp +import com.lorenzovainigli.foodexpirationdates.BuildConfig import com.lorenzovainigli.foodexpirationdates.R +import com.lorenzovainigli.foodexpirationdates.model.Language import com.lorenzovainigli.foodexpirationdates.model.NotificationManager import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository.Companion.getScreenProtectionEnabled @@ -46,6 +50,7 @@ import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme import com.lorenzovainigli.foodexpirationdates.view.MainActivity import com.lorenzovainigli.foodexpirationdates.view.composable.AutoResizedText import com.lorenzovainigli.foodexpirationdates.view.composable.DateFormatDialog +import com.lorenzovainigli.foodexpirationdates.view.composable.LanguagePickerDialog import com.lorenzovainigli.foodexpirationdates.view.composable.NotificationTimeBottomSheet import com.lorenzovainigli.foodexpirationdates.view.composable.SettingsItem import com.lorenzovainigli.foodexpirationdates.view.preview.LanguagePreviews @@ -64,6 +69,8 @@ fun SettingsScreen( ?: PreferencesRepository.Companion.ThemeMode.SYSTEM.ordinal val dynamicColorsState = prefsViewModel?.getDynamicColors(context)?.collectAsState()?.value ?: false + val monochromeIconsState = prefsViewModel?.getMonochromeIcons(context)?.collectAsState()?.value + ?: true val topBarFontState = prefsViewModel?.getTopBarFont(context)?.collectAsState()?.value ?: PreferencesRepository.Companion.TopBarFont.NORMAL.ordinal @@ -73,6 +80,9 @@ fun SettingsScreen( var isDateFormatDialogOpened by remember { mutableStateOf(false) } + var isLanguagePickerDialogOpened by remember { + mutableStateOf(false) + } val notificationTimeHour = prefsViewModel?.getNotificationTimeHour(context)?.collectAsState()?.value @@ -115,6 +125,14 @@ fun SettingsScreen( } ) } + prefsViewModel?.let { + LanguagePickerDialog( + isDialogOpen = isLanguagePickerDialogOpened, + onDismiss = { + isLanguagePickerDialogOpened = false + } + ) + } Column( modifier = Modifier .verticalScroll(rememberScrollState()) @@ -284,6 +302,46 @@ fun SettingsScreen( ) } } + SettingsItem( + label = stringResource(R.string.monochrome_icons) + ) { + Spacer( + Modifier + .weight(1f) + .fillMaxHeight() + ) + Switch( + checked = monochromeIconsState, + onCheckedChange = { + prefsViewModel?.setMonochromeIcons(context, it) + } + ) + } + + if (BuildConfig.DEBUG) { + Text( + text = stringResource(R.string.debug_options), + style = MaterialTheme.typography.labelLarge + ) + SettingsItem( + label = stringResource(R.string.language) + ) { + Spacer( + Modifier + .weight(1f) + .fillMaxHeight() + ) + BasicText( + modifier = Modifier.clickable { + isLanguagePickerDialogOpened = true + }, + text = Language.fromCode(PreferencesRepository.getLanguage(context)).label, + style = MaterialTheme.typography.headlineSmall.copy( + color = MaterialTheme.colorScheme.onSurface + ) + ) + } + } } } diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/viewmodel/PreferencesViewModel.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/viewmodel/PreferencesViewModel.kt index 813122cc..a5c1398b 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/viewmodel/PreferencesViewModel.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/viewmodel/PreferencesViewModel.kt @@ -27,6 +27,8 @@ class PreferencesViewModel @Inject constructor(): ViewModel() { private var themeMode = _themeMode.asStateFlow() private var _dynamicColors = MutableStateFlow(false) private var dynamicColors = _dynamicColors.asStateFlow() + private var _monochromeIcons = MutableStateFlow(true) + private var monochromeIcons = _monochromeIcons.asStateFlow() private var _topBarFont = MutableStateFlow(0) private var topbarFont = _topBarFont.asStateFlow() @@ -124,4 +126,20 @@ class PreferencesViewModel @Inject constructor(): ViewModel() { _dynamicColors.value = colors } + fun getMonochromeIcons(context: Context): StateFlow { + viewModelScope.launch { + _monochromeIcons.value = PreferencesRepository.getMonochromeIcons(context) + } + return monochromeIcons + } + + fun setMonochromeIcons(context: Context, icons: Boolean) { + viewModelScope.launch { + PreferencesRepository.setMonochromeIcons( + context = context, + monochromeIconsEnabled = icons + ) + } + _monochromeIcons.value = icons + } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index cfc24212..5ca6e4df 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -21,10 +21,7 @@ Eine Rezension schreiben Quelltext Funktionen - - Anzeige einer Liste mit Lebensmittelverfallsdaten in aufsteigender zeitlicher Reihenfolge -\n - Einträge hinzufügen, bearbeiten und löschen -\n - Eröffnungsdatum -\n - Benachrichtigungen + - Anzeige einer Liste mit Lebensmittelverfallsdaten in aufsteigender zeitlicher Reihenfolge \n - Einträge hinzufügen, bearbeiten und löschen \n - Produktabfrage mittels Strichtcodescanner und Open Food Facts API \n - Öffnungsdatum \n - Benachrichtigungen Dieses Projekt unterstützen Fehler melden Version %1$s @@ -57,7 +54,7 @@ System Dunkel %1$s gelöscht - Abbrechen + Rückgängig Normal Fett Extrafett @@ -104,4 +101,12 @@ Wiederholen Kontakte Hergestellt mit ❤️ + Bitte scannen Sie einen Strichcode + Produktinformationen abrufen… + Scanfehler + Bildschirmschutz aktivieren + Schützen Sie Ihren Bildschirm vor Screenshots und Bildschirmaufzeichnungen. + Datenschutz + Geöffnet + Strichcode-Scanner \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e3d1d1f7..00ffb466 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -61,7 +61,7 @@ Negrita Negrita extra Estilo de fuente de la barra superior - Comportamiento + Conducta Apariencia Enviar un e-mail Contactos @@ -111,4 +111,9 @@ Activar la protección de pantalla Protege tu pantalla de capturas y grabaciones de pantalla. Privacidad + Idioma + Aplicar + Opciones de depuración + Iconos monocromáticos + Selecciona el idioma \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e1ec40f2..bc359810 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,6 +1,6 @@ - Scadenze alimenti + Scadenze Alimenti @string/app_name %1$d giorni fa Ieri @@ -92,7 +92,6 @@ Attiva protezione schermo Proteggi la tua schermata dagli screenshot e dalla registrazione dello schermo. Privacy - Uova Formaggio @@ -111,8 +110,14 @@ Colori dinamici Scanner di codici a barre + Made with ❤️ + Applica + Seleziona lingua + Opzioni di debug + Lingua Giorni Mesi + Icone monocromatiche \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1e742422..4770e3ce 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,13 +1,14 @@ - Food Expiration Dates - Food Expirations + 食品の期限リマインダー + 食品の期限 %1$d日前 昨日 今日 明日 %1$d日後 食品を追加 + 画面をスクリーンショットや画面録画から保護する。 食品を編集 食品名 賞味期限 @@ -65,6 +66,7 @@ 上部バーのフォントスタイル 行動 外観 + プライバシー メールを送信 連絡先 このアプリケーションに関する詳細な情報を入手したい場合、またはフィードバックの送信、バグ報告、機能リクエスト、単に開発者に連絡したい場合は、次のボタンを使用してメールを送信してください。\nバグレポートや機能リクエストがある場合は、このアプリケーションのGitHubリポジトリでissueを開くこともできます。 @@ -109,4 +111,11 @@ スキャンエラー バーコードスキャナー ♥で作られました + 適用 + 言語を選択 + デバッグオプション + 言語 + 画面保護をオンにする + 開封済み + モノクロアイコン \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index da7a41a7..6cb3ac7e 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -107,4 +107,10 @@ Lỗi kết nối Đang lấy thông tin sản phẩm… Lỗi quét + Đã mở + Máy quét mã vạch + Làm bằng ❤️ + Bật bảo vệ màn hình + Quyền riêng tư + Bảo vệ màn hình của bạn khỏi chụp màn hình và ghi màn hình. \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index daeb9984..0f2ad5af 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -88,4 +88,8 @@ 選擇日期格式 語言環境格式 你的食物快過期了! + 已開啟的 + 禁止螢幕擷圖和螢幕錄影以保護您的隱私。 + 啟用螢幕隱私保護 + 隱私 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6bf12e85..a93ccf68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,6 +91,7 @@ Retry Scanning error Opened + Monochrome icons Eggs @@ -111,6 +112,10 @@ Barcode scanner Made with ❤️ + Apply + Select language + Debug Options + Language Days Months diff --git a/app/src/test/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepositoryTest.kt b/app/src/test/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepositoryTest.kt new file mode 100644 index 00000000..a871f3b1 --- /dev/null +++ b/app/src/test/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepositoryTest.kt @@ -0,0 +1,149 @@ +package com.lorenzovainigli.foodexpirationdates.model.repository + +import android.content.Context +import android.content.SharedPreferences +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class PreferencesRepositoryTest { + + @Test + fun `getDynamicColors() - SharedPreferences throws exception`() { + val context = mockk() + val sharedPrefs: SharedPreferences = mockk() + every { sharedPrefs.getBoolean(any(), any()) } throws RuntimeException("SharedPrefs Error") + every { context.getSharedPreferences(any(), any()) } returns sharedPrefs + val result = PreferencesRepository.getDynamicColors(context, "") + assertFalse(result) + } + + @Test + fun `getDynamicColors() - SharedPreferences returns the expected value`() { + val context = mockk() + val sharedPrefs: SharedPreferences = mockk() + every { sharedPrefs.getBoolean(any(), any()) } returns true + every { context.getSharedPreferences(any(), any()) } returns sharedPrefs + val result = PreferencesRepository.getDynamicColors(context, "") + assertTrue(result) + } + + @Test + fun `getDynamicColors() - Invalid sharedPrefs name`() { + val context = mockk() + val invalidSharedPrefsName = "invalid_name" + every { + context.getSharedPreferences(invalidSharedPrefsName, any()) + } throws RuntimeException("SharedPrefs Error") + val result = PreferencesRepository.getDynamicColors(context, invalidSharedPrefsName) + assertFalse(result) + } + + @Test + fun `setDynamicColors() - SharedPreferences throws exception`() { + val context = mockk() + val sharedPrefsEditor: SharedPreferences.Editor = mockk(relaxed = true) + val sharedPrefs: SharedPreferences = mockk() + every { sharedPrefs.edit() } returns sharedPrefsEditor + every { sharedPrefsEditor.putBoolean(any(), any()) } throws RuntimeException("SharedPrefs Error") + every { context.getSharedPreferences(any(), any()) } returns sharedPrefs + val result = PreferencesRepository.setDynamicColors(context, "", true) + assertFalse(result) + } + + @Test + fun `setDynamicColors() - SharedPreferences successfully sets the value`() { + val context = mockk() + val sharedPrefsEditor: SharedPreferences.Editor = mockk(relaxed = true) + val sharedPrefs: SharedPreferences = mockk() + every { sharedPrefs.edit() } returns sharedPrefsEditor + every { sharedPrefsEditor.putBoolean(any(), any()) } returns sharedPrefsEditor + every { sharedPrefsEditor.apply() } just Runs + every { context.getSharedPreferences(any(), any()) } returns sharedPrefs + val result = PreferencesRepository.setDynamicColors(context, "", true) + assertTrue(result) + } + + @Test + fun `setDynamicColors() - Invalid sharedPrefs name`() { + val context = mockk() + val invalidSharedPrefsName = "invalid_name" + every { + context.getSharedPreferences(invalidSharedPrefsName, any()) + } throws RuntimeException("SharedPrefs Error") + val result = PreferencesRepository.setDynamicColors(context, invalidSharedPrefsName, true) + assertFalse(result) + } + + @Test + fun `getMonochromeIcons() - SharedPreferences throws exception`() { + val context = mockk() + val sharedPrefs: SharedPreferences = mockk() + every { sharedPrefs.getBoolean(any(), any()) } throws RuntimeException("SharedPrefs Error") + every { context.getSharedPreferences(any(), any()) } returns sharedPrefs + val result = PreferencesRepository.getMonochromeIcons(context, "") + assertTrue(result) + } + + @Test + fun `getMonochromeIcons() - SharedPreferences returns the expected value`() { + val context = mockk() + val sharedPrefs: SharedPreferences = mockk() + every { sharedPrefs.getBoolean(any(), any()) } returns false + every { context.getSharedPreferences(any(), any()) } returns sharedPrefs + val result = PreferencesRepository.getMonochromeIcons(context, "") + assertFalse(result) + } + + @Test + fun `getMonochromeIcons() - Invalid sharedPrefs name`() { + val context = mockk() + val invalidSharedPrefsName = "invalid_name" + every { + context.getSharedPreferences(invalidSharedPrefsName, any()) + } throws RuntimeException("SharedPrefs Error") + val result = PreferencesRepository.getMonochromeIcons(context, invalidSharedPrefsName) + assertTrue(result) + } + + @Test + fun `setMonochromeIcons() - SharedPreferences throws exception`() { + val context = mockk() + val sharedPrefsEditor: SharedPreferences.Editor = mockk(relaxed = true) + val sharedPrefs: SharedPreferences = mockk() + every { sharedPrefs.edit() } returns sharedPrefsEditor + every { sharedPrefsEditor.putBoolean(any(), any()) } throws RuntimeException("SharedPrefs Error") + every { context.getSharedPreferences(any(), any()) } returns sharedPrefs + val result = PreferencesRepository.setMonochromeIcons(context, "", true) + assertFalse(result) + } + + @Test + fun `setMonochromeIcons() - SharedPreferences successfully sets the value`() { + val context = mockk() + val sharedPrefsEditor: SharedPreferences.Editor = mockk(relaxed = true) + val sharedPrefs: SharedPreferences = mockk() + every { sharedPrefs.edit() } returns sharedPrefsEditor + every { sharedPrefsEditor.putBoolean(any(), any()) } returns sharedPrefsEditor + every { sharedPrefsEditor.apply() } just Runs + every { context.getSharedPreferences(any(), any()) } returns sharedPrefs + val result = PreferencesRepository.setMonochromeIcons(context, "", true) + assertTrue(result) + } + + @Test + fun `setMonochromeIcons() - Invalid sharedPrefs name`() { + val context = mockk() + val invalidSharedPrefsName = "invalid_name" + every { + context.getSharedPreferences(invalidSharedPrefsName, any()) + } throws RuntimeException("SharedPrefs Error") + val result = PreferencesRepository.setMonochromeIcons(context, invalidSharedPrefsName, true) + assertFalse(result) + } + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b85d736..8c1fc671 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,37 +1,41 @@ [versions] -activity-compose = "1.9.2" -agp = "8.7.0" +activity-compose = "1.9.3" +agp = "8.7.3" androidx-test-core-ktx = "1.6.1" androidx-test-ext-junit = "1.2.1" -compose-bom = "2024.09.03" +compose-bom = "2024.12.01" barcode-scanning = "17.3.0" -camera-core = "1.3.4" -camera-mlkit-vision = "1.4.0-rc01" +camera-core = "1.4.1" +camera-mlkit-vision = "1.4.1" coil-compose = "2.7.0" converter-gson = "2.11.0" -core-ktx = "1.13.1" -dagger-hilt = "2.52" +core-ktx = "1.15.0" +dagger-hilt = "2.54" espresso-core = "3.6.1" -firebase-bom = "33.4.0" +firebase-bom = "33.7.0" firebase-crashlytics-gradle = "3.0.2" google-services = "4.4.2" -guava = "33.3.1-jre" +guava = "33.4.0-jre" hilt-common = "1.2.0" junit = "4.13.2" -junit-jupiter = "5.11.2" -kotlin = "2.0.20" -ksp = "2.0.20-1.0.25" -lifecycle-runtime-ktx = "2.8.6" -navigation-compose = "2.8.2" +junit-jupiter = "5.11.4" +kotlin = "2.1.0" +ksp = "2.1.0-1.0.29" +lifecycle-runtime-ktx = "2.8.7" +mockito = "3.12.4" +mockito-kotlin = "3.2.0" +mockk = "1.13.5" +navigation-compose = "2.8.5" opencsv = "5.9" -paparazzi = "1.3.4" +paparazzi = "1.3.5" retrofit = "2.11.0" +robolectric = "4.10" room = "2.6.1" +splashscreen = "1.0.1" test-rules = "1.6.1" test-runner = "1.6.2" uiautomator = "2.3.0" -work-runtime-ktx = "2.9.1" -splashscreen = "1.0.1" +work-runtime-ktx = "2.10.0" [libraries] activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } @@ -76,16 +80,20 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = " lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } material3 = { group = "androidx.compose.material3", name = "material3" } material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } +mockito-kotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version.ref = "mockito-kotlin" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } opencsv = { module = "com.opencsv:opencsv", version.ref = "opencsv" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } +splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } test-core-ktx = { module = "androidx.test:core-ktx", version.ref = "androidx-test-core-ktx" } ui = { group = "androidx.compose.ui", name = "ui" } ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } [plugins] app-cash-paparazzi = { id = "app.cash.paparazzi", version.ref = "paparazzi"}