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"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<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="previewFile" value="true" />
|
||||
</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>
|
||||
</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">
|
||||
<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" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ plugins {
|
|||
|
||||
// Protobuf
|
||||
id 'com.google.protobuf' version '0.8.17'
|
||||
|
||||
// Firebase
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -62,6 +66,7 @@ dependencies {
|
|||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation 'androidx.compose.material:material:1.2.0'
|
||||
|
||||
|
||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
|
||||
|
||||
// ViewModel
|
||||
|
|
@ -93,6 +98,9 @@ dependencies {
|
|||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
|
||||
// Coroutine testing
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||
|
||||
// Mocking
|
||||
testImplementation 'org.mockito.kotlin:mockito-kotlin:3.2.0'
|
||||
|
||||
|
|
@ -108,14 +116,13 @@ dependencies {
|
|||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
|
||||
//Firebase
|
||||
// implementation platform('com.google.firebase:firebase-bom:30.4.1')
|
||||
// implementation 'com.google.firebase:firebase-crashlytics-ktx'
|
||||
// implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
// implementation 'com.google.firebase:firebase-auth-ktx'
|
||||
// implementation 'com.google.firebase:firebase-firestore-ktx'
|
||||
// implementation 'com.google.firebase:firebase-perf-ktx'
|
||||
// implementation 'com.google.firebase:firebase-config-ktx'
|
||||
|
||||
implementation platform('com.google.firebase:firebase-bom:31.3.0')
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx'
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
implementation 'com.google.firebase:firebase-auth-ktx'
|
||||
implementation 'com.google.firebase:firebase-firestore-ktx'
|
||||
implementation 'com.google.firebase:firebase-perf-ktx'
|
||||
implementation 'com.google.firebase:firebase-config-ktx'
|
||||
}
|
||||
|
||||
// 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">
|
||||
|
||||
<application
|
||||
android:name=".StudeezHiltApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
|
|
@ -12,7 +13,7 @@
|
|||
android:theme="@style/Theme.Studeez"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
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 androidx.activity.ComponentActivity
|
||||
|
|
@ -10,8 +10,17 @@ import androidx.compose.material.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
var onTimerInvisible: Job? = null
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -22,11 +31,23 @@ class MainActivity : ComponentActivity() {
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
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
|
||||
|
|
@ -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