diff --git a/app/build.gradle b/app/build.gradle index 8d29d88..5dc4160 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,8 +90,10 @@ dependencies { testImplementation "org.mockito:mockito-android:4.1.0" // Hilt - implementation "com.google.dagger:hilt-android:2.48" - kapt "com.google.dagger:hilt-compiler:2.48" + implementation "com.google.dagger:hilt-android:2.49" + kapt "com.google.dagger:hilt-compiler:2.49" + + implementation "androidx.hilt:hilt-navigation-compose:1.2.0" // Room implementation 'androidx.room:room-runtime:2.6.1' @@ -99,8 +101,11 @@ dependencies { implementation 'androidx.room:room-ktx:2.6.1' // Proto DataStore - implementation 'androidx.datastore:datastore:1.1.1' implementation 'com.google.protobuf:protobuf-javalite:3.21.7' + implementation 'androidx.datastore:datastore:1.1.1' + implementation("androidx.datastore:datastore-core:1.1.1") +// implementation("androidx.datastore:datastore-preferences:1.1.1") +// implementation("androidx.datastore:datastore-preferences-core:1.1.1") debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad9d132..7228303 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> { output: OutputStream ) = t.writeTo(output) -} \ No newline at end of file +} + +/** + * Added field to Context to enable getting an instance of ProtoSettings where needed. + */ +val Context.protoSettingsDataStore: DataStore by dataStore( + fileName = "settings.proto", + serializer = SettingsSerializer() +) diff --git a/app/src/main/java/be/re/writand/data/local/models/UserSettings.kt b/app/src/main/java/be/re/writand/data/local/models/UserSettings.kt index adf09f0..145ca6a 100644 --- a/app/src/main/java/be/re/writand/data/local/models/UserSettings.kt +++ b/app/src/main/java/be/re/writand/data/local/models/UserSettings.kt @@ -5,7 +5,38 @@ package be.re.writand.data.local.models */ enum class UserLanguage { ENGLISH, - DUTCH + DUTCH; + + /** + * Overwritten toString function for displaying the different languages as text on the screen. + */ + override fun toString(): String { + return when (this) { + ENGLISH -> "English" + DUTCH -> "Dutch" + } + } +} + +/** + * Checks whether the given string is a language supported by the enum class UserLanguage. + */ +fun String.isUserLanguage(): Boolean { + return UserLanguage.entries.map { entry -> entry.toString() }.contains(this) +} + +/** + * Converts a given string to a valid UserLanguage or English when the string isn't valid. + * The choice to give English back when invalid is chosen as the checks for input strings should be + * checked in a use-case/viewmodel with the function isUserLanguage() first. + */ +fun String.toUserLanguage(): UserLanguage { + // when i18n: first translate to English then get back UserLanguage + return when (this) { + "English" -> UserLanguage.ENGLISH + "Dutch" -> UserLanguage.DUTCH + else -> UserLanguage.ENGLISH + } } /** @@ -13,7 +44,38 @@ enum class UserLanguage { */ enum class UserTheme { DARK, - LIGHT + LIGHT; + + /** + * Overwritten toString function for displaying the different themes as text on the screen. + */ + override fun toString(): String { + return when (this) { + DARK -> "Dark" + LIGHT -> "Light" + } + } +} + +/** + * Checks whether the given string is a theme supported by the enum class UserTheme. + */ +fun String.isUserTheme(): Boolean { + return UserTheme.entries.map { entry -> entry.toString() }.contains(this) +} + +/** + * Converts a given string to a valid UserTheme or the Dark when the string isn't valid. + * The choice to give Dark back when invalid is chosen as the checks for input strings should be + * checked in a use-case/viewmodel with the function isUserTheme() first. + */ +fun String.toUserTheme(): UserTheme { + // when i18n: first translate to English then get back UserLanguage + return when (this) { + "Dark" -> UserTheme.DARK + "Light" -> UserTheme.LIGHT + else -> UserTheme.DARK // should not occur: checks should happen in view models + } } /** @@ -31,5 +93,5 @@ data class UserSettings( var userTheme: UserTheme = UserTheme.DARK, var maxSavedProjects: Int = 3, var maxSavedFiles: Int = 5, - var fontSize: Int = 10 -) + var fontSize: Float = 14.0f +) \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/data/repos/settings/IUserSettingsRepository.kt b/app/src/main/java/be/re/writand/data/repos/settings/IUserSettingsRepository.kt index 905b018..7af4f55 100644 --- a/app/src/main/java/be/re/writand/data/repos/settings/IUserSettingsRepository.kt +++ b/app/src/main/java/be/re/writand/data/repos/settings/IUserSettingsRepository.kt @@ -1,15 +1,18 @@ package be.re.writand.data.repos.settings +import be.re.writand.ProtoSettings import be.re.writand.data.local.models.UserLanguage import be.re.writand.data.local.models.UserSettings import be.re.writand.data.local.models.UserTheme /** - * Repository containing getters and setters to interact with all of the user settings + * Repository containing initializer, and setters to interact with all of the user settings. */ interface IUserSettingsRepository { - suspend fun getUserSettings(): UserSettings + suspend fun initializeSettingsIfNull() + + suspend fun toUserSettings(protoSettings: ProtoSettings): UserSettings suspend fun setLanguage(userLanguage: UserLanguage) @@ -19,5 +22,5 @@ interface IUserSettingsRepository { suspend fun setMaxSavedFiles(maxSaved: Int) - suspend fun setFontSize(size: Int) + suspend fun setFontSize(size: Float) } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/data/repos/settings/UserSettingsRepository.kt b/app/src/main/java/be/re/writand/data/repos/settings/UserSettingsRepository.kt index 233e89d..8fc7645 100644 --- a/app/src/main/java/be/re/writand/data/repos/settings/UserSettingsRepository.kt +++ b/app/src/main/java/be/re/writand/data/repos/settings/UserSettingsRepository.kt @@ -1,37 +1,70 @@ package be.re.writand.data.repos.settings +import android.content.Context import androidx.datastore.core.DataStore +import androidx.datastore.core.IOException +import be.re.writand.ProtoLanguage import be.re.writand.ProtoSettings +import be.re.writand.ProtoTheme import be.re.writand.data.local.models.UserLanguage import be.re.writand.data.local.models.UserSettings import be.re.writand.data.local.models.UserTheme +import be.re.writand.data.local.protoSettingsDataStore import be.re.writand.utils.toProtoLanguage import be.re.writand.utils.toProtoTheme import be.re.writand.utils.toUserLanguage import be.re.writand.utils.toUserTheme +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton @Singleton class UserSettingsRepository @Inject constructor( - private val store: DataStore + context: Context ) : IUserSettingsRepository { - private val userSettings = store.data + private val dataStore: DataStore = context.protoSettingsDataStore + val userSettings: Flow = dataStore.data + .catch { exception -> + if (exception is IOException) { + emit(ProtoSettings.getDefaultInstance()) + } else { + throw exception + } + } + .map { protoSettings -> + toUserSettings(protoSettings) + } - override suspend fun getUserSettings(): UserSettings { + override suspend fun initializeSettingsIfNull() { + val currentSettings = dataStore.data.first(); + if (currentSettings == ProtoSettings.getDefaultInstance()) { + val defaultSettings = ProtoSettings.newBuilder() + .setLanguage(ProtoLanguage.PROTO_LANGUAGE_ENGLISH) + .setTheme(ProtoTheme.PROTO_THEME_DARK) + .setMaxSavedProjects(3) + .setMaxSavedFiles(5) + .setFontSize(14.0f) + .build() + dataStore.updateData { defaultSettings } + } + } + + override suspend fun toUserSettings(protoSettings: ProtoSettings): UserSettings { return UserSettings( - userLanguage = userSettings.first().language.toUserLanguage(), - userTheme = userSettings.first().theme.toUserTheme(), - maxSavedProjects = userSettings.first().maxSavedProjects, - maxSavedFiles = userSettings.first().maxSavedFiles, - fontSize = userSettings.first().fontSize + userLanguage = protoSettings.language.toUserLanguage(), + userTheme = protoSettings.theme.toUserTheme(), + maxSavedProjects = protoSettings.maxSavedProjects, + maxSavedFiles = protoSettings.maxSavedFiles, + fontSize = protoSettings.fontSize ) } override suspend fun setLanguage(userLanguage: UserLanguage) { - store.updateData { + dataStore.updateData { val builder = it.toBuilder() builder.setLanguage(userLanguage.toProtoLanguage()) builder.build() @@ -39,7 +72,7 @@ class UserSettingsRepository @Inject constructor( } override suspend fun setTheme(userTheme: UserTheme) { - store.updateData { + dataStore.updateData { val builder = it.toBuilder() builder.setTheme(userTheme.toProtoTheme()) builder.build() @@ -47,7 +80,7 @@ class UserSettingsRepository @Inject constructor( } override suspend fun setMaxSavedProjects(maxSaved: Int) { - store.updateData { + dataStore.updateData { val builder = it.toBuilder() builder.setMaxSavedProjects(maxSaved) builder.build() @@ -55,15 +88,15 @@ class UserSettingsRepository @Inject constructor( } override suspend fun setMaxSavedFiles(maxSaved: Int) { - store.updateData { + dataStore.updateData { val builder = it.toBuilder() builder.setMaxSavedFiles(maxSaved) builder.build() } } - override suspend fun setFontSize(size: Int) { - store.updateData { + override suspend fun setFontSize(size: Float) { + dataStore.updateData { val builder = it.toBuilder() builder.setFontSize(size) builder.build() diff --git a/app/src/main/java/be/re/writand/data/repos/tos/TOSRepository.kt b/app/src/main/java/be/re/writand/data/repos/tos/TOSRepository.kt index 1eae875..b59beb3 100644 --- a/app/src/main/java/be/re/writand/data/repos/tos/TOSRepository.kt +++ b/app/src/main/java/be/re/writand/data/repos/tos/TOSRepository.kt @@ -1,12 +1,9 @@ package be.re.writand.data.repos.tos import android.content.Context -import android.util.Log import be.re.writand.data.local.models.TermsOfService import be.re.writand.utils.Version -import java.io.File import java.io.InputStream -import java.util.concurrent.Flow import javax.inject.Inject class TOSRepository @Inject constructor( diff --git a/app/src/main/java/be/re/writand/di/DatabaseModule.kt b/app/src/main/java/be/re/writand/di/DatabaseModule.kt index aa9e9f6..5fc206c 100644 --- a/app/src/main/java/be/re/writand/di/DatabaseModule.kt +++ b/app/src/main/java/be/re/writand/di/DatabaseModule.kt @@ -1,9 +1,10 @@ package be.re.writand.di import android.content.Context -import android.content.res.AssetManager import be.re.writand.data.local.db.ProjectsDao import be.re.writand.data.local.db.ProjectsDatabase +import be.re.writand.data.repos.settings.IUserSettingsRepository +import be.re.writand.data.repos.settings.UserSettingsRepository import be.re.writand.data.repos.tos.ITOSRepository import be.re.writand.data.repos.tos.TOSRepository import dagger.Module @@ -38,7 +39,12 @@ class DatabaseModule { } @Provides - fun provideTOSRepository(repo: TOSRepository): ITOSRepository { - return repo + fun provideUserSettingsRepository(@ApplicationContext context: Context): IUserSettingsRepository { + return UserSettingsRepository(context) + } + + @Provides + fun provideTOSRepository(@ApplicationContext context: Context): ITOSRepository { + return TOSRepository(context) } } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/domain/.keep b/app/src/main/java/be/re/writand/domain/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/be/re/writand/domain/settings/SetFontSizeSettingsUseCase.kt b/app/src/main/java/be/re/writand/domain/settings/SetFontSizeSettingsUseCase.kt new file mode 100644 index 0000000..7d7ac85 --- /dev/null +++ b/app/src/main/java/be/re/writand/domain/settings/SetFontSizeSettingsUseCase.kt @@ -0,0 +1,27 @@ +package be.re.writand.domain.settings + +import be.re.writand.data.repos.settings.UserSettingsRepository +import javax.inject.Inject + +/** + * Use-case that takes in a string which will alter the current UserSettings if the string is a + * valid Float and above 0, otherwise nothing will be altered. + * @param[userSettingsRepository] repository to change the UserSettings when valid input is given. + */ +class SetFontSizeSettingsUseCase @Inject constructor( + private val userSettingsRepository: UserSettingsRepository +) { + + suspend operator fun invoke(fontSize: String): NumberFormatException? { + try { + val newFontSize: Float = fontSize.toFloat() + if (newFontSize > 0) { + userSettingsRepository.setFontSize(newFontSize) + } + return null + } catch (e: NumberFormatException) { + return e + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/domain/settings/SetLanguageSettingsUseCase.kt b/app/src/main/java/be/re/writand/domain/settings/SetLanguageSettingsUseCase.kt new file mode 100644 index 0000000..17b76dd --- /dev/null +++ b/app/src/main/java/be/re/writand/domain/settings/SetLanguageSettingsUseCase.kt @@ -0,0 +1,23 @@ +package be.re.writand.domain.settings + +import be.re.writand.data.local.models.isUserLanguage +import be.re.writand.data.local.models.toUserLanguage +import be.re.writand.data.repos.settings.UserSettingsRepository +import javax.inject.Inject + +/** + * Use-case that takes in a string which will alter the current UserSettings if the string is part + * of the defined UserLanguage fields, otherwise nothing will be altered. + * @param[userSettingsRepository] repository to change the UserSettings when valid input is given. + */ +class SetLanguageSettingsUseCase @Inject constructor( + private val userSettingsRepository: UserSettingsRepository +) { + + suspend operator fun invoke(language: String) { + if (language.isUserLanguage()) { + userSettingsRepository.setLanguage(language.toUserLanguage()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/domain/settings/SetThemeSettingsUseCase.kt b/app/src/main/java/be/re/writand/domain/settings/SetThemeSettingsUseCase.kt new file mode 100644 index 0000000..08df511 --- /dev/null +++ b/app/src/main/java/be/re/writand/domain/settings/SetThemeSettingsUseCase.kt @@ -0,0 +1,23 @@ +package be.re.writand.domain.settings + +import be.re.writand.data.local.models.isUserTheme +import be.re.writand.data.local.models.toUserTheme +import be.re.writand.data.repos.settings.UserSettingsRepository +import javax.inject.Inject + +/** + * Use-case that takes in a string which will alter the current UserSettings if the string is part + * of the defined UserTheme fields, otherwise nothing will be altered. + * @param[userSettingsRepository] repository to change the UserSettings when valid input is given. + */ +class SetThemeSettingsUseCase @Inject constructor( + private val userSettingsRepository: UserSettingsRepository +) { + + suspend operator fun invoke(theme: String) { + if (theme.isUserTheme()) { + userSettingsRepository.setTheme(theme.toUserTheme()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/domain/tos/GetTOSUseCase.kt b/app/src/main/java/be/re/writand/domain/tos/GetTOSUseCase.kt new file mode 100644 index 0000000..64d4f04 --- /dev/null +++ b/app/src/main/java/be/re/writand/domain/tos/GetTOSUseCase.kt @@ -0,0 +1,17 @@ +package be.re.writand.domain.tos + +import be.re.writand.data.repos.tos.ITOSRepository +import javax.inject.Inject + +/** + * Use-case for getting back the TOS file in a list. + */ +class GetTOSUseCase @Inject constructor( + private val tosRepository: ITOSRepository +) { + + operator fun invoke(): List { + return tosRepository.getTOS().text + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/SettingsViewModel.kt b/app/src/main/java/be/re/writand/screens/SettingsViewModel.kt new file mode 100644 index 0000000..1bebd61 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/SettingsViewModel.kt @@ -0,0 +1,31 @@ +package be.re.writand.screens + +import be.re.writand.data.local.models.UserSettings +import be.re.writand.data.repos.settings.UserSettingsRepository +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 components needing the UserSettings for font size, theme etc. + * This view model should be a general model offering the UserSettings, and nothing more. + * @param[userSettingsRepository] providing the UserSettings. + */ +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val userSettingsRepository: UserSettingsRepository +): WViewModel() { + + private val _settings = MutableStateFlow(null) + val settings: StateFlow = _settings + + init { + launchCatching { + userSettingsRepository.userSettings.collect { settings -> + _settings.value = settings + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/components/Buttons.kt b/app/src/main/java/be/re/writand/screens/components/Buttons.kt index 990ebbd..b3def35 100644 --- a/app/src/main/java/be/re/writand/screens/components/Buttons.kt +++ b/app/src/main/java/be/re/writand/screens/components/Buttons.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -14,21 +13,24 @@ import be.re.writand.ui.theme.MainGreen /** * A standard button with predefined color and shape. * @param[text] the text that should be shown in the button. + * @param[modifier] compose.ui modifier to change the button, not the text. * @param[onClick] the callback function to execute when the button is clicked. * @param[enabled] boolean to express if the button is enabled to be clicked or not. */ @Composable fun WButton( text: String, + modifier: Modifier = Modifier, onClick: () -> Unit, enabled: Boolean = true ) { Button( onClick = onClick, + modifier = modifier, colors = ButtonDefaults.buttonColors(containerColor = MainGreen), shape = RoundedCornerShape(10.dp), enabled = enabled ) { - Text(text = text, color = Color.Black, modifier = Modifier.padding(5.dp)) + WText(text = text, color = Color.Black, modifier = Modifier.padding(5.dp)) } } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/components/TextFields.kt b/app/src/main/java/be/re/writand/screens/components/TextFields.kt new file mode 100644 index 0000000..1f8e47c --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/components/TextFields.kt @@ -0,0 +1,89 @@ +package be.re.writand.screens.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Text field composable to use the themed colors and font size from settings. + * @param[title] label in text field itself instead of in front of text field. + * @param[value] initial value of the content of the text field. + * @param[onTextChange] lambda that is executed when text field is being edited. + * @param[keyboardOptions] options to have different keyboard layouts (numeric only etc). + * @param[leadingIcon] icon in the text field, in front of the 'value' parameter (e.g. search icon). + */ +@Composable +fun WTextField( + title: String, + value: String, + onTextChange: (String) -> Unit, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + leadingIcon: @Composable (() -> Unit)? = null +) { + TextField( + singleLine = true, + label = { WText(text = title, color = Color.Black) }, + value = value, + onValueChange = { onTextChange(it) }, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = MaterialTheme.colorScheme.tertiary + ), + keyboardOptions = keyboardOptions, + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxWidth(), + leadingIcon = leadingIcon + ) +} + +/** + * Text field composable with label in front to use the themed colors and font size from settings. + * This is the same as [WTextField], but the label in the text field is gone, and the label is + * placed in front of the text field. + * @param[title] label in text field itself instead of in front of text field. + * @param[labelSize] the width of the label [title]. + * @param[value] initial value of the content of the text field. + * @param[fullSize] the full width of the label plus text field. + * @param[onTextChange] lambda that is executed when text field is being edited. + * @param[keyboardOptions] options to have different keyboard layouts (numeric only etc). + * @param[leadingIcon] icon in the text field, in front of the 'value' parameter (e.g. search icon). + */ +@Composable +fun WLabelAndTextField( + title: String, + labelSize: Dp = 75.dp, + value: String, + fullSize: Dp = 500.dp, + onTextChange: (String) -> Unit, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + leadingIcon: @Composable (() -> Unit)? = null +) { + Row( + modifier = Modifier.width(fullSize), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + WText(text = title, modifier = Modifier.width(labelSize)) + Spacer(modifier = Modifier.width(20.dp)) + WTextField( + title = "", + value = value, + onTextChange = onTextChange, + keyboardOptions = keyboardOptions, + leadingIcon = leadingIcon + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/components/WCheckbox.kt b/app/src/main/java/be/re/writand/screens/components/WCheckbox.kt new file mode 100644 index 0000000..da108fe --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/components/WCheckbox.kt @@ -0,0 +1,30 @@ +package be.re.writand.screens.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Checkbox +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +/** + * Checkbox composable using the theme present in the users settings. + * @param[text] text present next to the checkbox. + * @param[modifier] compose.ui modifier to change the checkbox with its text. + * @param[checked] boolean that tells whether the checkbox is checked or not. + * @param[updateChecked] lambda that is called when a checkbox is clicked. + */ +@Composable +fun WCheckbox( + text: String, + modifier: Modifier = Modifier, + checked: Boolean, + updateChecked: (String) -> Unit +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = checked, onCheckedChange = { updateChecked(text) }) + WText(text = text) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/components/WLoadingIndicator.kt b/app/src/main/java/be/re/writand/screens/components/WLoadingIndicator.kt new file mode 100644 index 0000000..2161452 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/components/WLoadingIndicator.kt @@ -0,0 +1,30 @@ +package be.re.writand.screens.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +/** + * Loading indicator composable having a progress indicator and a quote. + */ +@Composable +fun WLoadingIndicator() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 30.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator(modifier = Modifier.padding(vertical = 10.dp)) + + WText(text = "while( !( succeed = try() ) );", textAlign = TextAlign.Center) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/components/WLogoImage.kt b/app/src/main/java/be/re/writand/screens/components/WLogoImage.kt new file mode 100644 index 0000000..3e16a92 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/components/WLogoImage.kt @@ -0,0 +1,28 @@ +package be.re.writand.screens.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import be.re.writand.R + +/** + * Logo composable having the background in the logo. + * @param[modifier] compose.ui modifier to change the Image itself, the .fillMaxSize() is always + * appended to the end of the modifier. + * @param[alignment] alignment parameter used to place the painterResource in the given bounds + * defined by the width and height. + */ +@Composable +fun WLogoImage(modifier: Modifier, alignment: Alignment) { + Image( + painterResource(id = R.drawable.writand_with_background), + contentDescription = "logo", + contentScale = ContentScale.Fit, + modifier = modifier.fillMaxSize(), + alignment = alignment + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/components/WRadioButtonsSelector.kt b/app/src/main/java/be/re/writand/screens/components/WRadioButtonsSelector.kt new file mode 100644 index 0000000..6d6dd1f --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/components/WRadioButtonsSelector.kt @@ -0,0 +1,124 @@ +package be.re.writand.screens.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Given a list of options, it produces a composable of elements having a radio button with text + * on the left, elements are placed all together on a ROW. + * @param[enable] a boolean to disable all the buttons when value is set to false. + * @param[textTitle] text to explain the category of options. + * @param[labelSize] the width [textTitle] is allowed to use. + * @param[options] list of string options the user should be choosing of. + * @param[selectedOption] the current option that is selected by the radiobuttons. + * @param[onOptionSelected] lambda to change which button is selected. + * @param[vMChangeField] view model lambda to call when selected option is changed. + */ +@Composable +fun WRadioButtonsSelectorRowWise( + enable: Boolean, + textTitle: String, + labelSize: Dp = 75.dp, + options: List, + selectedOption: String, + onOptionSelected: (String) -> Unit, + vMChangeField: () -> Unit = {} +) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + WText(text = textTitle, modifier = Modifier.width(labelSize)) + options.forEach { text -> + Row( + modifier = Modifier + .padding(10.dp) + .selectable( + selected = (text == selectedOption), + onClick = { + onOptionSelected(text) + vMChangeField() + } + ), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (text == selectedOption), + enabled = enable, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colorScheme.tertiary, + ), + onClick = { + onOptionSelected(text) + vMChangeField() + } + ) + WText(text = text) + } + } + } +} + +/** + * Given a list of options, it produces a composable of elements having a radio button with text + * on the left, elements are placed all together on a COLUMN. + * @param[enable] a boolean to disable all the buttons when value is set to false. + * @param[textTitle] text to explain the category of options. + * @param[labelSize] the width [textTitle] is allowed to use. + * @param[options] list of string options the user should be choosing of. + * @param[selectedOption] the current option that is selected by the radiobuttons. + * @param[onOptionSelected] lambda to change which button is selected. + * @param[vMChangeField] view model lambda to call when selected option is changed. + */ +@Composable +fun WRadioButtonsSelectorColumnWise( + enable: Boolean, + textTitle: String, + labelSize: Dp = 75.dp, + options: List, + selectedOption: String, + onOptionSelected: (String) -> Unit, + vMChangeField: () -> Unit = {} +) { + Row { + WText(text = textTitle, modifier = Modifier.width(labelSize)) + Column { + options.forEach { text -> + Row( + modifier = Modifier + .selectable( + selected = (text == selectedOption), + onClick = { + onOptionSelected(text) + vMChangeField() + } + ), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (text == selectedOption), + enabled = enable, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colorScheme.tertiary + ), + onClick = { + onOptionSelected(text) + vMChangeField() + } + ) + WText(text = text) + } + } + } + } +} diff --git a/app/src/main/java/be/re/writand/screens/components/WText.kt b/app/src/main/java/be/re/writand/screens/components/WText.kt new file mode 100644 index 0000000..f8b69f0 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/components/WText.kt @@ -0,0 +1,80 @@ +package be.re.writand.screens.components + +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import be.re.writand.screens.SettingsViewModel + +/** + * Composable to use UserSettings in the standard compose.material3.Text, + * all parameters are explained in there, but the [settingsViewModel]. + * @param[settingsViewModel] view model that provides the UserSettings. + * @see[Text]. + */ +@Composable +fun WText( + text: String, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.onPrimary, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: ((TextLayoutResult) -> Unit)? = null, + style: TextStyle = LocalTextStyle.current, + settingsViewModel: SettingsViewModel = hiltViewModel(), +) { + + val settings by settingsViewModel.settings.collectAsState() + val size = if (fontSize == TextUnit.Unspecified && settings != null) { + settings!!.fontSize.sp + } else if (fontSize != TextUnit.Unspecified) { + fontSize + } else { + 14.sp + } + + Text( + text = text, + modifier = modifier, + color = color, + fontSize = size, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + onTextLayout = onTextLayout, + style = style + ) +} diff --git a/app/src/main/java/be/re/writand/screens/splash/SplashScreen.kt b/app/src/main/java/be/re/writand/screens/splash/SplashScreen.kt index 5a440b8..9b9ebf1 100644 --- a/app/src/main/java/be/re/writand/screens/splash/SplashScreen.kt +++ b/app/src/main/java/be/re/writand/screens/splash/SplashScreen.kt @@ -5,13 +5,20 @@ import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import be.re.writand.navigation.WAppDestinations import be.re.writand.screens.components.WButton +/** + * Screen displayed on the startup of the app showing the logo while UserSettings are being loaded. + * @param[navHostController] controller to use the WNavGraph to change screen to right destination. + * @param[vM] view model corresponding to this screen. + */ @Composable fun SplashScreen( - navHostController: NavHostController + navHostController: NavHostController, + vM: SplashViewModel = hiltViewModel() ) { Box(modifier = Modifier.size(20.dp, 20.dp)) { diff --git a/app/src/main/java/be/re/writand/screens/splash/SplashViewModel.kt b/app/src/main/java/be/re/writand/screens/splash/SplashViewModel.kt index 8c7bf72..d5f1ba6 100644 --- a/app/src/main/java/be/re/writand/screens/splash/SplashViewModel.kt +++ b/app/src/main/java/be/re/writand/screens/splash/SplashViewModel.kt @@ -1,4 +1,26 @@ package be.re.writand.screens.splash -class SplashViewModel { +import android.util.Log +import be.re.writand.data.repos.settings.UserSettingsRepository +import be.re.writand.screens.WViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +/** + * View model to be used by SplashScreen that handles the UserSettings and navigation. + * @param[userSettingsRepository] repository to setup the settings if not initialized yet. + */ +@HiltViewModel +class SplashViewModel @Inject constructor( + userSettingsRepository: UserSettingsRepository +): WViewModel() { + + init { + // this is needed for every start of the app to direct the user: + // welcome screen, project picker, editor (--> version 2 is for the editor as well) + launchCatching { + userSettingsRepository.initializeSettingsIfNull() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/welcome/WelcomeSettingsScreen.kt b/app/src/main/java/be/re/writand/screens/welcome/WelcomeSettingsScreen.kt index 43f5e14..d534387 100644 --- a/app/src/main/java/be/re/writand/screens/welcome/WelcomeSettingsScreen.kt +++ b/app/src/main/java/be/re/writand/screens/welcome/WelcomeSettingsScreen.kt @@ -1,24 +1,160 @@ package be.re.writand.screens.welcome +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.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.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold 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.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController +import be.re.writand.data.local.models.UserLanguage +import be.re.writand.data.local.models.UserTheme import be.re.writand.navigation.WAppDestinations import be.re.writand.screens.components.WButton +import be.re.writand.screens.components.WLabelAndTextField +import be.re.writand.screens.components.WLogoImage +import be.re.writand.screens.components.WRadioButtonsSelectorRowWise +import be.re.writand.screens.components.WText +/** + * Screen displayed for first time users to set their basic settings such as language etc. + * @param[navHostController] controller to use the WNavGraph to change screen to right destination. + * @param[vM] view model that handles everything for this screen for changes in settings and ui. + */ @Composable fun WelcomeSettingsScreen( - navHostController: NavHostController + navHostController: NavHostController, + vM: WelcomeSettingsViewModel = hiltViewModel() ) { - - Box(modifier = Modifier.size(20.dp, 20.dp)) { - WButton( - text = "Finish", - onClick = { navHostController.navigate(WAppDestinations.PROJECT_PICKER) } - ) + val languageOptions = UserLanguage.entries.map { entry -> entry.toString() } + val (languageSelected, onLanguageSelect) = remember { + mutableStateOf(languageOptions[0]) } + val themeOptions = UserTheme.entries.map { entry -> entry.toString() } + val (themeSelected, themeSelect) = remember { + mutableStateOf(themeOptions[0]) + } + + Box( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.secondary), + contentAlignment = Alignment.Center + ) { + Scaffold(modifier = Modifier + .size(600.dp) + .border( + 1.dp, + color = MaterialTheme.colorScheme.tertiary, + shape = RoundedCornerShape(10.dp) + ) + .clip(shape = RoundedCornerShape(10.dp)), + topBar = { + Box(modifier = Modifier.height(75.dp)) { + WLogoImage( + modifier = Modifier.padding(25.dp), + alignment = Alignment.CenterStart + ) + } + }, + bottomBar = { + Row( + modifier = Modifier + .fillMaxWidth(1f) + .padding(15.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + WButton( + text = "Apply", + onClick = { + vM.onApply(languageSelected, themeSelected, vM.textFieldInput.value) + }, + modifier = Modifier.padding(end = 10.dp) + ) + + WButton( + text = "Finish", + onClick = { + navHostController.navigate(WAppDestinations.PROJECT_PICKER) + } + ) + } + } + ) { + Column( + modifier = Modifier + .padding(it) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(50.dp, 15.dp) + ) { + WText( + text = "Welcome", + color = MaterialTheme.colorScheme.tertiary, + fontWeight = FontWeight.ExtraBold, + fontSize = 25.sp + ) + WText( + text = " to settings", + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.ExtraBold, + fontSize = 25.sp + ) + } + + Column( + modifier = Modifier.padding(start = 100.dp) + ) { + WRadioButtonsSelectorRowWise( + enable = true, + textTitle = "Language", + options = languageOptions, + selectedOption = languageSelected, + onOptionSelected = onLanguageSelect + ) + + WRadioButtonsSelectorRowWise( + enable = true, + textTitle = "Theme", + options = themeOptions, + selectedOption = themeSelected, + onOptionSelected = themeSelect + ) + + WLabelAndTextField( + title = "Fontsize", + value = vM.textFieldInput.value, + fullSize = 200.dp, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + onTextChange = vM::onTextFieldChange + ) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/welcome/WelcomeSettingsViewModel.kt b/app/src/main/java/be/re/writand/screens/welcome/WelcomeSettingsViewModel.kt new file mode 100644 index 0000000..11730c1 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/welcome/WelcomeSettingsViewModel.kt @@ -0,0 +1,64 @@ +package be.re.writand.screens.welcome + +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.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 WelcomeSettingsScreen that handles the UserSettings and buttons. + * @param[userSettingsRepository] repository to setup the settings if not initialized yet. + * @param[setLanguage] use-case that alters the UserLanguage of the UserSettings if a valid String + * is given as argument. + * @param[setTheme] use-case that alters the UserTheme of the UserSettings if a valid String + * is given as argument. + * @param[setFontSize] use-case that alters the fontSize of the UserSettings if the field contains + * a String that can be converted to a Float above 0. + */ +@HiltViewModel +class WelcomeSettingsViewModel @Inject constructor( + private val userSettingsRepository: UserSettingsRepository, + private val setLanguage: SetLanguageSettingsUseCase, + private val setTheme: SetThemeSettingsUseCase, + private val setFontSize: SetFontSizeSettingsUseCase +) : WViewModel() { + + private val _settings = MutableStateFlow(null) + val settings: StateFlow = _settings + + val textFieldInput = mutableStateOf(_settings.value?.fontSize.toString()) + + init { + launchCatching { + userSettingsRepository.userSettings.collect { settings -> + _settings.value = settings + textFieldInput.value = settings.fontSize.toString() + } + } + } + + fun onTextFieldChange(newValue: String) { + textFieldInput.value = newValue + } + + fun onApply(language: String, theme: String, fontSize: String) { + launchCatching { + setLanguage(language) + setTheme(theme) + + val errorFontSize: NumberFormatException? = setFontSize(fontSize) + errorFontSize.let { + // reset the text field to its old value + textFieldInput.value = _settings.value?.fontSize.toString() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/welcome/WelcomeStartScreen.kt b/app/src/main/java/be/re/writand/screens/welcome/WelcomeStartScreen.kt index 352c3bf..99c72b1 100644 --- a/app/src/main/java/be/re/writand/screens/welcome/WelcomeStartScreen.kt +++ b/app/src/main/java/be/re/writand/screens/welcome/WelcomeStartScreen.kt @@ -1,24 +1,103 @@ package be.re.writand.screens.welcome +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.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.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import be.re.writand.navigation.WAppDestinations import be.re.writand.screens.components.WButton +import be.re.writand.screens.components.WLogoImage +import be.re.writand.screens.components.WText +/** + * Screen displayed for first time users to welcome them to the app. + * @param[navHostController] controller to use the WNavGraph to change screen to right destination. + */ @Composable fun WelcomeStartScreen( navHostController: NavHostController ) { - - Box(modifier = Modifier.size(20.dp, 20.dp)) { - WButton( - text = "Next", - onClick = { navHostController.navigate(WAppDestinations.WELCOME_TOS) } - ) + Box( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.secondary), + contentAlignment = Alignment.Center + ) { + Scaffold(modifier = Modifier + .size(600.dp) + .border( + 1.dp, + color = MaterialTheme.colorScheme.tertiary, + shape = RoundedCornerShape(10.dp) + ) + .clip(shape = RoundedCornerShape(10.dp)), + topBar = { + Box(modifier = Modifier.height(75.dp)) { + WLogoImage( + modifier = Modifier.padding(25.dp), + alignment = Alignment.CenterStart + ) + } + }, + bottomBar = { + Box( + modifier = Modifier + .fillMaxWidth(1f) + .padding(15.dp), + contentAlignment = Alignment.BottomEnd + ) { + WButton( + text = "Next", + onClick = { navHostController.navigate(WAppDestinations.WELCOME_TOS) } + ) + } + } + ) { it -> + Column( + modifier = Modifier + .padding(it) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(50.dp, 15.dp) + ) { + WText( + text = "Welcome", + color = MaterialTheme.colorScheme.tertiary, + fontWeight = FontWeight.ExtraBold, + fontSize = 25.sp + ) + WText( + text = " to Writand", + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.ExtraBold, + fontSize = 25.sp + ) + } + WText(text = "Any fool can write code that a computer can understand.\nGood programmers can write code that humans can understand.") + WText(text = "~ Martin Fowler") + } + } } - } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSScreen.kt b/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSScreen.kt index df8b933..891d6ae 100644 --- a/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSScreen.kt +++ b/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSScreen.kt @@ -1,24 +1,152 @@ package be.re.writand.screens.welcome +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.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.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text 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.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import be.re.writand.navigation.WAppDestinations import be.re.writand.screens.components.WButton +import be.re.writand.screens.components.WCheckbox +import be.re.writand.screens.components.WLoadingIndicator +import be.re.writand.screens.components.WLogoImage +import be.re.writand.screens.components.WText +/** + * Screen displayed for first time users to set their basic settings such as language etc. + * @param[navHostController] controller to use the WNavGraph to change screen to right destination. + * @param[vM] view model that gives access to the TOS file content. + */ @Composable fun WelcomeTOSScreen( - navHostController: NavHostController + navHostController: NavHostController, + vM: WelcomeTOSViewModel = hiltViewModel() ) { + var checked by remember { mutableStateOf(false) } + val uiState by vM.uiState.collectAsState() - Box(modifier = Modifier.size(20.dp, 20.dp)) { - WButton( - text = "Next", - onClick = { navHostController.navigate(WAppDestinations.WELCOME_SETTINGS) } - ) + Box( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.secondary), + contentAlignment = Alignment.Center + ) { + Scaffold(modifier = Modifier + .size(600.dp) + .border( + 1.dp, + color = MaterialTheme.colorScheme.tertiary, + shape = RoundedCornerShape(10.dp) + ) + .clip(shape = RoundedCornerShape(10.dp)), + topBar = { + Box(modifier = Modifier.height(75.dp)) { + WLogoImage( + modifier = Modifier.padding(25.dp), + alignment = Alignment.CenterStart + ) + } + }, + bottomBar = { + Box( + modifier = Modifier + .fillMaxWidth(1f) + .padding(15.dp), + contentAlignment = Alignment.BottomEnd + ) { + WButton( + text = "Next", + enabled = checked, + onClick = { navHostController.navigate(WAppDestinations.WELCOME_SETTINGS) } + ) + } + } + ) { it -> + Column( + modifier = Modifier + .padding(it) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(50.dp, 15.dp) + ) { + WText( + text = "Welcome", + color = MaterialTheme.colorScheme.tertiary, + fontWeight = FontWeight.ExtraBold, + fontSize = 25.sp + ) + WText( + text = " to TOS", + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.ExtraBold, + fontSize = 25.sp + ) + } + + Box( + modifier = Modifier + .height(350.dp) + .padding(horizontal = 50.dp, vertical = 25.dp) + .border(1.dp, color = MaterialTheme.colorScheme.secondary) + ) { + when (val value = uiState) { + WelcomeTOSUiState.Loading -> { + WLoadingIndicator() + } + + is WelcomeTOSUiState.Success -> { + LazyColumn { + items(value.tosStrings.size) { item -> + Text(text = value.tosStrings[item]) + } + } + } + + is WelcomeTOSUiState.Failed -> { + Text(text = value.message) + } + } + } + + WCheckbox( + text = "Accept", + modifier = Modifier + .padding(horizontal = 50.dp) + .fillMaxWidth(), + checked = checked, + updateChecked = { checked = !checked } + ) + } + } } - } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSUiState.kt b/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSUiState.kt new file mode 100644 index 0000000..f903cd8 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSUiState.kt @@ -0,0 +1,13 @@ +package be.re.writand.screens.welcome + +/** + * Interface to present the 3 differences stages of getting the TOS file content: + * - [Loading]: the content is pending. + * - [Success]: the TOS file is ready, and given in a list of Strings. + * - [Failed]: unable to get the TOS content from the file, give back an error message. + */ +sealed interface WelcomeTOSUiState { + object Loading: WelcomeTOSUiState + data class Success(val tosStrings: List): WelcomeTOSUiState + data class Failed(val message: String): WelcomeTOSUiState +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSViewModel.kt b/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSViewModel.kt new file mode 100644 index 0000000..c5a3ef0 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/welcome/WelcomeTOSViewModel.kt @@ -0,0 +1,33 @@ +package be.re.writand.screens.welcome + +import be.re.writand.domain.tos.GetTOSUseCase +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 WelcomeTOSScreen that handles the TOS content. + * This view model uses the WelcomeTOSUiState to enable handling failures of getting the file. + * @param[getTOSUseCase] use case to get the TOS content in a list of Strings. + */ +@HiltViewModel +class WelcomeTOSViewModel @Inject constructor( + private val getTOSUseCase: GetTOSUseCase +): WViewModel() { + + private val _uiState: MutableStateFlow = MutableStateFlow(WelcomeTOSUiState.Loading) + val uiState: StateFlow = _uiState + + init { + launchCatching { + val tosList: List = getTOSUseCase() + if (tosList.isNotEmpty()) { + _uiState.value = WelcomeTOSUiState.Success(tosList) + } else { + _uiState.value = WelcomeTOSUiState.Failed("Terms of Services could not be loaded.") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/ui/theme/Theme.kt b/app/src/main/java/be/re/writand/ui/theme/Theme.kt index 1279751..614f474 100644 --- a/app/src/main/java/be/re/writand/ui/theme/Theme.kt +++ b/app/src/main/java/be/re/writand/ui/theme/Theme.kt @@ -11,14 +11,14 @@ private val DarkColorPalette = darkColorScheme( primary = MainGrey, secondary = VariantDarkGrey, tertiary = MainGreen, - onBackground = Color.White + onPrimary = Color.White ) private val LightColorPalette = lightColorScheme( primary = Color.White, secondary = VariantLightGrey, tertiary = MainGreen, - onBackground = Color.Black + onPrimary = Color.Black /* Other default colors to override background = Color.White, diff --git a/app/src/main/proto/settings.proto b/app/src/main/proto/settings.proto index 032e8d1..2a00429 100644 --- a/app/src/main/proto/settings.proto +++ b/app/src/main/proto/settings.proto @@ -28,5 +28,5 @@ message ProtoSettings { ProtoTheme theme = 2; uint32 max_saved_projects = 3; uint32 max_saved_files = 4; - uint32 font_size = 5; + float font_size = 5; }; \ No newline at end of file diff --git a/app/src/main/res/drawable/writand_no_background.png b/app/src/main/res/drawable/writand_no_background.png new file mode 100644 index 0000000..02234c8 Binary files /dev/null and b/app/src/main/res/drawable/writand_no_background.png differ diff --git a/app/src/main/res/drawable/writand_with_background.png b/app/src/main/res/drawable/writand_with_background.png new file mode 100644 index 0000000..c4fdb55 Binary files /dev/null and b/app/src/main/res/drawable/writand_with_background.png differ diff --git a/build.gradle b/build.gradle index 343fae8..69a84fb 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,6 @@ plugins { id 'com.android.application' version '8.5.1' apply false id 'com.android.library' version '8.5.1' apply false id 'org.jetbrains.kotlin.android' version '1.9.0' apply false - id 'com.google.dagger.hilt.android' version '2.44' apply false + id 'com.google.dagger.hilt.android' version '2.49' apply false id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.21' } \ No newline at end of file