Merge branch 'parametrage' into 'master'

Parametrage

See merge request diamant-kennel/mememorisation_mobile_groupe11!1
This commit is contained in:
DIAMANT alexandre 2024-01-07 16:17:23 +01:00
commit 4f25c2d266
13 changed files with 509 additions and 149 deletions

View file

@ -51,14 +51,14 @@ android {
} }
dependencies { 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.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(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") 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.core:core-ktx:1.12.0")
implementation("androidx.work:work-runtime-ktx:2.8.1") implementation("androidx.work:work-runtime-ktx:2.8.1")

View file

@ -23,3 +23,8 @@ const val STATS_TOTAL_GOOD = "total_good"
/** DataStore clef lié au nombre de questions mal répondu */ /** DataStore clef lié au nombre de questions mal répondu */
const val STATS_TOTAL_BAD = "total_bad" const val STATS_TOTAL_BAD = "total_bad"
const val DELAY = "delay"
const val HOUR = "hour"
const val MINUTE = "minute"

View file

@ -68,7 +68,7 @@ fun HomeScreen(
val errorEntry by model.error val errorEntry by model.error
if (errorEntry != null) { ShowDialog(errorEntry != null) {
ErrorDialog( ErrorDialog(
when (errorEntry!!) { when (errorEntry!!) {
ErrorsAjout.BAD_ENTRY -> context.getString(R.string.error_bad_entry) ErrorsAjout.BAD_ENTRY -> context.getString(R.string.error_bad_entry)
@ -76,25 +76,30 @@ fun HomeScreen(
}, model::cleanErrors }, model::cleanErrors
) )
} }
ShowDialog(creationRequest) { CreationDialog(model::dismissCreation, model) }
if (creationRequest) { ShowDialog(importationRequest) { ImportDialog(model::dismissImportation, model) }
CreationDialog( ShowDialog(deletionRequest) {
dismiss = model::dismissCreation, model = model DeletionDialog(model::dismissDeleteOne, model::deleteSelected)
)
} }
if (importationRequest) { Home(
ImportDialog( padding,
dismiss = model::dismissImportation, model = model navController,
model,
setOfQuestions,
currentSelection
) )
} }
if (deletionRequest) { @Composable
DeletionDialog( private fun Home(
model::dismissDeleteOne, padding: PaddingValues,
model::deleteSelected navController: NavController,
) model: HomeViewModel,
} setOfQuestions: List<SetOfQuestions> = listOf(),
currentSelection: SetQuestions? = null,
) {
val context = LocalContext.current
Column( Column(
modifier = Modifier.padding(padding), horizontalAlignment = Alignment.CenterHorizontally modifier = Modifier.padding(padding), horizontalAlignment = Alignment.CenterHorizontally
@ -146,7 +151,6 @@ private fun ActionRow(context: Context, model: HomeViewModel, navController: Nav
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CreationDialog( fun CreationDialog(
dismiss: () -> Unit, model: HomeViewModel = viewModel() dismiss: () -> Unit, model: HomeViewModel = viewModel()
@ -172,7 +176,7 @@ fun CreationDialog(
}) })
} }
@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@Composable @Composable
fun ImportDialog( fun ImportDialog(
dismiss: () -> Unit, model: HomeViewModel dismiss: () -> Unit, model: HomeViewModel

View file

@ -1,9 +1,11 @@
package fr.uparis.diamantkennel.memorisationapplication package fr.uparis.diamantkennel.memorisationapplication
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.BottomNavigation import androidx.compose.material.BottomNavigation
@ -39,6 +41,7 @@ import fr.uparis.diamantkennel.memorisationapplication.ui.theme.MemorisationAppl
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = STATS) val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = STATS)
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
@ -54,18 +57,20 @@ class MainActivity : ComponentActivity() {
} }
} }
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable @Composable
fun MainScreenMainActivity() { fun MainScreenMainActivity() {
MainScreen() MainScreen()
} }
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun MainScreenPreview() { fun MainScreenPreview() {
MainScreen() MainScreen()
} }
@OptIn(ExperimentalMaterial3Api::class) @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable @Composable
fun MainScreen() { fun MainScreen() {
val navController = rememberNavController() 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 <T: Any> ShowDialog(
condition: Boolean,
dialog: @Composable () -> T
) {
if (condition) {
dialog()
}
}

View file

@ -4,15 +4,10 @@ import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import fr.uparis.diamantkennel.memorisationapplication.data.QuestionsDB 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() { class MemoApplication : Application() {
val database: QuestionsDB by lazy { QuestionsDB.getDataBase(this) } val database: QuestionsDB by lazy { QuestionsDB.getDataBase(this) }
@ -20,10 +15,6 @@ class MemoApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
createChannel(this) createChannel(this)
if (this.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
schedule()
}
} }
private fun createChannel(c: Context) { private fun createChannel(c: Context) {
@ -39,25 +30,4 @@ class MemoApplication : Application() {
notificationManager.createNotificationChannel(channel) 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()
}
} }

View file

@ -36,7 +36,6 @@ import fr.uparis.diamantkennel.memorisationapplication.ui.ActionModifySet
import fr.uparis.diamantkennel.memorisationapplication.ui.ModifySetViewModel import fr.uparis.diamantkennel.memorisationapplication.ui.ModifySetViewModel
import kotlin.text.Typography.ellipsis import kotlin.text.Typography.ellipsis
@Composable @Composable
fun ModifySetScreen( fun ModifySetScreen(
padding: PaddingValues, padding: PaddingValues,
@ -47,20 +46,33 @@ fun ModifySetScreen(
model.setId.value = idSet model.setId.value = idSet
model.updateQuestionList(idSet) model.updateQuestionList(idSet)
val context = LocalContext.current
val currentSelection by model.selection val currentSelection by model.selection
val questions by model.questions.collectAsState(listOf()) val questions by model.questions.collectAsState(listOf())
var action by model.action val action by model.action
if (action == ActionModifySet.AJOUT || action == ActionModifySet.MODIFICATION) { ShowDialog(action == ActionModifySet.AJOUT || action == ActionModifySet.MODIFICATION) {
AjoutModifDialog(action, currentSelection, model::ajoutQuestion) AjoutModifDialog(action, currentSelection, model::ajoutQuestion, model::dismissAction)
{ action = ActionModifySet.AUCUN } }
ShowDialog(action == ActionModifySet.SUPPRIMER) {
RemoveDialog(model::removeQuestion, model::dismissAction)
} }
if (action == ActionModifySet.SUPPRIMER) { Modify(
RemoveDialog(model::removeQuestion) padding = padding,
{ action = ActionModifySet.AUCUN } model = model,
} questions = questions,
currentSelection = currentSelection
)
}
@Composable
fun Modify(
padding: PaddingValues,
model: ModifySetViewModel,
questions: List<Question>,
currentSelection: Question?
) {
val context = LocalContext.current
Column( Column(
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding),
@ -71,13 +83,13 @@ fun ModifySetScreen(
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Button( Button(
enabled = currentSelection != null, enabled = currentSelection != null,
onClick = { action = ActionModifySet.MODIFICATION }) { onClick = model::modifAction) {
Text(text = context.getString(R.string.modify)) Text(text = context.getString(R.string.modify))
} }
Spacer(modifier = Modifier.padding(2.dp)) Spacer(modifier = Modifier.padding(2.dp))
Button(onClick = { action = ActionModifySet.AJOUT }) { Button(onClick = model::ajoutAction) {
Text(text = context.getString(R.string.add)) Text(text = context.getString(R.string.add))
} }
@ -85,7 +97,7 @@ fun ModifySetScreen(
Button( Button(
enabled = currentSelection != null, enabled = currentSelection != null,
onClick = { action = ActionModifySet.SUPPRIMER }) { onClick = model::supprAction) {
Text(text = context.getString(R.string.modify_button_delete)) Text(text = context.getString(R.string.modify_button_delete))
} }
} }

View file

@ -9,15 +9,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -26,10 +25,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import fr.uparis.diamantkennel.memorisationapplication.data.Question
import fr.uparis.diamantkennel.memorisationapplication.ui.AnswerType import fr.uparis.diamantkennel.memorisationapplication.ui.AnswerType
import fr.uparis.diamantkennel.memorisationapplication.ui.PlayViewModel import fr.uparis.diamantkennel.memorisationapplication.ui.PlayViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PlayScreen( fun PlayScreen(
padding: PaddingValues, padding: PaddingValues,
@ -41,40 +40,54 @@ fun PlayScreen(
// First update the list of questions // First update the list of questions
model.updateQuestionList(idSet) model.updateQuestionList(idSet)
val context = LocalContext.current
val question by model.currentQuestion val question by model.currentQuestion
val reponse by model.proposedAnswer val reponse by model.proposedAnswer
val correction by model.evaluatedAnswer val correction by model.evaluatedAnswer
var giveup by model.showAnswer val giveup by model.showAnswer
val gameEnded by model.end val gameEnded by model.end
val delay by model.delay.collectAsState(initial = 5000)
val cpt by model.compteurSb val cpt by model.compteurSb
if (correction != null) {
LaunchedEffect(cpt) { SnackbarAnswer(
model.sbUpdate() model,
snackbarHostState.showSnackbar( snackbarHostState,
when (correction!!) { cpt,
AnswerType.GOOD -> context.getString(R.string.good_answer) correction
AnswerType.BAD -> context.getString(R.string.bad_answer)
}, duration = SnackbarDuration.Short
) )
model.resetAfterSb()
}
}
if (gameEnded) { ShowDialog(gameEnded) { EndDialog { navController.navigate(HOME) } }
EndDialog { navController.navigate(HOME) } ShowDialog(giveup && question != null) {
}
if (giveup && question != null) {
SolutionDialog(question!!.reponse, model::newQuestion) SolutionDialog(question!!.reponse, model::newQuestion)
} }
// Update timer if needed // Update timer if needed
if (!model.isDelayElapsed() && question != null) { model.updateTimer(delay)
model.updateTime(System.currentTimeMillis())
} 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( Column(
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding),
@ -85,7 +98,7 @@ fun PlayScreen(
Text(context.getString(R.string.no_question), fontSize = 30.sp) Text(context.getString(R.string.no_question), fontSize = 30.sp)
} }
} else { } 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)) Spacer(modifier = Modifier.padding(top = 20.dp))
@ -109,8 +122,9 @@ fun PlayScreen(
} }
Button( Button(
enabled = model.isDelayElapsed(), enabled = model.isDelayElapsed(delay),
onClick = { giveup = true }) { onClick = model::giveUp
) {
Text(text = context.getString(R.string.see_answer)) Text(text = context.getString(R.string.see_answer))
} }
} }
@ -143,3 +157,25 @@ fun EndDialog(next: () -> Unit) =
confirmButton = { confirmButton = {
Button(onClick = next) { Text(text = LocalContext.current.getString(R.string.ok)) } 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()
}
}
}

View file

@ -1,45 +1,113 @@
package fr.uparis.diamantkennel.memorisationapplication package fr.uparis.diamantkennel.memorisationapplication
import android.content.Context
import android.os.Build import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import fr.uparis.diamantkennel.memorisationapplication.ui.SettingsViewModel import fr.uparis.diamantkennel.memorisationapplication.ui.SettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
@RequiresApi(Build.VERSION_CODES.TIRAMISU) @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen(padding: PaddingValues, model: SettingsViewModel = viewModel()) { fun SettingsScreen(model: SettingsViewModel = viewModel()) {
val context = LocalContext.current val context = LocalContext.current
var deletionDBRequest by model.deletionDB var deletionDBRequest by model.deletionDB
var cleanStatRequest by model.deletionStat 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) 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( val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(), 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( Column(
modifier = Modifier.padding(padding), modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Stats(model) Stats(model = model)
AddSpacedDivider()
NotificationSettings(context, model, permissionNotif, permissionLauncher, onTimeNotifButtonClick)
AddSpacedDivider()
GestionSettings(context, deletionDBRequest, cleanStatRequest)
AddSpacedDivider()
GameSettings(context, onDelayButtonClick, onResetButtonClick)
}
}
Spacer(modifier = Modifier.padding(top = 10.dp)) @RequiresApi(Build.VERSION_CODES.TIRAMISU)
Divider(color = Color.Gray) @Composable
Spacer(modifier = Modifier.padding(top = 10.dp)) fun NotificationSettings(
context: Context,
Text(text = "Gestion", fontSize = 30.sp) model: SettingsViewModel,
permissionNotif: Boolean,
permissionLauncher: ActivityResultLauncher<String>,
onTimeNotifButtonClick: () -> Unit,
) {
Text(text = context.getString(R.string.notification), fontSize = 30.sp)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly 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))
}
}
Button( Button(
enabled = !permissionNotif, enabled = !permissionNotif,
onClick = { onClick = {
@ -94,6 +152,59 @@ fun SettingsScreen(padding: PaddingValues, model: SettingsViewModel = viewModel(
) { ) {
Text(text = context.getString(R.string.permission_button)) 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)) } Button(onClick = confirm) { Text(text = LocalContext.current.getString(R.string.yes)) }
}, },
dismissButton = { Button(onClick = dismiss) { Text(text = LocalContext.current.getString(R.string.no)) } }) 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))
}

View file

@ -0,0 +1,6 @@
package fr.uparis.diamantkennel.memorisationapplication
data class TimeConfig(
val hour: Int = 8,
val minute: Int = 0
)

View file

@ -63,4 +63,18 @@ class ModifySetViewModel(application: Application) : AndroidViewModel(applicatio
dao.deleteQuestion(selection.value!!) 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
}
} }

View file

@ -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_DONE
import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_GOOD import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_GOOD
import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_TRIED 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.data.Question
import fr.uparis.diamantkennel.memorisationapplication.dataStore import fr.uparis.diamantkennel.memorisationapplication.dataStore
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class PlayViewModel(application: Application) : AndroidViewModel(application) { class PlayViewModel(application: Application) : AndroidViewModel(application) {
private val dao = (application as MemoApplication).database.memoDao() private val dao = (application as MemoApplication).database.memoDao()
private var questions = mutableStateOf<List<Question>>(listOf()) private var questions = mutableStateOf<List<Question>>(listOf())
private val stats = application.dataStore private val datastore = application.dataStore
private val statsKeyTotal = intPreferencesKey(STATS_TOTAL_TRIED) private val statsKeyTotal = intPreferencesKey(STATS_TOTAL_TRIED)
private val statsKeyTotalDone = intPreferencesKey(STATS_TOTAL_DONE) private val statsKeyTotalDone = intPreferencesKey(STATS_TOTAL_DONE)
private val statsKeyTotalGood = intPreferencesKey(STATS_TOTAL_GOOD) private val statsKeyTotalGood = intPreferencesKey(STATS_TOTAL_GOOD)
private val statsKeyTotalBad = intPreferencesKey(STATS_TOTAL_BAD) private val statsKeyTotalBad = intPreferencesKey(STATS_TOTAL_BAD)
private val delayKey= intPreferencesKey(DELAY)
val delay = datastore.data.map { it[delayKey] ?: 5000 }
var currentQuestion = mutableStateOf<Question?>(null) var currentQuestion = mutableStateOf<Question?>(null)
private var index = mutableStateOf(0) private var index = mutableStateOf(0)
var proposedAnswer = mutableStateOf("") var proposedAnswer = mutableStateOf("")
@ -42,7 +47,7 @@ class PlayViewModel(application: Application) : AndroidViewModel(application) {
dao.loadQuestions(setId).collect { questionList -> dao.loadQuestions(setId).collect { questionList ->
questions.value = questionList.shuffled() questions.value = questionList.shuffled()
if (questions.value.isNotEmpty()) { if (questions.value.isNotEmpty()) {
stats.edit { it[statsKeyTotal] = (it[statsKeyTotal] ?: 0) + 1 } datastore.edit { it[statsKeyTotal] = (it[statsKeyTotal] ?: 0) + 1 }
} }
updateQuestion() updateQuestion()
} }
@ -58,7 +63,7 @@ class PlayViewModel(application: Application) : AndroidViewModel(application) {
/* Fin des questions */ /* Fin des questions */
end.value = true end.value = true
viewModelScope.launch { viewModelScope.launch {
stats.edit { datastore.edit {
it[statsKeyTotalDone] = (it[statsKeyTotalDone] ?: 0) + 1 it[statsKeyTotalDone] = (it[statsKeyTotalDone] ?: 0) + 1
} }
} }
@ -114,7 +119,7 @@ class PlayViewModel(application: Application) : AndroidViewModel(application) {
when (evaluatedAnswer.value!!) { when (evaluatedAnswer.value!!) {
AnswerType.GOOD -> { AnswerType.GOOD -> {
viewModelScope.launch { viewModelScope.launch {
stats.edit { datastore.edit {
it[statsKeyTotalGood] = (it[statsKeyTotalGood] ?: 0) + 1 it[statsKeyTotalGood] = (it[statsKeyTotalGood] ?: 0) + 1
} }
} }
@ -123,7 +128,7 @@ class PlayViewModel(application: Application) : AndroidViewModel(application) {
AnswerType.BAD -> { AnswerType.BAD -> {
viewModelScope.launch { viewModelScope.launch {
stats.edit { datastore.edit {
it[statsKeyTotalBad] = (it[statsKeyTotalBad] ?: 0) + 1 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) { fun updateTime(time: Long) {
currentTime.value = time currentTime.value = time
} }
fun giveUp() {
showAnswer.value = true
}
} }

View file

@ -6,37 +6,57 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TimePickerState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope 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.MemoApplication
import fr.uparis.diamantkennel.memorisationapplication.RappelWorker
import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_BAD import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_BAD
import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_DONE import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_DONE
import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_GOOD import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_GOOD
import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_TRIED import fr.uparis.diamantkennel.memorisationapplication.STATS_TOTAL_TRIED
import fr.uparis.diamantkennel.memorisationapplication.TimeConfig
import fr.uparis.diamantkennel.memorisationapplication.dataStore import fr.uparis.diamantkennel.memorisationapplication.dataStore
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.concurrent.TimeUnit
class SettingsViewModel(application: Application) : AndroidViewModel(application) { class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val dao = (application as MemoApplication).database.memoDao() 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 statsKeyTotal = intPreferencesKey(STATS_TOTAL_TRIED)
private val statsKeyTotalDone = intPreferencesKey(STATS_TOTAL_DONE) private val statsKeyTotalDone = intPreferencesKey(STATS_TOTAL_DONE)
private val statsKeyTotalGood = intPreferencesKey(STATS_TOTAL_GOOD) private val statsKeyTotalGood = intPreferencesKey(STATS_TOTAL_GOOD)
private val statsKeyTotalBad = intPreferencesKey(STATS_TOTAL_BAD) private val statsKeyTotalBad = intPreferencesKey(STATS_TOTAL_BAD)
val statTotal = stats.data.map { it[statsKeyTotal] ?: 0 } private val notifH = intPreferencesKey(HOUR)
val statTotalDone = stats.data.map { it[statsKeyTotalDone] ?: 0 } private val notifM = intPreferencesKey(MINUTE)
val statTotalGood = stats.data.map { it[statsKeyTotalGood] ?: 0 }
val statTotalBad = stats.data.map { it[statsKeyTotalBad] ?: 0 }
val deletionDB = mutableStateOf(false) private val delay = intPreferencesKey(DELAY)
val deletionStat = mutableStateOf(false)
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) val gavePermissionNow = mutableStateOf(false)
@ -50,7 +70,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
fun cleanStats() { fun cleanStats() {
deletionStat.value = false deletionStat.value = false
viewModelScope.launch { viewModelScope.launch {
stats.edit { datastore.edit {
it[statsKeyTotal] = 0 it[statsKeyTotal] = 0
it[statsKeyTotalDone] = 0 it[statsKeyTotalDone] = 0
it[statsKeyTotalGood] = 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 { fun winrate(good: Int, bad: Int): Int {
val total = good + bad val total = good + bad
if (total == 0) { if (total == 0) {
@ -77,4 +125,47 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
gavePermissionNow.value = gavePermissionNow.value =
context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED 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
}
} }

View file

@ -21,7 +21,7 @@
<string name="error">Erreur</string> <string name="error">Erreur</string>
<string name="delete_set_questions">Supprimer un jeu de question</string> <string name="delete_set_questions">Supprimer un jeu de question</string>
<string name="delete_set_qestions_desc">Voulez-vous supprimer ce jeu de question ?</string> <string name="delete_set_qestions_desc">Voulez-vous supprimer ce jeu de question ?</string>
<string name="ok">Ok</string> <string name="ok">OK</string>
<string name="update_question">Mettre à jour la question</string> <string name="update_question">Mettre à jour la question</string>
<string name="add_question">Ajouter une question</string> <string name="add_question">Ajouter une question</string>
<string name="question">Question</string> <string name="question">Question</string>
@ -39,6 +39,7 @@
<string name="delete_db_desc">Voulez-vous supprimer la base de données ?</string> <string name="delete_db_desc">Voulez-vous supprimer la base de données ?</string>
<string name="yes">Oui</string> <string name="yes">Oui</string>
<string name="no">Non</string> <string name="no">Non</string>
<string name="validate">Valider</string>
<string name="bravo">Bravo</string> <string name="bravo">Bravo</string>
<string name="set_ended">Le set de questions est terminé !</string> <string name="set_ended">Le set de questions est terminé !</string>
<string name="stats">Statistiques</string> <string name="stats">Statistiques</string>
@ -55,4 +56,15 @@
<string name="notif_channel_desc">Notifications de rappel</string> <string name="notif_channel_desc">Notifications de rappel</string>
<string name="notif_reminder_title">Rappel</string> <string name="notif_reminder_title">Rappel</string>
<string name="notif_reminder_content">Il est temps de réviser !</string> <string name="notif_reminder_content">Il est temps de réviser !</string>
<string name="time_notif_button">Modifier heure de rappel</string>
<string name="time_notif">Choix heure de rappel</string>
<string name="gestion">Gestion</string>
<string name="notification">Notifications</string>
<string name="confirm">Confirmer</string>
<string name="game">Parametre du jeu</string>
<string name="choice_delay_button">Délai avant solution</string>
<string name="reset_button">Réinitialiser par défaut</string>
<string name="reset_game_settings_button">Réinitialiser les paramètres du jeu</string>
<string name="choice_delay">Choix du délai avant solution</string>
<string name="enter_integer">Entrez un entier (en ms)</string>
</resources> </resources>