Merge commit '1f8e2d1fe3'
This commit is contained in:
commit
886f452a21
160 changed files with 7672 additions and 49 deletions
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Studeez
|
||||||
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="11" />
|
<bytecodeTargetLevel target="17" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
|
|
||||||
5
.idea/inspectionProfiles/Project_Default.xml
generated
5
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -37,5 +37,10 @@
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||||
|
<option name="processCode" value="true" />
|
||||||
|
<option name="processLiterals" value="true" />
|
||||||
|
<option name="processComments" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.7.0" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ plugins {
|
||||||
|
|
||||||
// Protobuf
|
// Protobuf
|
||||||
id 'com.google.protobuf' version '0.8.17'
|
id 'com.google.protobuf' version '0.8.17'
|
||||||
|
|
||||||
|
// Firebase
|
||||||
|
id 'com.google.gms.google-services'
|
||||||
|
id 'com.google.firebase.crashlytics'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -62,6 +66,7 @@ dependencies {
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||||
implementation 'androidx.compose.material:material:1.2.0'
|
implementation 'androidx.compose.material:material:1.2.0'
|
||||||
|
|
||||||
|
|
||||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
|
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
|
||||||
|
|
||||||
// ViewModel
|
// ViewModel
|
||||||
|
|
@ -93,6 +98,9 @@ dependencies {
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
|
|
||||||
|
// Coroutine testing
|
||||||
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||||
|
|
||||||
// Mocking
|
// Mocking
|
||||||
testImplementation 'org.mockito.kotlin:mockito-kotlin:3.2.0'
|
testImplementation 'org.mockito.kotlin:mockito-kotlin:3.2.0'
|
||||||
|
|
||||||
|
|
@ -108,14 +116,13 @@ dependencies {
|
||||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||||
|
|
||||||
//Firebase
|
//Firebase
|
||||||
// implementation platform('com.google.firebase:firebase-bom:30.4.1')
|
implementation platform('com.google.firebase:firebase-bom:31.3.0')
|
||||||
// implementation 'com.google.firebase:firebase-crashlytics-ktx'
|
implementation 'com.google.firebase:firebase-crashlytics-ktx'
|
||||||
// implementation 'com.google.firebase:firebase-analytics-ktx'
|
implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||||
// implementation 'com.google.firebase:firebase-auth-ktx'
|
implementation 'com.google.firebase:firebase-auth-ktx'
|
||||||
// implementation 'com.google.firebase:firebase-firestore-ktx'
|
implementation 'com.google.firebase:firebase-firestore-ktx'
|
||||||
// implementation 'com.google.firebase:firebase-perf-ktx'
|
implementation 'com.google.firebase:firebase-perf-ktx'
|
||||||
// implementation 'com.google.firebase:firebase-config-ktx'
|
implementation 'com.google.firebase:firebase-config-ktx'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow references to generate code
|
// Allow references to generate code
|
||||||
|
|
|
||||||
39
app/google-services.json
Normal file
39
app/google-services.json
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "604222599611",
|
||||||
|
"project_id": "studeez-5f8d1",
|
||||||
|
"storage_bucket": "studeez-5f8d1.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:604222599611:android:1e6fe76fce9862b610949e",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "be.ugent.sel.studeez"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "604222599611-1s7ojfmep7u0kohismntl9a5vffflgjd.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyAzSB7P73KPOXFw2kjA1Hm0_mOwxVS5xhE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "604222599611-1s7ojfmep7u0kohismntl9a5vffflgjd.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".StudeezHiltApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
android:theme="@style/Theme.Studeez"
|
android:theme="@style/Theme.Studeez"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.Studeez">
|
android:theme="@style/Theme.Studeez">
|
||||||
|
|
|
||||||
62
app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt
Normal file
62
app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
package be.ugent.sel.studeez
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezNavGraph
|
||||||
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StudeezApp() {
|
||||||
|
StudeezTheme {
|
||||||
|
Surface(color = MaterialTheme.colors.background) {
|
||||||
|
val appState = rememberAppState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(
|
||||||
|
hostState = it,
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
snackbar = { snackbarData ->
|
||||||
|
Snackbar(snackbarData, contentColor = MaterialTheme.colors.onPrimary)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scaffoldState = appState.scaffoldState
|
||||||
|
) { innerPaddingModifier ->
|
||||||
|
StudeezNavGraph(appState, Modifier.padding(innerPaddingModifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberAppState(
|
||||||
|
scaffoldState: ScaffoldState = rememberScaffoldState(),
|
||||||
|
navController: NavHostController = rememberNavController(),
|
||||||
|
snackbarManager: SnackbarManager = SnackbarManager,
|
||||||
|
resources: Resources = resources(),
|
||||||
|
coroutineScope: CoroutineScope = rememberCoroutineScope()
|
||||||
|
) =
|
||||||
|
remember(scaffoldState, navController, snackbarManager, resources, coroutineScope) {
|
||||||
|
StudeezAppstate(scaffoldState, navController, snackbarManager, resources, coroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
fun resources(): Resources {
|
||||||
|
LocalConfiguration.current
|
||||||
|
return LocalContext.current.resources
|
||||||
|
}
|
||||||
51
app/src/main/java/be/ugent/sel/studeez/StudeezAppstate.kt
Normal file
51
app/src/main/java/be/ugent/sel/studeez/StudeezAppstate.kt
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package be.ugent.sel.studeez
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.compose.material.ScaffoldState
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
|
||||||
|
import be.ugent.sel.studeez.common.snackbar.SnackbarMessage.Companion.toMessage
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class StudeezAppstate(
|
||||||
|
val scaffoldState: ScaffoldState,
|
||||||
|
val navController: NavHostController,
|
||||||
|
private val snackbarManager: SnackbarManager,
|
||||||
|
private val resources: Resources,
|
||||||
|
coroutineScope: CoroutineScope
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
coroutineScope.launch {
|
||||||
|
snackbarManager.snackbarMessages.filterNotNull().collect { snackbarMessage ->
|
||||||
|
val text = snackbarMessage.toMessage(resources)
|
||||||
|
scaffoldState.snackbarHostState.showSnackbar(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun popUp() {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigate(route: String) {
|
||||||
|
navController.navigate(route) { launchSingleTop = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateAndPopUp(route: String, popUp: String) {
|
||||||
|
navController.navigate(route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
popUpTo(popUp) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAndNavigate(route: String) {
|
||||||
|
navController.navigate(route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
app/src/main/java/be/ugent/sel/studeez/StudeezHiltApp.kt
Normal file
7
app/src/main/java/be/ugent/sel/studeez/StudeezHiltApp.kt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package be.ugent.sel.studeez
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
|
@HiltAndroidApp
|
||||||
|
class StudeezHiltApp : Application()
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package be.ugent.sel.studeez
|
package be.ugent.sel.studeez.activities
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
|
@ -10,8 +10,17 @@ import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import be.ugent.sel.studeez.StudeezApp
|
||||||
|
import be.ugent.sel.studeez.screens.session.InvisibleSessionManager
|
||||||
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
var onTimerInvisible: Job? = null
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -22,11 +31,23 @@ class MainActivity : ComponentActivity() {
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colors.background
|
color = MaterialTheme.colors.background
|
||||||
) {
|
) {
|
||||||
Greeting("Android")
|
StudeezApp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
onTimerInvisible = lifecycleScope.launch {
|
||||||
|
InvisibleSessionManager.updateTimer()
|
||||||
|
}
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
onTimerInvisible?.cancel()
|
||||||
|
super.onStart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonColors
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import be.ugent.sel.studeez.common.ext.basicButton
|
||||||
|
import be.ugent.sel.studeez.common.ext.card
|
||||||
|
import be.ugent.sel.studeez.common.ext.defaultButtonShape
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) {
|
||||||
|
TextButton(
|
||||||
|
onClick = action,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BasicButton(
|
||||||
|
@StringRes text: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||||
|
border: BorderStroke? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
shape = defaultButtonShape(),
|
||||||
|
colors = colors,
|
||||||
|
border = border,
|
||||||
|
enabled = enabled,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(text),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun BasicButtonPreview() {
|
||||||
|
BasicButton(text = AppText.add_timer, modifier = Modifier.basicButton()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StealthButton(
|
||||||
|
@StringRes text: Int,
|
||||||
|
modifier: Modifier = Modifier.card(),
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
//val clickablemodifier = if (disabled) Modifier.clickable(indication = null) else modifier
|
||||||
|
val borderColor = if (enabled) MaterialTheme.colors.primary
|
||||||
|
else MaterialTheme.colors.onSurface.copy(alpha = 0.3f)
|
||||||
|
BasicButton(
|
||||||
|
text = text,
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.surface,
|
||||||
|
contentColor = borderColor
|
||||||
|
),
|
||||||
|
border = BorderStroke(2.dp, borderColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun StealthButtonCardPreview() {
|
||||||
|
StealthButton(text = AppText.edit) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DeleteButton(
|
||||||
|
@StringRes text: Int,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
BasicButton(
|
||||||
|
text = text,
|
||||||
|
modifier = Modifier.basicButton(),
|
||||||
|
onClick = onClick,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.error,
|
||||||
|
contentColor = MaterialTheme.colors.onSurface,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DeleteButtonPreview() {
|
||||||
|
DeleteButton(text = AppText.delete_subject) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogConfirmButton(@StringRes text: Int, action: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
onClick = action
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogCancelButton(@StringRes text: Int, action: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
onClick = action,
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.onPrimary,
|
||||||
|
contentColor = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewTaskSubjectButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
@StringRes text: Int,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp)
|
||||||
|
.padding(10.dp, 5.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.4f),
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(2.dp),
|
||||||
|
border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.4f)),
|
||||||
|
elevation = null,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = text))
|
||||||
|
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(id = text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun NewTaskButtonPreview() {
|
||||||
|
NewTaskSubjectButton(onClick = {}, text = AppText.new_task)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import be.ugent.sel.studeez.common.composable.drawer.Drawer
|
||||||
|
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawerScreenTemplate(
|
||||||
|
title: String,
|
||||||
|
drawerActions: DrawerActions,
|
||||||
|
barAction: @Composable RowScope.() -> Unit = {},
|
||||||
|
content: @Composable (PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
val scaffoldState: ScaffoldState = rememberScaffoldState()
|
||||||
|
val coroutineScope: CoroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
scaffoldState = scaffoldState,
|
||||||
|
|
||||||
|
topBar = { TopAppBar(
|
||||||
|
title = { Text(text = title) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
coroutineScope.launch { scaffoldState.drawerState.open() }
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Menu,
|
||||||
|
contentDescription = resources().getString(AppText.menu)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = barAction
|
||||||
|
)},
|
||||||
|
|
||||||
|
drawerContent = {
|
||||||
|
Drawer(drawerActions)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
content(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DrawerScreenPreview() {
|
||||||
|
StudeezTheme { DrawerScreenTemplate(
|
||||||
|
title = "Drawer screen preview",
|
||||||
|
drawerActions =DrawerActions({}, {}, {}, {}, {})
|
||||||
|
) {
|
||||||
|
Text(text = "Preview content")
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.FloatingActionButton
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.DateRange
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
const val TRANSITION = "transition"
|
||||||
|
val HEIGHT_DIFFERENCE = 30.dp
|
||||||
|
|
||||||
|
data class AddButtonActions(
|
||||||
|
val onTaskClick: () -> Unit,
|
||||||
|
val onFriendClick: () -> Unit,
|
||||||
|
val onSessionClick: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddButton(
|
||||||
|
addButtonActions: AddButtonActions
|
||||||
|
) {
|
||||||
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Rotate the button when expanded, normal when collapsed.
|
||||||
|
val transition = updateTransition(targetState = isExpanded, label = TRANSITION)
|
||||||
|
val rotate by transition.animateFloat(label = TRANSITION) { expanded -> if (expanded) 315f else 0f }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
// Show minis when expanded.
|
||||||
|
if (isExpanded) {
|
||||||
|
ExpandedAddButton(
|
||||||
|
addButtonActions = addButtonActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The base add button
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
// Toggle expanded/collapsed.
|
||||||
|
isExpanded = !isExpanded
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(bottom = if (isExpanded) 78.dp else 0.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = "fab",
|
||||||
|
modifier = Modifier.rotate(rotate) // The rotation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandedAddButton(
|
||||||
|
addButtonActions: AddButtonActions
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
ExpandedEntry(
|
||||||
|
name = AppText.task,
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
onClick = addButtonActions.onTaskClick,
|
||||||
|
modifier = Modifier.padding(36.dp, HEIGHT_DIFFERENCE, 36.dp, 0.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ExpandedEntry(
|
||||||
|
name = AppText.friend,
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
onClick = addButtonActions.onFriendClick
|
||||||
|
)
|
||||||
|
|
||||||
|
ExpandedEntry(
|
||||||
|
name = AppText.session,
|
||||||
|
imageVector = Icons.Default.DateRange,
|
||||||
|
onClick = addButtonActions.onSessionClick,
|
||||||
|
modifier = Modifier.padding(36.dp, HEIGHT_DIFFERENCE, 36.dp, 0.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandedEntry(
|
||||||
|
name: Int,
|
||||||
|
imageVector: ImageVector,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Icon(
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = resources().getString(name),
|
||||||
|
// TODO Dark overlay
|
||||||
|
// tint = colors.surface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = resources().getString(name),
|
||||||
|
// TODO Dark overlay
|
||||||
|
// color = colors.surface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AddButtonPreview() {
|
||||||
|
StudeezTheme { AddButton(
|
||||||
|
addButtonActions = AddButtonActions({}, {}, {})
|
||||||
|
)}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ExpandedAddButtonPreview() {
|
||||||
|
StudeezTheme { ExpandedAddButton (
|
||||||
|
addButtonActions = AddButtonActions({}, {}, {})
|
||||||
|
) }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FormComposable(
|
||||||
|
title: String,
|
||||||
|
popUp: () -> Unit,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
SecondaryScreenTemplate(title = title, popUp = popUp) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageBackgroundButton(
|
||||||
|
paint: Painter,
|
||||||
|
str: String,
|
||||||
|
background2: Color,
|
||||||
|
setBackground1: (Color) -> Unit,
|
||||||
|
setBackground2: (Color) -> Unit
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = paint,
|
||||||
|
str,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
if (background2 == Color.Transparent) {
|
||||||
|
setBackground1(Color.LightGray)
|
||||||
|
setBackground2(Color.Transparent)
|
||||||
|
} else {
|
||||||
|
setBackground2(Color.Transparent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.border(
|
||||||
|
width = 2.dp,
|
||||||
|
color = background2,
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.common.composable.drawer.Drawer
|
||||||
|
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
|
||||||
|
import be.ugent.sel.studeez.common.composable.navbar.NavigationBar
|
||||||
|
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrimaryScreenTemplate(
|
||||||
|
title: String,
|
||||||
|
drawerActions: DrawerActions,
|
||||||
|
navigationBarActions: NavigationBarActions,
|
||||||
|
barAction: @Composable RowScope.() -> Unit = {},
|
||||||
|
content: @Composable (PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
val scaffoldState: ScaffoldState = rememberScaffoldState()
|
||||||
|
val coroutineScope: CoroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
scaffoldState = scaffoldState,
|
||||||
|
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text = title) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
coroutineScope.launch { scaffoldState.drawerState.open() }
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Menu,
|
||||||
|
contentDescription = resources().getString(R.string.menu)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = barAction
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
drawerContent = {
|
||||||
|
Drawer(drawerActions)
|
||||||
|
},
|
||||||
|
|
||||||
|
bottomBar = { NavigationBar(navigationBarActions) },
|
||||||
|
floatingActionButtonPosition = FabPosition.Center,
|
||||||
|
isFloatingActionButtonDocked = true,
|
||||||
|
floatingActionButton = { AddButton(AddButtonActions(
|
||||||
|
onTaskClick = navigationBarActions.onAddTaskClick,
|
||||||
|
onFriendClick = navigationBarActions.onAddFriendClick,
|
||||||
|
onSessionClick = navigationBarActions.onAddSessionClick
|
||||||
|
)) }
|
||||||
|
) {
|
||||||
|
content(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PrimaryScreenPreview() {
|
||||||
|
StudeezTheme {
|
||||||
|
PrimaryScreenTemplate(
|
||||||
|
"Preview screen",
|
||||||
|
DrawerActions({}, {}, {}, {}, {}),
|
||||||
|
NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
|
||||||
|
{
|
||||||
|
IconButton(onClick = { /*TODO*/ }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
contentDescription = "Edit"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
// Does not contain floatingActionButton and bottom bar, used in all the other screens
|
||||||
|
fun SecondaryScreenTemplate(
|
||||||
|
title: String,
|
||||||
|
popUp: () -> Unit,
|
||||||
|
barAction: @Composable RowScope.() -> Unit = {},
|
||||||
|
content: @Composable (PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
// Everything at the top of the screen
|
||||||
|
topBar = { TopAppBar(
|
||||||
|
title = { Text(text = title) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { popUp() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowBack,
|
||||||
|
contentDescription = resources().getString(R.string.go_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = barAction
|
||||||
|
) },
|
||||||
|
) { paddingValues ->
|
||||||
|
content(paddingValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SecondaryScreenToolbarPreview() {
|
||||||
|
StudeezTheme { SecondaryScreenTemplate(
|
||||||
|
"Preview screen",
|
||||||
|
{}
|
||||||
|
) {} }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SimpleScreenTemplate(
|
||||||
|
title: String,
|
||||||
|
content: @Composable (PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
Scaffold( topBar = { TopAppBar ( title = { Text(text = title) } ) }
|
||||||
|
) { paddingValues -> content(paddingValues) }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Headline(
|
||||||
|
text: String
|
||||||
|
) {
|
||||||
|
Row (
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 34.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DateText(date: String) {
|
||||||
|
Text(
|
||||||
|
text = date,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Email
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import be.ugent.sel.studeez.common.ext.fieldModifier
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import kotlin.math.sin
|
||||||
|
import be.ugent.sel.studeez.R.drawable as AppIcon
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BasicField(
|
||||||
|
@StringRes text: Int,
|
||||||
|
value: String,
|
||||||
|
onNewValue: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
singleLine = true,
|
||||||
|
modifier = modifier,
|
||||||
|
value = value,
|
||||||
|
onValueChange = { onNewValue(it) },
|
||||||
|
placeholder = { Text(stringResource(text)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LabelledInputField(
|
||||||
|
value: String,
|
||||||
|
onNewValue: (String) -> Unit,
|
||||||
|
@StringRes label: Int,
|
||||||
|
singleLine: Boolean = false
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value,
|
||||||
|
singleLine = singleLine,
|
||||||
|
onValueChange = onNewValue,
|
||||||
|
label = { Text(text = stringResource(id = label)) },
|
||||||
|
modifier = Modifier.fieldModifier()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UsernameField(
|
||||||
|
value: String,
|
||||||
|
onNewValue: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
singleLine = true,
|
||||||
|
modifier = modifier,
|
||||||
|
value = value,
|
||||||
|
onValueChange = { onNewValue(it) },
|
||||||
|
placeholder = { Text(stringResource(AppText.username)) },
|
||||||
|
leadingIcon = { Icon(imageVector = Icons.Default.Person, contentDescription = "Username") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmailField(
|
||||||
|
value: String,
|
||||||
|
onNewValue: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
singleLine = true,
|
||||||
|
modifier = modifier,
|
||||||
|
value = value,
|
||||||
|
onValueChange = { onNewValue(it) },
|
||||||
|
placeholder = { Text(stringResource(AppText.email)) },
|
||||||
|
leadingIcon = { Icon(imageVector = Icons.Default.Email, contentDescription = "Email") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LabeledNumberInputField(
|
||||||
|
value: Int,
|
||||||
|
onNewValue: (Int) -> Unit,
|
||||||
|
@StringRes label: Int,
|
||||||
|
singleLine: Boolean = false
|
||||||
|
) {
|
||||||
|
var number by remember { mutableStateOf(value) }
|
||||||
|
OutlinedTextField(
|
||||||
|
value = number.toString(),
|
||||||
|
singleLine = singleLine,
|
||||||
|
label = { Text(resources().getString(label)) },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
onValueChange = {typedInt ->
|
||||||
|
val isNumber = typedInt.matches(Regex("[1-9]+\\d*]"))
|
||||||
|
if (isNumber) {
|
||||||
|
number = typedInt.toInt()
|
||||||
|
onNewValue(typedInt.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LabeledErrorTextField(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
initialValue: String,
|
||||||
|
@StringRes label: Int,
|
||||||
|
singleLine: Boolean = false,
|
||||||
|
errorText: Int,
|
||||||
|
keyboardType: KeyboardType,
|
||||||
|
predicate: (String) -> Boolean,
|
||||||
|
onNewCorrectValue: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var value by remember {
|
||||||
|
mutableStateOf(initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isValid by remember {
|
||||||
|
mutableStateOf(predicate(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = modifier.fieldModifier(),
|
||||||
|
value = value,
|
||||||
|
onValueChange = { newText ->
|
||||||
|
value = newText
|
||||||
|
isValid = predicate(value)
|
||||||
|
if (isValid) {
|
||||||
|
onNewCorrectValue(newText)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleLine = singleLine,
|
||||||
|
label = { Text(text = stringResource(id = label)) },
|
||||||
|
isError = !isValid,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = keyboardType,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
|
text = stringResource(id = errorText),
|
||||||
|
color = MaterialTheme.colors.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun IntInputPreview() {
|
||||||
|
LabeledNumberInputField(value = 1, onNewValue = {}, label = AppText.email)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PasswordField(
|
||||||
|
value: String,
|
||||||
|
onNewValue: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
PasswordField(value, AppText.password, onNewValue, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RepeatPasswordField(
|
||||||
|
value: String,
|
||||||
|
onNewValue: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
PasswordField(value, AppText.repeat_password, onNewValue, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PasswordField(
|
||||||
|
value: String,
|
||||||
|
@StringRes placeholder: Int,
|
||||||
|
onNewValue: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var isVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val icon =
|
||||||
|
if (isVisible) painterResource(AppIcon.ic_visibility_on)
|
||||||
|
else painterResource(AppIcon.ic_visibility_off)
|
||||||
|
|
||||||
|
val visualTransformation =
|
||||||
|
if (isVisible) VisualTransformation.None else PasswordVisualTransformation()
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = modifier,
|
||||||
|
value = value,
|
||||||
|
onValueChange = { onNewValue(it) },
|
||||||
|
placeholder = { Text(text = stringResource(placeholder)) },
|
||||||
|
leadingIcon = { Icon(imageVector = Icons.Default.Lock, contentDescription = "Lock") },
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { isVisible = !isVisible }) {
|
||||||
|
Icon(painter = icon, contentDescription = "Visibility")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
visualTransformation = visualTransformation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import android.app.TimePickerDialog
|
||||||
|
import android.app.TimePickerDialog.OnTimeSetListener
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.common.ext.fieldModifier
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
|
||||||
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimePickerCard(
|
||||||
|
@StringRes text: Int,
|
||||||
|
initialSeconds: Int,
|
||||||
|
onTimeChosen: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fieldModifier(),
|
||||||
|
elevation = 10.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fieldModifier(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = text),
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
|
||||||
|
TimePickerButton(
|
||||||
|
initialSeconds = initialSeconds,
|
||||||
|
onTimeChosen = onTimeChosen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimePickerButton(
|
||||||
|
initialSeconds: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||||
|
border: BorderStroke? = null,
|
||||||
|
onTimeChosen: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val timeState: MutableState<Int> = remember {
|
||||||
|
mutableStateOf(initialSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { pickDuration(context, onTimeChosen, timeState) },
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = colors,
|
||||||
|
border = border
|
||||||
|
) {
|
||||||
|
Text(text = HoursMinutesSeconds(timeState.value).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pickDuration(context: Context, onTimeChosen: (Int) -> Unit, timeState: MutableState<Int>) {
|
||||||
|
val listener = OnTimeSetListener { _, hour, minute ->
|
||||||
|
timeState.value = HoursMinutesSeconds(hour, minute, 0).getTotalSeconds()
|
||||||
|
onTimeChosen(timeState.value)
|
||||||
|
}
|
||||||
|
val hms = HoursMinutesSeconds(timeState.value)
|
||||||
|
val mTimePickerDialog = TimePickerDialog(
|
||||||
|
context,
|
||||||
|
listener,
|
||||||
|
hms.hours,
|
||||||
|
hms.minutes,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
mTimePickerDialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TimePickerButtonPreview() {
|
||||||
|
StudeezTheme {
|
||||||
|
TimePickerButton(initialSeconds = 5 * 60 + 12, onTimeChosen = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TimePickerCardPreview() {
|
||||||
|
StudeezTheme {
|
||||||
|
TimePickerCard(text = R.string.studyTime, initialSeconds = 5 * 60 + 12, onTimeChosen = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.CustomTimerInfo
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimerEntry(
|
||||||
|
timerInfo: TimerInfo,
|
||||||
|
rightButton: @Composable () -> Unit = {},
|
||||||
|
leftButton: @Composable () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.align(alignment = Alignment.CenterVertically)) {
|
||||||
|
leftButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.padding(
|
||||||
|
horizontal = 20.dp,
|
||||||
|
vertical = 11.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = timerInfo.name,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 20.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = timerInfo.description, fontWeight = FontWeight.Light, fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.align(alignment = Alignment.CenterVertically)) {
|
||||||
|
rightButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TimerEntryPreview() {
|
||||||
|
val timerInfo = CustomTimerInfo(
|
||||||
|
"my preview timer", "This is the description of the timer", 60
|
||||||
|
)
|
||||||
|
TimerEntry(timerInfo = timerInfo) {
|
||||||
|
StealthButton(text = R.string.edit) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TimerDefaultEntryPreview() {
|
||||||
|
val timerInfo = CustomTimerInfo(
|
||||||
|
"Default preview timer", "This is the description of the timer", 60
|
||||||
|
)
|
||||||
|
TimerEntry(timerInfo = timerInfo) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.drawer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
|
||||||
|
data class DrawerActions(
|
||||||
|
val onHomeButtonClick: () -> Unit,
|
||||||
|
val onTimersClick: () -> Unit,
|
||||||
|
val onSettingsClick: () -> Unit,
|
||||||
|
val onLogoutClick: () -> Unit,
|
||||||
|
val onAboutClick: (Context) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getDrawerActions(
|
||||||
|
drawerViewModel: DrawerViewModel,
|
||||||
|
open: (String) -> Unit,
|
||||||
|
openAndPopUp: (String, String) -> Unit,
|
||||||
|
): DrawerActions {
|
||||||
|
return DrawerActions(
|
||||||
|
onHomeButtonClick = { drawerViewModel.onHomeButtonClick(open) },
|
||||||
|
onTimersClick = { drawerViewModel.onTimersClick(open) },
|
||||||
|
onSettingsClick = { drawerViewModel.onSettingsClick(open) },
|
||||||
|
onLogoutClick = { drawerViewModel.onLogoutClick(openAndPopUp) },
|
||||||
|
onAboutClick = { context -> drawerViewModel.onAboutClick(open, context = context) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Drawer(
|
||||||
|
drawerActions: DrawerActions,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
DrawerEntry(
|
||||||
|
icon = Icons.Default.Home,
|
||||||
|
text = resources().getString(R.string.home),
|
||||||
|
onClick = drawerActions.onHomeButtonClick,
|
||||||
|
)
|
||||||
|
DrawerEntry(
|
||||||
|
icon = ImageVector.vectorResource(id = R.drawable.ic_timer),
|
||||||
|
text = resources().getString(R.string.timers),
|
||||||
|
onClick = drawerActions.onTimersClick,
|
||||||
|
)
|
||||||
|
DrawerEntry(
|
||||||
|
icon = Icons.Default.Settings,
|
||||||
|
text = resources().getString(R.string.settings),
|
||||||
|
onClick = drawerActions.onSettingsClick,
|
||||||
|
)
|
||||||
|
DrawerEntry(
|
||||||
|
icon = ImageVector.vectorResource(id = R.drawable.ic_logout),
|
||||||
|
text = resources().getString(R.string.log_out),
|
||||||
|
onClick = drawerActions.onLogoutClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
DrawerEntry(
|
||||||
|
icon = Icons.Outlined.Info,
|
||||||
|
text = resources().getString(R.string.about),
|
||||||
|
onClick = { drawerActions.onAboutClick(context) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawerEntry(
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
.fillMaxWidth(0.15f)
|
||||||
|
) {
|
||||||
|
Icon(imageVector = icon, contentDescription = text)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
.fillMaxWidth(0.85f)
|
||||||
|
) {
|
||||||
|
Text(text = text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DrawerPreview() {
|
||||||
|
val drawerActions = DrawerActions({}, {}, {}, {}, {})
|
||||||
|
StudeezTheme {
|
||||||
|
Drawer(drawerActions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.drawer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import be.ugent.sel.studeez.domain.AccountDAO
|
||||||
|
import be.ugent.sel.studeez.domain.LogService
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.LOGIN_SCREEN
|
||||||
|
import be.ugent.sel.studeez.screens.StudeezViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
const val REPO_URL: String = "https://github.ugent.be/SELab1/project2023-groep14/"
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class DrawerViewModel @Inject constructor(
|
||||||
|
private val accountDAO: AccountDAO,
|
||||||
|
logService: LogService
|
||||||
|
) : StudeezViewModel(logService) {
|
||||||
|
|
||||||
|
fun onHomeButtonClick(open: (String) -> Unit) {
|
||||||
|
open(HOME_SCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTimersClick(openAndPopup: (String) -> Unit) {
|
||||||
|
openAndPopup(StudeezDestinations.TIMER_SCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSettingsClick(open: (String) -> Unit) {
|
||||||
|
open(StudeezDestinations.SETTINGS_SCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onLogoutClick(openAndPopUp: (String, String) -> Unit) {
|
||||||
|
launchCatching {
|
||||||
|
accountDAO.signOut()
|
||||||
|
openAndPopUp(LOGIN_SCREEN, HOME_SCREEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAboutClick(open: (String) -> Unit, context: Context) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(REPO_URL))
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.feed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import be.ugent.sel.studeez.common.composable.BasicTextButton
|
||||||
|
import be.ugent.sel.studeez.common.composable.DateText
|
||||||
|
import be.ugent.sel.studeez.common.composable.Headline
|
||||||
|
import be.ugent.sel.studeez.common.ext.textButton
|
||||||
|
import be.ugent.sel.studeez.data.local.models.FeedEntry
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Feed(
|
||||||
|
uiState: FeedUiState,
|
||||||
|
continueTask: (String, String) -> Unit,
|
||||||
|
onEmptyFeedHelp: () -> Unit
|
||||||
|
) {
|
||||||
|
when (uiState) {
|
||||||
|
FeedUiState.Loading -> LoadingFeed()
|
||||||
|
is FeedUiState.Succes -> LoadedFeed(
|
||||||
|
uiState = uiState,
|
||||||
|
continueTask = continueTask,
|
||||||
|
onEmptyFeedHelp = onEmptyFeedHelp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadedFeed(
|
||||||
|
uiState: FeedUiState.Succes,
|
||||||
|
continueTask: (String, String) -> Unit,
|
||||||
|
onEmptyFeedHelp: () -> Unit,
|
||||||
|
) {
|
||||||
|
if (uiState.feedEntries.isEmpty()) EmptyFeed(onEmptyFeedHelp)
|
||||||
|
else FeedWithElements(uiState = uiState, continueTask = continueTask)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingFeed() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(color = MaterialTheme.colors.onBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeedWithElements(
|
||||||
|
uiState: FeedUiState.Succes,
|
||||||
|
continueTask: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
val feedEntries = uiState.feedEntries
|
||||||
|
LazyColumn {
|
||||||
|
items(feedEntries.toList()) { (date, feedEntries) ->
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val totalDayStudyTime: Int = feedEntries.sumOf { it.totalStudyTime }
|
||||||
|
DateText(date = date)
|
||||||
|
Text(
|
||||||
|
text = "${HoursMinutesSeconds(totalDayStudyTime)}",
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
feedEntries.forEach { feedEntry ->
|
||||||
|
FeedEntry(feedEntry = feedEntry) {
|
||||||
|
continueTask(feedEntry.subjectId, feedEntry.taskId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyFeed(onEmptyFeedHelp: () -> Unit) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Headline(text = stringResource(id = AppText.your_feed))
|
||||||
|
|
||||||
|
BasicTextButton(
|
||||||
|
AppText.empty_feed_help_text,
|
||||||
|
Modifier.textButton(),
|
||||||
|
action = onEmptyFeedHelp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun FeedLoadingPreview() {
|
||||||
|
Feed(
|
||||||
|
uiState = FeedUiState.Loading,
|
||||||
|
continueTask = { _, _ -> run {} }, {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun FeedPreview() {
|
||||||
|
Feed(
|
||||||
|
uiState = FeedUiState.Succes(
|
||||||
|
mapOf(
|
||||||
|
"08 May 2023" to listOf(
|
||||||
|
FeedEntry(
|
||||||
|
argb_color = 0xFFFFD200,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Task",
|
||||||
|
totalStudyTime = 600,
|
||||||
|
),
|
||||||
|
FeedEntry(
|
||||||
|
argb_color = 0xFFFFD200,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Task",
|
||||||
|
totalStudyTime = 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"09 May 2023" to listOf(
|
||||||
|
FeedEntry(
|
||||||
|
argb_color = 0xFFFD1200,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Task",
|
||||||
|
),
|
||||||
|
FeedEntry(
|
||||||
|
argb_color = 0xFFFFD200,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Task",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
continueTask = { _, _ -> run {} }, {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.feed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import be.ugent.sel.studeez.common.composable.StealthButton
|
||||||
|
import be.ugent.sel.studeez.data.local.models.FeedEntry
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeedEntry(
|
||||||
|
feedEntry: FeedEntry,
|
||||||
|
continueWithTask: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(11f)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(feedEntry.argb_color)),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = feedEntry.subJectName,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = feedEntry.taskName,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(text = HoursMinutesSeconds(feedEntry.totalStudyTime).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val buttonText: Int =
|
||||||
|
if (feedEntry.isArchived) AppText.deleted else AppText.continue_task
|
||||||
|
StealthButton(
|
||||||
|
text = buttonText,
|
||||||
|
enabled = !feedEntry.isArchived,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp, end = 5.dp)
|
||||||
|
.weight(6f)
|
||||||
|
) {
|
||||||
|
if (!feedEntry.isArchived) {
|
||||||
|
continueWithTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun FeedEntryPreview() {
|
||||||
|
FeedEntry(
|
||||||
|
continueWithTask = {},
|
||||||
|
feedEntry = FeedEntry(
|
||||||
|
argb_color = 0xFFFFD200,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Task",
|
||||||
|
totalStudyTime = 20,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun FeedEntryOverflowPreview() {
|
||||||
|
FeedEntry(
|
||||||
|
continueWithTask = {},
|
||||||
|
feedEntry = FeedEntry(
|
||||||
|
argb_color = 0xFFFFD200,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkk",
|
||||||
|
totalStudyTime = 20,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.feed
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.FeedEntry
|
||||||
|
|
||||||
|
sealed interface FeedUiState {
|
||||||
|
object Loading : FeedUiState
|
||||||
|
data class Succes(val feedEntries: Map<String, List<FeedEntry>>) : FeedUiState
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.feed
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import be.ugent.sel.studeez.data.SelectedTask
|
||||||
|
import be.ugent.sel.studeez.domain.FeedDAO
|
||||||
|
import be.ugent.sel.studeez.domain.LogService
|
||||||
|
import be.ugent.sel.studeez.domain.TaskDAO
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations
|
||||||
|
import be.ugent.sel.studeez.screens.StudeezViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class FeedViewModel @Inject constructor(
|
||||||
|
feedDAO: FeedDAO,
|
||||||
|
private val taskDAO: TaskDAO,
|
||||||
|
private val selectedTask: SelectedTask,
|
||||||
|
logService: LogService
|
||||||
|
) : StudeezViewModel(logService) {
|
||||||
|
|
||||||
|
val uiState: StateFlow<FeedUiState> = feedDAO.getFeedEntries()
|
||||||
|
.map { FeedUiState.Succes(it) }
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
initialValue = FeedUiState.Loading,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun continueTask(open: (String) -> Unit, subjectId: String, taskId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val task = taskDAO.getTask(subjectId, taskId)
|
||||||
|
selectedTask.set(task)
|
||||||
|
open(StudeezDestinations.TIMER_SELECTION_SCREEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEmptyFeedHelp(open: (String) -> Unit) {
|
||||||
|
open(StudeezDestinations.ADD_SUBJECT_FORM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.navbar
|
||||||
|
|
||||||
|
import androidx.compose.material.BottomNavigation
|
||||||
|
import androidx.compose.material.BottomNavigationItem
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.List
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.outlined.DateRange
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.SESSIONS_SCREEN
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.SUBJECT_SCREEN
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
data class NavigationBarActions(
|
||||||
|
val isSelectedTab: (String) -> Boolean,
|
||||||
|
|
||||||
|
val onHomeClick: () -> Unit,
|
||||||
|
val onTasksClick: () -> Unit,
|
||||||
|
val onSessionsClick: () -> Unit,
|
||||||
|
val onProfileClick: () -> Unit,
|
||||||
|
|
||||||
|
// AddButton
|
||||||
|
val onAddTaskClick: () -> Unit,
|
||||||
|
val onAddFriendClick: () -> Unit,
|
||||||
|
val onAddSessionClick: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getNavigationBarActions(
|
||||||
|
navigationBarViewModel: NavigationBarViewModel,
|
||||||
|
open: (String) -> Unit,
|
||||||
|
getCurrentScreen: () -> String?
|
||||||
|
): NavigationBarActions {
|
||||||
|
return NavigationBarActions(
|
||||||
|
isSelectedTab = { screen ->
|
||||||
|
screen == getCurrentScreen()
|
||||||
|
},
|
||||||
|
onHomeClick = {
|
||||||
|
navigationBarViewModel.onHomeClick(open)
|
||||||
|
},
|
||||||
|
onTasksClick = {
|
||||||
|
navigationBarViewModel.onTasksClick(open)
|
||||||
|
},
|
||||||
|
onSessionsClick = {
|
||||||
|
navigationBarViewModel.onSessionsClick(open)
|
||||||
|
},
|
||||||
|
onProfileClick = {
|
||||||
|
navigationBarViewModel.onProfileClick(open)
|
||||||
|
},
|
||||||
|
|
||||||
|
onAddTaskClick = {
|
||||||
|
navigationBarViewModel.onAddTaskClick(open)
|
||||||
|
},
|
||||||
|
onAddFriendClick = {
|
||||||
|
navigationBarViewModel.onAddFriendClick(open)
|
||||||
|
},
|
||||||
|
onAddSessionClick = {
|
||||||
|
navigationBarViewModel.onAddSessionClick(open)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavigationBar(
|
||||||
|
navigationBarActions: NavigationBarActions
|
||||||
|
) {
|
||||||
|
BottomNavigation(
|
||||||
|
elevation = 10.dp
|
||||||
|
) {
|
||||||
|
BottomNavigationItem(
|
||||||
|
icon = { Icon(imageVector = Icons.Default.List, resources().getString(AppText.home)) },
|
||||||
|
label = { Text(text = resources().getString(AppText.home)) },
|
||||||
|
selected = navigationBarActions.isSelectedTab(HOME_SCREEN),
|
||||||
|
onClick = navigationBarActions.onHomeClick
|
||||||
|
)
|
||||||
|
|
||||||
|
BottomNavigationItem(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check, resources().getString(AppText.tasks)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(text = resources().getString(AppText.tasks)) },
|
||||||
|
selected = navigationBarActions.isSelectedTab(SUBJECT_SCREEN),
|
||||||
|
onClick = navigationBarActions.onTasksClick
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hack to space the entries in the navigation bar, make space for fab
|
||||||
|
BottomNavigationItem(icon = {}, onClick = {}, selected = false, enabled = false)
|
||||||
|
|
||||||
|
BottomNavigationItem(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.DateRange, resources().getString(AppText.sessions)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(text = resources().getString(AppText.sessions)) },
|
||||||
|
selected = navigationBarActions.isSelectedTab(SESSIONS_SCREEN),
|
||||||
|
onClick = navigationBarActions.onSessionsClick
|
||||||
|
)
|
||||||
|
|
||||||
|
BottomNavigationItem(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Person, resources().getString(AppText.profile)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(text = resources().getString(AppText.profile)) },
|
||||||
|
selected = navigationBarActions.isSelectedTab(PROFILE_SCREEN),
|
||||||
|
onClick = navigationBarActions.onProfileClick
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun NavigationBarPreview() {
|
||||||
|
StudeezTheme {
|
||||||
|
NavigationBar(
|
||||||
|
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.navbar
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
|
||||||
|
import be.ugent.sel.studeez.domain.LogService
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.SELECT_SUBJECT
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.SESSIONS_SCREEN
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.SUBJECT_SCREEN
|
||||||
|
import be.ugent.sel.studeez.screens.StudeezViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class NavigationBarViewModel @Inject constructor(
|
||||||
|
logService: LogService
|
||||||
|
) : StudeezViewModel(logService) {
|
||||||
|
|
||||||
|
fun onHomeClick(open: (String) -> Unit) {
|
||||||
|
open(HOME_SCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTasksClick(open: (String) -> Unit) {
|
||||||
|
open(SUBJECT_SCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSessionsClick(open: (String) -> Unit) {
|
||||||
|
open(SESSIONS_SCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onProfileClick(open: (String) -> Unit) {
|
||||||
|
open(PROFILE_SCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddTaskClick(open: (String) -> Unit) {
|
||||||
|
open(SELECT_SUBJECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddFriendClick(open: (String) -> Unit) {
|
||||||
|
// TODO open(SEARCH_FRIENDS_SCREEN)
|
||||||
|
SnackbarManager.showMessage(AppText.add_friend_not_possible_yet) // TODO Remove
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddSessionClick(open: (String) -> Unit) {
|
||||||
|
// TODO open(CREATE_SESSION_SCREEN)
|
||||||
|
SnackbarManager.showMessage(AppText.create_session_not_possible_yet) // TODO Remove
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.navbar
|
||||||
|
|
||||||
|
import android.app.TimePickerDialog
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BasicTimePicker(
|
||||||
|
onHoursChange: (Int) -> Unit,
|
||||||
|
onMinutesChange: (Int) -> Unit,
|
||||||
|
Hours: Int,
|
||||||
|
Minutes: Int,
|
||||||
|
): TimePickerDialog {
|
||||||
|
return TimePickerDialog(
|
||||||
|
LocalContext.current,
|
||||||
|
{ _, mHour: Int, mMinute: Int ->
|
||||||
|
onHoursChange(mHour)
|
||||||
|
onMinutesChange(mMinute)
|
||||||
|
},
|
||||||
|
Hours,
|
||||||
|
Minutes,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.tasks
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.List
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import be.ugent.sel.studeez.common.composable.StealthButton
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Subject
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SubjectEntry(
|
||||||
|
subject: Subject,
|
||||||
|
getTaskCount: () -> Flow<Int>,
|
||||||
|
getCompletedTaskCount: () -> Flow<Int>,
|
||||||
|
getStudyTime: () -> Flow<Int>,
|
||||||
|
selectButton: @Composable (RowScope) -> Unit,
|
||||||
|
) {
|
||||||
|
val studytime by getStudyTime().collectAsState(initial = 0)
|
||||||
|
val taskCount by getTaskCount().collectAsState(initial = 0)
|
||||||
|
val completedTaskCount by getCompletedTaskCount().collectAsState(initial = 0)
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(3f)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(subject.argb_color)),
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = subject.name,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = HoursMinutesSeconds(studytime).toString(),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(3.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.List,
|
||||||
|
contentDescription = stringResource(id = AppText.tasks)
|
||||||
|
)
|
||||||
|
Text(text = "${completedTaskCount}/${taskCount}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectButton(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SubjectEntryPreview() {
|
||||||
|
SubjectEntry(
|
||||||
|
subject = Subject(
|
||||||
|
name = "Test Subject",
|
||||||
|
argb_color = 0xFFFFD200,
|
||||||
|
),
|
||||||
|
getTaskCount = { flowOf() },
|
||||||
|
getCompletedTaskCount = { flowOf() },
|
||||||
|
getStudyTime = { flowOf() },
|
||||||
|
) {
|
||||||
|
StealthButton(
|
||||||
|
text = AppText.view_tasks,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp, end = 5.dp)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun OverflowSubjectEntryPreview() {
|
||||||
|
SubjectEntry(
|
||||||
|
subject = Subject(
|
||||||
|
name = "Testttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt",
|
||||||
|
argb_color = 0xFFFFD200,
|
||||||
|
),
|
||||||
|
getTaskCount = { flowOf() },
|
||||||
|
getCompletedTaskCount = { flowOf() },
|
||||||
|
getStudyTime = { flowOf() },
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
package be.ugent.sel.studeez.common.composable.tasks
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
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.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.common.composable.StealthButton
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Task
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TaskEntry(
|
||||||
|
task: Task,
|
||||||
|
onCheckTask: (Boolean) -> Unit,
|
||||||
|
onArchiveTask: () -> Unit,
|
||||||
|
onStartTask: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||||
|
) {
|
||||||
|
val color = if (task.completed) Color.Gray else MaterialTheme.colors.onSurface
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(22f),
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = task.completed,
|
||||||
|
onCheckedChange = onCheckTask,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = Color.Gray,
|
||||||
|
uncheckedColor = MaterialTheme.colors.onSurface,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = task.name,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
color = color,
|
||||||
|
modifier = Modifier.weight(13f),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${HoursMinutesSeconds(task.time)}",
|
||||||
|
color = color,
|
||||||
|
modifier = Modifier.weight(7f)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier.weight(7f)) {
|
||||||
|
if (task.completed) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onArchiveTask,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 20.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = resources().getString(R.string.delete_task),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StealthButton(
|
||||||
|
text = R.string.start,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 5.dp),
|
||||||
|
) {
|
||||||
|
onStartTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TaskEntryPreview() {
|
||||||
|
TaskEntry(
|
||||||
|
task = Task(
|
||||||
|
name = "Test Task",
|
||||||
|
completed = false,
|
||||||
|
),
|
||||||
|
{}, {}, {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun CompletedTaskEntryPreview() {
|
||||||
|
TaskEntry(
|
||||||
|
task = Task(
|
||||||
|
name = "Test Task",
|
||||||
|
completed = true,
|
||||||
|
),
|
||||||
|
{}, {}, {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun OverflowTaskEntryPreview() {
|
||||||
|
TaskEntry(
|
||||||
|
task = Task(
|
||||||
|
name = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkkkk",
|
||||||
|
completed = false,
|
||||||
|
),
|
||||||
|
{}, {}, {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package be.ugent.sel.studeez.common.ext
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
fun Color.Companion.generateRandomArgb(): Long {
|
||||||
|
val random = Random
|
||||||
|
val mask: Long = (0x000000FFL shl random.nextInt(0, 3)).inv()
|
||||||
|
return random.nextLong(0xFF000000L, 0xFFFFFFFFL) and mask
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package be.ugent.sel.studeez.common.ext
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
fun Modifier.textButton(): Modifier {
|
||||||
|
return this.fillMaxWidth().padding(16.dp, 8.dp, 16.dp, 0.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.basicButton(): Modifier {
|
||||||
|
return this.fillMaxWidth().padding(16.dp, 8.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.card(): Modifier {
|
||||||
|
return this.padding(16.dp, 0.dp, 16.dp, 8.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.contextMenu(): Modifier {
|
||||||
|
return this.wrapContentWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.dropdownSelector(): Modifier {
|
||||||
|
return this.fillMaxWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.fieldModifier(): Modifier {
|
||||||
|
return this.fillMaxWidth().padding(16.dp, 4.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.toolbarActions(): Modifier {
|
||||||
|
return this.wrapContentSize(Alignment.TopEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.spacer(): Modifier {
|
||||||
|
return this.fillMaxWidth().padding(12.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.smallSpacer(): Modifier {
|
||||||
|
return this.fillMaxWidth().height(8.dp)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package be.ugent.sel.studeez.common.ext
|
||||||
|
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
fun defaultButtonShape(): RoundedCornerShape {
|
||||||
|
return RoundedCornerShape(20.dp)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package be.ugent.sel.studeez.common.ext
|
||||||
|
|
||||||
|
import android.util.Patterns
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
private const val MIN_PASS_LENGTH = 6
|
||||||
|
private const val PASS_PATTERN = "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=\\S+$).{4,}$"
|
||||||
|
|
||||||
|
fun String.isValidEmail(): Boolean {
|
||||||
|
return this.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(this).matches()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.isValidPassword(): Boolean {
|
||||||
|
return this.isNotBlank() &&
|
||||||
|
this.length >= MIN_PASS_LENGTH &&
|
||||||
|
Pattern.compile(PASS_PATTERN).matcher(this).matches()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.passwordMatches(repeated: String): Boolean {
|
||||||
|
return this == repeated
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.idFromParameter(): String {
|
||||||
|
return this.substring(1, this.length - 1)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package be.ugent.sel.studeez.common.snackbar
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
object SnackbarManager {
|
||||||
|
private val messages: MutableStateFlow<SnackbarMessage?> = MutableStateFlow(null)
|
||||||
|
val snackbarMessages: StateFlow<SnackbarMessage?>
|
||||||
|
get() = messages.asStateFlow()
|
||||||
|
|
||||||
|
fun showMessage(@StringRes message: Int) {
|
||||||
|
messages.value = SnackbarMessage.ResourceSnackbar(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMessage(message: SnackbarMessage) {
|
||||||
|
messages.value = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package be.ugent.sel.studeez.common.snackbar
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
sealed class SnackbarMessage {
|
||||||
|
class StringSnackbar(val message: String) : SnackbarMessage()
|
||||||
|
class ResourceSnackbar(@StringRes val message: Int) : SnackbarMessage()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun SnackbarMessage.toMessage(resources: Resources): String {
|
||||||
|
return when (this) {
|
||||||
|
is StringSnackbar -> this.message
|
||||||
|
is ResourceSnackbar -> resources.getString(this.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Throwable.toSnackbarMessage(): SnackbarMessage {
|
||||||
|
val message = this.message.orEmpty()
|
||||||
|
return if (message.isNotBlank()) StringSnackbar(message)
|
||||||
|
else ResourceSnackbar(AppText.generic_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt
Normal file
45
app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package be.ugent.sel.studeez.data
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.SessionReport
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Subject
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Task
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to cummunicate between viewmodels.
|
||||||
|
*/
|
||||||
|
abstract class SelectedState<T> {
|
||||||
|
abstract var value: T
|
||||||
|
operator fun invoke() = value
|
||||||
|
fun set(newValue: T) {
|
||||||
|
this.value = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class SelectedSessionReport @Inject constructor() : SelectedState<SessionReport>() {
|
||||||
|
override lateinit var value: SessionReport
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class SelectedTask @Inject constructor() : SelectedState<Task>() {
|
||||||
|
override lateinit var value: Task
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class SelectedTimer @Inject constructor() : SelectedState<FunctionalTimer>() {
|
||||||
|
override lateinit var value: FunctionalTimer
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class SelectedSubject @Inject constructor() : SelectedState<Subject>() {
|
||||||
|
override lateinit var value: Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class SelectedTimerInfo @Inject constructor() : SelectedState<TimerInfo>() {
|
||||||
|
override lateinit var value: TimerInfo
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models
|
||||||
|
|
||||||
|
import com.google.firebase.Timestamp
|
||||||
|
|
||||||
|
data class FeedEntry(
|
||||||
|
val argb_color: Long = 0,
|
||||||
|
val subJectName: String = "",
|
||||||
|
val taskName: String = "",
|
||||||
|
val taskId: String = "", // Name of task is not unique
|
||||||
|
val subjectId: String = "",
|
||||||
|
val totalStudyTime: Int = 0,
|
||||||
|
val endTime: Timestamp = Timestamp(0, 0),
|
||||||
|
val isArchived: Boolean = false
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models
|
||||||
|
|
||||||
|
import com.google.firebase.Timestamp
|
||||||
|
import com.google.firebase.firestore.DocumentId
|
||||||
|
|
||||||
|
data class SessionReport(
|
||||||
|
@DocumentId val id: String = "",
|
||||||
|
val studyTime: Int = 0,
|
||||||
|
val endTime: Timestamp = Timestamp(0, 0),
|
||||||
|
val taskId: String = "",
|
||||||
|
val subjectId: String = ""
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models
|
||||||
|
|
||||||
|
data class User(val id: String = "")
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.task
|
||||||
|
|
||||||
|
import com.google.firebase.firestore.DocumentId
|
||||||
|
import com.google.firebase.firestore.Exclude
|
||||||
|
|
||||||
|
data class Subject(
|
||||||
|
@DocumentId val id: String = "",
|
||||||
|
val name: String = "",
|
||||||
|
val argb_color: Long = 0,
|
||||||
|
var archived: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
object SubjectDocument {
|
||||||
|
const val id = "id"
|
||||||
|
const val name = "name"
|
||||||
|
const val archived = "archived"
|
||||||
|
const val argb_color = "argb_color"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.task
|
||||||
|
|
||||||
|
import com.google.firebase.firestore.DocumentId
|
||||||
|
|
||||||
|
data class Task(
|
||||||
|
@DocumentId val id: String = "",
|
||||||
|
val name: String = "",
|
||||||
|
var completed: Boolean = false,
|
||||||
|
val time: Int = 0,
|
||||||
|
val subjectId: String = "",
|
||||||
|
var archived: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
object TaskDocument {
|
||||||
|
const val id = "id"
|
||||||
|
const val name = "name"
|
||||||
|
const val completed = "completed"
|
||||||
|
const val time = "time"
|
||||||
|
const val subjectId = "subjectId"
|
||||||
|
const val archived = "archived"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_functional
|
||||||
|
|
||||||
|
class FunctionalCustomTimer(studyTime: Int) : FunctionalTimer(studyTime) {
|
||||||
|
|
||||||
|
override fun tick() {
|
||||||
|
if (!hasEnded()) {
|
||||||
|
time--
|
||||||
|
totalStudyTime++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasEnded(): Boolean {
|
||||||
|
return time.time == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasCurrentCountdownEnded(): Boolean {
|
||||||
|
return hasEnded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T> accept(visitor: FunctionalTimerVisitor<T>): T {
|
||||||
|
return visitor.visitFunctionalCustomTimer(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_functional
|
||||||
|
|
||||||
|
class FunctionalEndlessTimer : FunctionalTimer(0) {
|
||||||
|
|
||||||
|
override fun hasEnded(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasCurrentCountdownEnded(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun tick() {
|
||||||
|
time++
|
||||||
|
totalStudyTime++
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T> accept(visitor: FunctionalTimerVisitor<T>): T {
|
||||||
|
return visitor.visitFunctionalEndlessTimer(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_functional
|
||||||
|
|
||||||
|
class FunctionalPomodoroTimer(
|
||||||
|
private var studyTime: Int,
|
||||||
|
private var breakTime: Int,
|
||||||
|
val repeats: Int
|
||||||
|
) : FunctionalTimer(studyTime) {
|
||||||
|
|
||||||
|
var breaksRemaining = repeats - 1
|
||||||
|
var isInBreak = false
|
||||||
|
|
||||||
|
override fun tick() {
|
||||||
|
if (hasEnded()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (hasCurrentCountdownEnded()) {
|
||||||
|
if (isInBreak) {
|
||||||
|
breaksRemaining--
|
||||||
|
time.time = studyTime
|
||||||
|
} else {
|
||||||
|
time.time = breakTime
|
||||||
|
}
|
||||||
|
isInBreak = !isInBreak
|
||||||
|
}
|
||||||
|
time--
|
||||||
|
|
||||||
|
if (!isInBreak) {
|
||||||
|
totalStudyTime++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasEnded(): Boolean {
|
||||||
|
return !hasBreaksRemaining() && hasCurrentCountdownEnded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasBreaksRemaining(): Boolean {
|
||||||
|
return breaksRemaining > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasCurrentCountdownEnded(): Boolean {
|
||||||
|
return time.time == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T> accept(visitor: FunctionalTimerVisitor<T>): T {
|
||||||
|
return visitor.visitFunctionalBreakTimer(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_functional
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.SessionReport
|
||||||
|
import com.google.firebase.Timestamp
|
||||||
|
import com.google.firebase.firestore.DocumentReference
|
||||||
|
|
||||||
|
abstract class FunctionalTimer(initialValue: Int) {
|
||||||
|
var time: Time = Time(initialValue)
|
||||||
|
var totalStudyTime: Int = 0
|
||||||
|
|
||||||
|
fun getHoursMinutesSeconds(): HoursMinutesSeconds {
|
||||||
|
return time.getAsHMS()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun tick()
|
||||||
|
|
||||||
|
abstract fun hasEnded(): Boolean
|
||||||
|
|
||||||
|
abstract fun hasCurrentCountdownEnded(): Boolean
|
||||||
|
|
||||||
|
fun getSessionReport(subjectId: String, taskId: String): SessionReport {
|
||||||
|
return SessionReport(
|
||||||
|
studyTime = totalStudyTime,
|
||||||
|
endTime = Timestamp.now(),
|
||||||
|
taskId = taskId,
|
||||||
|
subjectId = subjectId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun <T> accept(visitor: FunctionalTimerVisitor<T>): T
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_functional
|
||||||
|
|
||||||
|
interface FunctionalTimerVisitor<T> {
|
||||||
|
|
||||||
|
fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): T
|
||||||
|
|
||||||
|
fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): T
|
||||||
|
|
||||||
|
fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): T
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_functional
|
||||||
|
|
||||||
|
data class HoursMinutesSeconds(val hours: Int, val minutes: Int, val seconds: Int) {
|
||||||
|
|
||||||
|
constructor(sec: Int): this(
|
||||||
|
hours = sec / (60 * 60),
|
||||||
|
minutes = (sec / (60)) % 60,
|
||||||
|
seconds = sec % 60,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getTotalSeconds(): Int {
|
||||||
|
return (hours * 60 * 60) + (minutes * 60) + seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val hoursString = hours.toString().padStart(2, '0')
|
||||||
|
val minutesString = minutes.toString().padStart(2, '0')
|
||||||
|
val secondsString = seconds.toString().padStart(2, '0')
|
||||||
|
return "$hoursString:$minutesString:$secondsString"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_functional
|
||||||
|
|
||||||
|
class Time(var time: Int) {
|
||||||
|
operator fun invoke() = time
|
||||||
|
|
||||||
|
operator fun inc(): Time = Time(time + 1)
|
||||||
|
|
||||||
|
operator fun dec(): Time = Time(time - 1)
|
||||||
|
|
||||||
|
fun getAsHMS(): HoursMinutesSeconds {
|
||||||
|
return HoursMinutesSeconds(time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_info
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
|
||||||
|
|
||||||
|
class CustomTimerInfo(
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
var studyTime: Int,
|
||||||
|
id: String = ""
|
||||||
|
): TimerInfo(id, name, description) {
|
||||||
|
|
||||||
|
override fun getFunctionalTimer(): FunctionalTimer {
|
||||||
|
return FunctionalCustomTimer(studyTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun asJson() : Map<String, Any> {
|
||||||
|
return mapOf(
|
||||||
|
"type" to "custom",
|
||||||
|
"name" to name,
|
||||||
|
"description" to description,
|
||||||
|
"studyTime" to studyTime,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T> accept(visitor: TimerInfoVisitor<T>): T {
|
||||||
|
return visitor.visitCustomTimerInfo(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_info
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
|
||||||
|
|
||||||
|
class EndlessTimerInfo(
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
id: String = ""
|
||||||
|
): TimerInfo(id, name, description) {
|
||||||
|
|
||||||
|
|
||||||
|
override fun getFunctionalTimer(): FunctionalTimer {
|
||||||
|
return FunctionalEndlessTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun asJson() : Map<String, Any> {
|
||||||
|
return mapOf(
|
||||||
|
"type" to "endless",
|
||||||
|
"name" to name,
|
||||||
|
"description" to description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T> accept(visitor: TimerInfoVisitor<T>): T {
|
||||||
|
return visitor.visitEndlessTimerInfo(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_info
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor
|
||||||
|
|
||||||
|
class PomodoroTimerInfo(
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
var studyTime: Int,
|
||||||
|
var breakTime: Int,
|
||||||
|
var repeats: Int,
|
||||||
|
id: String = ""
|
||||||
|
): TimerInfo(id, name, description) {
|
||||||
|
|
||||||
|
|
||||||
|
override fun getFunctionalTimer(): FunctionalTimer {
|
||||||
|
return FunctionalPomodoroTimer(studyTime, breakTime, repeats)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun asJson() : Map<String, Any> {
|
||||||
|
return mapOf(
|
||||||
|
"type" to "break",
|
||||||
|
"name" to name,
|
||||||
|
"description" to description,
|
||||||
|
"studyTime" to studyTime,
|
||||||
|
"breakTime" to breakTime,
|
||||||
|
"repeats" to repeats,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T> accept(visitor: TimerInfoVisitor<T>): T {
|
||||||
|
return visitor.visitBreakTimerInfo(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_info
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deze klasse stelt de de info van een timer weer. Elke timer heeft een id, naam en descriptie
|
||||||
|
*/
|
||||||
|
abstract class TimerInfo(
|
||||||
|
val id: String,
|
||||||
|
var name: String,
|
||||||
|
var description: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geef de functionele timer terug die kan gebruikt worden tijden een sessie.
|
||||||
|
*/
|
||||||
|
abstract fun getFunctionalTimer(): FunctionalTimer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geef deze timer weer als json. Wordt gebruikt om terug op te slaan in de databank.
|
||||||
|
* TODO implementaties hebben nog hardgecodeerde strings.
|
||||||
|
*/
|
||||||
|
abstract fun asJson(): Map<String, Any>
|
||||||
|
abstract fun <T> accept(visitor: TimerInfoVisitor<T>): T
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_info
|
||||||
|
|
||||||
|
interface TimerInfoVisitor<T> {
|
||||||
|
|
||||||
|
fun visitCustomTimerInfo(customTimerInfo: CustomTimerInfo): T
|
||||||
|
|
||||||
|
fun visitEndlessTimerInfo(endlessTimerInfo: EndlessTimerInfo): T
|
||||||
|
|
||||||
|
fun visitBreakTimerInfo(pomodoroTimerInfo: PomodoroTimerInfo): T
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_info
|
||||||
|
|
||||||
|
import com.google.firebase.firestore.DocumentId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timers uit de databank (remote config en firestore) worden als eerste stap omgezet naar dit type.
|
||||||
|
*/
|
||||||
|
data class TimerJson(
|
||||||
|
val type: String = "",
|
||||||
|
val name: String = "",
|
||||||
|
val description: String = "",
|
||||||
|
val studyTime: Int = 0,
|
||||||
|
val breakTime: Int = 0,
|
||||||
|
val repeats: Int = 0,
|
||||||
|
@DocumentId val id: String = ""
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package be.ugent.sel.studeez.data.local.models.timer_info
|
||||||
|
|
||||||
|
enum class TimerType {
|
||||||
|
BREAK,
|
||||||
|
ENDLESS,
|
||||||
|
CUSTOM
|
||||||
|
}
|
||||||
0
app/src/main/java/be/ugent/sel/studeez/data/remote/.keep
Normal file
0
app/src/main/java/be/ugent/sel/studeez/data/remote/.keep
Normal file
39
app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt
Normal file
39
app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package be.ugent.sel.studeez.di
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.domain.*
|
||||||
|
import be.ugent.sel.studeez.domain.implementation.*
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
abstract class DatabaseModule {
|
||||||
|
@Binds
|
||||||
|
abstract fun provideAccountDAO(impl: FirebaseAccountDAO): AccountDAO
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideLogService(impl: LogServiceImpl): LogService
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideSessionDAO(impl: FireBaseSessionDAO): SessionDAO
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideSubjectDAO(impl: FireBaseSubjectDAO): SubjectDAO
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideTaskDAO(impl: FireBaseTaskDAO): TaskDAO
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideFeedDAO(impl: FirebaseFeedDAO): FeedDAO
|
||||||
|
}
|
||||||
21
app/src/main/java/be/ugent/sel/studeez/di/FireBaseModule.kt
Normal file
21
app/src/main/java/be/ugent/sel/studeez/di/FireBaseModule.kt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package be.ugent.sel.studeez.di
|
||||||
|
|
||||||
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
|
import com.google.firebase.auth.ktx.auth
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import com.google.firebase.firestore.ktx.firestore
|
||||||
|
import com.google.firebase.ktx.Firebase
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object FirebaseModule {
|
||||||
|
@Provides
|
||||||
|
fun auth(): FirebaseAuth = Firebase.auth
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun firestore(): FirebaseFirestore = Firebase.firestore
|
||||||
|
}
|
||||||
34
app/src/main/java/be/ugent/sel/studeez/domain/AccountDAO.kt
Normal file
34
app/src/main/java/be/ugent/sel/studeez/domain/AccountDAO.kt
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 Google LLC
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package be.ugent.sel.studeez.domain
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.User
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface AccountDAO {
|
||||||
|
val currentUserId: String
|
||||||
|
val hasUser: Boolean
|
||||||
|
|
||||||
|
val currentUser: Flow<User>
|
||||||
|
|
||||||
|
suspend fun signInWithEmailAndPassword(email: String, password: String)
|
||||||
|
suspend fun sendRecoveryEmail(email: String)
|
||||||
|
suspend fun signUpWithEmailAndPassword(email: String, password: String)
|
||||||
|
suspend fun deleteAccount()
|
||||||
|
|
||||||
|
suspend fun signOut()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package be.ugent.sel.studeez.domain
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
|
||||||
|
|
||||||
|
interface ConfigurationService {
|
||||||
|
|
||||||
|
suspend fun fetchConfiguration(): Boolean
|
||||||
|
|
||||||
|
fun getDefaultTimers(): List<TimerInfo>
|
||||||
|
|
||||||
|
}
|
||||||
10
app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt
Normal file
10
app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package be.ugent.sel.studeez.domain
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.FeedEntry
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface FeedDAO {
|
||||||
|
|
||||||
|
fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package be.ugent.sel.studeez.domain
|
||||||
|
|
||||||
|
interface LogService {
|
||||||
|
fun logNonFatalCrash(throwable: Throwable)
|
||||||
|
}
|
||||||
15
app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt
Normal file
15
app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package be.ugent.sel.studeez.domain
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.SessionReport
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface SessionDAO {
|
||||||
|
|
||||||
|
fun getSessions(): Flow<List<SessionReport>>
|
||||||
|
|
||||||
|
fun saveSession(newSessionReport: SessionReport)
|
||||||
|
|
||||||
|
fun deleteSession(newTimer: TimerInfo)
|
||||||
|
|
||||||
|
}
|
||||||
23
app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt
Normal file
23
app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package be.ugent.sel.studeez.domain
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Subject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface SubjectDAO {
|
||||||
|
|
||||||
|
fun getSubjects(): Flow<List<Subject>>
|
||||||
|
|
||||||
|
fun saveSubject(newSubject: Subject)
|
||||||
|
|
||||||
|
fun deleteSubject(oldSubject: Subject)
|
||||||
|
|
||||||
|
fun updateSubject(newSubject: Subject)
|
||||||
|
|
||||||
|
suspend fun archiveSubject(subject: Subject)
|
||||||
|
|
||||||
|
fun getTaskCount(subject: Subject): Flow<Int>
|
||||||
|
fun getCompletedTaskCount(subject: Subject): Flow<Int>
|
||||||
|
fun getStudyTime(subject: Subject): Flow<Int>
|
||||||
|
|
||||||
|
suspend fun getSubject(subjectId: String): Subject?
|
||||||
|
}
|
||||||
18
app/src/main/java/be/ugent/sel/studeez/domain/TaskDAO.kt
Normal file
18
app/src/main/java/be/ugent/sel/studeez/domain/TaskDAO.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package be.ugent.sel.studeez.domain
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Subject
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Task
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface TaskDAO {
|
||||||
|
|
||||||
|
fun getTasks(subject: Subject): Flow<List<Task>>
|
||||||
|
|
||||||
|
fun saveTask(newTask: Task)
|
||||||
|
|
||||||
|
fun updateTask(newTask: Task)
|
||||||
|
|
||||||
|
fun deleteTask(oldTask: Task)
|
||||||
|
|
||||||
|
suspend fun getTask(subjectId: String, taskId: String): Task
|
||||||
|
}
|
||||||
19
app/src/main/java/be/ugent/sel/studeez/domain/TimerDAO.kt
Normal file
19
app/src/main/java/be/ugent/sel/studeez/domain/TimerDAO.kt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package be.ugent.sel.studeez.domain
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.TimerJson
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface TimerDAO {
|
||||||
|
|
||||||
|
fun getUserTimers(): Flow<List<TimerInfo>>
|
||||||
|
|
||||||
|
fun getAllTimers(): Flow<List<TimerInfo>>
|
||||||
|
|
||||||
|
fun saveTimer(newTimer: TimerInfo)
|
||||||
|
|
||||||
|
fun updateTimer(timerInfo: TimerInfo)
|
||||||
|
|
||||||
|
fun deleteTimer(timerInfo: TimerInfo)
|
||||||
|
|
||||||
|
}
|
||||||
13
app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt
Normal file
13
app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package be.ugent.sel.studeez.domain
|
||||||
|
|
||||||
|
interface UserDAO {
|
||||||
|
|
||||||
|
suspend fun getUsername(): String?
|
||||||
|
suspend fun save(newUsername: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all references to this user in the database. Similar to the deleteCascade in
|
||||||
|
* relational databases.
|
||||||
|
*/
|
||||||
|
suspend fun deleteUserReferences()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
object FireBaseCollections {
|
||||||
|
const val SESSION_COLLECTION = "sessions"
|
||||||
|
const val USER_COLLECTION = "users"
|
||||||
|
const val TIMER_COLLECTION = "timers"
|
||||||
|
const val SUBJECT_COLLECTION = "subjects"
|
||||||
|
const val TASK_COLLECTION = "tasks"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.SessionReport
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
|
||||||
|
import be.ugent.sel.studeez.domain.AccountDAO
|
||||||
|
import be.ugent.sel.studeez.domain.SessionDAO
|
||||||
|
import com.google.firebase.firestore.CollectionReference
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import com.google.firebase.firestore.ktx.snapshots
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FireBaseSessionDAO @Inject constructor(
|
||||||
|
private val firestore: FirebaseFirestore,
|
||||||
|
private val auth: AccountDAO
|
||||||
|
) : SessionDAO {
|
||||||
|
|
||||||
|
override fun getSessions(): Flow<List<SessionReport>> {
|
||||||
|
return currentUserSessionsCollection()
|
||||||
|
.snapshots()
|
||||||
|
.map { it.toObjects(SessionReport::class.java) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveSession(newSessionReport: SessionReport) {
|
||||||
|
currentUserSessionsCollection().add(newSessionReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteSession(newTimer: TimerInfo) {
|
||||||
|
currentUserSessionsCollection().document(newTimer.id).delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentUserSessionsCollection(): CollectionReference =
|
||||||
|
firestore.collection(FireBaseCollections.USER_COLLECTION)
|
||||||
|
.document(auth.currentUserId)
|
||||||
|
.collection(FireBaseCollections.SESSION_COLLECTION)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Subject
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.SubjectDocument
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Task
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.TaskDocument
|
||||||
|
import be.ugent.sel.studeez.domain.AccountDAO
|
||||||
|
import be.ugent.sel.studeez.domain.SubjectDAO
|
||||||
|
import be.ugent.sel.studeez.domain.TaskDAO
|
||||||
|
import com.google.firebase.firestore.CollectionReference
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import com.google.firebase.firestore.Query
|
||||||
|
import com.google.firebase.firestore.ktx.snapshots
|
||||||
|
import com.google.firebase.firestore.ktx.toObject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.tasks.await
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.count
|
||||||
|
|
||||||
|
class FireBaseSubjectDAO @Inject constructor(
|
||||||
|
private val firestore: FirebaseFirestore,
|
||||||
|
private val auth: AccountDAO,
|
||||||
|
private val taskDAO: TaskDAO,
|
||||||
|
) : SubjectDAO {
|
||||||
|
override fun getSubjects(): Flow<List<Subject>> {
|
||||||
|
return currentUserSubjectsCollection()
|
||||||
|
.subjectNotArchived()
|
||||||
|
.snapshots()
|
||||||
|
.map { it.toObjects(Subject::class.java) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getSubject(subjectId: String): Subject? {
|
||||||
|
return currentUserSubjectsCollection().document(subjectId).get().await().toObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveSubject(newSubject: Subject) {
|
||||||
|
currentUserSubjectsCollection().add(newSubject)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteSubject(oldSubject: Subject) {
|
||||||
|
currentUserSubjectsCollection().document(oldSubject.id).delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSubject(newSubject: Subject) {
|
||||||
|
currentUserSubjectsCollection().document(newSubject.id).set(newSubject)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun archiveSubject(subject: Subject) {
|
||||||
|
currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true)
|
||||||
|
currentUserSubjectsCollection().document(subject.id)
|
||||||
|
.collection(FireBaseCollections.TASK_COLLECTION)
|
||||||
|
.taskNotArchived()
|
||||||
|
.get().await()
|
||||||
|
.documents
|
||||||
|
.forEach {
|
||||||
|
it.reference.update(TaskDocument.archived, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTaskCount(subject: Subject): Flow<Int> {
|
||||||
|
return taskDAO.getTasks(subject)
|
||||||
|
.map(List<Task>::count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCompletedTaskCount(subject: Subject): Flow<Int> {
|
||||||
|
return taskDAO.getTasks(subject)
|
||||||
|
.map { tasks -> tasks.count { it.completed && !it.archived } }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStudyTime(subject: Subject): Flow<Int> {
|
||||||
|
return taskDAO.getTasks(subject)
|
||||||
|
.map { tasks -> tasks.sumOf { it.time } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentUserSubjectsCollection(): CollectionReference =
|
||||||
|
firestore.collection(FireBaseCollections.USER_COLLECTION)
|
||||||
|
.document(auth.currentUserId)
|
||||||
|
.collection(FireBaseCollections.SUBJECT_COLLECTION)
|
||||||
|
|
||||||
|
private fun subjectTasksCollection(subject: Subject): CollectionReference =
|
||||||
|
firestore.collection(FireBaseCollections.USER_COLLECTION)
|
||||||
|
.document(auth.currentUserId)
|
||||||
|
.collection(FireBaseCollections.SUBJECT_COLLECTION)
|
||||||
|
.document(subject.id)
|
||||||
|
.collection(FireBaseCollections.TASK_COLLECTION)
|
||||||
|
|
||||||
|
fun CollectionReference.subjectNotArchived(): Query =
|
||||||
|
this.whereEqualTo(SubjectDocument.archived, false)
|
||||||
|
|
||||||
|
fun Query.subjectNotArchived(): Query =
|
||||||
|
this.whereEqualTo(SubjectDocument.archived, false)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Subject
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Task
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.TaskDocument
|
||||||
|
import be.ugent.sel.studeez.domain.AccountDAO
|
||||||
|
import be.ugent.sel.studeez.domain.TaskDAO
|
||||||
|
import com.google.firebase.firestore.CollectionReference
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import com.google.firebase.firestore.Query
|
||||||
|
import com.google.firebase.firestore.ktx.snapshots
|
||||||
|
import com.google.firebase.firestore.ktx.toObject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.tasks.await
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FireBaseTaskDAO @Inject constructor(
|
||||||
|
private val firestore: FirebaseFirestore,
|
||||||
|
private val auth: AccountDAO,
|
||||||
|
) : TaskDAO {
|
||||||
|
override fun getTasks(subject: Subject): Flow<List<Task>> {
|
||||||
|
return selectedSubjectTasksCollection(subject.id)
|
||||||
|
.taskNotArchived()
|
||||||
|
.snapshots()
|
||||||
|
.map { it.toObjects(Task::class.java) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTask(subjectId: String, taskId: String): Task {
|
||||||
|
return selectedSubjectTasksCollection(subjectId).document(taskId).get().await().toObject()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveTask(newTask: Task) {
|
||||||
|
selectedSubjectTasksCollection(newTask.subjectId).add(newTask)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateTask(newTask: Task) {
|
||||||
|
selectedSubjectTasksCollection(newTask.subjectId)
|
||||||
|
.document(newTask.id)
|
||||||
|
.set(newTask)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteTask(oldTask: Task) {
|
||||||
|
selectedSubjectTasksCollection(oldTask.subjectId).document(oldTask.id).delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference =
|
||||||
|
firestore.collection(FireBaseCollections.USER_COLLECTION)
|
||||||
|
.document(auth.currentUserId)
|
||||||
|
.collection(FireBaseCollections.SUBJECT_COLLECTION)
|
||||||
|
.document(subjectId)
|
||||||
|
.collection(FireBaseCollections.TASK_COLLECTION)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend CollectionReference and Query with some filters
|
||||||
|
|
||||||
|
fun CollectionReference.taskNotArchived(): Query =
|
||||||
|
this.whereEqualTo(TaskDocument.archived, false)
|
||||||
|
|
||||||
|
fun Query.taskNotArchived(): Query =
|
||||||
|
this.whereEqualTo(TaskDocument.archived, false)
|
||||||
|
|
||||||
|
fun CollectionReference.taskNotCompleted(): Query =
|
||||||
|
this.whereEqualTo(TaskDocument.completed, true)
|
||||||
|
|
||||||
|
fun Query.taskNotCompleted(): Query =
|
||||||
|
this.whereEqualTo(TaskDocument.completed, true)
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 Google LLC
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.User
|
||||||
|
import be.ugent.sel.studeez.domain.AccountDAO
|
||||||
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.tasks.await
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FirebaseAccountDAO @Inject constructor(
|
||||||
|
private val auth: FirebaseAuth
|
||||||
|
) : AccountDAO {
|
||||||
|
|
||||||
|
override val currentUserId: String
|
||||||
|
get() = auth.currentUser?.uid.orEmpty()
|
||||||
|
|
||||||
|
override val hasUser: Boolean
|
||||||
|
get() = auth.currentUser != null
|
||||||
|
|
||||||
|
override val currentUser: Flow<User>
|
||||||
|
get() = callbackFlow {
|
||||||
|
val listener =
|
||||||
|
FirebaseAuth.AuthStateListener { auth ->
|
||||||
|
this.trySend(auth.currentUser?.let { User(it.uid) } ?: User())
|
||||||
|
}
|
||||||
|
auth.addAuthStateListener(listener)
|
||||||
|
awaitClose { auth.removeAuthStateListener(listener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun signInWithEmailAndPassword(email: String, password: String) {
|
||||||
|
auth.signInWithEmailAndPassword(email, password).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendRecoveryEmail(email: String) {
|
||||||
|
auth.sendPasswordResetEmail(email).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun signUpWithEmailAndPassword(email: String, password: String) {
|
||||||
|
auth.createUserWithEmailAndPassword(email, password).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteAccount() {
|
||||||
|
auth.currentUser!!.delete().await()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun signOut() {
|
||||||
|
auth.signOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.*
|
||||||
|
import be.ugent.sel.studeez.domain.ConfigurationService
|
||||||
|
import com.google.firebase.ktx.Firebase
|
||||||
|
import com.google.firebase.remoteconfig.ktx.get
|
||||||
|
import com.google.firebase.remoteconfig.ktx.remoteConfig
|
||||||
|
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import kotlinx.coroutines.tasks.await
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FirebaseConfigurationService @Inject constructor() : ConfigurationService {
|
||||||
|
|
||||||
|
init {
|
||||||
|
// fetch configs elke keer als app wordt opgestart
|
||||||
|
val configSettings = remoteConfigSettings { minimumFetchIntervalInSeconds = 0 }
|
||||||
|
remoteConfig.setConfigSettingsAsync(configSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val remoteConfig
|
||||||
|
get() = Firebase.remoteConfig
|
||||||
|
|
||||||
|
override suspend fun fetchConfiguration(): Boolean {
|
||||||
|
return remoteConfig.fetchAndActivate().await()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDefaultTimers(): List<TimerInfo> {
|
||||||
|
val jsonString: String = remoteConfig[DEFAULT_TIMERS].asString()
|
||||||
|
// Json is een lijst van timers
|
||||||
|
val timerJsonList: List<TimerJson> = ToTimerConverter().jsonToTimerJsonList(jsonString)
|
||||||
|
return ToTimerConverter().convertToTimerInfoList(timerJsonList)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DEFAULT_TIMERS = "default_timers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import android.icu.text.DateFormat
|
||||||
|
import be.ugent.sel.studeez.data.local.models.FeedEntry
|
||||||
|
import be.ugent.sel.studeez.data.local.models.SessionReport
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Subject
|
||||||
|
import be.ugent.sel.studeez.data.local.models.task.Task
|
||||||
|
import be.ugent.sel.studeez.domain.FeedDAO
|
||||||
|
import be.ugent.sel.studeez.domain.SessionDAO
|
||||||
|
import be.ugent.sel.studeez.domain.SubjectDAO
|
||||||
|
import be.ugent.sel.studeez.domain.TaskDAO
|
||||||
|
import com.google.firebase.Timestamp
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FirebaseFeedDAO @Inject constructor(
|
||||||
|
private val sessionDAO: SessionDAO,
|
||||||
|
private val taskDAO: TaskDAO,
|
||||||
|
private val subjectDAO: SubjectDAO
|
||||||
|
) : FeedDAO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a map as with key the day and value a list of feedentries for that day.
|
||||||
|
*/
|
||||||
|
override fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>> {
|
||||||
|
return sessionDAO.getSessions().map { sessionReports ->
|
||||||
|
sessionReports
|
||||||
|
.map { sessionReport -> sessionToFeedEntry(sessionReport) }
|
||||||
|
.sortedByDescending { it.endTime }
|
||||||
|
.groupBy { getFormattedTime(it) }
|
||||||
|
.mapValues { (_, entries) ->
|
||||||
|
entries
|
||||||
|
.groupBy { it.taskId }
|
||||||
|
.map { fuseFeedEntries(it.component2()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFormattedTime(entry: FeedEntry): String {
|
||||||
|
return DateFormat.getDateInstance().format(entry.endTime.toDate())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Givin a list of entries referencing the same task, in the same day, fuse them into one
|
||||||
|
* feed-entry by adding the studytime and keeping the most recent end-timestamp
|
||||||
|
*/
|
||||||
|
private fun fuseFeedEntries(entries: List<FeedEntry>): FeedEntry =
|
||||||
|
entries.drop(1).fold(entries[0]) { accEntry, newEntry ->
|
||||||
|
accEntry.copy(
|
||||||
|
totalStudyTime = accEntry.totalStudyTime + newEntry.totalStudyTime,
|
||||||
|
endTime = getMostRecent(accEntry.endTime, newEntry.endTime)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMostRecent(t1: Timestamp, t2: Timestamp): Timestamp {
|
||||||
|
return if (t1 < t2) t2 else t1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a sessionReport to a feedEntry. Fetch Task and Subject to get names
|
||||||
|
*/
|
||||||
|
private suspend fun sessionToFeedEntry(sessionReport: SessionReport): FeedEntry {
|
||||||
|
val subjectId: String = sessionReport.subjectId
|
||||||
|
val taskId: String = sessionReport.taskId
|
||||||
|
|
||||||
|
val task: Task = taskDAO.getTask(subjectId, taskId)
|
||||||
|
val subject: Subject = subjectDAO.getSubject(subjectId)!!
|
||||||
|
|
||||||
|
return FeedEntry(
|
||||||
|
argb_color = subject.argb_color,
|
||||||
|
subJectName = subject.name,
|
||||||
|
taskName = task.name,
|
||||||
|
taskId = task.id,
|
||||||
|
subjectId = subject.id,
|
||||||
|
totalStudyTime = sessionReport.studyTime,
|
||||||
|
endTime = sessionReport.endTime,
|
||||||
|
isArchived = task.archived || subject.archived
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.*
|
||||||
|
import be.ugent.sel.studeez.domain.AccountDAO
|
||||||
|
import be.ugent.sel.studeez.domain.TimerDAO
|
||||||
|
import com.google.firebase.firestore.CollectionReference
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import com.google.firebase.firestore.ktx.snapshots
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FirebaseTimerDAO @Inject constructor(
|
||||||
|
private val firestore: FirebaseFirestore,
|
||||||
|
private val configurationService: FirebaseConfigurationService,
|
||||||
|
private val auth: AccountDAO
|
||||||
|
) : TimerDAO {
|
||||||
|
|
||||||
|
override fun getUserTimers(): Flow<List<TimerInfo>> {
|
||||||
|
return currentUserTimersCollection()
|
||||||
|
.snapshots()
|
||||||
|
.map { it.toObjects(TimerJson::class.java) }
|
||||||
|
.map { ToTimerConverter().convertToTimerInfoList(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllTimers(): Flow<List<TimerInfo>> {
|
||||||
|
// Wrap default timers in een flow en combineer met de userTimer flow.
|
||||||
|
val defaultTimers: List<TimerInfo> = configurationService.getDefaultTimers()
|
||||||
|
val defaultTimersFlow: Flow<List<TimerInfo>> = flowOf(defaultTimers)
|
||||||
|
val userTimersFlow: Flow<List<TimerInfo>> = getUserTimers()
|
||||||
|
return defaultTimersFlow.combine(userTimersFlow) { defaultTimersList, userTimersList ->
|
||||||
|
defaultTimersList + userTimersList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveTimer(newTimer: TimerInfo) {
|
||||||
|
currentUserTimersCollection().add(newTimer.asJson())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateTimer(timerInfo: TimerInfo) {
|
||||||
|
currentUserTimersCollection().document(timerInfo.id).set(timerInfo.asJson())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteTimer(timerInfo: TimerInfo) {
|
||||||
|
currentUserTimersCollection().document(timerInfo.id).delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentUserTimersCollection(): CollectionReference =
|
||||||
|
firestore.collection(FireBaseCollections.USER_COLLECTION)
|
||||||
|
.document(auth.currentUserId)
|
||||||
|
.collection(FireBaseCollections.TIMER_COLLECTION)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
|
||||||
|
import be.ugent.sel.studeez.domain.AccountDAO
|
||||||
|
import be.ugent.sel.studeez.domain.UserDAO
|
||||||
|
import com.google.firebase.firestore.DocumentReference
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import kotlinx.coroutines.tasks.await
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FirebaseUserDAO @Inject constructor(
|
||||||
|
private val firestore: FirebaseFirestore,
|
||||||
|
private val auth: AccountDAO
|
||||||
|
) : UserDAO {
|
||||||
|
|
||||||
|
override suspend fun getUsername(): String? {
|
||||||
|
return currentUserDocument().get().await().getString("username")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(newUsername: String) {
|
||||||
|
currentUserDocument().set(mapOf("username" to newUsername))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentUserDocument(): DocumentReference =
|
||||||
|
firestore.collection(USER_COLLECTION).document(auth.currentUserId)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val USER_COLLECTION = "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteUserReferences() {
|
||||||
|
currentUserDocument().delete()
|
||||||
|
.addOnSuccessListener { SnackbarManager.showMessage(R.string.success) }
|
||||||
|
.addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.domain.LogService
|
||||||
|
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||||
|
import com.google.firebase.ktx.Firebase
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LogServiceImpl @Inject constructor() : LogService {
|
||||||
|
override fun logNonFatalCrash(throwable: Throwable) {
|
||||||
|
Firebase.crashlytics.recordException(throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package be.ugent.sel.studeez.domain.implementation
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_info.*
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by ConfigurationService and TimerDAO.
|
||||||
|
*
|
||||||
|
* ConfigurationService: configuration is fetched as a JSON-string,
|
||||||
|
* which is converted into a TimerJson, and converted here into the correct TimerInfo
|
||||||
|
*
|
||||||
|
* timerDAO: Timers are being fetched directly to TinerJson and convertes into the correct timerInfo
|
||||||
|
*/
|
||||||
|
class ToTimerConverter {
|
||||||
|
|
||||||
|
fun interface TimerFactory {
|
||||||
|
fun makeTimer(map: TimerJson) : TimerInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
private val timerInfoMap: Map<TimerType, TimerFactory> = mapOf(
|
||||||
|
TimerType.ENDLESS to TimerFactory { EndlessTimerInfo(
|
||||||
|
it.name,
|
||||||
|
it.description,
|
||||||
|
it.id
|
||||||
|
) },
|
||||||
|
TimerType.CUSTOM to TimerFactory { CustomTimerInfo(
|
||||||
|
it.name,
|
||||||
|
it.description,
|
||||||
|
it.studyTime,
|
||||||
|
it.id
|
||||||
|
) },
|
||||||
|
TimerType.BREAK to TimerFactory { PomodoroTimerInfo(
|
||||||
|
it.name,
|
||||||
|
it.description,
|
||||||
|
it.studyTime,
|
||||||
|
it.breakTime,
|
||||||
|
it.repeats,
|
||||||
|
it.id
|
||||||
|
) }
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getTimer(timerJson: TimerJson): TimerInfo{
|
||||||
|
val type: TimerType = TimerType.valueOf(timerJson.type.uppercase())
|
||||||
|
return timerInfoMap.getValue(type).makeTimer(timerJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertToTimerInfoList(timerJsonList: List<TimerJson>): List<TimerInfo> {
|
||||||
|
return timerJsonList.map(this::getTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun jsonToTimerJsonList(json: String): List<TimerJson> {
|
||||||
|
val type = object : TypeToken<List<TimerJson>>() {}.type
|
||||||
|
return Gson().fromJson(json, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package be.ugent.sel.studeez.navigation
|
||||||
|
|
||||||
|
object StudeezDestinations {
|
||||||
|
// NavBar
|
||||||
|
const val HOME_SCREEN = "home"
|
||||||
|
const val SUBJECT_SCREEN = "subjects"
|
||||||
|
const val SESSIONS_SCREEN = "sessions"
|
||||||
|
const val PROFILE_SCREEN = "profile"
|
||||||
|
|
||||||
|
// Drawer
|
||||||
|
const val TIMER_SCREEN = "timer_overview"
|
||||||
|
const val SETTINGS_SCREEN = "settings"
|
||||||
|
|
||||||
|
// Login flow
|
||||||
|
const val SPLASH_SCREEN = "splash"
|
||||||
|
const val LOGIN_SCREEN = "login"
|
||||||
|
const val SIGN_UP_SCREEN = "signup"
|
||||||
|
|
||||||
|
// Studying flow
|
||||||
|
const val TIMER_SELECTION_SCREEN = "timer_selection"
|
||||||
|
const val TIMER_EDIT_SCREEN = "timer_edit"
|
||||||
|
const val TIMER_TYPE_CHOOSING_SCREEN = "timer_type_choosing_screen"
|
||||||
|
const val SESSION_SCREEN = "session"
|
||||||
|
const val SESSION_RECAP = "session_recap"
|
||||||
|
|
||||||
|
const val ADD_SUBJECT_FORM = "add_subject"
|
||||||
|
const val EDIT_SUBJECT_FORM = "edit_subject"
|
||||||
|
const val TASKS_SCREEN = "tasks"
|
||||||
|
const val ADD_TASK_FORM = "add_task"
|
||||||
|
const val SELECT_SUBJECT = "select_subject"
|
||||||
|
const val EDIT_TASK_FORM = "edit_task"
|
||||||
|
|
||||||
|
// Friends flow
|
||||||
|
const val SEARCH_FRIENDS_SCREEN = "search_friends"
|
||||||
|
|
||||||
|
// Create & edit screens
|
||||||
|
const val CREATE_TASK_SCREEN = "create_task"
|
||||||
|
const val CREATE_SESSION_SCREEN = "create_session"
|
||||||
|
const val EDIT_PROFILE_SCREEN = "edit_profile"
|
||||||
|
|
||||||
|
const val ADD_TIMER_SCREEN = "add_timer"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
package be.ugent.sel.studeez.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
import be.ugent.sel.studeez.StudeezAppstate
|
||||||
|
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
|
||||||
|
import be.ugent.sel.studeez.common.composable.drawer.DrawerViewModel
|
||||||
|
import be.ugent.sel.studeez.common.composable.drawer.getDrawerActions
|
||||||
|
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
|
||||||
|
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel
|
||||||
|
import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions
|
||||||
|
import be.ugent.sel.studeez.screens.home.HomeRoute
|
||||||
|
import be.ugent.sel.studeez.screens.log_in.LoginRoute
|
||||||
|
import be.ugent.sel.studeez.screens.profile.EditProfileRoute
|
||||||
|
import be.ugent.sel.studeez.screens.profile.ProfileRoute
|
||||||
|
import be.ugent.sel.studeez.screens.session.SessionRoute
|
||||||
|
import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute
|
||||||
|
import be.ugent.sel.studeez.screens.sessions.SessionsRoute
|
||||||
|
import be.ugent.sel.studeez.screens.settings.SettingsRoute
|
||||||
|
import be.ugent.sel.studeez.screens.sign_up.SignUpRoute
|
||||||
|
import be.ugent.sel.studeez.screens.splash.SplashRoute
|
||||||
|
import be.ugent.sel.studeez.screens.subjects.SubjectRoute
|
||||||
|
import be.ugent.sel.studeez.screens.subjects.form.SubjectCreateRoute
|
||||||
|
import be.ugent.sel.studeez.screens.subjects.form.SubjectEditRoute
|
||||||
|
import be.ugent.sel.studeez.screens.subjects.select.SubjectSelectionRoute
|
||||||
|
import be.ugent.sel.studeez.screens.tasks.TaskRoute
|
||||||
|
import be.ugent.sel.studeez.screens.tasks.form.TaskCreateRoute
|
||||||
|
import be.ugent.sel.studeez.screens.tasks.form.TaskEditRoute
|
||||||
|
import be.ugent.sel.studeez.screens.timer_form.TimerAddRoute
|
||||||
|
import be.ugent.sel.studeez.screens.timer_form.TimerEditRoute
|
||||||
|
import be.ugent.sel.studeez.screens.timer_form.timer_type_select.TimerTypeSelectScreen
|
||||||
|
import be.ugent.sel.studeez.screens.timer_overview.TimerOverviewRoute
|
||||||
|
import be.ugent.sel.studeez.screens.timer_selection.TimerSelectionRoute
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StudeezNavGraph(
|
||||||
|
appState: StudeezAppstate,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val drawerViewModel: DrawerViewModel = hiltViewModel()
|
||||||
|
val navBarViewModel: NavigationBarViewModel = hiltViewModel()
|
||||||
|
|
||||||
|
val backStackEntry by appState.navController.currentBackStackEntryAsState()
|
||||||
|
val getCurrentScreen: () -> String? = { backStackEntry?.destination?.route }
|
||||||
|
|
||||||
|
val goBack: () -> Unit = { appState.popUp() }
|
||||||
|
val open: (String) -> Unit = { appState.navigate(it) }
|
||||||
|
val openAndPopUp: (String, String) -> Unit =
|
||||||
|
{ route, popUp -> appState.navigateAndPopUp(route, popUp) }
|
||||||
|
val clearAndNavigate: (route: String) -> Unit = { route -> appState.clearAndNavigate(route) }
|
||||||
|
|
||||||
|
val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp)
|
||||||
|
val navigationBarActions: NavigationBarActions =
|
||||||
|
getNavigationBarActions(navBarViewModel, open, getCurrentScreen)
|
||||||
|
|
||||||
|
NavHost(
|
||||||
|
navController = appState.navController,
|
||||||
|
startDestination = StudeezDestinations.SPLASH_SCREEN,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
// NavBar
|
||||||
|
composable(StudeezDestinations.HOME_SCREEN) {
|
||||||
|
HomeRoute(
|
||||||
|
open = open,
|
||||||
|
drawerActions = drawerActions,
|
||||||
|
navigationBarActions = navigationBarActions,
|
||||||
|
feedViewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.SUBJECT_SCREEN) {
|
||||||
|
SubjectRoute(
|
||||||
|
open = open,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
drawerActions = drawerActions,
|
||||||
|
navigationBarActions = navigationBarActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.SELECT_SUBJECT) {
|
||||||
|
SubjectSelectionRoute(
|
||||||
|
open = { openAndPopUp(it, StudeezDestinations.SELECT_SUBJECT) },
|
||||||
|
goBack = goBack,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.ADD_SUBJECT_FORM) {
|
||||||
|
SubjectCreateRoute(
|
||||||
|
goBack = goBack,
|
||||||
|
openAndPopUp = openAndPopUp,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.EDIT_SUBJECT_FORM) {
|
||||||
|
SubjectEditRoute(
|
||||||
|
goBack = goBack,
|
||||||
|
openAndPopUp = openAndPopUp,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.TASKS_SCREEN) {
|
||||||
|
TaskRoute(
|
||||||
|
goBack = { openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.TASKS_SCREEN) },
|
||||||
|
open = open,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.ADD_TASK_FORM) {
|
||||||
|
TaskCreateRoute(
|
||||||
|
goBack = goBack,
|
||||||
|
openAndPopUp = openAndPopUp,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.EDIT_TASK_FORM) {
|
||||||
|
TaskEditRoute(
|
||||||
|
goBack = goBack,
|
||||||
|
openAndPopUp = openAndPopUp,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
composable(StudeezDestinations.SESSIONS_SCREEN) {
|
||||||
|
SessionsRoute(
|
||||||
|
drawerActions = drawerActions,
|
||||||
|
navigationBarActions = navigationBarActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.PROFILE_SCREEN) {
|
||||||
|
ProfileRoute(
|
||||||
|
open,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
drawerActions = drawerActions,
|
||||||
|
navigationBarActions = navigationBarActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawer
|
||||||
|
composable(StudeezDestinations.TIMER_SCREEN) {
|
||||||
|
TimerOverviewRoute(
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
drawerActions = drawerActions,
|
||||||
|
open = open
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.SETTINGS_SCREEN) {
|
||||||
|
SettingsRoute(
|
||||||
|
drawerActions = drawerActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login flow
|
||||||
|
composable(StudeezDestinations.SPLASH_SCREEN) {
|
||||||
|
SplashRoute(
|
||||||
|
openAndPopUp,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.LOGIN_SCREEN) {
|
||||||
|
LoginRoute(
|
||||||
|
openAndPopUp,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.SIGN_UP_SCREEN) {
|
||||||
|
SignUpRoute(
|
||||||
|
openAndPopUp,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Studying flow
|
||||||
|
composable(StudeezDestinations.TIMER_SELECTION_SCREEN) {
|
||||||
|
TimerSelectionRoute(
|
||||||
|
open,
|
||||||
|
goBack,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.TIMER_TYPE_CHOOSING_SCREEN) {
|
||||||
|
TimerTypeSelectScreen(
|
||||||
|
open = open,
|
||||||
|
popUp = goBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.SESSION_SCREEN) {
|
||||||
|
SessionRoute(
|
||||||
|
open,
|
||||||
|
openAndPopUp,
|
||||||
|
viewModel = hiltViewModel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.SESSION_RECAP) {
|
||||||
|
SessionRecapRoute(
|
||||||
|
clearAndNavigate = clearAndNavigate,
|
||||||
|
viewModel = hiltViewModel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.ADD_TIMER_SCREEN) {
|
||||||
|
TimerAddRoute(
|
||||||
|
popUp = goBack,
|
||||||
|
viewModel = hiltViewModel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.TIMER_EDIT_SCREEN) {
|
||||||
|
TimerEditRoute(
|
||||||
|
popUp = goBack,
|
||||||
|
viewModel = hiltViewModel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Friends flow
|
||||||
|
composable(StudeezDestinations.SEARCH_FRIENDS_SCREEN) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create & edit screens
|
||||||
|
composable(StudeezDestinations.CREATE_TASK_SCREEN) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.CREATE_SESSION_SCREEN) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(StudeezDestinations.EDIT_PROFILE_SCREEN) {
|
||||||
|
EditProfileRoute(
|
||||||
|
goBack,
|
||||||
|
openAndPopUp,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package be.ugent.sel.studeez.screens
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
|
||||||
|
import be.ugent.sel.studeez.common.snackbar.SnackbarMessage.Companion.toSnackbarMessage
|
||||||
|
import be.ugent.sel.studeez.domain.LogService
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
open class StudeezViewModel(private val logService: LogService) : ViewModel() {
|
||||||
|
fun launchCatching(snackbar: Boolean = true, block: suspend CoroutineScope.() -> Unit) =
|
||||||
|
viewModelScope.launch(
|
||||||
|
CoroutineExceptionHandler { _, throwable ->
|
||||||
|
if (snackbar) {
|
||||||
|
SnackbarManager.showMessage(throwable.toSnackbarMessage())
|
||||||
|
}
|
||||||
|
logService.logNonFatalCrash(throwable)
|
||||||
|
},
|
||||||
|
block = block
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package be.ugent.sel.studeez.screens.home
|
||||||
|
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
|
||||||
|
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
|
||||||
|
import be.ugent.sel.studeez.common.composable.feed.Feed
|
||||||
|
import be.ugent.sel.studeez.common.composable.feed.FeedUiState
|
||||||
|
import be.ugent.sel.studeez.common.composable.feed.FeedViewModel
|
||||||
|
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
|
||||||
|
import be.ugent.sel.studeez.data.local.models.FeedEntry
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeRoute(
|
||||||
|
open: (String) -> Unit,
|
||||||
|
drawerActions: DrawerActions,
|
||||||
|
navigationBarActions: NavigationBarActions,
|
||||||
|
feedViewModel: FeedViewModel,
|
||||||
|
) {
|
||||||
|
val feedUiState by feedViewModel.uiState.collectAsState()
|
||||||
|
HomeScreen(
|
||||||
|
drawerActions = drawerActions,
|
||||||
|
open = open,
|
||||||
|
navigationBarActions = navigationBarActions,
|
||||||
|
feedUiState = feedUiState,
|
||||||
|
continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) },
|
||||||
|
onEmptyFeedHelp = { feedViewModel.onEmptyFeedHelp(open) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen(
|
||||||
|
open: (String) -> Unit,
|
||||||
|
drawerActions: DrawerActions,
|
||||||
|
navigationBarActions: NavigationBarActions,
|
||||||
|
feedUiState: FeedUiState,
|
||||||
|
continueTask: (String, String) -> Unit,
|
||||||
|
onEmptyFeedHelp: () -> Unit,
|
||||||
|
) {
|
||||||
|
PrimaryScreenTemplate(
|
||||||
|
title = resources().getString(R.string.home),
|
||||||
|
drawerActions = drawerActions,
|
||||||
|
navigationBarActions = navigationBarActions,
|
||||||
|
// TODO barAction = { FriendsAction() }
|
||||||
|
) {
|
||||||
|
Feed(feedUiState, continueTask, onEmptyFeedHelp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FriendsAction() {
|
||||||
|
IconButton(onClick = { /*TODO*/ }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = resources().getString(R.string.friends)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun HomeScreenPreview() {
|
||||||
|
HomeScreen(
|
||||||
|
drawerActions = DrawerActions({}, {}, {}, {}, {}),
|
||||||
|
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
|
||||||
|
open = {},
|
||||||
|
feedUiState = FeedUiState.Succes(
|
||||||
|
mapOf(
|
||||||
|
"08 May 2023" to listOf(
|
||||||
|
FeedEntry(
|
||||||
|
argb_color = 0xFFABD200,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Task",
|
||||||
|
totalStudyTime = 600,
|
||||||
|
),
|
||||||
|
FeedEntry(
|
||||||
|
argb_color = 0xFFFFD200,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Task",
|
||||||
|
totalStudyTime = 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"09 May 2023" to listOf(
|
||||||
|
FeedEntry(
|
||||||
|
argb_color = 0xFFFD1200,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Task",
|
||||||
|
),
|
||||||
|
FeedEntry(
|
||||||
|
argb_color = 0xFFFF5C89,
|
||||||
|
subJectName = "Test Subject",
|
||||||
|
taskName = "Test Task",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
continueTask = { _, _ -> run {} },
|
||||||
|
onEmptyFeedHelp = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
package be.ugent.sel.studeez.screens.log_in
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import be.ugent.sel.studeez.common.composable.*
|
||||||
|
import be.ugent.sel.studeez.common.ext.basicButton
|
||||||
|
import be.ugent.sel.studeez.common.ext.fieldModifier
|
||||||
|
import be.ugent.sel.studeez.common.ext.textButton
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
data class LoginScreenActions(
|
||||||
|
val onEmailChange: (String) -> Unit,
|
||||||
|
val onPasswordChange: (String) -> Unit,
|
||||||
|
val onSignUpClick: () -> Unit,
|
||||||
|
val onSignInClick: () -> Unit,
|
||||||
|
val onForgotPasswordClick: () -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getLoginScreenActions(
|
||||||
|
viewModel: LoginViewModel,
|
||||||
|
openAndPopUp: (String, String) -> Unit,
|
||||||
|
): LoginScreenActions {
|
||||||
|
return LoginScreenActions(
|
||||||
|
onEmailChange = { viewModel.onEmailChange(it) },
|
||||||
|
onPasswordChange = { viewModel.onPasswordChange(it) },
|
||||||
|
onSignUpClick = { viewModel.onSignUpClick(openAndPopUp) },
|
||||||
|
onSignInClick = { viewModel.onSignInClick(openAndPopUp) },
|
||||||
|
onForgotPasswordClick = { viewModel.onForgotPasswordClick() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginRoute(
|
||||||
|
openAndPopUp: (String, String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: LoginViewModel,
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState
|
||||||
|
|
||||||
|
LoginScreen(
|
||||||
|
modifier = modifier,
|
||||||
|
uiState = uiState,
|
||||||
|
loginScreenActions = getLoginScreenActions(viewModel = viewModel, openAndPopUp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
uiState: LoginUiState,
|
||||||
|
loginScreenActions: LoginScreenActions,
|
||||||
|
) {
|
||||||
|
SimpleScreenTemplate(title = resources().getString(AppText.sign_in)) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
EmailField(
|
||||||
|
uiState.email,
|
||||||
|
loginScreenActions.onEmailChange,
|
||||||
|
Modifier.fieldModifier()
|
||||||
|
)
|
||||||
|
PasswordField(
|
||||||
|
uiState.password,
|
||||||
|
loginScreenActions.onPasswordChange,
|
||||||
|
Modifier.fieldModifier()
|
||||||
|
)
|
||||||
|
BasicButton(
|
||||||
|
AppText.sign_in,
|
||||||
|
Modifier.basicButton(),
|
||||||
|
onClick = loginScreenActions.onSignInClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicTextButton(
|
||||||
|
AppText.not_already_user,
|
||||||
|
Modifier.textButton(),
|
||||||
|
action = loginScreenActions.onSignUpClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicTextButton(
|
||||||
|
AppText.forgot_password,
|
||||||
|
Modifier.textButton(),
|
||||||
|
action = loginScreenActions.onForgotPasswordClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun LoginScreenPreview() {
|
||||||
|
LoginScreen(
|
||||||
|
uiState = LoginUiState(),
|
||||||
|
loginScreenActions = LoginScreenActions({}, {}, {}, {}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package be.ugent.sel.studeez.screens.log_in
|
||||||
|
|
||||||
|
data class LoginUiState(
|
||||||
|
val email: String = "",
|
||||||
|
val password: String = ""
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package be.ugent.sel.studeez.screens.log_in
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import be.ugent.sel.studeez.common.ext.isValidEmail
|
||||||
|
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
|
||||||
|
import be.ugent.sel.studeez.domain.AccountDAO
|
||||||
|
import be.ugent.sel.studeez.domain.LogService
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.LOGIN_SCREEN
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations.SIGN_UP_SCREEN
|
||||||
|
import be.ugent.sel.studeez.screens.StudeezViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class LoginViewModel @Inject constructor(
|
||||||
|
private val accountDAO: AccountDAO,
|
||||||
|
logService: LogService
|
||||||
|
) : StudeezViewModel(logService) {
|
||||||
|
var uiState = mutableStateOf(LoginUiState())
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val email
|
||||||
|
get() = uiState.value.email
|
||||||
|
private val password
|
||||||
|
get() = uiState.value.password
|
||||||
|
|
||||||
|
fun onEmailChange(newValue: String) {
|
||||||
|
uiState.value = uiState.value.copy(email = newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPasswordChange(newValue: String) {
|
||||||
|
uiState.value = uiState.value.copy(password = newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSignInClick(openAndPopUp: (String, String) -> Unit) {
|
||||||
|
if (!email.isValidEmail()) {
|
||||||
|
SnackbarManager.showMessage(AppText.email_error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.isBlank()) {
|
||||||
|
SnackbarManager.showMessage(AppText.empty_password_error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
launchCatching {
|
||||||
|
accountDAO.signInWithEmailAndPassword(email, password)
|
||||||
|
openAndPopUp(HOME_SCREEN, LOGIN_SCREEN) // Is not reached when error occurs.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onForgotPasswordClick() {
|
||||||
|
if (!email.isValidEmail()) {
|
||||||
|
SnackbarManager.showMessage(AppText.email_error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
launchCatching {
|
||||||
|
accountDAO.sendRecoveryEmail(email)
|
||||||
|
SnackbarManager.showMessage(AppText.recovery_email_sent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSignUpClick(openAndPopUp: (String, String) -> Unit) {
|
||||||
|
openAndPopUp(SIGN_UP_SCREEN, LOGIN_SCREEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package be.ugent.sel.studeez.screens.profile
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.common.composable.BasicTextButton
|
||||||
|
import be.ugent.sel.studeez.common.composable.LabelledInputField
|
||||||
|
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
|
||||||
|
import be.ugent.sel.studeez.common.ext.textButton
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||||
|
|
||||||
|
data class EditProfileActions(
|
||||||
|
val onUserNameChange: (String) -> Unit,
|
||||||
|
val onSaveClick: () -> Unit,
|
||||||
|
val onDeleteClick: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getEditProfileActions(
|
||||||
|
viewModel: ProfileEditViewModel,
|
||||||
|
openAndPopUp: (String, String) -> Unit,
|
||||||
|
): EditProfileActions {
|
||||||
|
return EditProfileActions(
|
||||||
|
onUserNameChange = { viewModel.onUsernameChange(it) },
|
||||||
|
onSaveClick = { viewModel.onSaveClick() },
|
||||||
|
onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditProfileRoute(
|
||||||
|
goBack: () -> Unit,
|
||||||
|
openAndPopUp: (String, String) -> Unit,
|
||||||
|
viewModel: ProfileEditViewModel,
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState
|
||||||
|
EditProfileScreen(
|
||||||
|
goBack = goBack,
|
||||||
|
uiState = uiState,
|
||||||
|
editProfileActions = getEditProfileActions(viewModel, openAndPopUp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditProfileScreen(
|
||||||
|
goBack: () -> Unit,
|
||||||
|
uiState: ProfileEditUiState,
|
||||||
|
editProfileActions: EditProfileActions,
|
||||||
|
) {
|
||||||
|
SecondaryScreenTemplate(
|
||||||
|
title = resources().getString(R.string.editing_profile),
|
||||||
|
popUp = goBack
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
LabelledInputField(
|
||||||
|
value = uiState.username,
|
||||||
|
onNewValue = editProfileActions.onUserNameChange,
|
||||||
|
label = R.string.username
|
||||||
|
)
|
||||||
|
BasicTextButton(
|
||||||
|
text = R.string.save,
|
||||||
|
Modifier.textButton(),
|
||||||
|
action = {
|
||||||
|
editProfileActions.onSaveClick()
|
||||||
|
goBack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
BasicTextButton(
|
||||||
|
text = R.string.delete_profile,
|
||||||
|
Modifier.textButton(),
|
||||||
|
action = editProfileActions.onDeleteClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun EditProfileScreenComposable() {
|
||||||
|
StudeezTheme {
|
||||||
|
EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package be.ugent.sel.studeez.screens.profile
|
||||||
|
|
||||||
|
data class ProfileEditUiState (
|
||||||
|
val username: String = ""
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package be.ugent.sel.studeez.screens.profile
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
|
||||||
|
import be.ugent.sel.studeez.domain.AccountDAO
|
||||||
|
import be.ugent.sel.studeez.domain.LogService
|
||||||
|
import be.ugent.sel.studeez.domain.UserDAO
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations
|
||||||
|
import be.ugent.sel.studeez.screens.StudeezViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ProfileEditViewModel @Inject constructor(
|
||||||
|
private val accountDAO: AccountDAO,
|
||||||
|
private val userDAO: UserDAO,
|
||||||
|
logService: LogService
|
||||||
|
) : StudeezViewModel(logService) {
|
||||||
|
|
||||||
|
var uiState = mutableStateOf(ProfileEditUiState())
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
launchCatching {
|
||||||
|
uiState.value = uiState.value.copy(username = userDAO.getUsername()!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUsernameChange(newValue: String) {
|
||||||
|
uiState.value = uiState.value.copy(username = newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSaveClick() {
|
||||||
|
launchCatching {
|
||||||
|
userDAO.save(uiState.value.username)
|
||||||
|
SnackbarManager.showMessage(R.string.success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeleteClick(openAndPopUp: (String, String) -> Unit) {
|
||||||
|
launchCatching {
|
||||||
|
userDAO.deleteUserReferences() // Delete references
|
||||||
|
accountDAO.deleteAccount() // Delete authentication
|
||||||
|
}
|
||||||
|
openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package be.ugent.sel.studeez.screens.profile
|
||||||
|
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import be.ugent.sel.studeez.R
|
||||||
|
import be.ugent.sel.studeez.common.composable.Headline
|
||||||
|
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
|
||||||
|
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
|
||||||
|
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
|
||||||
|
import be.ugent.sel.studeez.resources
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import be.ugent.sel.studeez.R.string as AppText
|
||||||
|
|
||||||
|
data class ProfileActions(
|
||||||
|
val getUsername: suspend CoroutineScope.() -> String?,
|
||||||
|
val onEditProfileClick: () -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getProfileActions(
|
||||||
|
viewModel: ProfileViewModel,
|
||||||
|
open: (String) -> Unit,
|
||||||
|
): ProfileActions {
|
||||||
|
return ProfileActions(
|
||||||
|
getUsername = { viewModel.getUsername() },
|
||||||
|
onEditProfileClick = { viewModel.onEditProfileClick(open) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProfileRoute(
|
||||||
|
open: (String) -> Unit,
|
||||||
|
viewModel: ProfileViewModel,
|
||||||
|
drawerActions: DrawerActions,
|
||||||
|
navigationBarActions: NavigationBarActions,
|
||||||
|
) {
|
||||||
|
ProfileScreen(
|
||||||
|
profileActions = getProfileActions(viewModel, open),
|
||||||
|
drawerActions = drawerActions,
|
||||||
|
navigationBarActions = navigationBarActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProfileScreen(
|
||||||
|
profileActions: ProfileActions,
|
||||||
|
drawerActions: DrawerActions,
|
||||||
|
navigationBarActions: NavigationBarActions,
|
||||||
|
) {
|
||||||
|
var username: String? by remember { mutableStateOf("") }
|
||||||
|
LaunchedEffect(key1 = Unit) {
|
||||||
|
username = profileActions.getUsername(this)
|
||||||
|
}
|
||||||
|
PrimaryScreenTemplate(
|
||||||
|
title = resources().getString(AppText.profile),
|
||||||
|
drawerActions = drawerActions,
|
||||||
|
navigationBarActions = navigationBarActions,
|
||||||
|
barAction = { EditAction(onClick = profileActions.onEditProfileClick) }
|
||||||
|
) {
|
||||||
|
Headline(text = (username ?: resources().getString(R.string.no_username)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditAction(
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
contentDescription = resources().getString(AppText.edit_profile)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ProfileScreenPreview() {
|
||||||
|
ProfileScreen(
|
||||||
|
profileActions = ProfileActions({ null }, {}),
|
||||||
|
drawerActions = DrawerActions({}, {}, {}, {}, {}),
|
||||||
|
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package be.ugent.sel.studeez.screens.profile
|
||||||
|
|
||||||
|
import be.ugent.sel.studeez.domain.LogService
|
||||||
|
import be.ugent.sel.studeez.domain.UserDAO
|
||||||
|
import be.ugent.sel.studeez.navigation.StudeezDestinations
|
||||||
|
import be.ugent.sel.studeez.screens.StudeezViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ProfileViewModel @Inject constructor(
|
||||||
|
private val userDAO: UserDAO,
|
||||||
|
logService: LogService
|
||||||
|
) : StudeezViewModel(logService) {
|
||||||
|
|
||||||
|
suspend fun getUsername(): String? {
|
||||||
|
return userDAO.getUsername()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditProfileClick(open: (String) -> Unit) {
|
||||||
|
open(StudeezDestinations.EDIT_PROFILE_SCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package be.ugent.sel.studeez.screens.session
|
||||||
|
|
||||||
|
import android.media.MediaPlayer
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
object InvisibleSessionManager {
|
||||||
|
private var viewModel: SessionViewModel? = null
|
||||||
|
private lateinit var mediaPlayer: MediaPlayer
|
||||||
|
|
||||||
|
fun setParameters(viewModel: SessionViewModel, mediaplayer: MediaPlayer) {
|
||||||
|
this.viewModel = viewModel
|
||||||
|
this.mediaPlayer = mediaplayer
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateTimer() {
|
||||||
|
viewModel?.let {
|
||||||
|
while (!it.getTimer().hasEnded()) {
|
||||||
|
delay(1.seconds)
|
||||||
|
it.getTimer().tick()
|
||||||
|
if (it.getTimer().hasCurrentCountdownEnded()) {
|
||||||
|
mediaPlayer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package be.ugent.sel.studeez.screens.session
|
||||||
|
|
||||||
|
import android.media.MediaPlayer
|
||||||
|
import android.media.RingtoneManager
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
|
||||||
|
import be.ugent.sel.studeez.screens.session.sessionScreens.AbstractSessionScreen
|
||||||
|
import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreen
|
||||||
|
|
||||||
|
data class SessionActions(
|
||||||
|
val getTimer: () -> FunctionalTimer,
|
||||||
|
val getTask: () -> String,
|
||||||
|
val startMediaPlayer: () -> Unit,
|
||||||
|
val releaseMediaPlayer: () -> Unit,
|
||||||
|
val endSession: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getSessionActions(
|
||||||
|
viewModel: SessionViewModel,
|
||||||
|
openAndPopUp: (String, String) -> Unit,
|
||||||
|
mediaplayer: MediaPlayer,
|
||||||
|
): SessionActions {
|
||||||
|
return SessionActions(
|
||||||
|
getTimer = viewModel::getTimer,
|
||||||
|
getTask = viewModel::getTask,
|
||||||
|
endSession = { viewModel.endSession(openAndPopUp) },
|
||||||
|
startMediaPlayer = mediaplayer::start,
|
||||||
|
releaseMediaPlayer = mediaplayer::release,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SessionRoute(
|
||||||
|
open: (String) -> Unit,
|
||||||
|
openAndPopUp: (String, String) -> Unit,
|
||||||
|
viewModel: SessionViewModel,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
||||||
|
val mediaplayer = MediaPlayer.create(context, uri)
|
||||||
|
mediaplayer.isLooping = false
|
||||||
|
|
||||||
|
InvisibleSessionManager.setParameters(
|
||||||
|
viewModel = viewModel,
|
||||||
|
mediaplayer = mediaplayer
|
||||||
|
)
|
||||||
|
|
||||||
|
val sessionScreen: AbstractSessionScreen = viewModel.getTimer().accept(GetSessionScreen(mediaplayer))
|
||||||
|
|
||||||
|
sessionScreen(
|
||||||
|
open = open,
|
||||||
|
sessionActions = getSessionActions(viewModel, openAndPopUp, mediaplayer)
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue