diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a7cdee8..c42a35f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,14 +51,14 @@ android { } dependencies { - implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") - implementation("androidx.activity:activity-compose:1.8.0") + implementation("androidx.activity:activity-compose:1.8.1") implementation(platform("androidx.compose:compose-bom:2023.03.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") - implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material3:material3:1.1.2") implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.work:work-runtime-ktx:2.8.1") diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/DataStore.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/DataStore.kt index 22d3187..d040a39 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/DataStore.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/DataStore.kt @@ -23,3 +23,8 @@ const val STATS_TOTAL_GOOD = "total_good" /** DataStore clef lié au nombre de questions mal répondu */ const val STATS_TOTAL_BAD = "total_bad" + +const val DELAY = "delay" + +const val HOUR = "hour" +const val MINUTE = "minute" diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/HomeScreen.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/HomeScreen.kt index 44d8990..b7db3c8 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/HomeScreen.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/HomeScreen.kt @@ -68,7 +68,7 @@ fun HomeScreen( val errorEntry by model.error - if (errorEntry != null) { + ShowDialog(errorEntry != null) { ErrorDialog( when (errorEntry!!) { ErrorsAjout.BAD_ENTRY -> context.getString(R.string.error_bad_entry) @@ -76,25 +76,30 @@ fun HomeScreen( }, model::cleanErrors ) } - - if (creationRequest) { - CreationDialog( - dismiss = model::dismissCreation, model = model - ) + ShowDialog(creationRequest) { CreationDialog(model::dismissCreation, model) } + ShowDialog(importationRequest) { ImportDialog(model::dismissImportation, model) } + ShowDialog(deletionRequest) { + DeletionDialog(model::dismissDeleteOne, model::deleteSelected) } - if (importationRequest) { - ImportDialog( - dismiss = model::dismissImportation, model = model - ) - } + Home( + padding, + navController, + model, + setOfQuestions, + currentSelection + ) +} - if (deletionRequest) { - DeletionDialog( - model::dismissDeleteOne, - model::deleteSelected - ) - } +@Composable +private fun Home( + padding: PaddingValues, + navController: NavController, + model: HomeViewModel, + setOfQuestions: List = listOf(), + currentSelection: SetQuestions? = null, +) { + val context = LocalContext.current Column( modifier = Modifier.padding(padding), horizontalAlignment = Alignment.CenterHorizontally @@ -146,7 +151,6 @@ private fun ActionRow(context: Context, model: HomeViewModel, navController: Nav } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun CreationDialog( dismiss: () -> Unit, model: HomeViewModel = viewModel() @@ -172,7 +176,7 @@ fun CreationDialog( }) } -@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class) +@OptIn(DelicateCoroutinesApi::class) @Composable fun ImportDialog( dismiss: () -> Unit, model: HomeViewModel diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/MainActivity.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/MainActivity.kt index a4b928d..65c24ff 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/MainActivity.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/MainActivity.kt @@ -1,9 +1,11 @@ package fr.uparis.diamantkennel.memorisationapplication import android.content.Context +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.BottomNavigation @@ -39,6 +41,7 @@ import fr.uparis.diamantkennel.memorisationapplication.ui.theme.MemorisationAppl val Context.dataStore: DataStore by preferencesDataStore(name = STATS) class MainActivity : ComponentActivity() { + @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { @@ -54,18 +57,20 @@ class MainActivity : ComponentActivity() { } } +@RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable fun MainScreenMainActivity() { MainScreen() } +@RequiresApi(Build.VERSION_CODES.TIRAMISU) @Preview(showBackground = true) @Composable fun MainScreenPreview() { MainScreen() } -@OptIn(ExperimentalMaterial3Api::class) +@RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable fun MainScreen() { val navController = rememberNavController() @@ -97,7 +102,7 @@ fun MainScreen() { ) } } - composable(SETTINGS) { SettingsScreen(padding) } + composable(SETTINGS) { SettingsScreen() } } } } @@ -135,3 +140,14 @@ fun BottomBar(navController: NavHostController) = ) }) } + +@Composable +fun ShowDialog( + condition: Boolean, + dialog: @Composable () -> T +) { + if (condition) { + dialog() + } +} + diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/MemoApplication.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/MemoApplication.kt index 0ad4780..64c2b4e 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/MemoApplication.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/MemoApplication.kt @@ -4,15 +4,10 @@ import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context -import android.content.pm.PackageManager import android.os.Build -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager import fr.uparis.diamantkennel.memorisationapplication.data.QuestionsDB -import java.util.Calendar -import java.util.concurrent.TimeUnit -const val CHANNEL_ID = "MY_CHANNEL_ID" +const val CHANNEL_ID = "REMINDERS" class MemoApplication : Application() { val database: QuestionsDB by lazy { QuestionsDB.getDataBase(this) } @@ -20,10 +15,6 @@ class MemoApplication : Application() { override fun onCreate() { super.onCreate() createChannel(this) - - if (this.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { - schedule() - } } private fun createChannel(c: Context) { @@ -39,25 +30,4 @@ class MemoApplication : Application() { notificationManager.createNotificationChannel(channel) } } - - private fun schedule() { - val wm = WorkManager.getInstance(this) - wm.cancelAllWork() - wm.enqueue(request(10, 45)) - } - - private fun request(h: Int, m: Int): PeriodicWorkRequest { - val now = Calendar.getInstance() - val target = Calendar.getInstance().apply { - set(Calendar.HOUR_OF_DAY, h) - set(Calendar.MINUTE, m) - } - if (target.before(now)) - target.add(Calendar.DAY_OF_YEAR, 1) - val delta = target.timeInMillis - now.timeInMillis - - return PeriodicWorkRequest.Builder(RappelWorker::class.java, 1, TimeUnit.DAYS) - .setInitialDelay(delta, TimeUnit.MILLISECONDS) - .build() - } } diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ModifySetScreen.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ModifySetScreen.kt index 518089f..a364795 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ModifySetScreen.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ModifySetScreen.kt @@ -36,7 +36,6 @@ import fr.uparis.diamantkennel.memorisationapplication.ui.ActionModifySet import fr.uparis.diamantkennel.memorisationapplication.ui.ModifySetViewModel import kotlin.text.Typography.ellipsis - @Composable fun ModifySetScreen( padding: PaddingValues, @@ -47,20 +46,33 @@ fun ModifySetScreen( model.setId.value = idSet model.updateQuestionList(idSet) - val context = LocalContext.current val currentSelection by model.selection val questions by model.questions.collectAsState(listOf()) - var action by model.action + val action by model.action - if (action == ActionModifySet.AJOUT || action == ActionModifySet.MODIFICATION) { - AjoutModifDialog(action, currentSelection, model::ajoutQuestion) - { action = ActionModifySet.AUCUN } + ShowDialog(action == ActionModifySet.AJOUT || action == ActionModifySet.MODIFICATION) { + AjoutModifDialog(action, currentSelection, model::ajoutQuestion, model::dismissAction) + } + ShowDialog(action == ActionModifySet.SUPPRIMER) { + RemoveDialog(model::removeQuestion, model::dismissAction) } - if (action == ActionModifySet.SUPPRIMER) { - RemoveDialog(model::removeQuestion) - { action = ActionModifySet.AUCUN } - } + Modify( + padding = padding, + model = model, + questions = questions, + currentSelection = currentSelection + ) +} + +@Composable +fun Modify( + padding: PaddingValues, + model: ModifySetViewModel, + questions: List, + currentSelection: Question? +) { + val context = LocalContext.current Column( modifier = Modifier.padding(padding), @@ -71,13 +83,13 @@ fun ModifySetScreen( Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Button( enabled = currentSelection != null, - onClick = { action = ActionModifySet.MODIFICATION }) { + onClick = model::modifAction) { Text(text = context.getString(R.string.modify)) } Spacer(modifier = Modifier.padding(2.dp)) - Button(onClick = { action = ActionModifySet.AJOUT }) { + Button(onClick = model::ajoutAction) { Text(text = context.getString(R.string.add)) } @@ -85,7 +97,7 @@ fun ModifySetScreen( Button( enabled = currentSelection != null, - onClick = { action = ActionModifySet.SUPPRIMER }) { + onClick = model::supprAction) { Text(text = context.getString(R.string.modify_button_delete)) } } diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/PlayScreen.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/PlayScreen.kt index 6c0589f..67e3f1b 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/PlayScreen.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/PlayScreen.kt @@ -9,15 +9,14 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -26,10 +25,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import fr.uparis.diamantkennel.memorisationapplication.data.Question import fr.uparis.diamantkennel.memorisationapplication.ui.AnswerType import fr.uparis.diamantkennel.memorisationapplication.ui.PlayViewModel -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PlayScreen( padding: PaddingValues, @@ -41,40 +40,54 @@ fun PlayScreen( // First update the list of questions model.updateQuestionList(idSet) - val context = LocalContext.current - val question by model.currentQuestion val reponse by model.proposedAnswer val correction by model.evaluatedAnswer - var giveup by model.showAnswer + val giveup by model.showAnswer val gameEnded by model.end + val delay by model.delay.collectAsState(initial = 5000) val cpt by model.compteurSb - if (correction != null) { - LaunchedEffect(cpt) { - model.sbUpdate() - snackbarHostState.showSnackbar( - when (correction!!) { - AnswerType.GOOD -> context.getString(R.string.good_answer) - AnswerType.BAD -> context.getString(R.string.bad_answer) - }, duration = SnackbarDuration.Short - ) - model.resetAfterSb() - } - } - if (gameEnded) { - EndDialog { navController.navigate(HOME) } - } + SnackbarAnswer( + model, + snackbarHostState, + cpt, + correction + ) - if (giveup && question != null) { + ShowDialog(gameEnded) { EndDialog { navController.navigate(HOME) } } + ShowDialog(giveup && question != null) { SolutionDialog(question!!.reponse, model::newQuestion) } // Update timer if needed - if (!model.isDelayElapsed() && question != null) { - model.updateTime(System.currentTimeMillis()) - } + model.updateTimer(delay) + + Play( + padding, + navController, + idSet, + model, + question, + reponse, + correction, + delay + ) +} + +@Composable +fun Play( + padding: PaddingValues, + navController: NavController, + idSet: Int, + model: PlayViewModel, + question: Question?, + reponse: String, + correction: AnswerType?, + delay: Int +) { + val context = LocalContext.current Column( modifier = Modifier.padding(padding), @@ -85,7 +98,7 @@ fun PlayScreen( Text(context.getString(R.string.no_question), fontSize = 30.sp) } } else { - Text(text = question!!.enonce, fontSize = 30.sp, textAlign = TextAlign.Center) + Text(text = question.enonce, fontSize = 30.sp, textAlign = TextAlign.Center) Spacer(modifier = Modifier.padding(top = 20.dp)) @@ -109,8 +122,9 @@ fun PlayScreen( } Button( - enabled = model.isDelayElapsed(), - onClick = { giveup = true }) { + enabled = model.isDelayElapsed(delay), + onClick = model::giveUp + ) { Text(text = context.getString(R.string.see_answer)) } } @@ -143,3 +157,25 @@ fun EndDialog(next: () -> Unit) = confirmButton = { Button(onClick = next) { Text(text = LocalContext.current.getString(R.string.ok)) } }) + +@Composable +fun SnackbarAnswer( + model: PlayViewModel, + snackbarHostState: SnackbarHostState, + cpt: Int, + correction: AnswerType? +) { + val context = LocalContext.current + if (correction != null) { + LaunchedEffect(cpt) { + model.sbUpdate() + snackbarHostState.showSnackbar( + when (correction) { + AnswerType.GOOD -> context.getString(R.string.good_answer) + AnswerType.BAD -> context.getString(R.string.bad_answer) + }, duration = SnackbarDuration.Short + ) + model.resetAfterSb() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/SettingsScreen.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/SettingsScreen.kt index 9ed1951..5962d02 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/SettingsScreen.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/SettingsScreen.kt @@ -1,45 +1,113 @@ package fr.uparis.diamantkennel.memorisationapplication +import android.content.Context import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues 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.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TimePicker +import androidx.compose.material3.TimePickerLayoutType +import androidx.compose.material3.TimePickerState +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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import androidx.lifecycle.viewmodel.compose.viewModel import fr.uparis.diamantkennel.memorisationapplication.ui.SettingsViewModel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking @RequiresApi(Build.VERSION_CODES.TIRAMISU) +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun SettingsScreen(padding: PaddingValues, model: SettingsViewModel = viewModel()) { +fun SettingsScreen(model: SettingsViewModel = viewModel()) { val context = LocalContext.current var deletionDBRequest by model.deletionDB var cleanStatRequest by model.deletionStat - var permissionNotif by model.gavePermissionNow + var choiceTimeNotifRequest by model.notif + var choiceDelayRequest by model.delayRequest + + val prefConfigTime = runBlocking { model.prefConfigTime.first() } + + val stateTime = rememberTimePickerState( + initialHour = prefConfigTime.hour, + initialMinute = prefConfigTime.minute, + is24Hour = true + ) + model.checkPermission(context) + ShowDialog(deletionDBRequest) { DeletionDBDialog(model::deleteDb, model::dismissDeletionDB) } + ShowDialog(cleanStatRequest) { CleanStatDialog(model::cleanStats, model::dismissDeletionStat) } + ShowDialog(choiceTimeNotifRequest) { + ChoiceTimeNotifDialog( + { model.choiceTimeNotif(stateTime, context) }, + model::dismissNotif, + stateTime + ) + } + ShowDialog(choiceDelayRequest) { + ChoiceDelayDialog( + { model.choiceDelay(it) }, + model::dismissDelayRequest + ) + } + + Settings( + model, + { choiceTimeNotifRequest = true }, + { deletionDBRequest = true }, + { cleanStatRequest = true }, + { choiceDelayRequest = true }, + model::resetDelay + ) +} + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@Composable +fun Settings( + model: SettingsViewModel = viewModel(), + onTimeNotifButtonClick: () -> Unit, + deletionDBRequest: () -> Unit, + cleanStatRequest: () -> Unit, + onDelayButtonClick: () -> Unit, + onResetButtonClick: () -> Unit, +) { + val context = LocalContext.current + + var permissionNotif by model.gavePermissionNow val permissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), ) { @@ -48,44 +116,34 @@ fun SettingsScreen(padding: PaddingValues, model: SettingsViewModel = viewModel( } } - if (deletionDBRequest) { - DeletionDBDialog(model::deleteDb) { deletionDBRequest = false } - } - - if (cleanStatRequest) { - CleanStatDialog(model::cleanStats) { cleanStatRequest = false } - } - Column( - modifier = Modifier.padding(padding), + modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Stats(model) - - Spacer(modifier = Modifier.padding(top = 10.dp)) - Divider(color = Color.Gray) - Spacer(modifier = Modifier.padding(top = 10.dp)) - - Text(text = "Gestion", fontSize = 30.sp) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Button( - onClick = { deletionDBRequest = true }, - colors = ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.red)) - ) { - Text(text = context.getString(R.string.main_button_deletebase)) - } - - Button( - onClick = { cleanStatRequest = true }, - colors = ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.red)) - ) { - Text(text = context.getString(R.string.clean_stat_button)) - } - } + Stats(model = model) + AddSpacedDivider() + NotificationSettings(context, model, permissionNotif, permissionLauncher, onTimeNotifButtonClick) + AddSpacedDivider() + GestionSettings(context, deletionDBRequest, cleanStatRequest) + AddSpacedDivider() + GameSettings(context, onDelayButtonClick, onResetButtonClick) + } +} +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@Composable +fun NotificationSettings( + context: Context, + model: SettingsViewModel, + permissionNotif: Boolean, + permissionLauncher: ActivityResultLauncher, + onTimeNotifButtonClick: () -> Unit, +) { + Text(text = context.getString(R.string.notification), fontSize = 30.sp) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { Button( enabled = !permissionNotif, onClick = { @@ -94,6 +152,59 @@ fun SettingsScreen(padding: PaddingValues, model: SettingsViewModel = viewModel( ) { Text(text = context.getString(R.string.permission_button)) } + Button( + enabled = permissionNotif, + onClick = onTimeNotifButtonClick + ) { + Text(text = context.getString(R.string.time_notif_button)) + } + } +} + +@Composable +fun GestionSettings(context: Context, deletionDBRequest: () -> Unit, cleanStatRequest: () -> Unit) { + Text(text = context.getString(R.string.gestion), fontSize = 30.sp) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Button( + onClick = deletionDBRequest, + colors = ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.red)) + ) { + Text(text = context.getString(R.string.main_button_deletebase)) + } + + Button( + onClick = cleanStatRequest, + colors = ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.red)) + ) { + Text(text = context.getString(R.string.clean_stat_button)) + } + } +} + +@Composable +fun GameSettings(context: Context, onDelayButtonClick: () -> Unit, onResetButtonClick: () -> Unit) { + Text(text = context.getString(R.string.game), fontSize = 30.sp) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Column ( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ){ + Button(onClick = onDelayButtonClick) { + Text(text = context.getString(R.string.choice_delay_button)) + } + Button( + onClick = onResetButtonClick, + colors = ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.red)) + ) { + Text(text = context.getString(R.string.reset_button)) + } + } } } @@ -144,3 +255,71 @@ fun CleanStatDialog(confirm: () -> Unit, dismiss: () -> Unit) = Button(onClick = confirm) { Text(text = LocalContext.current.getString(R.string.yes)) } }, dismissButton = { Button(onClick = dismiss) { Text(text = LocalContext.current.getString(R.string.no)) } }) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChoiceTimeNotifDialog(confirm: () -> Unit, dismiss: () -> Unit, state: TimePickerState) { + Dialog(onDismissRequest = dismiss) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .height(550.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = LocalContext.current.getString(R.string.time_notif), + fontSize = 30.sp, + textAlign = TextAlign.Center + ) + + TimePicker(state = state, layoutType = TimePickerLayoutType.Vertical) + + Button(onClick = confirm) { + Text(text = LocalContext.current.getString(R.string.confirm)) + } + } + } + } +} + +@Composable +fun ChoiceDelayDialog(confirm: (Int) -> Unit, dismiss: () -> Unit) { + var value by remember { mutableStateOf("") } + + AlertDialog(onDismissRequest = dismiss, + title = { Text(text = LocalContext.current.getString(R.string.choice_delay)) }, + text = { + OutlinedTextField( + value = value, + onValueChange = { + value = if (it.toIntOrNull() != null) { it } else "" + }, + label = { Text(text = LocalContext.current.getString(R.string.enter_integer)) }, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number + ), + singleLine = true + ) + }, + confirmButton = { + Button(onClick = { + if (value.isNotBlank()) { confirm(value.toInt()) } else dismiss() + }) { + Text(text = LocalContext.current.getString(R.string.confirm)) + } + } + ) +} + + +@Composable +fun AddSpacedDivider() { + Spacer(modifier = Modifier.padding(top = 20.dp)) + Divider(color = Color.Gray) + Spacer(modifier = Modifier.padding(top = 20.dp)) +} diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/TimeConfig.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/TimeConfig.kt new file mode 100644 index 0000000..eaf35c4 --- /dev/null +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/TimeConfig.kt @@ -0,0 +1,6 @@ +package fr.uparis.diamantkennel.memorisationapplication + +data class TimeConfig( + val hour: Int = 8, + val minute: Int = 0 +) diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/ModifySetViewModel.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/ModifySetViewModel.kt index e8638e7..c9a7c32 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/ModifySetViewModel.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/ModifySetViewModel.kt @@ -63,4 +63,18 @@ class ModifySetViewModel(application: Application) : AndroidViewModel(applicatio dao.deleteQuestion(selection.value!!) } } + + fun modifAction() { + action.value = ActionModifySet.MODIFICATION + } + fun ajoutAction() { + action.value = ActionModifySet.AJOUT + } + fun supprAction() { + action.value = ActionModifySet.SUPPRIMER + } + + fun dismissAction() { + action.value = ActionModifySet.AUCUN + } } diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/PlayViewModel.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/PlayViewModel.kt index f9e6c5a..5d2e71f 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/PlayViewModel.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/PlayViewModel.kt @@ -11,21 +11,26 @@ import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_BAD import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_DONE import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_GOOD import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_TRIED +import fr.uparis.diamantkennel.memorisationapplication.DELAY import fr.uparis.diamantkennel.memorisationapplication.data.Question import fr.uparis.diamantkennel.memorisationapplication.dataStore import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class PlayViewModel(application: Application) : AndroidViewModel(application) { private val dao = (application as MemoApplication).database.memoDao() private var questions = mutableStateOf>(listOf()) - private val stats = application.dataStore + private val datastore = application.dataStore private val statsKeyTotal = intPreferencesKey(STATS_TOTAL_TRIED) private val statsKeyTotalDone = intPreferencesKey(STATS_TOTAL_DONE) private val statsKeyTotalGood = intPreferencesKey(STATS_TOTAL_GOOD) private val statsKeyTotalBad = intPreferencesKey(STATS_TOTAL_BAD) + private val delayKey= intPreferencesKey(DELAY) + val delay = datastore.data.map { it[delayKey] ?: 5000 } + var currentQuestion = mutableStateOf(null) private var index = mutableStateOf(0) var proposedAnswer = mutableStateOf("") @@ -42,7 +47,7 @@ class PlayViewModel(application: Application) : AndroidViewModel(application) { dao.loadQuestions(setId).collect { questionList -> questions.value = questionList.shuffled() if (questions.value.isNotEmpty()) { - stats.edit { it[statsKeyTotal] = (it[statsKeyTotal] ?: 0) + 1 } + datastore.edit { it[statsKeyTotal] = (it[statsKeyTotal] ?: 0) + 1 } } updateQuestion() } @@ -58,7 +63,7 @@ class PlayViewModel(application: Application) : AndroidViewModel(application) { /* Fin des questions */ end.value = true viewModelScope.launch { - stats.edit { + datastore.edit { it[statsKeyTotalDone] = (it[statsKeyTotalDone] ?: 0) + 1 } } @@ -114,7 +119,7 @@ class PlayViewModel(application: Application) : AndroidViewModel(application) { when (evaluatedAnswer.value!!) { AnswerType.GOOD -> { viewModelScope.launch { - stats.edit { + datastore.edit { it[statsKeyTotalGood] = (it[statsKeyTotalGood] ?: 0) + 1 } } @@ -123,7 +128,7 @@ class PlayViewModel(application: Application) : AndroidViewModel(application) { AnswerType.BAD -> { viewModelScope.launch { - stats.edit { + datastore.edit { it[statsKeyTotalBad] = (it[statsKeyTotalBad] ?: 0) + 1 } } @@ -132,10 +137,20 @@ class PlayViewModel(application: Application) : AndroidViewModel(application) { } } - fun isDelayElapsed() = currentTime.value - timestampQuestion.value >= 3000 + fun updateTimer(delay: Int) { + if (!isDelayElapsed(delay) && currentQuestion.value != null) { + updateTime(System.currentTimeMillis()) + } + } + + fun isDelayElapsed(delay: Int) = currentTime.value - timestampQuestion.value >= delay fun updateTime(time: Long) { currentTime.value = time } -} + fun giveUp() { + showAnswer.value = true + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/SettingsViewModel.kt b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/SettingsViewModel.kt index a80e26b..aa63325 100644 --- a/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/SettingsViewModel.kt +++ b/app/src/main/java/fr/uparis/diamantkennel/memorisationapplication/ui/SettingsViewModel.kt @@ -6,37 +6,57 @@ import android.content.pm.PackageManager import android.os.Build import androidx.activity.result.ActivityResultLauncher import androidx.annotation.RequiresApi +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TimePickerState import androidx.compose.runtime.mutableStateOf import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import fr.uparis.diamantkennel.memorisationapplication.HOUR +import fr.uparis.diamantkennel.memorisationapplication.MINUTE +import fr.uparis.diamantkennel.memorisationapplication.DELAY import fr.uparis.diamantkennel.memorisationapplication.MemoApplication +import fr.uparis.diamantkennel.memorisationapplication.RappelWorker import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_BAD import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_DONE import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_GOOD import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_TRIED +import fr.uparis.diamantkennel.memorisationapplication.TimeConfig import fr.uparis.diamantkennel.memorisationapplication.dataStore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import java.util.Calendar +import java.util.concurrent.TimeUnit class SettingsViewModel(application: Application) : AndroidViewModel(application) { private val dao = (application as MemoApplication).database.memoDao() - private val stats = application.dataStore + private val datastore = application.dataStore private val statsKeyTotal = intPreferencesKey(STATS_TOTAL_TRIED) private val statsKeyTotalDone = intPreferencesKey(STATS_TOTAL_DONE) private val statsKeyTotalGood = intPreferencesKey(STATS_TOTAL_GOOD) private val statsKeyTotalBad = intPreferencesKey(STATS_TOTAL_BAD) - val statTotal = stats.data.map { it[statsKeyTotal] ?: 0 } - val statTotalDone = stats.data.map { it[statsKeyTotalDone] ?: 0 } - val statTotalGood = stats.data.map { it[statsKeyTotalGood] ?: 0 } - val statTotalBad = stats.data.map { it[statsKeyTotalBad] ?: 0 } + private val notifH = intPreferencesKey(HOUR) + private val notifM = intPreferencesKey(MINUTE) - val deletionDB = mutableStateOf(false) - val deletionStat = mutableStateOf(false) + private val delay = intPreferencesKey(DELAY) + + val statTotal = datastore.data.map { it[statsKeyTotal] ?: 0 } + val statTotalDone = datastore.data.map { it[statsKeyTotalDone] ?: 0 } + val statTotalGood = datastore.data.map { it[statsKeyTotalGood] ?: 0 } + val statTotalBad = datastore.data.map { it[statsKeyTotalBad] ?: 0 } + + var prefConfigTime = datastore.data.map { TimeConfig(it[notifH] ?: 8, it[notifM] ?: 0) } + + var deletionDB = mutableStateOf(false) + var deletionStat = mutableStateOf(false) + var notif = mutableStateOf(false) + var delayRequest = mutableStateOf(false) val gavePermissionNow = mutableStateOf(false) @@ -50,7 +70,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application fun cleanStats() { deletionStat.value = false viewModelScope.launch { - stats.edit { + datastore.edit { it[statsKeyTotal] = 0 it[statsKeyTotalDone] = 0 it[statsKeyTotalGood] = 0 @@ -59,6 +79,34 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application } } + @OptIn(ExperimentalMaterial3Api::class) + fun choiceTimeNotif(state: TimePickerState, context: Context) { + notif.value = false + val newConfig = TimeConfig( + state.hour, + state.minute + ) + save(newConfig) + schedule(newConfig, context) + } + + fun choiceDelay(value: Int) { + delayRequest.value = false + viewModelScope.launch { + datastore.edit { + it[delay] = value + } + } + } + + fun resetDelay() { + viewModelScope.launch { + datastore.edit { + it[delay] = 5000 + } + } + } + fun winrate(good: Int, bad: Int): Int { val total = good + bad if (total == 0) { @@ -77,4 +125,47 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application gavePermissionNow.value = context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED } + + private fun save(config: TimeConfig) { + viewModelScope.launch { + datastore.edit { + it[notifH] = config.hour + it[notifM] = config.minute + } + } + } + + private fun schedule(config: TimeConfig, context: Context) { + val wm = WorkManager.getInstance(context) + wm.cancelAllWork() + wm.enqueue(request(config.hour, config.minute)) + } + + private fun request(h: Int, m: Int): PeriodicWorkRequest { + val now = Calendar.getInstance() + val target = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, h) + set(Calendar.MINUTE, m) + } + if (target.before(now)) + target.add(Calendar.DAY_OF_YEAR, 1) + val delta = target.timeInMillis - now.timeInMillis + + return PeriodicWorkRequest.Builder(RappelWorker::class.java, 1, TimeUnit.DAYS) + .setInitialDelay(delta, TimeUnit.MILLISECONDS) + .build() + } + + fun dismissDeletionDB() { + deletionDB.value = false + } + fun dismissDeletionStat() { + deletionStat.value = false + } + fun dismissNotif() { + notif.value = false + } + fun dismissDelayRequest() { + delayRequest.value = false + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea7ff63..c9d8cc6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,7 +21,7 @@ Erreur Supprimer un jeu de question Voulez-vous supprimer ce jeu de question ? - Ok + OK Mettre à jour la question Ajouter une question Question @@ -39,6 +39,7 @@ Voulez-vous supprimer la base de données ? Oui Non + Valider Bravo Le set de questions est terminé ! Statistiques @@ -55,4 +56,15 @@ Notifications de rappel Rappel Il est temps de réviser ! + Modifier heure de rappel + Choix heure de rappel + Gestion + Notifications + Confirmer + Parametre du jeu + Délai avant solution + Réinitialiser par défaut + Réinitialiser les paramètres du jeu + Choix du délai avant solution + Entrez un entier (en ms)