Merge branch 'frontend/settings-popup' into 'main'

feat: settings popup implemented

Closes #1

See merge request EmmaVandewalle/writand!41
This commit is contained in:
Emma Vandewalle 2024-09-01 11:17:44 +00:00
commit 7609fd4d9d
11 changed files with 573 additions and 4 deletions

View file

@ -0,0 +1,17 @@
package be.re.writand.domain.settings
import be.re.writand.data.repos.settings.UserSettingsRepository
import javax.inject.Inject
/**
* Use-case to change the amount of open/saved files to have in the app, this changes the settings.
* @param[userSettingsRepository] repository to change the UserSettings when valid input is given.
*/
class SetMaxSavedFilesUseCase @Inject constructor(
private val userSettingsRepository: UserSettingsRepository
) {
suspend operator fun invoke(maxSavedFiles: Int) {
userSettingsRepository.setMaxSavedFiles(maxSavedFiles)
}
}

View file

@ -0,0 +1,18 @@
package be.re.writand.domain.settings
import be.re.writand.data.repos.settings.UserSettingsRepository
import javax.inject.Inject
/**
* Use-case to change the amount of saved projects to have in the app, this changes the settings.
* @param[userSettingsRepository] repository to change the UserSettings when valid input is given.
*/
class SetMaxSavedProjectsUseCase @Inject constructor(
private val userSettingsRepository: UserSettingsRepository
) {
suspend operator fun invoke(maxSavedProjects: Int) {
userSettingsRepository.setMaxSavedProjects(maxSavedProjects)
}
}

View file

@ -9,6 +9,7 @@ import androidx.navigation.NavHostController
import be.re.writand.screens.editor.bottom.BottomEditorBar
import be.re.writand.screens.editor.editorspace.EditorSpace
import be.re.writand.screens.editor.top.TopEditorBar
import be.re.writand.screens.settings.SettingsPopup
/**
* Composable presenting the full screen when IDE is open. This holds all the different composables
@ -30,6 +31,7 @@ fun EditorScreen(
Row(modifier = Modifier.padding(it)) {
// TODO: show filetree when open
EditorSpace()
SettingsPopup()
}
}

View file

@ -1,7 +1,6 @@
package be.re.writand.screens.editor.bottom
import be.re.writand.screens.WViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

View file

@ -1,7 +1,6 @@
package be.re.writand.screens.editor.top
import be.re.writand.screens.WViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

View file

@ -0,0 +1,171 @@
package be.re.writand.screens.settings
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import be.re.writand.data.local.models.UserSettings
import be.re.writand.data.repos.settings.UserSettingsRepository
import be.re.writand.domain.settings.SetFontSizeSettingsUseCase
import be.re.writand.domain.settings.SetLanguageSettingsUseCase
import be.re.writand.domain.settings.SetMaxSavedFilesUseCase
import be.re.writand.domain.settings.SetMaxSavedProjectsUseCase
import be.re.writand.domain.settings.SetThemeSettingsUseCase
import be.re.writand.screens.WViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
/**
* View model to be used by SettingsPopup handling all the different changes in the settings and ui.
* @param[userSettingsRepository] repository that holds the settings of the current user.
* @param[setLanguage] use case to change the language.
* @param[setTheme] use case to change the theme.
* @param[setFontSize] use case to change the font size used in the different text fields.
* @param[setMaxSavedProjects] use case to change the max amount of projects kept in memory.
* @param[setMaxSavedFiles] use case to change the max amount of open files in the editor.
*/
@HiltViewModel
class EditorSettingsViewModel @Inject constructor(
private val userSettingsRepository: UserSettingsRepository,
private val setLanguage: SetLanguageSettingsUseCase,
private val setTheme: SetThemeSettingsUseCase,
private val setFontSize: SetFontSizeSettingsUseCase,
private val setMaxSavedProjects: SetMaxSavedProjectsUseCase,
private val setMaxSavedFiles: SetMaxSavedFilesUseCase
) : WViewModel() {
private val _settings = MutableStateFlow<UserSettings?>(null)
val settings: StateFlow<UserSettings?> = _settings
val changedAppearance = mutableStateOf(false)
val changedAdvanced = mutableStateOf(false)
val textFieldFontSize = mutableStateOf(_settings.value?.fontSize.toString())
val language = mutableStateOf(_settings.value?.userLanguage.toString())
val theme = mutableStateOf(_settings.value?.userTheme.toString())
val amountSavedProjects = mutableIntStateOf(_settings.value?.maxSavedProjects?: 1)
val amountSavedFiles = mutableIntStateOf(_settings.value?.maxSavedFiles?: 1)
init {
launchCatching {
userSettingsRepository.userSettings.collect { settings ->
_settings.value = settings
textFieldFontSize.value = settings.fontSize.toString()
language.value = settings.userLanguage.toString()
theme.value = settings.userTheme.toString()
amountSavedProjects.intValue = settings.maxSavedProjects
amountSavedFiles.intValue = settings.maxSavedFiles
}
}
}
fun onTextFieldFontSizeChange(newValue: String) {
textFieldFontSize.value = newValue
if (!changedAppearance.value && newValue != _settings.value?.fontSize.toString()) {
changedAppearance.value = true
}
}
fun onLanguageSelect(newLanguage: String) {
language.value = newLanguage
if (!changedAppearance.value && newLanguage != _settings.value?.userLanguage.toString()) {
changedAppearance.value = true
}
}
fun onThemeSelect(newTheme: String) {
theme.value = newTheme
if (!changedAppearance.value && newTheme != _settings.value?.userTheme.toString()) {
changedAppearance.value = true
}
}
fun onApplyAppearance() {
launchCatching {
changedAppearance.value = false
setLanguage(language.value)
setTheme(theme.value)
val errorFontSize: NumberFormatException? = setFontSize(textFieldFontSize.value)
errorFontSize.let {
// reset the text field to its old value
textFieldFontSize.value = _settings.value?.fontSize.toString()
}
}
}
fun onApplyAdvanced() {
launchCatching {
changedAdvanced.value = false
setMaxSavedProjects(amountSavedProjects.intValue)
setMaxSavedFiles(amountSavedFiles.intValue)
}
}
fun onCancelAppearance() {
changedAppearance.value = false
textFieldFontSize.value = _settings.value?.fontSize.toString()
language.value = _settings.value?.userLanguage.toString()
theme.value = _settings.value?.userTheme.toString()
}
fun onCancelAdvanced() {
changedAdvanced.value = false
if (_settings.value != null) {
amountSavedProjects.intValue = _settings.value?.maxSavedProjects!!
amountSavedFiles.intValue = _settings.value?.maxSavedFiles!!
}
}
fun onMinValueProjects() {
changedAdvanced.value = true
amountSavedProjects.intValue = 0
}
fun onDecrementProjects() {
if (amountSavedProjects.intValue > 1) {
changedAdvanced.value = true
amountSavedProjects.intValue = amountSavedProjects.intValue.dec()
}
}
fun onIncrementProjects() {
if (amountSavedProjects.intValue < 100) {
changedAdvanced.value = true
amountSavedProjects.intValue = amountSavedProjects.intValue.inc()
}
}
fun onMaxValueProjects() {
changedAdvanced.value = true
amountSavedProjects.intValue = 25
}
fun onMinValueFiles() {
changedAdvanced.value = true
amountSavedFiles.intValue = 1
}
fun onDecrementFiles() {
if (amountSavedFiles.intValue > 1) {
changedAdvanced.value = true
amountSavedFiles.intValue = amountSavedFiles.intValue.dec()
}
}
fun onIncrementFiles() {
if (amountSavedFiles.intValue < 100) {
changedAdvanced.value = true
amountSavedFiles.intValue = amountSavedFiles.intValue.inc()
}
}
fun onMaxValueFiles() {
changedAdvanced.value = true
amountSavedFiles.intValue = 50
}
}

View file

@ -0,0 +1,349 @@
package be.re.writand.screens.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.PermanentDrawerSheet
import androidx.compose.material3.PermanentNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import be.re.writand.R
import be.re.writand.data.local.models.UserLanguage
import be.re.writand.data.local.models.UserTheme
import be.re.writand.screens.components.WButton
import be.re.writand.screens.components.WLabelAndTextField
import be.re.writand.screens.components.WRadioButtonsSelectorRowWise
import be.re.writand.screens.components.WText
/**
* Popup screen to handle let the user change the settings once the welcome part of the app has
* gone through. This should involve all the settings possible to be changed.
* @param[vM] view model corresponding to this screen.
*/
@Composable
fun SettingsPopup(
vM: EditorSettingsViewModel = hiltViewModel()
) {
val openDialog = remember { mutableStateOf(false) }
val buttonTitle = remember {
mutableStateOf("Show Pop Up")
}
// keep these variables for settings at the top level, so it remembers the state even though
// settings were temporarily closed
val items = listOf("Appearance", "Advanced")
val selectedItem = remember {
mutableStateOf(items[0])
}
Button(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
onClick = {
openDialog.value = !openDialog.value
if (!openDialog.value) {
buttonTitle.value = "Show Pop Up"
}
}
) {
Text(text = buttonTitle.value, modifier = Modifier.padding(3.dp))
}
if (openDialog.value) {
buttonTitle.value = "Hide Pop Up"
Dialog(
onDismissRequest = { openDialog.value = false },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Box(
modifier = Modifier
.height(600.dp)
.width(800.dp),
contentAlignment = Alignment.Center
) {
Scaffold(
modifier = Modifier
.fillMaxSize()
.clip(shape = RoundedCornerShape(10.dp))
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.tertiary,
shape = RoundedCornerShape(10.dp)
),
topBar = {
Row(
modifier = Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.tertiary),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
// spacer to divide the row in 3 parts and spread them over the width
Spacer(modifier = Modifier.size(0.dp))
WText(text = "Settings", fontSize = 25.sp)
IconButton(
onClick = { openDialog.value = false },
modifier = Modifier.padding(start = 10.dp),
) {
Icon(
modifier = Modifier.size(35.dp),
imageVector = Icons.Default.Close,
contentDescription = "Close"
)
}
}
}
) {
PermanentNavigationDrawer(
modifier = Modifier
.padding(it)
.fillMaxSize(),
drawerContent = {
PermanentDrawerSheet(
modifier = Modifier
.padding(top = 10.dp)
.width(240.dp)
) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
items.forEach { item ->
NavigationDrawerItem(
label = { Text(text = item) },
selected = item == selectedItem.value,
onClick = { selectedItem.value = item },
modifier = Modifier.padding(horizontal = 12.dp)
)
}
}
}
}) {
// cancel the last changes to the other tab first to the ones not
// applied yet, otherwise it looks like there are applied
if (selectedItem.value == items[0]) {
if (vM.changedAdvanced.value) {
vM.onCancelAdvanced()
}
AppearanceSettings(editorSettingsVM = vM)
} else {
if (vM.changedAppearance.value) {
vM.onCancelAppearance()
}
AdvancedSettings(editorSettingsVM = vM)
}
}
}
}
}
}
}
@Composable
fun AppearanceSettings(
editorSettingsVM: EditorSettingsViewModel
) {
val languageOptions = UserLanguage.entries.map { entry -> entry.toString() }
val themeOptions = UserTheme.entries.map { entry -> entry.toString() }
Scaffold(
bottomBar = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
WButton(
modifier = Modifier.padding(end = 10.dp),
text = "Cancel",
onClick = editorSettingsVM::onCancelAppearance,
enabled = editorSettingsVM.changedAppearance.value
)
WButton(
text = "Apply",
onClick = editorSettingsVM::onApplyAppearance,
enabled = editorSettingsVM.changedAppearance.value
)
}
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.secondary)
.padding(it)
.padding(start = 16.dp)
) {
WText(modifier = Modifier.padding(top = 25.dp), text = "Language", fontSize = 25.sp)
WRadioButtonsSelectorRowWise(
enable = true,
textTitle = "",
options = languageOptions,
selectedOption = editorSettingsVM.language.value,
onOptionSelected = editorSettingsVM::onLanguageSelect,
labelSize = 0.dp
)
WText(modifier = Modifier.padding(top = 25.dp), text = "Theme", fontSize = 25.sp)
WRadioButtonsSelectorRowWise(
enable = true,
textTitle = "",
options = themeOptions,
selectedOption = editorSettingsVM.theme.value,
onOptionSelected = editorSettingsVM::onThemeSelect,
labelSize = 0.dp
)
WText(modifier = Modifier.padding(top = 25.dp), text = "Font", fontSize = 25.sp)
WLabelAndTextField(
title = "Fontsize",
value = editorSettingsVM.textFieldFontSize.value,
fullSize = 200.dp,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
onTextChange = editorSettingsVM::onTextFieldFontSizeChange
)
}
}
}
@Composable
fun AdvancedSettings(
editorSettingsVM: EditorSettingsViewModel
) {
Scaffold(
bottomBar = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
WButton(
modifier = Modifier.padding(end = 10.dp),
text = "Cancel",
onClick = editorSettingsVM::onCancelAdvanced,
enabled = editorSettingsVM.changedAdvanced.value
)
WButton(
text = "Apply",
onClick = editorSettingsVM::onApplyAdvanced,
enabled = editorSettingsVM.changedAdvanced.value
)
}
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.secondary)
.padding(it)
.padding(start = 16.dp)
) {
WText(text = "History", modifier = Modifier.padding(top = 25.dp), fontSize = 25.sp)
ArrowAmountChooser(
text = "Max amount of recently opened projects:",
amountInt = editorSettingsVM.amountSavedProjects.intValue,
onMinValue = editorSettingsVM::onMinValueProjects,
onDecrementValue = editorSettingsVM::onDecrementProjects,
onIncrementValue = editorSettingsVM::onIncrementProjects,
onMaxValue = editorSettingsVM::onMaxValueProjects
)
WText(text = "IDE tabs", modifier = Modifier.padding(top = 25.dp), fontSize = 25.sp)
ArrowAmountChooser(
text = "Max amount of opened files:",
amountInt = editorSettingsVM.amountSavedFiles.intValue,
onMinValue = editorSettingsVM::onMinValueFiles,
onDecrementValue = editorSettingsVM::onDecrementFiles,
onIncrementValue = editorSettingsVM::onIncrementFiles,
onMaxValue = editorSettingsVM::onMaxValueFiles
)
// LATER ON: add a checkbox whether they want to automatically start the last project
// in history or always get the project picker when opening the app
}
}
}
@Composable
fun ArrowAmountChooser(
text: String,
amountInt: Int,
onMinValue: () -> Unit,
onDecrementValue: () -> Unit,
onIncrementValue: () -> Unit,
onMaxValue: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
WText(text = text)
IconButton(onClick = onMinValue) {
Icon(
modifier = Modifier.size(25.dp),
painter = painterResource(R.drawable.baseline_keyboard_double_arrow_left_24),
contentDescription = "Min"
)
}
IconButton(onClick = onDecrementValue) {
Icon(
modifier = Modifier.size(25.dp),
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
contentDescription = "Less"
)
}
WText(text = amountInt.toString())
IconButton(onClick = onIncrementValue) {
Icon(
modifier = Modifier.size(25.dp),
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "More"
)
}
IconButton(onClick = onMaxValue) {
Icon(
modifier = Modifier.size(25.dp),
painter = painterResource(R.drawable.baseline_keyboard_double_arrow_right_24),
contentDescription = "Max"
)
}
}
}

View file

@ -69,7 +69,7 @@ fun WelcomeStartScreen(
)
}
}
) { it ->
) {
Column(
modifier = Modifier
.padding(it)

View file

@ -85,7 +85,7 @@ fun WelcomeTOSScreen(
)
}
}
) { it ->
) {
Column(
modifier = Modifier
.padding(it)

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M17.59,18l1.41,-1.41l-4.58,-4.59l4.58,-4.59l-1.41,-1.41l-6,6z"/>
<path android:fillColor="@android:color/white" android:pathData="M11,18l1.41,-1.41l-4.58,-4.59l4.58,-4.59l-1.41,-1.41l-6,6z"/>
</vector>

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M6.41,6l-1.41,1.41l4.58,4.59l-4.58,4.59l1.41,1.41l6,-6z"/>
<path android:fillColor="@android:color/white" android:pathData="M13,6l-1.41,1.41l4.58,4.59l-4.58,4.59l1.41,1.41l6,-6z"/>
</vector>