forked from Writand/writand
feat: welcome screens
This commit is contained in:
parent
c822126703
commit
5c56947c4f
36 changed files with 1176 additions and 65 deletions
|
@ -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"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".WApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
|
|
|
@ -3,18 +3,18 @@ package be.re.writand
|
|||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import be.re.writand.navigation.WNavGraph
|
||||
import be.re.writand.ui.theme.WritandTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@AndroidEntryPoint
|
||||
class MainActivity: ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
|
@ -26,7 +26,7 @@ class MainActivity : ComponentActivity() {
|
|||
) {
|
||||
WNavGraph(
|
||||
navHostController = rememberNavController(),
|
||||
modifier = Modifier.padding(5.dp)
|
||||
modifier = Modifier.background(color = MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
7
app/src/main/java/be/re/writand/WApplication.kt
Normal file
7
app/src/main/java/be/re/writand/WApplication.kt
Normal file
|
@ -0,0 +1,7 @@
|
|||
package be.re.writand
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class WApplication : Application() {}
|
|
@ -1,7 +1,10 @@
|
|||
package be.re.writand.data.local
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.Serializer
|
||||
import androidx.datastore.dataStore
|
||||
import be.re.writand.ProtoSettings
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import java.io.InputStream
|
||||
|
@ -29,3 +32,11 @@ class SettingsSerializer @Inject constructor() : Serializer<ProtoSettings> {
|
|||
) = t.writeTo(output)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Added field to Context to enable getting an instance of ProtoSettings where needed.
|
||||
*/
|
||||
val Context.protoSettingsDataStore: DataStore<ProtoSettings> by dataStore(
|
||||
fileName = "settings.proto",
|
||||
serializer = SettingsSerializer()
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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<ProtoSettings>
|
||||
context: Context
|
||||
) : IUserSettingsRepository {
|
||||
|
||||
private val userSettings = store.data
|
||||
private val dataStore: DataStore<ProtoSettings> = context.protoSettingsDataStore
|
||||
val userSettings: Flow<UserSettings> = 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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
17
app/src/main/java/be/re/writand/domain/tos/GetTOSUseCase.kt
Normal file
17
app/src/main/java/be/re/writand/domain/tos/GetTOSUseCase.kt
Normal file
|
@ -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<String> {
|
||||
return tosRepository.getTOS().text
|
||||
}
|
||||
|
||||
}
|
31
app/src/main/java/be/re/writand/screens/SettingsViewModel.kt
Normal file
31
app/src/main/java/be/re/writand/screens/SettingsViewModel.kt
Normal file
|
@ -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<UserSettings?>(null)
|
||||
val settings: StateFlow<UserSettings?> = _settings
|
||||
|
||||
init {
|
||||
launchCatching {
|
||||
userSettingsRepository.userSettings.collect { settings ->
|
||||
_settings.value = settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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<String>,
|
||||
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<String>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
app/src/main/java/be/re/writand/screens/components/WText.kt
Normal file
80
app/src/main/java/be/re/writand/screens/components/WText.kt
Normal file
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
) {
|
||||
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)
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.size(20.dp, 20.dp)) {
|
||||
WButton(
|
||||
text = "Finish",
|
||||
onClick = { navHostController.navigate(WAppDestinations.PROJECT_PICKER) }
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<UserSettings?>(null)
|
||||
val settings: StateFlow<UserSettings?> = _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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String>): WelcomeTOSUiState
|
||||
data class Failed(val message: String): WelcomeTOSUiState
|
||||
}
|
|
@ -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<WelcomeTOSUiState> = MutableStateFlow(WelcomeTOSUiState.Loading)
|
||||
val uiState: StateFlow<WelcomeTOSUiState> = _uiState
|
||||
|
||||
init {
|
||||
launchCatching {
|
||||
val tosList: List<String> = getTOSUseCase()
|
||||
if (tosList.isNotEmpty()) {
|
||||
_uiState.value = WelcomeTOSUiState.Success(tosList)
|
||||
} else {
|
||||
_uiState.value = WelcomeTOSUiState.Failed("Terms of Services could not be loaded.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
BIN
app/src/main/res/drawable/writand_no_background.png
Normal file
BIN
app/src/main/res/drawable/writand_no_background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
BIN
app/src/main/res/drawable/writand_with_background.png
Normal file
BIN
app/src/main/res/drawable/writand_with_background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -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'
|
||||
}
|
Loading…
Reference in a new issue