Skip to content

Commit

Permalink
🏗️Restructured the app using Jetpack Navigation
Browse files Browse the repository at this point in the history
lorenzovngl committed Dec 3, 2023
1 parent cb85980 commit 8e56514
Showing 48 changed files with 1,658 additions and 1,657 deletions.
5 changes: 2 additions & 3 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -99,6 +99,7 @@ dependencies {
implementation(libs.core.ktx)
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.activity.compose)
implementation(libs.androidx.navigation.compose)
testImplementation(libs.junit)
androidTestImplementation(libs.test.core.ktx)
androidTestImplementation(libs.androidx.test.ext.junit)
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.rule.GrantPermissionRule
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.view.activity.MainActivity
import com.lorenzovainigli.foodexpirationdates.view.composable.DateFormatDialog
import com.lorenzovainigli.foodexpirationdates.view.composable.DateFormatRow
import org.junit.Rule
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.GrantPermissionRule
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.view.MainActivity
import com.lorenzovainigli.foodexpirationdates.view.composable.FOOD_CARD
import org.junit.Before
import org.junit.Rule
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import com.lorenzovainigli.foodexpirationdates.view.activity.MainActivity
import com.lorenzovainigli.foodexpirationdates.view.MainActivity
import junit.framework.TestCase.assertTrue
import org.junit.Rule
import org.junit.Test
12 changes: 1 addition & 11 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
android:theme="@style/Theme.FoodExpirationDates"
tools:targetApi="31">
<activity
android:name=".view.activity.MainActivity"
android:name=".view.MainActivity"
android:exported="true"
android:theme="@style/Theme.CustomSplashScreenTheme">
<intent-filter>
@@ -25,16 +25,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".view.activity.InsertActivity">
</activity>
<activity
android:exported="true"
android:name=".view.activity.InfoActivity">
</activity>
<activity
android:name=".view.activity.SettingsActivity">
</activity>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
Original file line number Diff line number Diff line change
@@ -10,7 +10,13 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.lorenzovainigli.foodexpirationdates.BuildConfig
import com.lorenzovainigli.foodexpirationdates.model.worker.CheckExpirationsWorker
import java.util.Calendar
import java.util.concurrent.TimeUnit

class NotificationManager {

@@ -59,6 +65,58 @@ class NotificationManager {
}
}

fun scheduleDailyNotification(context: Context, hour: Int, minute: Int) {
val currentTime = Calendar.getInstance()
val dueTime = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0)
}
if (currentTime > dueTime)
dueTime.add(Calendar.DAY_OF_MONTH, 1)
val initialDelay = dueTime.timeInMillis - currentTime.timeInMillis
val formattedTime = formatTimeDifference(initialDelay)
if (BuildConfig.DEBUG) {
Toast.makeText(
context,
"Notification in $formattedTime",
Toast.LENGTH_SHORT
).show()
}
val workRequest = PeriodicWorkRequestBuilder<CheckExpirationsWorker>(
1, TimeUnit.DAYS
)
.setInitialDelay(initialDelay, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
CheckExpirationsWorker.workerID,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
workRequest
)
}

private fun formatTimeDifference(timeDifference: Long): String {
val days = TimeUnit.MILLISECONDS.toDays(timeDifference)
val hours = TimeUnit.MILLISECONDS.toHours(timeDifference) % 24
val minutes = TimeUnit.MILLISECONDS.toMinutes(timeDifference) % 60
val seconds = TimeUnit.MILLISECONDS.toSeconds(timeDifference) % 60
val formattedTime = StringBuilder()
if (days > 0) {
formattedTime.append("$days day${if (days > 1) "s" else ""} ")
}
if (hours > 0) {
formattedTime.append("$hours hour${if (hours > 1) "s" else ""} ")
}
if (minutes > 0) {
formattedTime.append("$minutes minute${if (minutes > 1) "s" else ""} ")
}
if (seconds > 0) {
formattedTime.append("$seconds second${if (seconds > 1) "s" else ""} ")
}
return formattedTime.toString().trim()
}

}

}
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.model.repository.ExpirationDateRepository
import com.lorenzovainigli.foodexpirationdates.view.activity.MainActivity
import com.lorenzovainigli.foodexpirationdates.view.MainActivity
import kotlinx.coroutines.flow.first
import java.util.Calendar
import javax.inject.Inject
Original file line number Diff line number Diff line change
@@ -5,8 +5,14 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository
import com.lorenzovainigli.foodexpirationdates.viewmodel.PreferencesViewModel

private val LightColors = lightColorScheme(
primary = md_theme_light_primary,
@@ -85,16 +91,27 @@ fun FoodExpirationDatesTheme(
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}

darkTheme -> DarkColors
else -> LightColors
}

val systemUiController = rememberSystemUiController()
//val useDarkIcons = isSystemInDarkTheme()
var useDarkIcons: Boolean = !isSystemInDarkTheme()
if (LocalViewModelStoreOwner.current != null) {
val prefsViewModel: PreferencesViewModel = viewModel()
useDarkIcons =
when (prefsViewModel.getThemeMode(LocalContext.current).collectAsState().value) {
PreferencesRepository.Companion.ThemeMode.LIGHT.ordinal -> true
PreferencesRepository.Companion.ThemeMode.DARK.ordinal -> false
else -> !isSystemInDarkTheme()
}
}

SideEffect {
systemUiController.setSystemBarsColor(
color = colorScheme.surface
darkIcons = useDarkIcons,
color = Color.Transparent
)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.lorenzovainigli.foodexpirationdates.view

import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.collectAsState
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.di.AppModule
import com.lorenzovainigli.foodexpirationdates.di.DaggerAppComponent
import com.lorenzovainigli.foodexpirationdates.model.NotificationManager
import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.composable.MyScaffold
import com.lorenzovainigli.foodexpirationdates.viewmodel.ExpirationDatesViewModel
import com.lorenzovainigli.foodexpirationdates.viewmodel.PreferencesViewModel
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

val viewModel: ExpirationDatesViewModel by viewModels()
val preferencesViewModel: PreferencesViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)

val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenLoading.value }

DaggerAppComponent.builder()
.appModule(AppModule())
.build()
NotificationManager.setupNotificationChannel(this)
}

@RequiresApi(Build.VERSION_CODES.O)
override fun onResume() {
super.onResume()
val context = this
setContent {
val viewModel: ExpirationDatesViewModel = viewModel()
val prefsViewModel: PreferencesViewModel = viewModel()
val darkThemeState = prefsViewModel.getThemeMode(context).collectAsState().value
val dynamicColorsState = prefsViewModel.getDynamicColors(context).collectAsState().value
val isInDarkTheme = when (darkThemeState) {
PreferencesRepository.Companion.ThemeMode.LIGHT.ordinal -> false
PreferencesRepository.Companion.ThemeMode.DARK.ordinal -> true
else -> isSystemInDarkTheme()
}
FoodExpirationDatesTheme(
darkTheme = isInDarkTheme,
dynamicColor = dynamicColorsState
) {
Surface(
modifier = Modifier
.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val navController = rememberNavController()
val showSnackbar = remember {
mutableStateOf(false)
}
MyScaffold(
activity = this,
navController = navController,
showSnackbar = showSnackbar
) {
Navigation(
activity = this,
navController = navController,
showSnackbar = showSnackbar
)
}
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.lorenzovainigli.foodexpirationdates.view

import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.InfoScreen
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.InsertScreen
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.MainScreen
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.Screen
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.SettingsScreen

@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun Navigation(
activity: MainActivity? = null,
navController: NavHostController,
showSnackbar: MutableState<Boolean>
) {
NavHost(
modifier = Modifier.fillMaxSize(),
navController = navController,
startDestination = Screen.MainScreen.route
) {
composable(route = Screen.MainScreen.route) {
MainScreen(
activity = activity,
navController = navController,
showSnackbar = showSnackbar
)
}
composable(
route = Screen.InsertScreen.route + "?itemId={itemId}",
arguments = listOf(
navArgument("itemId"){
type = NavType.StringType
nullable = true
}
)
){ entry ->
InsertScreen(
activity = activity,
navController = navController,
itemId = entry.arguments?.getString("itemId")
)
}
composable(route = Screen.AboutScreen.route){
InfoScreen()
}
composable(route = Screen.SettingsScreen.route){
SettingsScreen(activity = activity)
}
}
}

//@RequiresApi(Build.VERSION_CODES.O)
//@Preview
//@Composable
//fun NavigationPreview(){
// FoodExpirationDatesTheme {
// Surface(modifier = Modifier.fillMaxSize()) {
// Navigation(
// navController = rememberNavController(),
// coroutineScope = rememberCoroutineScope(),
// snackbarHostState = SnackbarHostState()
// )
// }
// }
//}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -18,7 +18,8 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asComposeColorFilter
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewDynamicColors
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
@@ -69,7 +70,8 @@ fun AppIcon(
}
}

@Preview
@PreviewLightDark
@PreviewDynamicColors
@Composable
fun AppIconPreview() {
FoodExpirationDatesTheme {
@@ -78,6 +80,8 @@ fun AppIconPreview() {
) {
Column {
AppIcon(size = 48.dp)
AppIcon(size = 48.dp, color = MaterialTheme.colorScheme.secondary)
AppIcon(size = 48.dp, color = MaterialTheme.colorScheme.tertiary)
}
}
}
Original file line number Diff line number Diff line change
@@ -16,10 +16,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isUnspecified
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.preview.DefaultPreviews

@Composable
fun AutoResizedText(
@@ -61,7 +61,7 @@ fun AutoResizedText(
)
}

@DefaultPreviews
@Preview
@Composable
fun AutoResizedTextPreview(){
FoodExpirationDatesTheme {
Original file line number Diff line number Diff line change
@@ -22,7 +22,6 @@ import androidx.compose.ui.window.Dialog
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.preview.DefaultPreviews
import com.lorenzovainigli.foodexpirationdates.view.preview.LanguagePreviews
import java.text.SimpleDateFormat
import java.util.Calendar
@@ -105,7 +104,6 @@ fun DateFormatRow(
)
}

@DefaultPreviews
@LanguagePreviews
@Composable
fun DateFormatDialogPreview(){
Original file line number Diff line number Diff line change
@@ -35,17 +35,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository
import com.lorenzovainigli.foodexpirationdates.model.entity.ExpirationDate
import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository
import com.lorenzovainigli.foodexpirationdates.ui.theme.DarkOrange
import com.lorenzovainigli.foodexpirationdates.ui.theme.DarkRed
import com.lorenzovainigli.foodexpirationdates.ui.theme.DarkYellow
import com.lorenzovainigli.foodexpirationdates.ui.theme.LightOrange
import com.lorenzovainigli.foodexpirationdates.ui.theme.LightRed
import com.lorenzovainigli.foodexpirationdates.ui.theme.LightYellow
import com.lorenzovainigli.foodexpirationdates.ui.theme.TonalElevation
import com.lorenzovainigli.foodexpirationdates.view.composable.activity.getItemsForPreview
import com.lorenzovainigli.foodexpirationdates.view.preview.DefaultPreviews
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.getItemsForPreview
import com.lorenzovainigli.foodexpirationdates.view.preview.LanguagePreviews
import java.text.SimpleDateFormat
import java.util.Calendar
@@ -104,7 +103,7 @@ fun FoodCard(
.testTag(FOOD_CARD)
.padding(4.dp)
.clip(RoundedCornerShape(10.dp)),
tonalElevation = TonalElevation.level5()
tonalElevation = TonalElevation.level1()
) {
Row(
modifier = Modifier
@@ -155,7 +154,6 @@ fun FoodCard(
}
}

@DefaultPreviews
@LanguagePreviews
@Composable
fun FoodCardPreview() {

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.lorenzovainigli.foodexpirationdates.view.composable

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.List
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.Screen
import com.lorenzovainigli.foodexpirationdates.view.preview.LanguagePreviews

@Composable
fun MyBottomAppBar(
navController: NavHostController,
currentBackStackEntry: NavBackStackEntry?
){
val navigationItems = listOf(
NavigationItem(
label = stringResource(id = R.string.about_this_app),
route = Screen.AboutScreen.route,
selectedIcon = Icons.Filled.Info,
unselectedIcon = Icons.Outlined.Info
),
NavigationItem(
label = stringResource(id = R.string.list),
route = Screen.MainScreen.route,
selectedIcon = Icons.Filled.List,
unselectedIcon = Icons.Outlined.List
),
NavigationItem(
label = stringResource(id = R.string.settings),
route = Screen.SettingsScreen.route,
selectedIcon = Icons.Filled.Settings,
unselectedIcon = Icons.Outlined.Settings
)
)
NavigationBar {
var selectedItem = when (currentBackStackEntry?.destination?.route) {
Screen.AboutScreen.route -> 0
Screen.SettingsScreen.route -> 2
else -> 1
}
navigationItems.forEachIndexed { index, item ->
NavigationBarItem(
selected = selectedItem == index,
onClick = {
selectedItem = index
navController.navigate(item.route)
},
icon = {
Icon(
imageVector = when (selectedItem == index) {
true -> item.selectedIcon
else -> item.unselectedIcon
},
contentDescription = item.label
)
},
label = {
Text(
text = item.label,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
}

@PreviewLightDark
@LanguagePreviews
@Composable
fun MyBottomAppBarPreview(){
FoodExpirationDatesTheme {
Surface {
MyBottomAppBar(
navController = rememberNavController(),
currentBackStackEntry = null
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.lorenzovainigli.foodexpirationdates.view.composable

import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.view.MainActivity
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.Screen
import kotlinx.coroutines.launch

data class NavigationItem(
val label: String,
val route: String,
val selectedIcon: ImageVector,
val unselectedIcon: ImageVector
)

@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScaffold(
activity: MainActivity? = null,
navController: NavHostController,
showSnackbar: MutableState<Boolean>,
content: @Composable () -> Unit
) {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val context = LocalContext.current
if (showSnackbar.value){
coroutineScope.launch {
try {
val deletedItem = activity?.viewModel?.deletedItem?.value
val snackbarResult =
snackbarHostState.showSnackbar(
message = context.resources.getString(
R.string.x_deleted,
deletedItem?.foodName ?: ""
),
actionLabel = context.resources.getString(R.string.undo),
duration = SnackbarDuration.Short
)
when (snackbarResult) {
SnackbarResult.ActionPerformed -> {
deletedItem?.let {
activity.viewModel.addExpirationDate(it)
}
}

else -> {
Log.i("ERROR", "Error showing snackbar")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
showSnackbar.value = false
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
MyTopAppBar(
activity = activity,
title = when (currentBackStackEntry?.destination?.route) {
Screen.AboutScreen.route -> stringResource(id = R.string.about_this_app)
Screen.InsertScreen.route -> stringResource(id = R.string.insert)
Screen.SettingsScreen.route -> stringResource(id = R.string.settings)
else -> stringResource(id = R.string.app_name)
},
actions = {
AppIcon(size = 48.dp)
},
navigationIcon = {
if (currentBackStackEntry?.destination?.route == Screen.InsertScreen.route) {
Icon(
modifier = Modifier.clickable {
navController.popBackStack()
},
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.back),
tint = MaterialTheme.colorScheme.primary
)
}
},
scrollBehavior = scrollBehavior
)
},
bottomBar = {
MyBottomAppBar(
navController = navController,
currentBackStackEntry = currentBackStackEntry
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
) {
content()
}
}
}

//@RequiresApi(Build.VERSION_CODES.O)
//@PreviewLightDark
//@PreviewScreenSizes
//@Composable
//fun MyScaffoldPreview() {
// val navController = rememberNavController()
// FoodExpirationDatesTheme {
// Surface(modifier = Modifier.fillMaxSize()) {
// MyScaffold(
// navController = navController
// )
// }
// }
//}
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@
package com.lorenzovainigli.foodexpirationdates.view.composable

import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.MainActivity
import com.lorenzovainigli.foodexpirationdates.viewmodel.PreferencesViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyTopAppBar(
activity: MainActivity?,
title: String,
actions: @Composable RowScope.() -> Unit = {},
navigationIcon: @Composable () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null,
prefsViewModel: PreferencesViewModel? = null
) {
val context = LocalContext.current
val topBarFontState = prefsViewModel?.getTopBarFont(context)?.collectAsState()?.value
val topBarFontState = activity?.preferencesViewModel?.getTopBarFont(context)?.collectAsState()?.value
?: PreferencesRepository.Companion.TopBarFont.NORMAL.ordinal

LargeTopAppBar(
modifier = Modifier
.padding(bottom = 4.dp)
.shadow(
elevation = 4.dp,
spotColor = MaterialTheme.colorScheme.primary
),
.padding(bottom = 4.dp),
title = {
Text(
text = title,
@@ -51,24 +55,27 @@ fun MyTopAppBar(
},
actions = actions,
navigationIcon = navigationIcon,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface
),
scrollBehavior = scrollBehavior
)
}

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@PreviewLightDark
@Composable
fun MyTopAppBarPreview(){
FoodExpirationDatesTheme(
dynamicColor = false
) {
MyTopAppBar(
title = "Title"
activity = null,
title = "Lorem Ipsum",
navigationIcon = {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.back),
tint = MaterialTheme.colorScheme.primary
)
}
)
Spacer(modifier = Modifier.fillMaxHeight())
}
}
Original file line number Diff line number Diff line change
@@ -8,17 +8,21 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Email
import androidx.compose.material3.*
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.preview.DefaultPreviews

@Composable
fun TextIconButton(
@@ -63,7 +67,7 @@ fun TextIconButton(
}
}

@DefaultPreviews
@Preview
@Composable
fun TextIconButtonPreview(){
FoodExpirationDatesTheme {

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package com.lorenzovainigli.foodexpirationdates.view.composable.screen

import android.content.Context
import android.content.Intent
import androidx.compose.foundation.Image
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.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Email
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.lorenzovainigli.foodexpirationdates.BuildConfig
import com.lorenzovainigli.foodexpirationdates.DEVELOPER_EMAIL
import com.lorenzovainigli.foodexpirationdates.GITHUB_URL
import com.lorenzovainigli.foodexpirationdates.PLAY_STORE_URL
import com.lorenzovainigli.foodexpirationdates.PRIVACY_POLICY_URL
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.model.contributors
import com.lorenzovainigli.foodexpirationdates.view.composable.TextIconButton
import com.lorenzovainigli.foodexpirationdates.view.composable.TextIconButtonData

@Composable
fun InfoScreen(
context: Context = LocalContext.current
) {
val uriHandler = LocalUriHandler.current
val features = stringArrayResource(id = R.array.features)
.joinToString(separator = "\n") { it.asListItem() }
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(10.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Image(
modifier = Modifier
.fillMaxWidth()
.height(128.dp)
.padding(top = 16.dp),
painter = painterResource(id = R.drawable.fed_icon),
alignment = Alignment.Center,
contentDescription = null
)
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally),
text = stringResource(id = R.string.app_name),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
style = MaterialTheme.typography.bodySmall,
text = stringResource(
id = R.string.version_x,
BuildConfig.VERSION_NAME
),
textAlign = TextAlign.Center
)
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(
id = R.string.app_description,
stringResource(id = R.string.app_name)
)
)
TextIconButton(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(256.dp),
onClick = {
uriHandler.openUri(
uri = "https://github.com/lorenzovngl/FoodExpirationDates"
)
},
imagePainter = painterResource(id = R.drawable.github),
text = stringResource(id = R.string.source_code)
)
Text(
modifier = Modifier.padding(top = 16.dp),
text = stringResource(id = R.string.features),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
Text(
modifier = Modifier.fillMaxWidth(),
text = features
)
/*TextIconButton(
modifier = Modifier.align(CenterHorizontally),
onClick = {
},
imagePainter = painterResource(id = R.drawable.bug_report),
contentDescription = "Star",
text = stringResource(id = R.string.report_a_bug)
)*/
Text(
modifier = Modifier.padding(top = 16.dp),
text = stringResource(id = R.string.support_this_project),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
arrayOf(
TextIconButtonData(
iconImageVector = Icons.Outlined.Star,
text = stringResource(id = R.string.leave_a_star_on_github),
onClick = {
uriHandler.openUri(
uri = GITHUB_URL
)
},
),
TextIconButtonData(
iconImageVector = Icons.Outlined.Edit,
text = stringResource(id = R.string.write_a_review),
onClick = {
uriHandler.openUri(
uri = PLAY_STORE_URL
)
},
),
TextIconButtonData(
iconImageVector = Icons.Outlined.Share,
text = stringResource(id = R.string.share),
onClick = {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, PLAY_STORE_URL)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
},
),
).forEach {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
TextIconButton(
modifier = Modifier.width(256.dp),
onClick = it.onClick,
iconImageVector = it.iconImageVector,
imagePainter = it.imagePainter,
text = it.text
)
}
}
ContactSection()
ContributorsList()
ClickableText(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 4.dp),
text = AnnotatedString(text = stringResource(id = R.string.privacy_policy)),
style = TextStyle.Default.copy(color = MaterialTheme.colorScheme.primary),
onClick = {
uriHandler.openUri(
uri = PRIVACY_POLICY_URL
)
}
)
}
}

@Composable
fun ContactSection(
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
Text(
modifier = Modifier.padding(top = 16.dp),
text = "Contacts",
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 8.dp),
text = stringResource(id = R.string.contacts_text)
)
val uriHandler = LocalUriHandler.current
TextIconButton(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(256.dp),
onClick = {
uriHandler.openUri(
uri = "mailto:$DEVELOPER_EMAIL"
)
},
iconImageVector = Icons.Outlined.Email,
text = stringResource(id = R.string.send_an_email)
)
}
}

@Composable
fun ContributorsList(
modifier: Modifier = Modifier
) {
val contributorsText = remember {
contributors.joinToString(separator = "\n") {
"${it.name} (@${it.username})".asListItem()
}
}
Column(modifier = modifier.fillMaxWidth()) {
Text(
modifier = Modifier.padding(top = 16.dp),
text = stringResource(id = R.string.contributors_list_title),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 8.dp),
text = stringResource(id = R.string.contributors_list_subtitle)
)
Text(
modifier = Modifier.fillMaxWidth(),
text = contributorsText
)
}
}

private fun String.asListItem() = "$this"

//@PreviewLightDark
//@Composable
//fun InfoScreenPreview() {
// FoodExpirationDatesTheme {
// Surface (modifier = Modifier.fillMaxHeight()) {
// InfoScreen()
// }
// }
//}

@Preview(showBackground = true)
@Composable
fun ContributorsListPreview() {
ContributorsList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package com.lorenzovainigli.foodexpirationdates.view.composable.screen

import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.model.entity.ExpirationDate
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.MainActivity
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InsertScreen(
activity: MainActivity? = null,
navController: NavController,
itemId: String? = null,
) {
val itemToEdit = itemId?.let { activity?.viewModel?.getDate(it.toInt()) }
var foodNameToEdit = ""
var expDate: Long? = null
if (itemToEdit != null) {
foodNameToEdit = itemToEdit.foodName
expDate = itemToEdit.expirationDate
}
var foodName by remember {
mutableStateOf(foodNameToEdit)
}
val datePickerState = rememberDatePickerState(expDate)
var isDialogOpen by remember {
mutableStateOf(false)
}
if (isDialogOpen) {
DatePickerDialog(
dismissButton = {
OutlinedButton(
onClick = { isDialogOpen = false },
border = BorderStroke(
1.dp,
MaterialTheme.colorScheme.tertiary
),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.tertiary
)
) {
Text(text = stringResource(id = R.string.cancel))
}
},
confirmButton = {
Button(
modifier = Modifier.testTag("Insert date"),
onClick = { isDialogOpen = false },
// colors = ButtonDefaults.buttonColors(
// containerColor = MaterialTheme.colorScheme.tertiary,
// contentColor = MaterialTheme.colorScheme.onTertiary
// )
) {
Text(text = stringResource(id = R.string.insert))
}
},
content = {
DatePicker(
state = datePickerState
)
},
onDismissRequest = {
isDialogOpen = false
}
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
TextField(
label = {
Text(
text = stringResource(id = R.string.food_name),
modifier = Modifier.fillMaxWidth()
)
},
value = foodName,
onValueChange = { newText ->
foodName = newText
},
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences
)
)
Spacer(modifier = Modifier.height(16.dp))
TextField(
modifier = Modifier.clickable(onClick = {
isDialogOpen = true
}),
enabled = false,
label = {
Text(
text = stringResource(id = R.string.expiration_date),
modifier = Modifier.fillMaxWidth()
)
},
value = if (datePickerState.selectedDateMillis == null) "" else {
// TODO What kind of date format is the best here?
val dateFormat = (DateFormat.getDateInstance(
DateFormat.MEDIUM, Locale.getDefault()
) as SimpleDateFormat).toLocalizedPattern()
val sdf = SimpleDateFormat(dateFormat, Locale.getDefault())
sdf.format(datePickerState.selectedDateMillis)
},
onValueChange = {},
colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
//For Icons
disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant,
)
)
Row {
OutlinedButton(
onClick = { navController.popBackStack() },
modifier = Modifier
.weight(0.5f)
.padding(top = 8.dp, end = 4.dp),
border = BorderStroke(
1.dp,
MaterialTheme.colorScheme.tertiary
),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.tertiary
),
) {
Text(text = stringResource(id = R.string.cancel))
}
Button(
modifier = Modifier
.testTag("Insert item")
.weight(0.5f)
.padding(top = 8.dp, start = 4.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.tertiary,
contentColor = MaterialTheme.colorScheme.onTertiary
),
onClick = {
try {
if (foodName.isNotEmpty()) {
if (datePickerState.selectedDateMillis != null) {
var id = 0
if (itemToEdit != null) {
id = itemId.toInt()
}
val entry = ExpirationDate(
id = id,
foodName = foodName,
expirationDate = datePickerState.selectedDateMillis!!
)
activity?.viewModel?.addExpirationDate(entry)
navController.popBackStack()
} else {
Toast.makeText(
activity,
R.string.please_select_a_date,
Toast.LENGTH_SHORT
).show()
}
} else {
Toast.makeText(
activity,
R.string.please_enter_a_food_name,
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Toast.makeText(
activity,
e.message,
Toast.LENGTH_SHORT
).show()
}
}
) {
if (itemToEdit != null)
Text(text = stringResource(id = R.string.update))
else
Text(text = stringResource(id = R.string.insert))
}
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
@PreviewLightDark
@Composable
fun InsertScreenPreview() {
FoodExpirationDatesTheme {
Surface {
InsertScreen(navController = rememberNavController())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package com.lorenzovainigli.foodexpirationdates.view.composable.screen

import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
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.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.model.entity.ExpirationDate
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.MainActivity
import com.lorenzovainigli.foodexpirationdates.view.composable.FoodCard
import java.util.Calendar
import kotlin.math.min

@Composable
fun MainScreen(
activity: MainActivity? = null,
navController: NavHostController,
showSnackbar: MutableState<Boolean>? = null
) {
Box(
modifier = Modifier
.padding(4.dp)
.fillMaxSize()
) {
val itemsState = activity?.viewModel?.getDates()?.collectAsState(emptyList())
val items = itemsState?.value ?: getItemsForPreview(LocalContext.current)
if (items.isNotEmpty()) {
ListOfItems(
activity = activity,
items = items,
showSnackbar = showSnackbar,
navController = navController
)
} else {
EmptyList()
}
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(12.dp),
onClick = {
navController.navigate(Screen.InsertScreen.route)
},
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(id = R.string.insert)
)
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
@Preview
@Composable
fun MainScreenPreview() {
FoodExpirationDatesTheme {
Surface {
MainScreen(
navController = rememberNavController()
)
}
}
}

@Composable
fun ListOfItems(
activity: MainActivity? = null,
items: List<ExpirationDate>,
navController: NavHostController,
showSnackbar: MutableState<Boolean>?
) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
for (item in items) {
FoodCard(
item = item,
onClickEdit = {
navController.navigate(Screen.InsertScreen.route + "?itemId=${item.id}")
},
onClickDelete = {
showSnackbar?.value = true
activity?.viewModel?.deleteExpirationDate(item)
}
)
}
}
}

@Composable
@Preview
fun ListOfItemsPreview() {
FoodExpirationDatesTheme {
Surface {
ListOfItems(
items = getItemsForPreview(LocalContext.current),
navController = rememberNavController(),
showSnackbar = null
)
}
}
}

@Composable
fun EmptyList() {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.CenterVertically
),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.no_items_found),
style = MaterialTheme.typography.displaySmall,
color = Color.Gray.copy(alpha = 0.5f),
textAlign = TextAlign.Center
)
Text(
text = stringResource(id = R.string.please_insert_one),
textAlign = TextAlign.Center
)
}
}
}

@Composable
@Preview
fun EmptyListPreview() {
FoodExpirationDatesTheme {
Surface {
EmptyList()
}
}
}

fun getItemsForPreview(context: Context): List<ExpirationDate> {
val items = ArrayList<ExpirationDate>()
val foods = context.resources.getStringArray(R.array.example_foods)
val daysLeft = arrayOf(-1, 0, 1, 3, 7, 10, 30)
for (i in 0 until min(foods.size, daysLeft.size)) {
val cal = Calendar.getInstance()
cal.add(Calendar.DATE, daysLeft[i])
items.add(
ExpirationDate(
id = 0,
foodName = foods[i],
expirationDate = cal.time.time
)
)
}
return items
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.lorenzovainigli.foodexpirationdates.view.composable.screen

sealed class Screen(val route: String) {
data object MainScreen : Screen("main_screen")
data object InsertScreen : Screen("insert_screen")
data object AboutScreen : Screen("about_screen")
data object SettingsScreen : Screen("setting_screen")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package com.lorenzovainigli.foodexpirationdates.view.composable.screen

import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.model.NotificationManager
import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository
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.NotificationTimeBottomSheet
import com.lorenzovainigli.foodexpirationdates.view.composable.SettingsItem
import java.text.SimpleDateFormat
import java.util.Calendar

@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
activity: MainActivity? = null
) {
val context = LocalContext.current
val prefsViewModel = activity?.preferencesViewModel
val darkThemeState = prefsViewModel?.getThemeMode(context)?.collectAsState()?.value
?: PreferencesRepository.Companion.ThemeMode.SYSTEM
val dynamicColorsState = prefsViewModel?.getDynamicColors(context)?.collectAsState()?.value
?: false
val topBarFontState = prefsViewModel?.getTopBarFont(context)?.collectAsState()?.value
?: PreferencesRepository.Companion.TopBarFont.NORMAL.ordinal

val dateFormat = prefsViewModel?.getDateFormat(context)?.collectAsState()?.value
?: PreferencesRepository.getAvailOtherDateFormats()[0]
var sdf = SimpleDateFormat(dateFormat, context.resources.configuration.locales[0])
var isDateFormatDialogOpened by remember {
mutableStateOf(false)
}

val notificationTimeHour =
prefsViewModel?.getNotificationTimeHour(context)?.collectAsState()?.value
?: 11
val notificationTimeMinute =
prefsViewModel?.getNotificationTimeMinute(context)?.collectAsState()?.value
?: 0
val timePickerState =
rememberTimePickerState(notificationTimeHour, notificationTimeMinute, true)
var isNotificationTimeBottomSheetOpen by remember {
mutableStateOf(false)
}
prefsViewModel?.let {
DateFormatDialog(
isDialogOpen = isDateFormatDialogOpened,
onDismissRequest = {
sdf = SimpleDateFormat(dateFormat, context.resources.configuration.locales[0])
isDateFormatDialogOpened = false
},
onClickDate = it::setDateFormat
)
}
if (isNotificationTimeBottomSheetOpen) {
NotificationTimeBottomSheet(
timePickerState = timePickerState,
onDismissRequest = {
prefsViewModel?.setNotificationTime(
context,
timePickerState.hour, timePickerState.minute
)
NotificationManager.scheduleDailyNotification(
context,
timePickerState.hour,
timePickerState.minute
)
isNotificationTimeBottomSheetOpen = false
}
)
}
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(10.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = stringResource(R.string.behaviour),
style = MaterialTheme.typography.labelLarge
)
SettingsItem(
label = stringResource(id = R.string.date_format)
) {
ClickableText(
modifier = Modifier.testTag(stringResource(id = R.string.date_format)),
text = AnnotatedString(sdf.format(Calendar.getInstance().time)),
style = MaterialTheme.typography.headlineMedium.copy(color = MaterialTheme.colorScheme.onSurface),
onClick = {
isDateFormatDialogOpened = true
}
)
}
SettingsItem(
label = stringResource(R.string.notification_time)
) {
var text = ""
if (timePickerState.hour < 10) {
text += "0"
}
text = timePickerState.hour.toString() + ":"
if (timePickerState.minute < 10) {
text += "0"
}
text += timePickerState.minute.toString()
ClickableText(
modifier = Modifier.testTag("Notification time"),
text = AnnotatedString(text),
style = MaterialTheme.typography.headlineMedium.copy(color = MaterialTheme.colorScheme.onSurface),
onClick = {
isNotificationTimeBottomSheetOpen = true
}
)
}
Text(
text = stringResource(R.string.appearance),
style = MaterialTheme.typography.labelLarge
)
SettingsItem(
label = stringResource(R.string.theme)
) {
PreferencesRepository.Companion.ThemeMode.entries.forEach {
Spacer(
modifier = Modifier
.fillMaxHeight()
.weight(0.1f)
)
if (it.ordinal == darkThemeState) {
Button(onClick = {}) {
AutoResizedText(
text = context.getString(it.label)
)
}
}
if (it.ordinal != darkThemeState) {
OutlinedButton(
onClick = {
prefsViewModel?.setThemeMode(context, it)
},
) {
AutoResizedText(
text = context.getString(it.label)
)
}
}
Spacer(
modifier = Modifier
.fillMaxHeight()
.weight(0.1f)
)
}
}
SettingsItem(
label = stringResource(R.string.dynamic_colors)
) {
Spacer(
Modifier
.weight(1f)
.fillMaxHeight()
)
Switch(
checked = dynamicColorsState,
onCheckedChange = {
prefsViewModel?.setDynamicColors(context, it)
}
)
}
SettingsItem(
label = stringResource(R.string.top_bar_font_style)
) {
PreferencesRepository.Companion.TopBarFont.entries.forEach { topBarFont ->
Spacer(
modifier = Modifier
.fillMaxHeight()
.weight(0.1f)
)
if (topBarFont.ordinal != topBarFontState) {
OutlinedButton(
onClick = {
prefsViewModel?.setTopBarFont(context, topBarFont)
},
) {
AutoResizedText(
text = context.getString(topBarFont.label)
)
}
}
if (topBarFont.ordinal == topBarFontState) {
Button(onClick = {}) {
AutoResizedText(
text = context.getString(topBarFont.label)
)
}
}
}
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
@Preview(showBackground = true)
@Composable
fun SettingsScreenPreview() {
SettingsScreen()
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,103 @@
package com.lorenzovainigli.foodexpirationdates.view.preview

import android.content.res.Configuration
import androidx.compose.ui.tooling.preview.Preview
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.tooling.preview.PreviewDynamicColors
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.navigation.compose.rememberNavController
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.InfoScreen
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.MainScreen
import com.lorenzovainigli.foodexpirationdates.view.composable.MyScaffold
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.InsertScreen
import com.lorenzovainigli.foodexpirationdates.view.composable.screen.SettingsScreen

@Preview(name = "Light mode")
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
annotation class DefaultPreviews
class DefaultPreviews {
@RequiresApi(Build.VERSION_CODES.O)
@PreviewLightDark
@Composable
fun MainScreenPreview() {
FoodExpirationDatesTheme(
dynamicColor = false
) {
val navController = rememberNavController()
val showSnackbar = remember {
mutableStateOf(false)
}
MyScaffold(navController = navController, showSnackbar = showSnackbar) {
MainScreen(navController = navController)
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
@PreviewLightDark
@PreviewDynamicColors
@Composable
fun MainScreenDynamicColorsPreview() {
FoodExpirationDatesTheme {
val navController = rememberNavController()
val showSnackbar = remember {
mutableStateOf(false)
}
MyScaffold(navController = navController, showSnackbar = showSnackbar) {
MainScreen(navController = navController)
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
@PreviewLightDark
@Composable
fun InsertScreenPreview() {
FoodExpirationDatesTheme(
dynamicColor = false
) {
val navController = rememberNavController()
val showSnackbar = remember {
mutableStateOf(false)
}
MyScaffold(navController = navController, showSnackbar = showSnackbar) {
InsertScreen(navController = navController)
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
@PreviewLightDark
@Composable
fun SettingsScreenPreview() {
FoodExpirationDatesTheme(
dynamicColor = false
) {
val navController = rememberNavController()
val showSnackbar = remember {
mutableStateOf(false)
}
MyScaffold(navController = navController, showSnackbar = showSnackbar) {
SettingsScreen()
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
@PreviewLightDark
@Composable
fun InfoScreenPreview() {
FoodExpirationDatesTheme(
dynamicColor = false
) {
val navController = rememberNavController()
val showSnackbar = remember {
mutableStateOf(false)
}
MyScaffold(navController = navController, showSnackbar = showSnackbar) {
InfoScreen()
}
}
}

}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -2,10 +2,12 @@ package com.lorenzovainigli.foodexpirationdates.view.preview

import androidx.compose.ui.tooling.preview.Preview

@Preview(name = "Italian", locale = "it", showBackground = true)
@Preview(name = "Arabic", locale = "ar", showBackground = true)
@Preview(name = "French", locale = "fr", showBackground = true)
@Preview(name = "German", locale = "de", showBackground = true)
@Preview(name = "Hindi", locale = "hi", showBackground = true)
@Preview(name = "Indonesian", locale = "in", showBackground = true)
@Preview(name = "Italian", locale = "it", showBackground = true)
@Preview(name = "Japanese", locale = "ja", showBackground = true)
@Preview(name = "Spanish", locale = "es", showBackground = true)
annotation class LanguagePreviews
Original file line number Diff line number Diff line change
@@ -25,6 +25,9 @@ class ExpirationDatesViewModel @Inject constructor(
private val _isSplashScreenLoading: MutableState<Boolean> = mutableStateOf(value = true)
val isSplashScreenLoading: State<Boolean> = _isSplashScreenLoading

private val _deletedItem: MutableState<ExpirationDate?> = mutableStateOf(value = null)
val deletedItem: State<ExpirationDate?> = _deletedItem

fun getDates(): Flow<List<ExpirationDate>> {
viewModelScope.launch {
_isSplashScreenLoading.value = true
@@ -57,5 +60,6 @@ class ExpirationDatesViewModel @Inject constructor(
repository.deleteExpirationDate(expirationDate)
expirationDates = repository.getAll()
}
_deletedItem.value = expirationDate
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
@@ -54,6 +54,7 @@
<string name="appearance">Aussehen</string>
<string name="send_an_email">E-Mail senden</string>
<string name="contacts_text">Wenn Sie weitere Informationen zu dieser Anwendung erhalten, Feedback senden, einen Fehler melden, eine Funktion anfordern oder einfach den Entwickler kontaktieren möchten, senden Sie bitte eine E-Mail über die folgende Schaltfläche.\nWenn Sie Fehlerberichte und Funktionsanfragen haben, können Sie auch ein Issue im GitHub-Repository dieser Anwendung öffnen.</string>
<string name="list">Liste</string>

<string-array name="features">
<item>Anzeige einer Liste mit Lebensmittelverfallsdaten in aufsteigender zeitlicher Reihenfolge.</item>
1 change: 1 addition & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@
<string name="appearance">Apariencia</string>
<string name="send_an_email">Enviar un e-mail</string>
<string name="contacts_text">Si desea obtener más información sobre esta aplicación, enviar comentarios, informar de un error, solicitar una función o simplemente ponerse en contacto con el desarrollador, envíe un correo electrónico utilizando el siguiente botón.\nEn caso de informes de errores y solicitudes de funciones, puede abrir una incidencia en el repositorio de GitHub de esta aplicación.</string>
<string name="list">Lista</string>

<string-array name="features">
<item>Muestra una lista con las fechas de caducidad de los alimentos, ordenadas cronológicamente de forma ascendente.</item>
1 change: 1 addition & 0 deletions app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@
<string name="top_bar_font_style">Style de police de la barre supérieure</string>
<string name="behaviour">Comportement</string>
<string name="appearance">Apparence</string>
<string name="list">Liste</string>

<string-array name="features">
<item>Afficher une liste d\'aliments avec des dates d\'expiration dans l\'ordre croissant du temps.</item>
Loading

0 comments on commit 8e56514

Please sign in to comment.