diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..4f1665a
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+Studeez
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index fb7f4a8..b589d56 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index a9f4e52..a2d7c21 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -1,5 +1,6 @@
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..ff9696e
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index bdd9278..8978d23 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,7 +1,6 @@
-
-
+
diff --git a/app/build.gradle b/app/build.gradle
index 52f7cb8..68d4e47 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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
diff --git a/app/google-services.json b/app/google-services.json
new file mode 100644
index 0000000..d0e5703
--- /dev/null
+++ b/app/google-services.json
@@ -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"
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 307071e..4426f42 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
diff --git a/app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt b/app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt
new file mode 100644
index 0000000..c3d1634
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt
@@ -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
+}
diff --git a/app/src/main/java/be/ugent/sel/studeez/StudeezAppstate.kt b/app/src/main/java/be/ugent/sel/studeez/StudeezAppstate.kt
new file mode 100644
index 0000000..cc28cd3
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/StudeezAppstate.kt
@@ -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 }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/StudeezHiltApp.kt b/app/src/main/java/be/ugent/sel/studeez/StudeezHiltApp.kt
new file mode 100644
index 0000000..5db407f
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/StudeezHiltApp.kt
@@ -0,0 +1,7 @@
+package be.ugent.sel.studeez
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class StudeezHiltApp : Application()
diff --git a/app/src/main/java/be/ugent/sel/studeez/MainActivity.kt b/app/src/main/java/be/ugent/sel/studeez/activities/MainActivity.kt
similarity index 64%
rename from app/src/main/java/be/ugent/sel/studeez/MainActivity.kt
rename to app/src/main/java/be/ugent/sel/studeez/activities/MainActivity.kt
index 3c0deee..b020cc8 100644
--- a/app/src/main/java/be/ugent/sel/studeez/MainActivity.kt
+++ b/app/src/main/java/be/ugent/sel/studeez/activities/MainActivity.kt
@@ -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
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt
new file mode 100644
index 0000000..c96994d
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/DrawerScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/DrawerScreenComposable.kt
new file mode 100644
index 0000000..b0b1829
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/DrawerScreenComposable.kt
@@ -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")
+ } }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt
new file mode 100644
index 0000000..ea2b52d
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt
@@ -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({}, {}, {})
+ ) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/FormComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/FormComposable.kt
new file mode 100644
index 0000000..1fbcfb2
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/FormComposable.kt
@@ -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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/ImageComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/ImageComposable.kt
new file mode 100644
index 0000000..39e7272
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/ImageComposable.kt
@@ -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)
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/PrimaryScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/PrimaryScreenComposable.kt
new file mode 100644
index 0000000..0b3ee6e
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/PrimaryScreenComposable.kt
@@ -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"
+ )
+ }
+ },
+ ) {}
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/SecondaryScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/SecondaryScreenComposable.kt
new file mode 100644
index 0000000..5999072
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/SecondaryScreenComposable.kt
@@ -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",
+ {}
+ ) {} }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/SimpleScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/SimpleScreenComposable.kt
new file mode 100644
index 0000000..0e3c684
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/SimpleScreenComposable.kt
@@ -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) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextComposable.kt
new file mode 100644
index 0000000..25fa3c4
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextComposable.kt
@@ -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)
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt
new file mode 100644
index 0000000..aadcee3
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt
@@ -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
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/TimePickerButtonComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/TimePickerButtonComposable.kt
new file mode 100644
index 0000000..c5e75cc
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/TimePickerButtonComposable.kt
@@ -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 = 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) {
+ 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 = {})
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/TimerEntry.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/TimerEntry.kt
new file mode 100644
index 0000000..7dc105b
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/TimerEntry.kt
@@ -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) {}
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/drawer/DrawerComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/drawer/DrawerComposable.kt
new file mode 100644
index 0000000..2d4eab3
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/drawer/DrawerComposable.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/drawer/DrawerViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/drawer/DrawerViewModel.kt
new file mode 100644
index 0000000..e55c342
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/drawer/DrawerViewModel.kt
@@ -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)
+ }
+}
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/Feed.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/Feed.kt
new file mode 100644
index 0000000..54be2ea
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/Feed.kt
@@ -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 {} }, {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedEntry.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedEntry.kt
new file mode 100644
index 0000000..ff950d6
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedEntry.kt
@@ -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,
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedUiState.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedUiState.kt
new file mode 100644
index 0000000..1b938ca
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedUiState.kt
@@ -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>) : FeedUiState
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedViewModel.kt
new file mode 100644
index 0000000..b5e2b1a
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedViewModel.kt
@@ -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 = 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarComposable.kt
new file mode 100644
index 0000000..c4d6e33
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarComposable.kt
@@ -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 }, {}, {}, {}, {}, {}, {}, {}),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarViewModel.kt
new file mode 100644
index 0000000..d2f4f09
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarViewModel.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/TimePickerComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/TimePickerComposable.kt
new file mode 100644
index 0000000..3a59519
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/TimePickerComposable.kt
@@ -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
+ )
+}
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt
new file mode 100644
index 0000000..63d4fbe
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt
@@ -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,
+ getCompletedTaskCount: () -> Flow,
+ getStudyTime: () -> Flow,
+ 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() },
+ ) {}
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/TaskEntry.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/TaskEntry.kt
new file mode 100644
index 0000000..35e7a44
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/TaskEntry.kt
@@ -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,
+ ),
+ {}, {}, {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/ext/ColorExt.kt b/app/src/main/java/be/ugent/sel/studeez/common/ext/ColorExt.kt
new file mode 100644
index 0000000..87ce226
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/ext/ColorExt.kt
@@ -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
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/ext/ModifierExt.kt b/app/src/main/java/be/ugent/sel/studeez/common/ext/ModifierExt.kt
new file mode 100644
index 0000000..66ade69
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/ext/ModifierExt.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/ext/ShapeExt.kt b/app/src/main/java/be/ugent/sel/studeez/common/ext/ShapeExt.kt
new file mode 100644
index 0000000..2114a74
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/ext/ShapeExt.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/ext/StringExt.kt b/app/src/main/java/be/ugent/sel/studeez/common/ext/StringExt.kt
new file mode 100644
index 0000000..02af993
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/ext/StringExt.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/snackbar/SnackBarManager.kt b/app/src/main/java/be/ugent/sel/studeez/common/snackbar/SnackBarManager.kt
new file mode 100644
index 0000000..ad743d5
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/snackbar/SnackBarManager.kt
@@ -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 = MutableStateFlow(null)
+ val snackbarMessages: StateFlow
+ get() = messages.asStateFlow()
+
+ fun showMessage(@StringRes message: Int) {
+ messages.value = SnackbarMessage.ResourceSnackbar(message)
+ }
+
+ fun showMessage(message: SnackbarMessage) {
+ messages.value = message
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/common/snackbar/SnackBarMessage.kt b/app/src/main/java/be/ugent/sel/studeez/common/snackbar/SnackBarMessage.kt
new file mode 100644
index 0000000..495ceb1
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/common/snackbar/SnackBarMessage.kt
@@ -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)
+ }
+ }
+}
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt b/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt
new file mode 100644
index 0000000..c52939f
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt
@@ -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 {
+ abstract var value: T
+ operator fun invoke() = value
+ fun set(newValue: T) {
+ this.value = newValue
+ }
+}
+
+@Singleton
+class SelectedSessionReport @Inject constructor() : SelectedState() {
+ override lateinit var value: SessionReport
+}
+
+@Singleton
+class SelectedTask @Inject constructor() : SelectedState() {
+ override lateinit var value: Task
+}
+
+@Singleton
+class SelectedTimer @Inject constructor() : SelectedState() {
+ override lateinit var value: FunctionalTimer
+}
+
+@Singleton
+class SelectedSubject @Inject constructor() : SelectedState() {
+ override lateinit var value: Subject
+}
+
+@Singleton
+class SelectedTimerInfo @Inject constructor() : SelectedState() {
+ override lateinit var value: TimerInfo
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/db/.keep b/app/src/main/java/be/ugent/sel/studeez/data/local/db/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/FeedEntry.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/FeedEntry.kt
new file mode 100644
index 0000000..8733c48
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/FeedEntry.kt
@@ -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
+)
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/SessionReport.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/SessionReport.kt
new file mode 100644
index 0000000..5835538
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/SessionReport.kt
@@ -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 = ""
+)
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt
new file mode 100644
index 0000000..2fba2ce
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt
@@ -0,0 +1,3 @@
+package be.ugent.sel.studeez.data.local.models
+
+data class User(val id: String = "")
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt
new file mode 100644
index 0000000..261f3e0
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt
@@ -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"
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Task.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Task.kt
new file mode 100644
index 0000000..ff2748d
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Task.kt
@@ -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"
+}
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalCustomTimer.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalCustomTimer.kt
new file mode 100644
index 0000000..7bc51f8
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalCustomTimer.kt
@@ -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 accept(visitor: FunctionalTimerVisitor): T {
+ return visitor.visitFunctionalCustomTimer(this)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalEndlessTimer.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalEndlessTimer.kt
new file mode 100644
index 0000000..51ee182
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalEndlessTimer.kt
@@ -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 accept(visitor: FunctionalTimerVisitor): T {
+ return visitor.visitFunctionalEndlessTimer(this)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt
new file mode 100644
index 0000000..f5237d6
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt
@@ -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 accept(visitor: FunctionalTimerVisitor): T {
+ return visitor.visitFunctionalBreakTimer(this)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimer.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimer.kt
new file mode 100644
index 0000000..656f52f
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimer.kt
@@ -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 accept(visitor: FunctionalTimerVisitor): T
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimerVisitor.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimerVisitor.kt
new file mode 100644
index 0000000..3acc805
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimerVisitor.kt
@@ -0,0 +1,11 @@
+package be.ugent.sel.studeez.data.local.models.timer_functional
+
+interface FunctionalTimerVisitor {
+
+ fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): T
+
+ fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): T
+
+ fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): T
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/HoursMinutesSeconds.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/HoursMinutesSeconds.kt
new file mode 100644
index 0000000..edccbd0
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/HoursMinutesSeconds.kt
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/Time.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/Time.kt
new file mode 100644
index 0000000..e37b374
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/Time.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/CustomTimerInfo.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/CustomTimerInfo.kt
new file mode 100644
index 0000000..d88e39f
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/CustomTimerInfo.kt
@@ -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 {
+ return mapOf(
+ "type" to "custom",
+ "name" to name,
+ "description" to description,
+ "studyTime" to studyTime,
+ )
+ }
+
+ override fun accept(visitor: TimerInfoVisitor): T {
+ return visitor.visitCustomTimerInfo(this)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/EndlessTimerInfo.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/EndlessTimerInfo.kt
new file mode 100644
index 0000000..45f7fd7
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/EndlessTimerInfo.kt
@@ -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 {
+ return mapOf(
+ "type" to "endless",
+ "name" to name,
+ "description" to description
+ )
+ }
+
+ override fun accept(visitor: TimerInfoVisitor): T {
+ return visitor.visitEndlessTimerInfo(this)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/PomodoroTimerInfo.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/PomodoroTimerInfo.kt
new file mode 100644
index 0000000..7316630
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/PomodoroTimerInfo.kt
@@ -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 {
+ return mapOf(
+ "type" to "break",
+ "name" to name,
+ "description" to description,
+ "studyTime" to studyTime,
+ "breakTime" to breakTime,
+ "repeats" to repeats,
+ )
+ }
+
+ override fun accept(visitor: TimerInfoVisitor): T {
+ return visitor.visitBreakTimerInfo(this)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerInfo.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerInfo.kt
new file mode 100644
index 0000000..e4deded
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerInfo.kt
@@ -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
+ abstract fun accept(visitor: TimerInfoVisitor): T
+
+}
+
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerInfoVisitor.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerInfoVisitor.kt
new file mode 100644
index 0000000..e331c8d
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerInfoVisitor.kt
@@ -0,0 +1,11 @@
+package be.ugent.sel.studeez.data.local.models.timer_info
+
+interface TimerInfoVisitor {
+
+ fun visitCustomTimerInfo(customTimerInfo: CustomTimerInfo): T
+
+ fun visitEndlessTimerInfo(endlessTimerInfo: EndlessTimerInfo): T
+
+ fun visitBreakTimerInfo(pomodoroTimerInfo: PomodoroTimerInfo): T
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerJson.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerJson.kt
new file mode 100644
index 0000000..37a7b9f
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerJson.kt
@@ -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 = ""
+)
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerType.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerType.kt
new file mode 100644
index 0000000..20fb36d
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_info/TimerType.kt
@@ -0,0 +1,7 @@
+package be.ugent.sel.studeez.data.local.models.timer_info
+
+enum class TimerType {
+ BREAK,
+ ENDLESS,
+ CUSTOM
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/data/remote/.keep b/app/src/main/java/be/ugent/sel/studeez/data/remote/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt b/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt
new file mode 100644
index 0000000..4c5fea1
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt
@@ -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
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/di/FireBaseModule.kt b/app/src/main/java/be/ugent/sel/studeez/di/FireBaseModule.kt
new file mode 100644
index 0000000..e8510f3
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/di/FireBaseModule.kt
@@ -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
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/AccountDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/AccountDAO.kt
new file mode 100644
index 0000000..c813ec6
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/domain/AccountDAO.kt
@@ -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
+
+ 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()
+}
diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/ConfigurationService.kt b/app/src/main/java/be/ugent/sel/studeez/domain/ConfigurationService.kt
new file mode 100644
index 0000000..26aeba5
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/domain/ConfigurationService.kt
@@ -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
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt
new file mode 100644
index 0000000..2d91781
--- /dev/null
+++ b/app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt
@@ -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