Merge branch 'development' into refactor

This commit is contained in:
brreynie 2023-05-16 11:39:33 +02:00
commit 6542d2dbf2
141 changed files with 5058 additions and 1136 deletions

3
.idea/misc.xml generated
View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View file

@ -123,9 +123,6 @@ dependencies {
implementation 'com.google.firebase:firebase-firestore-ktx' implementation 'com.google.firebase:firebase-firestore-ktx'
implementation 'com.google.firebase:firebase-perf-ktx' implementation 'com.google.firebase:firebase-perf-ktx'
implementation 'com.google.firebase:firebase-config-ktx' implementation 'com.google.firebase:firebase-config-ktx'
// Colorpicker
implementation 'com.github.skydoves:colorpicker-compose:1.0.2'
} }
// Allow references to generate code // Allow references to generate code
@ -150,4 +147,4 @@ protobuf {
} }
} }
} }
} }

View file

@ -0,0 +1,74 @@
package be.ugent.sel.studeez
import androidx.compose.material.FloatingActionButton
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.common.composable.AddButtonActions
import be.ugent.sel.studeez.common.composable.ExpandedAddButton
import org.junit.Rule
import org.junit.Test
class FabTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun expandFabTest() {
var expand = false
composeTestRule.setContent {
FloatingActionButton(
onClick = {expand = true}
) {}
}
composeTestRule.waitForIdle()
composeTestRule
.onNode(hasClickAction())
.assertExists()
.performClick()
assert(expand)
}
@Test
fun fabTest() {
var task = false
var session = false
var friend = false
composeTestRule.setContent {
ExpandedAddButton(
addButtonActions = AddButtonActions(
{task = true},
{friend = true},
{session = true}
)
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithContentDescription("Session")
.assertExists()
.performClick()
composeTestRule
.onNodeWithContentDescription("Task")
.assertExists()
.performClick()
composeTestRule
.onNodeWithContentDescription("Friend")
.assertExists()
.performClick()
assert(task)
assert(session)
assert(friend)
}
}

View file

@ -0,0 +1,207 @@
package be.ugent.sel.studeez
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.feed.FeedUiState
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.screens.home.HomeScreen
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
class HomeScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun homeScreenTest() {
var continueTask = false
composeTestRule.setContent {
HomeScreen(
drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions({false}, {}, {}, {}, {}, {}, {}, {}),
feedUiState = FeedUiState.Succes(mapOf(
"08 May 2023" to listOf(
FeedEntry(
argb_color = 0xFFABD200,
subJectName = "Test Subject",
taskName = "Test Task",
totalStudyTime = 600,
)
)
)),
continueTask = {_, _ -> continueTask = true },
onEmptyFeedHelp = {},
onViewFriendsClick = {},
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
"continue",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
Assert.assertTrue(continueTask)
}
@Test
fun drawerTest() {
var homebuttontest = false
var timersbuttontest = false
var settingsbuttontest = false
var logoutbuttontest = false
var aboutbuttontest = false
composeTestRule.setContent {
HomeScreen(
drawerActions = DrawerActions(
{homebuttontest = true},
{timersbuttontest = true},
{settingsbuttontest = true},
{logoutbuttontest = true},
{aboutbuttontest = true}
),
navigationBarActions = NavigationBarActions({false}, {}, {}, {}, {}, {}, {}, {}),
feedUiState = FeedUiState.Succes(mapOf()),
continueTask = {_, _ -> },
onEmptyFeedHelp = {},
onViewFriendsClick = {},
)
}
composeTestRule.waitForIdle()
composeTestRule
.onAllNodesWithText(
"home",
substring = true,
ignoreCase = true
)[2] // Third node has the button
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
"timer",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
"settings",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
"log out",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
"about",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
Assert.assertTrue(homebuttontest)
Assert.assertTrue(timersbuttontest)
Assert.assertTrue(settingsbuttontest)
Assert.assertTrue(logoutbuttontest)
Assert.assertTrue(aboutbuttontest)
}
@Test
fun navigationbarTest() {
var hometest = false
var tasktest = false
var sessiontest = false
var profiletest = false
composeTestRule.setContent {
HomeScreen(
drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions(
{false},
{hometest = true},
{tasktest = true},
{sessiontest = true},
{profiletest = true},
{}, {}, {}
),
feedUiState = FeedUiState.Succes(mapOf()),
continueTask = {_, _ -> },
onEmptyFeedHelp = {},
onViewFriendsClick = {},
)
}
composeTestRule.waitForIdle()
composeTestRule
.onAllNodesWithContentDescription(
"Home",
substring = true,
ignoreCase = true
)[0] // Third node has the button
.assertExists()
.performClick()
composeTestRule
.onNodeWithContentDescription(
"tasks",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithContentDescription(
"session",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithContentDescription(
"profile",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
Assert.assertTrue(hometest)
Assert.assertTrue(tasktest)
Assert.assertTrue(sessiontest)
Assert.assertTrue(profiletest)
}
}

View file

@ -14,7 +14,7 @@ import org.junit.Assert.*
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class InstrumentedTest {
@Test @Test
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.

View file

@ -0,0 +1,68 @@
package be.ugent.sel.studeez
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.screens.log_in.LoginScreen
import be.ugent.sel.studeez.screens.log_in.LoginScreenActions
import be.ugent.sel.studeez.screens.log_in.LoginUiState
import org.junit.Rule
import org.junit.Test
class LoginScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun loginScreenTest() {
var login = false
var signup = false
var forgot_password = false
composeTestRule.setContent {
LoginScreen(
uiState = LoginUiState(),
loginScreenActions = LoginScreenActions(
{}, {},
{signup = true},
{login = true},
{forgot_password = true}
)
)
}
composeTestRule.waitForIdle()
composeTestRule
.onAllNodesWithText(
text = "Sign in",
substring = true,
ignoreCase = true
)[0] // The first object is the button
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "Forgot",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "Sign up",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(signup)
assert(login)
assert(forgot_password)
}
}

View file

@ -0,0 +1,70 @@
package be.ugent.sel.studeez
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.screens.profile.edit_profile.EditProfileActions
import be.ugent.sel.studeez.screens.profile.edit_profile.EditProfileScreen
import be.ugent.sel.studeez.screens.profile.edit_profile.ProfileEditUiState
import org.junit.Rule
import org.junit.Test
class ProfileEditScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun profileEditScreenTest() {
var edit_save = false
var goback = false
var delete_click = false
composeTestRule.setContent {
EditProfileScreen(
goBack = {goback = true},
uiState = ProfileEditUiState(),
editProfileActions = EditProfileActions(
onUserNameChange = {},
onBiographyChange = {},
onSaveClick = {edit_save = true},
onDeleteClick = { delete_click = true },
),
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
text = "save",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "delete",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithContentDescription(
label = "go back",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(edit_save)
assert(goback)
assert(delete_click)
}
}

View file

@ -0,0 +1,61 @@
package be.ugent.sel.studeez
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.screens.profile.ProfileActions
import be.ugent.sel.studeez.screens.profile.ProfileScreen
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
class ProfileScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun profileScreenTest() {
var edit = false
var view_friends = false
composeTestRule.setContent {
ProfileScreen(
profileActions = ProfileActions(
getUsername = {null},
onEditProfileClick = {edit = true},
getBiography = {null},
getAmountOfFriends = { flowOf(0) },
onViewFriendsClick = {view_friends = true}
),
drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {})
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithContentDescription(
label = "edit profile",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "friends",
substring = true,
ignoreCase = true,
)
.assertExists()
.performClick()
assert(edit)
assert(view_friends)
}
}

View file

@ -0,0 +1,75 @@
package be.ugent.sel.studeez
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.data.local.models.SessionReport
import be.ugent.sel.studeez.screens.session_recap.SessionRecapActions
import be.ugent.sel.studeez.screens.session_recap.SessionRecapScreen
import com.google.firebase.Timestamp
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
class SessionRecapScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun sessionRecapTest() {
var saveCalled = false
var discardCalled = false
composeTestRule.setContent {
SessionRecapScreen(
Modifier,
SessionRecapActions(
{
SessionReport(
"",
0,
Timestamp(0, 0),
"")
},
{ saveCalled = true },
{ discardCalled = true }
)
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
"You studied",
substring = true,
ignoreCase = true
)
.assertExists()
composeTestRule
.onNodeWithText(
"save",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
"discard",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
Assert.assertTrue(saveCalled)
Assert.assertTrue(discardCalled)
}
}

View file

@ -0,0 +1,52 @@
package be.ugent.sel.studeez
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.screens.sign_up.SignUpActions
import be.ugent.sel.studeez.screens.sign_up.SignUpScreen
import be.ugent.sel.studeez.screens.sign_up.SignUpUiState
import org.junit.Rule
import org.junit.Test
class SignUpScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun signupScreenTest() {
var create = false
var login = false
composeTestRule.setContent {
SignUpScreen(
uiState = SignUpUiState(),
signUpActions = SignUpActions({}, {}, {}, {}, {create = true}, {login = true})
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
text = "log in",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onAllNodesWithText(
text = "Create account",
substring = true,
ignoreCase = true
)[0] // First node has the button
.assertExists()
.performClick()
assert(login)
assert(create)
}
}

View file

@ -0,0 +1,40 @@
package be.ugent.sel.studeez
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.screens.splash.SplashScreen
import org.junit.Rule
import org.junit.Test
class SplashScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun splashScreenTest() {
var tryAgain = false
composeTestRule.setContent {
SplashScreen(
Modifier,
{tryAgain = true},
true
)
}
composeTestRule.waitForIdle()
composeTestRule
.onAllNodesWithText(
text = "try again",
substring = true,
ignoreCase = true
)[1] // Second node is the button
.assertExists()
.performClick()
assert(tryAgain)
}
}

View file

@ -0,0 +1,158 @@
package be.ugent.sel.studeez
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.common.composable.DeleteButton
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.screens.subjects.SubjectScreen
import be.ugent.sel.studeez.screens.subjects.SubjectUiState
import be.ugent.sel.studeez.screens.subjects.form.SubjectForm
import be.ugent.sel.studeez.screens.subjects.form.SubjectFormUiState
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
class SubjectScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun addSubjectScreenTest() {
var confirm = false
var goback = false
composeTestRule.setContent {
SubjectForm(
title = R.string.new_subject,
goBack = {goback = true},
uiState = SubjectFormUiState(),
onConfirm = {confirm = true},
onNameChange = {},
onColorChange = {},
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
text = "confirm",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithContentDescription(
label = "go back",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(confirm)
assert(goback)
}
@Test
fun editSubjectScreenTest() {
var confirm = false
var delete = false
composeTestRule.setContent {
SubjectForm(
title = R.string.edit_subject,
goBack = {},
uiState = SubjectFormUiState(
name = "Test Subject",
),
onConfirm = {confirm = true},
onNameChange = {},
onColorChange = {},
)
DeleteButton(text = R.string.delete_subject) {
delete = true
}
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
text = "confirm",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "delete",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(confirm)
assert(delete)
}
@Test
fun subjectScreenTest() {
var view = false
var add = false
composeTestRule.setContent {
SubjectScreen(
drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions({false}, {}, {}, {}, {}, {}, {}, {}),
onAddSubject = { add = true },
onViewSubject = { view = true },
getStudyTime = { flowOf() },
getCompletedTaskCount = { flowOf() },
getTaskCount = { flowOf() },
uiState = SubjectUiState.Succes(
listOf(
Subject(
id = "",
name = "Test Subject",
argb_color = 0xFFFFD200,
archived = false
)
)
)
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
text = "view",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "new subject",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(add)
assert(view)
}
}

View file

@ -0,0 +1,160 @@
package be.ugent.sel.studeez
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.common.composable.DeleteButton
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.screens.tasks.TaskActions
import be.ugent.sel.studeez.screens.tasks.TaskScreen
import be.ugent.sel.studeez.screens.tasks.form.TaskForm
import be.ugent.sel.studeez.screens.tasks.form.TaskFormUiState
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
class TaskScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun addTaskScreenTest() {
var confirm = false
var goback = false
composeTestRule.setContent {
TaskForm(
title = R.string.new_task,
goBack = {goback = true},
uiState = TaskFormUiState(),
onConfirm = {confirm = true},
onNameChange = {},
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
text = "confirm",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithContentDescription(
label = "go back",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(confirm)
assert(goback)
}
@Test
fun editTaskScreenTest() {
var confirm = false
var delete = false
composeTestRule.setContent {
TaskForm(
title = R.string.edit_task,
goBack = {},
uiState = TaskFormUiState(
name = "Test Task",
),
onConfirm = {confirm = true},
onNameChange = {},
) {
DeleteButton(text = R.string.delete_task) {
delete = true
}
}
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
text = "confirm",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "delete",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(confirm)
assert(delete)
}
@Test
fun taskScreenTest() {
var add = false
var edit = false
var start = false
composeTestRule.setContent {
TaskScreen(
goBack = {},
taskActions = TaskActions(
{add = true},
{ Subject(name = "Test Subject") },
{ flowOf(listOf(Task())) },
{ _, _ -> run {} },
{edit = true},
{start = true},
{},
)
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithContentDescription(
label = "edit",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "new",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "start",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(add)
assert(edit)
assert(start)
}
}

View file

@ -0,0 +1,58 @@
package be.ugent.sel.studeez
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.data.local.models.timer_info.EndlessTimerInfo
import be.ugent.sel.studeez.screens.timer_overview.TimerOverviewActions
import be.ugent.sel.studeez.screens.timer_overview.TimerOverviewScreen
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
class TimerOverviewScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun timerOverviewScreenTest() {
var add = false
var edit = false
composeTestRule.setContent {
TimerOverviewScreen(
timerOverviewActions = TimerOverviewActions(
{ flowOf(listOf(EndlessTimerInfo("", ""))) },
{ listOf() },
{edit = true},
{add = true}
),
drawerActions = DrawerActions({}, {}, {}, {}, {})
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
text = "add",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
composeTestRule
.onNodeWithText(
text = "edit",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(add)
assert(edit)
}
}

View file

@ -0,0 +1,40 @@
package be.ugent.sel.studeez
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import be.ugent.sel.studeez.screens.timer_selection.TimerSelectionActions
import be.ugent.sel.studeez.screens.timer_selection.TimerSelectionScreen
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
class TimerSelectionScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun timerOverviewScreenTest() {
var start = false
composeTestRule.setContent {
TimerSelectionScreen(
timerSelectionActions = TimerSelectionActions({ flowOf()}, {start = true}, 0),
popUp = {}
)
}
composeTestRule.waitForIdle()
composeTestRule
.onNodeWithText(
text = "start",
substring = true,
ignoreCase = true
)
.assertExists()
.performClick()
assert(start)
}
}

View file

@ -2,19 +2,9 @@ package be.ugent.sel.studeez.common.composable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
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.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button import androidx.compose.material.*
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.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -31,7 +21,11 @@ import be.ugent.sel.studeez.common.ext.defaultButtonShape
import be.ugent.sel.studeez.R.string as AppText import be.ugent.sel.studeez.R.string as AppText
@Composable @Composable
fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { fun BasicTextButton(
@StringRes text: Int,
modifier: Modifier,
action: () -> Unit
) {
TextButton( TextButton(
onClick = action, onClick = action,
modifier = modifier modifier = modifier
@ -48,6 +42,7 @@ fun BasicButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colors: ButtonColors = ButtonDefaults.buttonColors(), colors: ButtonColors = ButtonDefaults.buttonColors(),
border: BorderStroke? = null, border: BorderStroke? = null,
enabled: Boolean = true,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Button( Button(
@ -56,6 +51,7 @@ fun BasicButton(
shape = defaultButtonShape(), shape = defaultButtonShape(),
colors = colors, colors = colors,
border = border, border = border,
enabled = enabled,
) { ) {
Text( Text(
text = stringResource(text), text = stringResource(text),
@ -74,17 +70,22 @@ fun BasicButtonPreview() {
fun StealthButton( fun StealthButton(
@StringRes text: Int, @StringRes text: Int,
modifier: Modifier = Modifier.card(), modifier: Modifier = Modifier.card(),
enabled: Boolean = true,
onClick: () -> Unit, 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( BasicButton(
text = text, text = text,
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.surface, backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.4f) contentColor = borderColor
), ),
border = BorderStroke(3.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.4f)) border = BorderStroke(2.dp, borderColor)
) )
} }

View file

@ -2,7 +2,6 @@ package be.ugent.sel.studeez.common.composable
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.FloatingActionButton import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon import androidx.compose.material.Icon

View file

@ -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()
}
}
}

View file

@ -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)
)
)
}

View file

@ -0,0 +1,44 @@
package be.ugent.sel.studeez.common.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.ui.theme.StudeezTheme
@Composable
fun ProfilePicture() {
Box(
modifier = Modifier
.size(40.dp)
.background(MaterialTheme.colors.primary, CircleShape)
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = stringResource(id = R.string.username),
modifier = Modifier
.size(30.dp)
.align(Alignment.Center),
tint = MaterialTheme.colors.onPrimary
)
}
}
@Preview
@Composable
fun ProfilePicturePreview() {
StudeezTheme {
ProfilePicture()
}
}

View file

@ -3,10 +3,13 @@ package be.ugent.sel.studeez.common.composable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@Composable @Composable
@ -23,4 +26,14 @@ fun Headline(
fontSize = 34.sp fontSize = 34.sp
) )
} }
}
@Composable
fun DateText(date: String) {
Text(
text = date,
fontWeight = FontWeight.Medium,
fontSize = 20.sp,
modifier = Modifier.padding(horizontal = 10.dp)
)
} }

View file

@ -3,13 +3,13 @@ package be.ugent.sel.studeez.common.composable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.common.ext.fieldModifier import be.ugent.sel.studeez.common.ext.fieldModifier
import be.ugent.sel.studeez.resources 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.drawable as AppIcon
import be.ugent.sel.studeez.R.string as AppText import be.ugent.sel.studeez.R.string as AppText
@ -47,7 +46,7 @@ fun LabelledInputField(
value: String, value: String,
onNewValue: (String) -> Unit, onNewValue: (String) -> Unit,
@StringRes label: Int, @StringRes label: Int,
singleLine: Boolean = false singleLine: Boolean = true
) { ) {
OutlinedTextField( OutlinedTextField(
value = value, value = value,
@ -119,7 +118,9 @@ fun LabeledErrorTextField(
initialValue: String, initialValue: String,
@StringRes label: Int, @StringRes label: Int,
singleLine: Boolean = false, singleLine: Boolean = false,
errorText: Int, isValid: MutableState<Boolean> = remember { mutableStateOf(true) },
isFirst: MutableState<Boolean> = remember { mutableStateOf(false) },
@StringRes errorText: Int,
keyboardType: KeyboardType, keyboardType: KeyboardType,
predicate: (String) -> Boolean, predicate: (String) -> Boolean,
onNewCorrectValue: (String) -> Unit onNewCorrectValue: (String) -> Unit
@ -128,31 +129,28 @@ fun LabeledErrorTextField(
mutableStateOf(initialValue) mutableStateOf(initialValue)
} }
var isValid by remember {
mutableStateOf(predicate(value))
}
Column { Column {
OutlinedTextField( OutlinedTextField(
modifier = modifier.fieldModifier(), modifier = modifier.fieldModifier(),
value = value, value = value,
onValueChange = { newText -> onValueChange = { newText ->
isFirst.value = false
value = newText value = newText
isValid = predicate(value) isValid.value = predicate(value)
if (isValid) { if (isValid.value) {
onNewCorrectValue(newText) onNewCorrectValue(newText)
} }
}, },
singleLine = singleLine, singleLine = singleLine,
label = { Text(text = stringResource(id = label)) }, label = { Text(text = stringResource(id = label)) },
isError = !isValid, isError = !isValid.value && !isFirst.value,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
keyboardType = keyboardType, keyboardType = keyboardType,
imeAction = ImeAction.Done imeAction = ImeAction.Done
) )
) )
if (!isValid) { if (!isValid.value && !isFirst.value) {
Text( Text(
modifier = Modifier.padding(start = 16.dp), modifier = Modifier.padding(start = 16.dp),
text = stringResource(id = errorText), text = stringResource(id = errorText),
@ -218,4 +216,36 @@ private fun PasswordField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
visualTransformation = visualTransformation visualTransformation = visualTransformation
) )
}
@Composable
fun SearchField(
value: String,
onValueChange: (String) -> Unit,
onSubmit: () -> Unit,
@StringRes label: Int,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = { Text(text = stringResource(id = label)) },
trailingIcon = {
IconButton(onClick = onSubmit) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(label),
tint = MaterialTheme.colors.primary
)
}
},
singleLine = true,
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = MaterialTheme.colors.onBackground,
backgroundColor = MaterialTheme.colors.background
)
)
} }

View file

@ -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.Medium
)
}
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 {} }, {}
)
}

View file

@ -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.Medium,
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,
)
)
}

View file

@ -0,0 +1,8 @@
package be.ugent.sel.studeez.common.composable.feed
import be.ugent.sel.studeez.data.local.models.FeedEntry
sealed interface FeedUiState {
object Loading : FeedUiState
data class Succes(val feedEntries: Map<String, List<FeedEntry>>) : FeedUiState
}

View file

@ -0,0 +1,45 @@
package be.ugent.sel.studeez.common.composable.feed
import androidx.lifecycle.viewModelScope
import be.ugent.sel.studeez.data.SelectedTask
import be.ugent.sel.studeez.domain.FeedDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.TaskDAO
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class FeedViewModel @Inject constructor(
feedDAO: FeedDAO,
private val taskDAO: TaskDAO,
private val selectedTask: SelectedTask,
logService: LogService
) : StudeezViewModel(logService) {
val uiState: StateFlow<FeedUiState> = feedDAO.getFeedEntries()
.map { FeedUiState.Succes(it) }
.stateIn(
scope = viewModelScope,
initialValue = FeedUiState.Loading,
started = SharingStarted.Eagerly,
)
fun continueTask(open: (String) -> Unit, subjectId: String, taskId: String) {
viewModelScope.launch {
val task = taskDAO.getTask(subjectId, taskId)
selectedTask.set(task)
open(StudeezDestinations.TIMER_SELECTION_SCREEN)
}
}
fun onEmptyFeedHelp(open: (String) -> Unit) {
open(StudeezDestinations.ADD_SUBJECT_FORM)
}
}

View file

@ -8,13 +8,15 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.DateRange import androidx.compose.material.icons.outlined.DateRange
import androidx.compose.material.icons.outlined.Face
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.navigation.StudeezDestinations.FRIENDS_FEED
import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN 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.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.navigation.StudeezDestinations.SUBJECT_SCREEN
import be.ugent.sel.studeez.resources import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.ui.theme.StudeezTheme import be.ugent.sel.studeez.ui.theme.StudeezTheme
@ -99,11 +101,11 @@ fun NavigationBar(
BottomNavigationItem( BottomNavigationItem(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Outlined.DateRange, resources().getString(AppText.sessions) imageVector = Icons.Outlined.Face, resources().getString(AppText.friends_feed)
) )
}, },
label = { Text(text = resources().getString(AppText.sessions)) }, label = { Text(text = resources().getString(AppText.friends_feed)) },
selected = navigationBarActions.isSelectedTab(SESSIONS_SCREEN), selected = navigationBarActions.isSelectedTab(FRIENDS_FEED),
onClick = navigationBarActions.onSessionsClick onClick = navigationBarActions.onSessionsClick
) )

View file

@ -2,9 +2,11 @@ package be.ugent.sel.studeez.common.composable.navbar
import be.ugent.sel.studeez.common.snackbar.SnackbarManager import be.ugent.sel.studeez.common.snackbar.SnackbarManager
import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.navigation.StudeezDestinations.FRIENDS_FEED
import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN 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.PROFILE_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.SESSIONS_SCREEN import be.ugent.sel.studeez.navigation.StudeezDestinations.SEARCH_FRIENDS_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.SELECT_SUBJECT
import be.ugent.sel.studeez.navigation.StudeezDestinations.SUBJECT_SCREEN import be.ugent.sel.studeez.navigation.StudeezDestinations.SUBJECT_SCREEN
import be.ugent.sel.studeez.screens.StudeezViewModel import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -25,7 +27,7 @@ class NavigationBarViewModel @Inject constructor(
} }
fun onSessionsClick(open: (String) -> Unit) { fun onSessionsClick(open: (String) -> Unit) {
open(SESSIONS_SCREEN) open(FRIENDS_FEED)
} }
fun onProfileClick(open: (String) -> Unit) { fun onProfileClick(open: (String) -> Unit) {
@ -33,13 +35,11 @@ class NavigationBarViewModel @Inject constructor(
} }
fun onAddTaskClick(open: (String) -> Unit) { fun onAddTaskClick(open: (String) -> Unit) {
// TODO open(CREATE_TASK_SCREEN) open(SELECT_SUBJECT)
SnackbarManager.showMessage(AppText.create_task_not_possible_yet) // TODO Remove
} }
fun onAddFriendClick(open: (String) -> Unit) { fun onAddFriendClick(open: (String) -> Unit) {
// TODO open(SEARCH_FRIENDS_SCREEN) open(SEARCH_FRIENDS_SCREEN)
SnackbarManager.showMessage(AppText.add_friend_not_possible_yet) // TODO Remove
} }
fun onAddSessionClick(open: (String) -> Unit) { fun onAddSessionClick(open: (String) -> Unit) {

View file

@ -1,20 +1,17 @@
package be.ugent.sel.studeez.common.composable.tasks package be.ugent.sel.studeez.common.composable.tasks
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
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.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.List
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -24,16 +21,24 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.R.string as AppText
import be.ugent.sel.studeez.common.composable.StealthButton 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.task.Subject
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds 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 @Composable
fun SubjectEntry( fun SubjectEntry(
subject: Subject, subject: Subject,
onViewSubject: () -> Unit, getTaskCount: () -> Flow<Int>,
getCompletedTaskCount: () -> Flow<Int>,
getStudyTime: () -> Flow<Int>,
selectButton: @Composable (RowScope) -> Unit,
) { ) {
val studytime by getStudyTime().collectAsState(initial = 0)
val taskCount by getTaskCount().collectAsState(initial = 0)
val completedTaskCount by getCompletedTaskCount().collectAsState(initial = 0)
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -61,16 +66,17 @@ fun SubjectEntry(
) { ) {
Text( Text(
text = subject.name, text = subject.name,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
fontWeight = FontWeight.Medium
) )
Row( Row(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = HoursMinutesSeconds(subject.time).toString(), text = HoursMinutesSeconds(studytime).toString(),
color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f)
) )
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -78,21 +84,18 @@ fun SubjectEntry(
) { ) {
Icon( Icon(
imageVector = Icons.Default.List, imageVector = Icons.Default.List,
contentDescription = stringResource(id = AppText.tasks) contentDescription = stringResource(id = AppText.tasks),
tint = MaterialTheme.colors.onBackground.copy(alpha = 0.6f)
)
Text(
text = "${completedTaskCount}/${taskCount}",
color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f)
) )
Text(text = "0/0") // TODO
} }
} }
} }
} }
StealthButton( selectButton(this)
text = AppText.view_tasks,
modifier = Modifier
.padding(start = 10.dp, end = 5.dp)
.weight(1f)
) {
onViewSubject()
}
} }
} }
} }
@ -104,9 +107,17 @@ fun SubjectEntryPreview() {
subject = Subject( subject = Subject(
name = "Test Subject", name = "Test Subject",
argb_color = 0xFFFFD200, argb_color = 0xFFFFD200,
time = 60
), ),
) {} getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
) {
StealthButton(
text = AppText.view_tasks,
modifier = Modifier
.padding(start = 10.dp, end = 5.dp)
) {}
}
} }
@Preview @Preview
@ -116,7 +127,9 @@ fun OverflowSubjectEntryPreview() {
subject = Subject( subject = Subject(
name = "Testttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt", name = "Testttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt",
argb_color = 0xFFFFD200, argb_color = 0xFFFFD200,
time = 60
), ),
getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
) {} ) {}
} }

View file

@ -1,17 +1,7 @@
package be.ugent.sel.studeez.common.composable.tasks package be.ugent.sel.studeez.common.composable.tasks
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box import androidx.compose.material.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.Checkbox
import androidx.compose.material.CheckboxDefaults
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -31,7 +21,8 @@ import be.ugent.sel.studeez.resources
fun TaskEntry( fun TaskEntry(
task: Task, task: Task,
onCheckTask: (Boolean) -> Unit, onCheckTask: (Boolean) -> Unit,
onDeleteTask: () -> Unit, onArchiveTask: () -> Unit,
onStartTask: () -> Unit
) { ) {
Card( Card(
modifier = Modifier modifier = Modifier
@ -80,7 +71,7 @@ fun TaskEntry(
Box(modifier = Modifier.weight(7f)) { Box(modifier = Modifier.weight(7f)) {
if (task.completed) { if (task.completed) {
IconButton( IconButton(
onClick = onDeleteTask, onClick = onArchiveTask,
modifier = Modifier modifier = Modifier
.padding(start = 20.dp) .padding(start = 20.dp)
) { ) {
@ -95,6 +86,7 @@ fun TaskEntry(
modifier = Modifier modifier = Modifier
.padding(end = 5.dp), .padding(end = 5.dp),
) { ) {
onStartTask()
} }
} }
} }
@ -110,7 +102,7 @@ fun TaskEntryPreview() {
name = "Test Task", name = "Test Task",
completed = false, completed = false,
), ),
{}, {}, {}, {}, {}
) )
} }
@ -122,7 +114,7 @@ fun CompletedTaskEntryPreview() {
name = "Test Task", name = "Test Task",
completed = true, completed = true,
), ),
{}, {}, {}, {}, {},
) )
} }
@ -134,6 +126,6 @@ fun OverflowTaskEntryPreview() {
name = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkkkk", name = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkkkk",
completed = false, completed = false,
), ),
{}, {}, {}, {}, {}
) )
} }

View file

@ -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
}

View file

@ -1,11 +0,0 @@
package be.ugent.sel.studeez.data
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
@Singleton
class EditTimerState @Inject constructor(){
lateinit var timerInfo: TimerInfo
}

View file

@ -0,0 +1,53 @@
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 be.ugent.sel.studeez.domain.UserDAO
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to cummunicate between viewmodels.
*/
abstract class SelectedState<T> {
abstract var value: T
operator fun invoke() = value
fun set(newValue: T) {
this.value = newValue
}
}
@Singleton
class SelectedSessionReport @Inject constructor() : SelectedState<SessionReport>() {
override lateinit var value: SessionReport
}
@Singleton
class SelectedTask @Inject constructor() : SelectedState<Task>() {
override lateinit var value: Task
}
@Singleton
class SelectedTimer @Inject constructor() : SelectedState<FunctionalTimer>() {
override lateinit var value: FunctionalTimer
}
@Singleton
class SelectedSubject @Inject constructor() : SelectedState<Subject>() {
override lateinit var value: Subject
}
@Singleton
class SelectedTimerInfo @Inject constructor() : SelectedState<TimerInfo>() {
override lateinit var value: TimerInfo
}
@Singleton
class SelectedUserId @Inject constructor(
userDAO: UserDAO
): SelectedState<String>() {
override var value: String = userDAO.getCurrentUserId()
}

View file

@ -1,20 +0,0 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.task.Subject
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to communicate the selected subject from the subject overview other screens.
* Because this is a singleton-class the view-models of both screens observe the same data.
*/
@Singleton
class SelectedSubject @Inject constructor() {
private lateinit var subject: Subject
operator fun invoke() = subject
fun set(subject: Subject) {
this.subject = subject
}
fun isSet() = this::subject.isInitialized
}

View file

@ -1,21 +0,0 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.task.Task
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to communicate the selected task from the task overview other screens.
* Because this is a singleton-class the view-models of both screens observe the same data.
*/
@Singleton
class SelectedTask @Inject constructor() {
private lateinit var task: Task
operator fun invoke() = task
fun set(task: Task) {
this.task = task
}
fun isSet() = this::task.isInitialized
}

View file

@ -1,14 +0,0 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to communicate the SelectedTimer from the selection screen to the session screen.
* Because this is a singleton-class the view-models of both screens observe the same data.
*/
@Singleton
class SelectedTimerState @Inject constructor(){
var selectedTimer: FunctionalTimer? = null
}

View file

@ -1,14 +0,0 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.SessionReport
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to communicate the SelectedTimer from the selection screen to the session screen.
* Because this is a singleton-class the view-models of both screens observe the same data.
*/
@Singleton
class SessionReportState @Inject constructor(){
var sessionReport: SessionReport? = null
}

View file

@ -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
)

View file

@ -0,0 +1,11 @@
package be.ugent.sel.studeez.data.local.models
import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentId
data class Friendship(
@DocumentId val id: String = "",
val friendId: String = "",
val friendsSince: Timestamp = Timestamp.now(),
val accepted: Boolean = false
)

View file

@ -6,5 +6,7 @@ import com.google.firebase.firestore.DocumentId
data class SessionReport( data class SessionReport(
@DocumentId val id: String = "", @DocumentId val id: String = "",
val studyTime: Int = 0, val studyTime: Int = 0,
val endTime: Timestamp = Timestamp(0, 0) val endTime: Timestamp = Timestamp(0, 0),
val taskId: String = "",
val subjectId: String = ""
) )

View file

@ -1,3 +1,9 @@
package be.ugent.sel.studeez.data.local.models package be.ugent.sel.studeez.data.local.models
data class User(val id: String = "") import com.google.firebase.firestore.DocumentId
data class User(
@DocumentId val id: String = "",
val username: String = "",
val biography: String = ""
)

View file

@ -5,6 +5,13 @@ import com.google.firebase.firestore.DocumentId
data class Subject( data class Subject(
@DocumentId val id: String = "", @DocumentId val id: String = "",
val name: String = "", val name: String = "",
val time: Int = 0,
val argb_color: Long = 0, 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"
}

View file

@ -5,9 +5,10 @@ import com.google.firebase.firestore.DocumentId
data class Task( data class Task(
@DocumentId val id: String = "", @DocumentId val id: String = "",
val name: String = "", val name: String = "",
val completed: Boolean = false, var completed: Boolean = false,
val time: Int = 0, val time: Int = 0,
val subjectId: String = "", val subjectId: String = "",
var archived: Boolean = false,
) )
object TaskDocument { object TaskDocument {
@ -16,4 +17,5 @@ object TaskDocument {
const val completed = "completed" const val completed = "completed"
const val time = "time" const val time = "time"
const val subjectId = "subjectId" const val subjectId = "subjectId"
const val archived = "archived"
} }

View file

@ -2,17 +2,17 @@ package be.ugent.sel.studeez.data.local.models.timer_functional
class FunctionalPomodoroTimer( class FunctionalPomodoroTimer(
private var studyTime: Int, private var studyTime: Int,
private var breakTime: Int, repeats: Int private var breakTime: Int,
val repeats: Int
) : FunctionalTimer(studyTime) { ) : FunctionalTimer(studyTime) {
var breaksRemaining = repeats var breaksRemaining = repeats - 1
var isInBreak = false var isInBreak = false
override fun tick() { override fun tick() {
if (hasEnded()) { if (hasEnded()) {
return return
} }
if (hasCurrentCountdownEnded()) { if (hasCurrentCountdownEnded()) {
if (isInBreak) { if (isInBreak) {
breaksRemaining-- breaksRemaining--

View file

@ -17,10 +17,12 @@ abstract class FunctionalTimer(initialValue: Int) {
abstract fun hasCurrentCountdownEnded(): Boolean abstract fun hasCurrentCountdownEnded(): Boolean
fun getSessionReport(): SessionReport { fun getSessionReport(subjectId: String, taskId: String): SessionReport {
return SessionReport( return SessionReport(
studyTime = totalStudyTime, studyTime = totalStudyTime,
endTime = Timestamp.now() endTime = Timestamp.now(),
taskId = taskId,
subjectId = subjectId
) )
} }

View file

@ -0,0 +1,7 @@
package be.ugent.sel.studeez.data.remote
object FirebaseFriendship {
const val FRIENDID: String = "friendId"
const val ACCEPTED: String = "accepted"
const val FRIENDSSINCE: String = "friendsSince"
}

View file

@ -0,0 +1,6 @@
package be.ugent.sel.studeez.data.remote
object FirebaseSessionReport {
const val STUDYTIME: String = "studyTime"
const val ENDTIME: String = "endTime"
}

View file

@ -0,0 +1,6 @@
package be.ugent.sel.studeez.data.remote
object FirebaseUser {
const val USERNAME: String = "username"
const val BIOGRAPHY: String = "biography"
}

View file

@ -16,6 +16,9 @@ abstract class DatabaseModule {
@Binds @Binds
abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO
@Binds
abstract fun provideFriendshipDAO(impl: FirebaseFriendshipDAO): FriendshipDAO
@Binds @Binds
abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO
@ -26,11 +29,14 @@ abstract class DatabaseModule {
abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService
@Binds @Binds
abstract fun provideSessionDAO(impl: FireBaseSessionDAO): SessionDAO abstract fun provideSessionDAO(impl: FirebaseSessionDAO): SessionDAO
@Binds @Binds
abstract fun provideSubjectDAO(impl: FireBaseSubjectDAO): SubjectDAO abstract fun provideSubjectDAO(impl: FirebaseSubjectDAO): SubjectDAO
@Binds @Binds
abstract fun provideTaskDAO(impl: FireBaseTaskDAO): TaskDAO abstract fun provideTaskDAO(impl: FirebaseTaskDAO): TaskDAO
@Binds
abstract fun provideFeedDAO(impl: FirebaseFeedDAO): FeedDAO
} }

View file

@ -0,0 +1,13 @@
package be.ugent.sel.studeez.domain
import be.ugent.sel.studeez.data.local.models.FeedEntry
import kotlinx.coroutines.flow.Flow
interface FeedDAO {
fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>>
suspend fun getFeedEntriesFromUser(id: String): Map<String, List<FeedEntry>>
fun getFriendsSessions(): Flow<Map<String, List<Pair<String, FeedEntry>>>>
}

View file

@ -0,0 +1,54 @@
package be.ugent.sel.studeez.domain
import be.ugent.sel.studeez.data.local.models.Friendship
import kotlinx.coroutines.flow.Flow
/**
* Should be used for interactions between friends.
*/
interface FriendshipDAO {
/**
* @return all friendships of a chosen user.
*/
fun getAllFriendships(
userId: String
): Flow<List<Friendship>>
/**
* @return the amount of friends of a chosen user.
* This method should be faster than just counting the length of getAllFriends()
*/
fun getFriendshipCount(
userId: String
): Flow<Int>
/**
* @param id the id of the friendship that you want details of
* @return the details of a Friendship
*/
fun getFriendshipDetails(id: String): Friendship
/**
* Send a friend request to a user.
* @param id of the user that you want to add as a friend
* @return Success/faillure of transaction
*/
fun sendFriendshipRequest(id: String): Boolean
/**
* Accept a friend request that has already been sent.
* @param id of the friendship that you want to update
* @return: Success/faillure of transaction
*/
fun acceptFriendship(id: String): Boolean
/**
* Remove a friend or decline a friendrequest.
* @param friendship the one you want to remove
* @return: Success/faillure of transaction
*/
fun removeFriendship(
friendship: Friendship
): Boolean
}

View file

@ -1,12 +1,16 @@
package be.ugent.sel.studeez.domain package be.ugent.sel.studeez.domain
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.SessionReport import be.ugent.sel.studeez.data.local.models.SessionReport
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SessionDAO { interface SessionDAO {
fun getSessions(): Flow<List<SessionReport>> fun getSessions(): Flow<List<SessionReport>>
suspend fun getSessionsOfUser(userId: String): List<SessionReport>
fun saveSession(newSessionReport: SessionReport) fun saveSession(newSessionReport: SessionReport)

View file

@ -12,4 +12,13 @@ interface SubjectDAO {
fun deleteSubject(oldSubject: Subject) fun deleteSubject(oldSubject: Subject)
fun updateSubject(newSubject: Subject) fun updateSubject(newSubject: Subject)
suspend fun archiveSubject(subject: Subject)
fun getTaskCount(subject: Subject): Flow<Int>
fun getCompletedTaskCount(subject: Subject): Flow<Int>
fun getStudyTime(subject: Subject): Flow<Int>
suspend fun getSubject(subjectId: String): Subject?
suspend fun getSubjectOfUSer(subjectId: String, userId: String): Subject
} }

View file

@ -14,5 +14,7 @@ interface TaskDAO {
fun deleteTask(oldTask: Task) fun deleteTask(oldTask: Task)
fun toggleTaskCompleted(task: Task, completed: Boolean) suspend fun getTask(subjectId: String, taskId: String): Task
suspend fun getTaskFromUser(subjectId: String, taskId: String, userId: String): Task
} }

View file

@ -1,13 +1,52 @@
package be.ugent.sel.studeez.domain package be.ugent.sel.studeez.domain
import be.ugent.sel.studeez.data.local.models.User
import kotlinx.coroutines.flow.Flow
interface UserDAO { interface UserDAO {
suspend fun getUsername(): String? fun getCurrentUserId(): String
suspend fun save(newUsername: String)
/** /**
* Delete all references to this user in the database. Similar to the deleteCascade in * @return all users
*/
fun getAllUsers(): Flow<List<User>>
/**
* @return all users based on a query, a trimmed down version of getAllUsers()
*/
fun getUsersWithQuery(
fieldName: String,
value: String
): Flow<List<User>>
/**
* Request information about a user
*/
fun getUserDetails(
userId: String
): Flow<User>
suspend fun getUsername(
userId: String
): String
/**
* @return information on the currently logged in user.
*/
suspend fun getLoggedInUser(): User
// TODO Should be refactored to fun getLoggedInUser(): Flow<User>, without suspend.
suspend fun saveLoggedInUser(
newUsername: String,
newBiography: String = ""
)
// TODO Should be refactored to fun saveLoggedInUser(...): Boolean, without suspend.
/**
* Delete all references to the logged in user in the database. Similar to the deleteCascade in
* relational databases. * relational databases.
*/ */
suspend fun deleteUserReferences() suspend fun deleteLoggedInUserReferences()
// TODO Should be refactored to fun deleteLoggedInUserReferences(): Boolean, without suspend.
} }

View file

@ -1,37 +0,0 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.data.local.models.SessionReport
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.SessionDAO
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.snapshots
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class FireBaseSessionDAO @Inject constructor(
private val firestore: FirebaseFirestore,
private val auth: AccountDAO
) : SessionDAO {
override fun getSessions(): Flow<List<SessionReport>> {
return currentUserSessionsCollection()
.snapshots()
.map { it.toObjects(SessionReport::class.java) }
}
override fun saveSession(newSessionReport: SessionReport) {
currentUserSessionsCollection().add(newSessionReport)
}
override fun deleteSession(newTimer: TimerInfo) {
currentUserSessionsCollection().document(newTimer.id).delete()
}
private fun currentUserSessionsCollection(): CollectionReference =
firestore.collection(FireBaseCollections.USER_COLLECTION)
.document(auth.currentUserId)
.collection(FireBaseCollections.SESSION_COLLECTION)
}

View file

@ -1,39 +0,0 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.SubjectDAO
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.snapshots
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class FireBaseSubjectDAO @Inject constructor(
private val firestore: FirebaseFirestore,
private val auth: AccountDAO,
) : SubjectDAO {
override fun getSubjects(): Flow<List<Subject>> {
return currentUserSubjectsCollection()
.snapshots()
.map { it.toObjects(Subject::class.java) }
}
override fun saveSubject(newSubject: Subject) {
currentUserSubjectsCollection().add(newSubject)
}
override fun deleteSubject(oldSubject: Subject) {
currentUserSubjectsCollection().document(oldSubject.id).delete()
}
override fun updateSubject(newSubject: Subject) {
currentUserSubjectsCollection().document(newSubject.id).set(newSubject)
}
private fun currentUserSubjectsCollection(): CollectionReference =
firestore.collection(FireBaseCollections.USER_COLLECTION)
.document(auth.currentUserId)
.collection(FireBaseCollections.SUBJECT_COLLECTION)
}

View file

@ -1,49 +0,0 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.data.local.models.task.TaskDocument
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.TaskDAO
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.snapshots
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class FireBaseTaskDAO @Inject constructor(
private val firestore: FirebaseFirestore,
private val auth: AccountDAO,
) : TaskDAO {
override fun getTasks(subject: Subject): Flow<List<Task>> {
return selectedSubjectTasksCollection(subject.id)
.snapshots()
.map { it.toObjects(Task::class.java) }
}
override fun saveTask(newTask: Task) {
selectedSubjectTasksCollection(newTask.subjectId).add(newTask)
}
override fun updateTask(newTask: Task) {
selectedSubjectTasksCollection(newTask.id).document(newTask.id).set(newTask)
}
override fun deleteTask(oldTask: Task) {
selectedSubjectTasksCollection(oldTask.subjectId).document(oldTask.id).delete()
}
override fun toggleTaskCompleted(task: Task, completed: Boolean) {
selectedSubjectTasksCollection(task.subjectId)
.document(task.id)
.update(TaskDocument.completed, completed)
}
private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference =
firestore.collection(FireBaseCollections.USER_COLLECTION)
.document(auth.currentUserId)
.collection(FireBaseCollections.SUBJECT_COLLECTION)
.document(subjectId)
.collection(FireBaseCollections.TASK_COLLECTION)
}

View file

@ -1,8 +1,9 @@
package be.ugent.sel.studeez.domain.implementation package be.ugent.sel.studeez.domain.implementation
object FireBaseCollections { object FirebaseCollections {
const val SESSION_COLLECTION = "sessions" const val SESSION_COLLECTION = "sessions"
const val USER_COLLECTION = "users" const val USER_COLLECTION = "users"
const val FRIENDS_COLLECTION = "friends"
const val TIMER_COLLECTION = "timers" const val TIMER_COLLECTION = "timers"
const val SUBJECT_COLLECTION = "subjects" const val SUBJECT_COLLECTION = "subjects"
const val TASK_COLLECTION = "tasks" const val TASK_COLLECTION = "tasks"

View file

@ -0,0 +1,137 @@
package be.ugent.sel.studeez.domain.implementation
import android.icu.text.DateFormat
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.SessionReport
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.domain.*
import com.google.firebase.Timestamp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class FirebaseFeedDAO @Inject constructor(
private val friendshipDAO: FriendshipDAO,
private val sessionDAO: SessionDAO,
private val taskDAO: TaskDAO,
private val subjectDAO: SubjectDAO,
private val auth: AccountDAO,
private val userDAO: UserDAO,
) : FeedDAO {
/**
* Return a map as with key the day and value a list of feedentries for that day.
*/
override fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>> {
return sessionDAO.getSessions().map { sessionReports ->
sessionReports
.map { sessionReport -> sessionToFeedEntry(sessionReport) }
.sortedByDescending { it.endTime }
.groupBy { getFormattedTime(it) }
.mapValues { (_, entries) ->
entries
.groupBy { it.taskId }
.map { fuseFeedEntries(it.component2()) }
}
}
}
/**
* Return a map as with key the day and value a list of feedentries for that day.
*/
override suspend fun getFeedEntriesFromUser(id: String): Map<String, List<FeedEntry>> {
return sessionDAO.getSessionsOfUser(id)
.map { sessionReport -> sessionToFeedEntryFromUser(sessionReport, id) }
.sortedByDescending { it.endTime }
.groupBy { getFormattedTime(it) }
.mapValues { (_, entries) ->
entries
.groupBy { it.taskId }
.map { fuseFeedEntries(it.component2()) }
}
}
override fun getFriendsSessions(): Flow<Map<String, List<Pair<String, FeedEntry>>>> {
return friendshipDAO.getAllFriendships(auth.currentUserId)
.map { friendships ->
friendships.map { friendship ->
val userId: String = friendship.friendId
val username = userDAO.getUsername(userId)
val friendFeed = getFeedEntriesFromUser(userId)
Pair(username, friendFeed)
}
}.map {
mergeNameAndEntries(it)
}
}
private fun mergeNameAndEntries(l: List<Pair<String, Map<String, List<FeedEntry>>>>): Map<String, List<Pair<String, FeedEntry>>> {
val new: MutableMap<String, List<Pair<String, FeedEntry>>> = mutableMapOf()
for ((name, map) in l) {
for ((day, feedEntries: List<FeedEntry>) in map) {
new[day] = new.getOrDefault(day, listOf()) + feedEntries.map { Pair(name, it) }
}
}
return new
}
private fun getFormattedTime(entry: FeedEntry): String {
return DateFormat.getDateInstance().format(entry.endTime.toDate())
}
/**
* Givin a list of entries referencing the same task, in the same day, fuse them into one
* feed-entry by adding the studytime and keeping the most recent end-timestamp
*/
private fun fuseFeedEntries(entries: List<FeedEntry>): FeedEntry =
entries.drop(1).fold(entries[0]) { accEntry, newEntry ->
accEntry.copy(
totalStudyTime = accEntry.totalStudyTime + newEntry.totalStudyTime,
endTime = getMostRecent(accEntry.endTime, newEntry.endTime)
)
}
private fun getMostRecent(t1: Timestamp, t2: Timestamp): Timestamp {
return if (t1 < t2) t2 else t1
}
/**
* Convert a sessionReport to a feedEntry. Fetch Task and Subject to get names
*/
private suspend fun sessionToFeedEntry(sessionReport: SessionReport): FeedEntry {
val subjectId: String = sessionReport.subjectId
val taskId: String = sessionReport.taskId
val task: Task = taskDAO.getTask(subjectId, taskId)
val subject: Subject = subjectDAO.getSubject(subjectId)!!
return makeFeedEntry(sessionReport, subject, task)
}
private fun makeFeedEntry(sessionReport: SessionReport, subject: Subject, task: Task): FeedEntry {
return FeedEntry(
argb_color = subject.argb_color,
subJectName = subject.name,
taskName = task.name,
taskId = task.id,
subjectId = subject.id,
totalStudyTime = sessionReport.studyTime,
endTime = sessionReport.endTime,
isArchived = task.archived || subject.archived
)
}
/**
* Convert a sessionReport to a feedEntry. Fetch Task and Subject to get names
*/
private suspend fun sessionToFeedEntryFromUser(sessionReport: SessionReport, id: String): FeedEntry {
val subjectId: String = sessionReport.subjectId
val taskId: String = sessionReport.taskId
val task: Task = taskDAO.getTaskFromUser(subjectId, taskId, id)
val subject: Subject = subjectDAO.getSubjectOfUSer(subjectId, id)
return makeFeedEntry(sessionReport, subject, task)
}
}

View file

@ -0,0 +1,150 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
import be.ugent.sel.studeez.data.local.models.Friendship
import be.ugent.sel.studeez.data.remote.FirebaseFriendship.ACCEPTED
import be.ugent.sel.studeez.data.remote.FirebaseFriendship.FRIENDID
import be.ugent.sel.studeez.data.remote.FirebaseFriendship.FRIENDSSINCE
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.FriendshipDAO
import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.FRIENDS_COLLECTION
import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION
import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.snapshots
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import be.ugent.sel.studeez.R.string as AppText
class FirebaseFriendshipDAO @Inject constructor(
private val firestore: FirebaseFirestore,
private val auth: AccountDAO
): FriendshipDAO {
private fun currentUserDocument(): DocumentReference = firestore
.collection(USER_COLLECTION)
.document(auth.currentUserId)
override fun getAllFriendships(
userId: String
): Flow<List<Friendship>> {
return firestore
.collection(USER_COLLECTION)
.document(userId)
.collection(FRIENDS_COLLECTION)
.snapshots()
.map { it.toObjects(Friendship::class.java) }
}
override fun getFriendshipCount(
userId: String
): Flow<Int> {
return flow {
val friendshipCount = suspendCoroutine { continuation ->
firestore
.collection(USER_COLLECTION)
.document(userId)
.collection(FRIENDS_COLLECTION)
.get()
.addOnSuccessListener { querySnapshot ->
continuation.resume(querySnapshot.size())
}
.addOnFailureListener { exception ->
continuation.resumeWithException(exception)
}
}
emit(friendshipCount)
}.catch {
SnackbarManager.showMessage(AppText.generic_error)
}
}
override fun getFriendshipDetails(id: String): Friendship {
TODO("Not yet implemented")
}
override fun sendFriendshipRequest(id: String): Boolean {
val currentUserId: String = auth.currentUserId
val otherUserId: String = id
// Check if the friendship already exists for the logged in user
var allowed = false
firestore.collection(USER_COLLECTION)
.document(currentUserId)
.collection(FRIENDS_COLLECTION)
.whereEqualTo(FRIENDID, otherUserId)
.get()
.addOnSuccessListener {
allowed = it.documents.isEmpty()
if (allowed) {
// Add entry to current user
currentUserDocument()
.collection(FRIENDS_COLLECTION)
.add(mapOf(
FRIENDID to otherUserId,
ACCEPTED to true, // TODO Make it not automatically accepted.
FRIENDSSINCE to Timestamp.now()
))
// Add entry to other user
firestore.collection(USER_COLLECTION)
.document(otherUserId)
.collection(FRIENDS_COLLECTION)
.add(mapOf(
FRIENDID to currentUserId,
ACCEPTED to true, // TODO Make it not automatically accepted.
FRIENDSSINCE to Timestamp.now()
))
}
}.addOnSuccessListener {
val message = if (allowed) AppText.success else AppText.already_friend
SnackbarManager.showMessage(message)
}
return true
}
override fun acceptFriendship(id: String): Boolean {
TODO("Not yet implemented")
}
override fun removeFriendship(
friendship: Friendship
): Boolean {
val currentUserId: String = auth.currentUserId
val otherUserId: String = friendship.friendId
// Remove at logged in user
firestore.collection(USER_COLLECTION)
.document(currentUserId)
.collection(FRIENDS_COLLECTION)
.document(friendship.id)
.delete()
// Remove at other user
firestore.collection(USER_COLLECTION)
.document(otherUserId)
.collection(FRIENDS_COLLECTION)
.whereEqualTo(FRIENDID, currentUserId)
.get()
.addOnSuccessListener {
for (document in it) {
document.reference.delete()
}
}.addOnFailureListener {
SnackbarManager.showMessage(AppText.generic_error)
}
return true
}
}

View file

@ -0,0 +1,58 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.SessionReport
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import be.ugent.sel.studeez.data.remote.FirebaseSessionReport
import be.ugent.sel.studeez.data.remote.FirebaseSessionReport.ENDTIME
import be.ugent.sel.studeez.data.remote.FirebaseSessionReport.STUDYTIME
import be.ugent.sel.studeez.domain.*
import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.SESSION_COLLECTION
import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION
import com.google.firebase.Timestamp
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.getField
import com.google.firebase.firestore.ktx.snapshots
import com.google.firebase.firestore.ktx.toObject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
class FirebaseSessionDAO @Inject constructor(
private val firestore: FirebaseFirestore,
private val auth: AccountDAO,
) : SessionDAO {
override fun getSessions(): Flow<List<SessionReport>> {
return currentUserSessionsCollection()
.snapshots()
.map { it.toObjects(SessionReport::class.java) }
}
override suspend fun getSessionsOfUser(userId: String): List<SessionReport> {
return firestore.collection(USER_COLLECTION)
.document(userId)
.collection(SESSION_COLLECTION)
.get().await()
.map { it.toObject(SessionReport::class.java) }
}
override fun saveSession(newSessionReport: SessionReport) {
currentUserSessionsCollection().add(newSessionReport)
}
override fun deleteSession(newTimer: TimerInfo) {
currentUserSessionsCollection().document(newTimer.id).delete()
}
private fun currentUserSessionsCollection(): CollectionReference =
firestore.collection(USER_COLLECTION)
.document(auth.currentUserId)
.collection(SESSION_COLLECTION)
}

View file

@ -0,0 +1,97 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.data.local.models.task.SubjectDocument
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.data.local.models.task.TaskDocument
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.SubjectDAO
import be.ugent.sel.studeez.domain.TaskDAO
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.ktx.snapshots
import com.google.firebase.firestore.ktx.toObject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import kotlin.collections.count
class FirebaseSubjectDAO @Inject constructor(
private val firestore: FirebaseFirestore,
private val auth: AccountDAO,
private val taskDAO: TaskDAO,
) : SubjectDAO {
override fun getSubjects(): Flow<List<Subject>> {
return currentUserSubjectsCollection()
.subjectNotArchived()
.snapshots()
.map { it.toObjects(Subject::class.java) }
}
override suspend fun getSubject(subjectId: String): Subject? {
return currentUserSubjectsCollection().document(subjectId).get().await().toObject()
}
override suspend fun getSubjectOfUSer(subjectId: String, userId: String): Subject {
return currentUserSubjectsCollection(userId).document(subjectId).get().await().toObject()!!
}
override fun saveSubject(newSubject: Subject) {
currentUserSubjectsCollection().add(newSubject)
}
override fun deleteSubject(oldSubject: Subject) {
currentUserSubjectsCollection().document(oldSubject.id).delete()
}
override fun updateSubject(newSubject: Subject) {
currentUserSubjectsCollection().document(newSubject.id).set(newSubject)
}
override suspend fun archiveSubject(subject: Subject) {
currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true)
currentUserSubjectsCollection().document(subject.id)
.collection(FirebaseCollections.TASK_COLLECTION)
.taskNotArchived()
.get().await()
.documents
.forEach {
it.reference.update(TaskDocument.archived, true)
}
}
override fun getTaskCount(subject: Subject): Flow<Int> {
return taskDAO.getTasks(subject)
.map(List<Task>::count)
}
override fun getCompletedTaskCount(subject: Subject): Flow<Int> {
return taskDAO.getTasks(subject)
.map { tasks -> tasks.count { it.completed && !it.archived } }
}
override fun getStudyTime(subject: Subject): Flow<Int> {
return taskDAO.getTasks(subject)
.map { tasks -> tasks.sumOf { it.time } }
}
private fun currentUserSubjectsCollection(id: String = auth.currentUserId): CollectionReference =
firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(id)
.collection(FirebaseCollections.SUBJECT_COLLECTION)
private fun subjectTasksCollection(subject: Subject, id: String = auth.currentUserId): CollectionReference =
firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(id)
.collection(FirebaseCollections.SUBJECT_COLLECTION)
.document(subject.id)
.collection(FirebaseCollections.TASK_COLLECTION)
fun CollectionReference.subjectNotArchived(): Query =
this.whereEqualTo(SubjectDocument.archived, false)
fun Query.subjectNotArchived(): Query =
this.whereEqualTo(SubjectDocument.archived, false)
}

View file

@ -0,0 +1,74 @@
package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.data.local.models.task.TaskDocument
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.TaskDAO
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.ktx.snapshots
import com.google.firebase.firestore.ktx.toObject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
class FirebaseTaskDAO @Inject constructor(
private val firestore: FirebaseFirestore,
private val auth: AccountDAO,
) : TaskDAO {
override fun getTasks(subject: Subject): Flow<List<Task>> {
return selectedSubjectTasksCollection(subject.id)
.taskNotArchived()
.snapshots()
.map { it.toObjects(Task::class.java) }
}
override suspend fun getTask(subjectId: String, taskId: String): Task {
return selectedSubjectTasksCollection(subjectId).document(taskId).get().await().toObject()!!
}
override suspend fun getTaskFromUser(subjectId: String, taskId: String, userId: String): Task {
return selectedSubjectTasksCollection(subjectId, userId)
.document(taskId)
.get()
.await().toObject(Task::class.java)!!
}
override fun saveTask(newTask: Task) {
selectedSubjectTasksCollection(newTask.subjectId).add(newTask)
}
override fun updateTask(newTask: Task) {
selectedSubjectTasksCollection(newTask.subjectId)
.document(newTask.id)
.set(newTask)
}
override fun deleteTask(oldTask: Task) {
selectedSubjectTasksCollection(oldTask.subjectId).document(oldTask.id).delete()
}
private fun selectedSubjectTasksCollection(subjectId: String, id: String = auth.currentUserId): CollectionReference =
firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(id)
.collection(FirebaseCollections.SUBJECT_COLLECTION)
.document(subjectId)
.collection(FirebaseCollections.TASK_COLLECTION)
}
// Extend CollectionReference and Query with some filters
fun CollectionReference.taskNotArchived(): Query =
this.whereEqualTo(TaskDocument.archived, false)
fun Query.taskNotArchived(): Query =
this.whereEqualTo(TaskDocument.archived, false)
fun CollectionReference.taskNotCompleted(): Query =
this.whereEqualTo(TaskDocument.completed, true)
fun Query.taskNotCompleted(): Query =
this.whereEqualTo(TaskDocument.completed, true)

View file

@ -48,8 +48,8 @@ class FirebaseTimerDAO @Inject constructor(
} }
private fun currentUserTimersCollection(): CollectionReference = private fun currentUserTimersCollection(): CollectionReference =
firestore.collection(FireBaseCollections.USER_COLLECTION) firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(auth.currentUserId) .document(auth.currentUserId)
.collection(FireBaseCollections.TIMER_COLLECTION) .collection(FirebaseCollections.TIMER_COLLECTION)
} }

View file

@ -2,34 +2,91 @@ package be.ugent.sel.studeez.domain.implementation
import be.ugent.sel.studeez.R import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.snackbar.SnackbarManager import be.ugent.sel.studeez.common.snackbar.SnackbarManager
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.data.remote.FirebaseUser.BIOGRAPHY
import be.ugent.sel.studeez.data.remote.FirebaseUser.USERNAME
import be.ugent.sel.studeez.domain.AccountDAO import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.UserDAO import be.ugent.sel.studeez.domain.UserDAO
import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION
import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.snapshots
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import javax.inject.Inject import javax.inject.Inject
class FirebaseUserDAO @Inject constructor( class FirebaseUserDAO @Inject constructor(
private val firestore: FirebaseFirestore, private val firestore: FirebaseFirestore,
private val auth: AccountDAO private val auth: AccountDAO
) : UserDAO { ) : UserDAO {
override suspend fun getUsername(): String? { override fun getCurrentUserId(): String {
return currentUserDocument().get().await().getString("username") return auth.currentUserId
}
override suspend fun save(newUsername: String) {
currentUserDocument().set(mapOf("username" to newUsername))
} }
private fun currentUserDocument(): DocumentReference = private fun currentUserDocument(): DocumentReference =
firestore.collection(USER_COLLECTION).document(auth.currentUserId) firestore
.collection(USER_COLLECTION)
.document(auth.currentUserId)
companion object { override fun getAllUsers(): Flow<List<User>> {
private const val USER_COLLECTION = "users" return firestore
.collection(USER_COLLECTION)
.snapshots()
.map { it.toObjects(User::class.java) }
} }
override suspend fun deleteUserReferences() { override fun getUsersWithQuery(
fieldName: String,
value: String
): Flow<List<User>> {
return firestore
.collection(USER_COLLECTION)
.whereEqualTo(fieldName, value)
.snapshots()
.map { it.toObjects(User::class.java) }
}
override fun getUserDetails(userId: String): Flow<User> {
return flow {
val snapshot = firestore
.collection(USER_COLLECTION)
.document(userId)
.get()
.await()
val user = snapshot.toObject(User::class.java)!!
emit(user)
}
}
override suspend fun getUsername(userId: String): String {
val user = firestore.collection(USER_COLLECTION)
.document(userId)
.get().await()
return user.getString(USERNAME)!!
}
override suspend fun getLoggedInUser(): User {
val userDocument = currentUserDocument().get().await()
return User(
username = userDocument.getString(USERNAME) ?: "",
biography = userDocument.getString(BIOGRAPHY) ?: ""
)
}
override suspend fun saveLoggedInUser(
newUsername: String,
newBiography: String
) {
currentUserDocument().set(mapOf(
USERNAME to newUsername,
BIOGRAPHY to newBiography
))
}
override suspend fun deleteLoggedInUserReferences() {
currentUserDocument().delete() currentUserDocument().delete()
.addOnSuccessListener { SnackbarManager.showMessage(R.string.success) } .addOnSuccessListener { SnackbarManager.showMessage(R.string.success) }
.addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) } .addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) }

View file

@ -4,7 +4,7 @@ object StudeezDestinations {
// NavBar // NavBar
const val HOME_SCREEN = "home" const val HOME_SCREEN = "home"
const val SUBJECT_SCREEN = "subjects" const val SUBJECT_SCREEN = "subjects"
const val SESSIONS_SCREEN = "sessions" const val FRIENDS_FEED = "friends_feed"
const val PROFILE_SCREEN = "profile" const val PROFILE_SCREEN = "profile"
// Drawer // Drawer
@ -27,10 +27,13 @@ object StudeezDestinations {
const val EDIT_SUBJECT_FORM = "edit_subject" const val EDIT_SUBJECT_FORM = "edit_subject"
const val TASKS_SCREEN = "tasks" const val TASKS_SCREEN = "tasks"
const val ADD_TASK_FORM = "add_task" const val ADD_TASK_FORM = "add_task"
const val SELECT_SUBJECT = "select_subject"
const val EDIT_TASK_FORM = "edit_task" const val EDIT_TASK_FORM = "edit_task"
// Friends flow // Friends flow
const val FRIENDS_OVERVIEW_SCREEN = "friends_overview"
const val SEARCH_FRIENDS_SCREEN = "search_friends" const val SEARCH_FRIENDS_SCREEN = "search_friends"
const val PUBLIC_PROFILE_SCREEN = "public_profile"
// Create & edit screens // Create & edit screens
const val CREATE_TASK_SCREEN = "create_task" const val CREATE_TASK_SCREEN = "create_task"

View file

@ -14,22 +14,26 @@ import be.ugent.sel.studeez.common.composable.drawer.getDrawerActions
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel
import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions
import be.ugent.sel.studeez.screens.friends.friends_overview.FriendsOveriewRoute
import be.ugent.sel.studeez.screens.friends.friends_search.SearchFriendsRoute
import be.ugent.sel.studeez.screens.friends_feed.FriendsFeedRoute
import be.ugent.sel.studeez.screens.home.HomeRoute import be.ugent.sel.studeez.screens.home.HomeRoute
import be.ugent.sel.studeez.screens.log_in.LoginRoute import be.ugent.sel.studeez.screens.log_in.LoginRoute
import be.ugent.sel.studeez.screens.profile.EditProfileRoute
import be.ugent.sel.studeez.screens.profile.ProfileRoute import be.ugent.sel.studeez.screens.profile.ProfileRoute
import be.ugent.sel.studeez.screens.profile.edit_profile.EditProfileRoute
import be.ugent.sel.studeez.screens.profile.public_profile.PublicProfileRoute
import be.ugent.sel.studeez.screens.session.SessionRoute import be.ugent.sel.studeez.screens.session.SessionRoute
import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute
import be.ugent.sel.studeez.screens.sessions.SessionsRoute
import be.ugent.sel.studeez.screens.settings.SettingsRoute import be.ugent.sel.studeez.screens.settings.SettingsRoute
import be.ugent.sel.studeez.screens.sign_up.SignUpRoute import be.ugent.sel.studeez.screens.sign_up.SignUpRoute
import be.ugent.sel.studeez.screens.splash.SplashRoute import be.ugent.sel.studeez.screens.splash.SplashRoute
import be.ugent.sel.studeez.screens.tasks.SubjectRoute import be.ugent.sel.studeez.screens.subjects.SubjectRoute
import be.ugent.sel.studeez.screens.subjects.form.SubjectCreateRoute
import be.ugent.sel.studeez.screens.subjects.form.SubjectEditRoute
import be.ugent.sel.studeez.screens.subjects.select.SubjectSelectionRoute
import be.ugent.sel.studeez.screens.tasks.TaskRoute import be.ugent.sel.studeez.screens.tasks.TaskRoute
import be.ugent.sel.studeez.screens.tasks.forms.SubjectAddRoute import be.ugent.sel.studeez.screens.tasks.form.TaskCreateRoute
import be.ugent.sel.studeez.screens.tasks.forms.SubjectEditRoute import be.ugent.sel.studeez.screens.tasks.form.TaskEditRoute
import be.ugent.sel.studeez.screens.tasks.forms.TaskAddRoute
import be.ugent.sel.studeez.screens.tasks.forms.TaskEditRoute
import be.ugent.sel.studeez.screens.timer_form.TimerAddRoute import be.ugent.sel.studeez.screens.timer_form.TimerAddRoute
import be.ugent.sel.studeez.screens.timer_form.TimerEditRoute import be.ugent.sel.studeez.screens.timer_form.TimerEditRoute
import be.ugent.sel.studeez.screens.timer_form.timer_type_select.TimerTypeSelectScreen import be.ugent.sel.studeez.screens.timer_form.timer_type_select.TimerTypeSelectScreen
@ -51,6 +55,7 @@ fun StudeezNavGraph(
val open: (String) -> Unit = { appState.navigate(it) } val open: (String) -> Unit = { appState.navigate(it) }
val openAndPopUp: (String, String) -> Unit = val openAndPopUp: (String, String) -> Unit =
{ route, popUp -> appState.navigateAndPopUp(route, popUp) } { route, popUp -> appState.navigateAndPopUp(route, popUp) }
val clearAndNavigate: (route: String) -> Unit = { route -> appState.clearAndNavigate(route) }
val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp) val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp)
val navigationBarActions: NavigationBarActions = val navigationBarActions: NavigationBarActions =
@ -64,10 +69,11 @@ fun StudeezNavGraph(
// NavBar // NavBar
composable(StudeezDestinations.HOME_SCREEN) { composable(StudeezDestinations.HOME_SCREEN) {
HomeRoute( HomeRoute(
open, open = open,
viewModel = hiltViewModel(),
drawerActions = drawerActions, drawerActions = drawerActions,
navigationBarActions = navigationBarActions navigationBarActions = navigationBarActions,
feedViewModel = hiltViewModel(),
viewModel = hiltViewModel()
) )
} }
@ -80,8 +86,16 @@ fun StudeezNavGraph(
) )
} }
composable(StudeezDestinations.SELECT_SUBJECT) {
SubjectSelectionRoute(
open = { openAndPopUp(it, StudeezDestinations.SELECT_SUBJECT) },
goBack = goBack,
viewModel = hiltViewModel(),
)
}
composable(StudeezDestinations.ADD_SUBJECT_FORM) { composable(StudeezDestinations.ADD_SUBJECT_FORM) {
SubjectAddRoute( SubjectCreateRoute(
goBack = goBack, goBack = goBack,
openAndPopUp = openAndPopUp, openAndPopUp = openAndPopUp,
viewModel = hiltViewModel(), viewModel = hiltViewModel(),
@ -98,14 +112,14 @@ fun StudeezNavGraph(
composable(StudeezDestinations.TASKS_SCREEN) { composable(StudeezDestinations.TASKS_SCREEN) {
TaskRoute( TaskRoute(
goBack = goBack, goBack = { openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.TASKS_SCREEN) },
open = open, open = open,
viewModel = hiltViewModel(), viewModel = hiltViewModel(),
) )
} }
composable(StudeezDestinations.ADD_TASK_FORM) { composable(StudeezDestinations.ADD_TASK_FORM) {
TaskAddRoute( TaskCreateRoute(
goBack = goBack, goBack = goBack,
openAndPopUp = openAndPopUp, openAndPopUp = openAndPopUp,
viewModel = hiltViewModel(), viewModel = hiltViewModel(),
@ -121,10 +135,11 @@ fun StudeezNavGraph(
} }
composable(StudeezDestinations.SESSIONS_SCREEN) { composable(StudeezDestinations.FRIENDS_FEED) {
SessionsRoute( FriendsFeedRoute(
drawerActions = drawerActions, drawerActions = drawerActions,
navigationBarActions = navigationBarActions navigationBarActions = navigationBarActions,
viewModel = hiltViewModel()
) )
} }
@ -200,7 +215,7 @@ fun StudeezNavGraph(
composable(StudeezDestinations.SESSION_RECAP) { composable(StudeezDestinations.SESSION_RECAP) {
SessionRecapRoute( SessionRecapRoute(
openAndPopUp = openAndPopUp, clearAndNavigate = clearAndNavigate,
viewModel = hiltViewModel() viewModel = hiltViewModel()
) )
} }
@ -220,8 +235,28 @@ fun StudeezNavGraph(
} }
// Friends flow // Friends flow
composable(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) {
FriendsOveriewRoute(
open = open,
popUp = goBack,
viewModel = hiltViewModel()
)
}
composable(StudeezDestinations.SEARCH_FRIENDS_SCREEN) { composable(StudeezDestinations.SEARCH_FRIENDS_SCREEN) {
// TODO SearchFriendsRoute(
popUp = goBack,
open = open,
viewModel = hiltViewModel()
)
}
composable(StudeezDestinations.PUBLIC_PROFILE_SCREEN) {
PublicProfileRoute(
popUp = goBack,
open = open,
viewModel = hiltViewModel()
)
} }
// Create & edit screens // Create & edit screens

View file

@ -0,0 +1,308 @@
package be.ugent.sel.studeez.screens.friends.friends_overview
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
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.common.composable.BasicButton
import be.ugent.sel.studeez.common.composable.ProfilePicture
import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry
import be.ugent.sel.studeez.common.ext.basicButton
import be.ugent.sel.studeez.data.local.models.Friendship
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.ui.theme.StudeezTheme
import com.google.firebase.Timestamp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import be.ugent.sel.studeez.R.string as AppText
data class FriendsOverviewActions(
val getFriendsFlow: () -> Flow<List<Pair<User, Friendship>>>,
val searchFriends: () -> Unit,
val onQueryStringChange: (String) -> Unit,
val onSubmit: () -> Unit,
val viewProfile: (String) -> Unit,
val removeFriend: (Friendship) -> Unit
)
fun getFriendsOverviewActions(
viewModel: FriendsOverviewViewModel,
open: (String) -> Unit
): FriendsOverviewActions {
return FriendsOverviewActions(
getFriendsFlow = viewModel::getAllFriends,
searchFriends = { viewModel.searchFriends(open) },
onQueryStringChange = viewModel::onQueryStringChange,
onSubmit = { viewModel.onSubmit(open) },
viewProfile = { userId ->
viewModel.viewProfile(userId, open)
},
removeFriend = viewModel::removeFriend
)
}
@Composable
fun FriendsOveriewRoute(
open: (String) -> Unit,
popUp: () -> Unit,
viewModel: FriendsOverviewViewModel
) {
val uiState by viewModel.uiState
FriendsOverviewScreen(
popUp = popUp,
uiState = uiState,
friendsOverviewActions = getFriendsOverviewActions(
viewModel = viewModel,
open = open
)
)
}
@Composable
fun FriendsOverviewScreen(
popUp: () -> Unit,
uiState: FriendsOverviewUiState,
friendsOverviewActions: FriendsOverviewActions
) {
val friends = friendsOverviewActions.getFriendsFlow().collectAsState(initial = emptyList())
Scaffold(
topBar = {
TopAppBar(
title = {
// TODO Make search field
// SearchField(
// value = uiState.queryString,
// onValueChange = friendsOverviewActions.onQueryStringChange,
// onSubmit = friendsOverviewActions.onSubmit,
// label = AppText.search_friends,
// enabled = false
// )
IconButton(
onClick = friendsOverviewActions.onSubmit,
// modifier = Modifier.background(
// color = MaterialTheme.colors.background
// ),
) {
Row {
Text(
text = stringResource(id = AppText.click_search_friends),
color = MaterialTheme.colors.onPrimary
)
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(AppText.search_friends),
tint = MaterialTheme.colors.onPrimary
)
}
}
},
navigationIcon = {
IconButton(onClick = popUp) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = resources().getString(R.string.go_back)
)
}
}
// TODO Add inbox action
)
}
) { paddingValues ->
LazyColumn (
modifier = Modifier.padding(paddingValues)
) {
if (friends.value.isEmpty()) {
// Show a quick button to search friends when the user does not have any friends yet.
item {
BasicButton(
text = AppText.no_friends,
modifier = Modifier.basicButton()
) {
friendsOverviewActions.searchFriends()
}
}
}
items(friends.value) { friend ->
FriendsEntry(
user = friend.first,
friendship = friend.second,
viewProfile = { userId -> friendsOverviewActions.viewProfile(userId) },
removeFriend = friendsOverviewActions.removeFriend
)
}
}
}
}
@Preview
@Composable
fun FriendsOverviewPreview() {
StudeezTheme {
FriendsOverviewScreen(
popUp = {},
uiState = FriendsOverviewUiState(""),
friendsOverviewActions = FriendsOverviewActions(
getFriendsFlow = { emptyFlow() },
searchFriends = {},
onQueryStringChange = {},
onSubmit = {},
viewProfile = {},
removeFriend = {}
)
)
}
}
@Composable
fun FriendsEntry(
user: User,
friendship: Friendship,
viewProfile: (String) -> Unit,
removeFriend: (Friendship) -> Unit
) {
Card {
Row (
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 7.dp),
horizontalArrangement = Arrangement.spacedBy(15.dp)
) {
Box(
modifier = Modifier
.padding(vertical = 4.dp)
) {
ProfilePicture()
}
Box (
modifier = Modifier
.fillMaxWidth()
) {
Column (
modifier = Modifier
.padding(vertical = 4.dp)
) {
Text(
text = user.username,
fontSize = 16.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "${resources().getString(AppText.app_name)} ${resources().getString(AppText.friend)}",
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd
) {
FriendsOverviewDropDown(
friendship = friendship,
viewProfile = viewProfile,
removeFriend = removeFriend
)
}
}
}
}
}
@Preview
@Composable
fun FriendsEntryPreview() {
StudeezTheme {
FriendsEntry(
user = User(
id = "",
username = "Tibo De Peuter",
biography = "short bio"
),
friendship = Friendship(
id = "",
friendId = "someId",
friendsSince = Timestamp.now(),
accepted = true
),
viewProfile = {},
removeFriend = {}
)
}
}
@Composable
fun FriendsOverviewDropDown(
friendship: Friendship,
viewProfile: (String) -> Unit,
removeFriend: (Friendship) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
IconButton(
onClick = { expanded = true }
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal),
contentDescription = resources().getString(AppText.view_more),
modifier = Modifier.fillMaxSize()
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DrawerEntry(
icon = Icons.Default.Person,
text = stringResource(id = AppText.show_profile)
) {
viewProfile(friendship.friendId)
}
DrawerEntry(
icon = Icons.Default.Delete,
text = stringResource(id = AppText.remove_friend)
) {
removeFriend(friendship)
expanded = false
}
}
}
@Preview
@Composable
fun FriendsOverviewDropDownPreview() {
StudeezTheme {
FriendsOverviewDropDown(
friendship = Friendship(
id = "",
friendId = "someId",
friendsSince = Timestamp.now(),
accepted = true
),
viewProfile = {},
removeFriend = { }
)
}
}

View file

@ -0,0 +1,6 @@
package be.ugent.sel.studeez.screens.friends.friends_overview
data class FriendsOverviewUiState(
val userId: String,
val queryString: String = ""
)

View file

@ -0,0 +1,78 @@
package be.ugent.sel.studeez.screens.friends.friends_overview
import androidx.compose.runtime.mutableStateOf
import be.ugent.sel.studeez.data.SelectedUserId
import be.ugent.sel.studeez.data.local.models.Friendship
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.domain.FriendshipDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.UserDAO
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapConcat
import javax.inject.Inject
@HiltViewModel
class FriendsOverviewViewModel @Inject constructor(
private val userDAO: UserDAO,
private val friendshipDAO: FriendshipDAO,
private val selectedUserIdState: SelectedUserId,
logService: LogService
) : StudeezViewModel(logService) {
var uiState = mutableStateOf(FriendsOverviewUiState(
userId = selectedUserIdState.value
))
private set
fun getAllFriends(): Flow<List<Pair<User, Friendship>>> {
return friendshipDAO.getAllFriendships(
userId = uiState.value.userId
)
.flatMapConcat { friendships ->
val userFlows = friendships.map { friendship ->
userDAO.getUserDetails(friendship.friendId)
}
combine(userFlows) { users ->
friendships.zip(users) { friendship, user ->
Pair(user, friendship)
}
}
}
}
fun searchFriends(open: (String) -> Unit) {
open(StudeezDestinations.SEARCH_FRIENDS_SCREEN)
}
fun onQueryStringChange(newValue: String) {
uiState.value = uiState.value.copy(
queryString = newValue
)
}
fun onSubmit(open: (String) -> Unit) {
val query = uiState.value.queryString // TODO Pass as argument
open(StudeezDestinations.SEARCH_FRIENDS_SCREEN)
}
fun viewProfile(
userId: String,
open: (String) -> Unit
) {
selectedUserIdState.value = userId
open(StudeezDestinations.PUBLIC_PROFILE_SCREEN)
}
fun removeFriend(
friendship: Friendship
) {
friendshipDAO.removeFriendship(
friendship = friendship
)
}
}

View file

@ -0,0 +1,10 @@
package be.ugent.sel.studeez.screens.friends.friends_search
import be.ugent.sel.studeez.data.local.models.User
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
data class SearchFriendUiState(
val queryString: String = "",
val searchResults: Flow<List<User>> = emptyFlow()
)

View file

@ -0,0 +1,264 @@
package be.ugent.sel.studeez.screens.friends.friends_search
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
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.common.composable.ProfilePicture
import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.ui.theme.StudeezTheme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import be.ugent.sel.studeez.R.string as AppText
data class SearchFriendsActions(
val onQueryStringChange: (String) -> Unit,
val getUsersWithUsername: (String) -> Unit,
val getAllUsers: () -> Flow<List<User>>,
val goToProfile: (String) -> Unit
)
fun getSearchFriendsActions(
viewModel: SearchFriendsViewModel,
open: (String) -> Unit
): SearchFriendsActions {
return SearchFriendsActions(
onQueryStringChange = viewModel::onQueryStringChange,
getUsersWithUsername = viewModel::getUsersWithUsername,
getAllUsers = { viewModel.getAllUsers() },
goToProfile = { userId -> viewModel.goToProfile(userId, open) }
)
}
@Composable
fun SearchFriendsRoute(
popUp: () -> Unit,
open: (String) -> Unit,
viewModel: SearchFriendsViewModel
) {
val uiState by viewModel.uiState
SearchFriendsScreen(
popUp = popUp,
uiState = uiState,
searchFriendsActions = getSearchFriendsActions(
viewModel = viewModel,
open = open
)
)
}
@Composable
fun SearchFriendsScreen(
popUp: () -> Unit,
uiState: SearchFriendUiState,
searchFriendsActions: SearchFriendsActions
) {
var query by remember { mutableStateOf(uiState.queryString) }
val searchResults = searchFriendsActions.getAllUsers().collectAsState(
initial = emptyList()
)
Scaffold(
topBar = {
TopAppBar(
title = {
// TODO Make search field
// SearchField(
// value = uiState.queryString,
// onValueChange = friendsOverviewActions.onQueryStringChange,
// onSubmit = friendsOverviewActions.onSubmit,
// label = AppText.search_friends,
// enabled = false
// )
Text(
text = stringResource(id = AppText.searching_friends)
)
},
navigationIcon = {
IconButton(onClick = popUp) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = resources().getString(R.string.go_back)
)
}
}
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues)
) {
items (searchResults.value) { user ->
UserEntry(
user = user,
goToProfile = searchFriendsActions.goToProfile
)
}
}
}
}
@Preview
@Composable
fun SearchFriendsPreview() {
StudeezTheme {
SearchFriendsScreen(
popUp = {},
uiState = SearchFriendUiState(
queryString = "dit is een test",
searchResults = flowOf(listOf(User(
id = "someid",
username = "Eerste user",
biography = "blah blah blah"
)))
),
searchFriendsActions = SearchFriendsActions(
onQueryStringChange = {},
getUsersWithUsername = {},
getAllUsers = {
flowOf(listOf(User(
id = "someid",
username = "Eerste user",
biography = "blah blah blah"
)))
},
goToProfile = { }
)
)
}
}
@Composable
fun UserEntry(
user: User,
goToProfile: (String) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 7.dp),
horizontalArrangement = Arrangement.spacedBy(15.dp)
) {
Box(
modifier = Modifier
.padding(vertical = 4.dp)
) {
ProfilePicture()
}
Box (
modifier = Modifier
.fillMaxWidth()
) {
Column (
modifier = Modifier
.padding(vertical = 4.dp)
) {
Text(
text = user.username,
fontSize = 16.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "${resources().getString(AppText.app_name)} ${resources().getString(AppText.friend)}",
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd
) {
SearchFriendsDropDown(
user = user,
goToProfile = goToProfile
)
}
}
}
}
@Preview
@Composable
fun UserEntryPreview() {
StudeezTheme {
UserEntry(
user = User(
id = "someid",
username = "Eerste user",
biography = "blah blah blah"
),
goToProfile = { }
)
}
}
/**
* Three dots that open a dropdown menu that allow to go the users profile.
*/
@Composable
fun SearchFriendsDropDown(
user: User,
goToProfile: (String) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
IconButton(
onClick = { expanded = true }
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal),
contentDescription = stringResource(AppText.view_more),
modifier = Modifier.fillMaxSize()
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = { expanded = false }) {
DrawerEntry(
icon = Icons.Default.Person,
text = stringResource(id = AppText.show_profile)
) {
goToProfile(user.id)
}
}
}
}
@Preview
@Composable
fun SearchFriendsDropDownPreview() {
StudeezTheme {
SearchFriendsDropDown(
user = User(
id = "someid",
username = "Eerste user",
biography = "blah blah blah"
),
goToProfile = { }
)
}
}

View file

@ -0,0 +1,66 @@
package be.ugent.sel.studeez.screens.friends.friends_search
import androidx.compose.runtime.mutableStateOf
import be.ugent.sel.studeez.data.SelectedUserId
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.data.remote.FirebaseUser
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.UserDAO
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@HiltViewModel
class SearchFriendsViewModel @Inject constructor(
private val userDAO: UserDAO,
private val selectedProfileState: SelectedUserId,
logService: LogService
): StudeezViewModel(logService) {
var uiState = mutableStateOf(SearchFriendUiState())
private set
fun onQueryStringChange(newValue: String) {
uiState.value = uiState.value.copy(
queryString = newValue
)
uiState.value = uiState.value.copy(
searchResults = userDAO.getUsersWithQuery(
fieldName = FirebaseUser.USERNAME,
value = uiState.value.queryString
)
)
}
fun getUsersWithUsername(
value: String
): Flow<List<User>> {
return userDAO.getUsersWithQuery(
fieldName = FirebaseUser.USERNAME,
value = value
)
}
/**
* Get all users, except for the current user.
*/
fun getAllUsers(): Flow<List<User>> {
return userDAO.getAllUsers()
.map { users ->
users.filter { user ->
user.id != userDAO.getCurrentUserId()
}
}
}
fun goToProfile(
userId: String,
open: (String) -> Unit
) {
selectedProfileState.value = userId
open(StudeezDestinations.PUBLIC_PROFILE_SCREEN)
}
}

View file

@ -9,7 +9,6 @@ import androidx.compose.material.Card
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -20,7 +19,6 @@ import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.common.composable.DateText import be.ugent.sel.studeez.common.composable.DateText
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.feed.LoadingFeed
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.data.local.models.FeedEntry 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.data.local.models.timer_functional.HoursMinutesSeconds
@ -33,11 +31,10 @@ fun FriendsFeedRoute(
drawerActions: DrawerActions, drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions navigationBarActions: NavigationBarActions
) { ) {
val friendsFeedUiState by viewModel.uiState.collectAsState()
FriendsFeedScreen( FriendsFeedScreen(
drawerActions = drawerActions, drawerActions = drawerActions,
navigationBarActions = navigationBarActions, navigationBarActions = navigationBarActions,
uiState = friendsFeedUiState, viewModel = viewModel
) )
} }
@ -45,30 +42,31 @@ fun FriendsFeedRoute(
fun FriendsFeedScreen( fun FriendsFeedScreen(
drawerActions: DrawerActions, drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions, navigationBarActions: NavigationBarActions,
uiState: FriendsFeedUiState, viewModel: FriendsFeedViewModel
) { ) {
PrimaryScreenTemplate( PrimaryScreenTemplate(
title = resources().getString(AppText.friends_feed), title = resources().getString(AppText.friends_feed),
drawerActions = drawerActions, drawerActions = drawerActions,
navigationBarActions = navigationBarActions navigationBarActions = navigationBarActions
) { ) {
when (uiState) {
FriendsFeedUiState.Loading -> LoadingFeed() val friendsSessions = viewModel.getFriendsSessions().collectAsState(initial = emptyList())
is FriendsFeedUiState.Succes -> {
val friendsSessions = uiState.friendSessions
LazyColumn() {
// Default Timers, cannot be edited
items(friendsSessions) { LazyColumn() {
val (day, feedEntries) = it // Default Timers, cannot be edited
DateText(date = day) items(friendsSessions.value) {
feedEntries.forEach { (name, feedEntry) -> val (day, feedEntries) = it
FriendsFeedEntry(name = name, feedEntry = feedEntry) DateText(date = day)
} feedEntries.forEach { (name, feedEntry) ->
Spacer(modifier = Modifier.height(10.dp)) FriendsFeedEntry(name = name, feedEntry = feedEntry)
}
} }
Spacer(modifier = Modifier.height(10.dp))
} }
} }
} }
} }

View file

@ -1,14 +1,15 @@
package be.ugent.sel.studeez.screens.friends_feed package be.ugent.sel.studeez.screens.friends_feed
import androidx.lifecycle.viewModelScope import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.domain.FeedDAO import be.ugent.sel.studeez.domain.FeedDAO
import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.SessionDAO
import be.ugent.sel.studeez.screens.StudeezViewModel import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.toList
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -17,13 +18,10 @@ class FriendsFeedViewModel @Inject constructor(
logService: LogService logService: LogService
) : StudeezViewModel(logService) { ) : StudeezViewModel(logService) {
val uiState: StateFlow<FriendsFeedUiState> = feedDAO.getFriendsSessions() fun getFriendsSessions(): Flow<List<Pair<String, List<Pair<String, FeedEntry>>>>> {
.map { it.toList() } return feedDAO.getFriendsSessions().map { it.toList() }
.map { FriendsFeedUiState.Succes(it) } }
.stateIn(
scope = viewModelScope,
initialValue = FriendsFeedUiState.Loading,
started = SharingStarted.Eagerly,
)
} }

View file

@ -5,14 +5,17 @@ import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import be.ugent.sel.studeez.R import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.composable.BasicButton
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.feed.Feed
import be.ugent.sel.studeez.common.composable.feed.FeedUiState
import be.ugent.sel.studeez.common.composable.feed.FeedViewModel
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.common.ext.basicButton import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.resources import be.ugent.sel.studeez.resources
@Composable @Composable
@ -21,35 +24,43 @@ fun HomeRoute(
viewModel: HomeViewModel, viewModel: HomeViewModel,
drawerActions: DrawerActions, drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions, navigationBarActions: NavigationBarActions,
feedViewModel: FeedViewModel,
) { ) {
val feedUiState by feedViewModel.uiState.collectAsState()
HomeScreen( HomeScreen(
onStartSessionClick = { viewModel.onStartSessionClick(open) }, onViewFriendsClick = { viewModel.onViewFriendsClick(open) },
drawerActions = drawerActions, drawerActions = drawerActions,
navigationBarActions = navigationBarActions, navigationBarActions = navigationBarActions,
feedUiState = feedUiState,
continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) },
onEmptyFeedHelp = { feedViewModel.onEmptyFeedHelp(open) }
) )
} }
@Composable @Composable
fun HomeScreen( fun HomeScreen(
onStartSessionClick: () -> Unit, onViewFriendsClick: () -> Unit,
drawerActions: DrawerActions, drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions navigationBarActions: NavigationBarActions,
feedUiState: FeedUiState,
continueTask: (String, String) -> Unit,
onEmptyFeedHelp: () -> Unit,
) { ) {
PrimaryScreenTemplate( PrimaryScreenTemplate(
title = resources().getString(R.string.home), title = resources().getString(R.string.home),
drawerActions = drawerActions, drawerActions = drawerActions,
navigationBarActions = navigationBarActions, navigationBarActions = navigationBarActions,
// TODO barAction = { FriendsAction() } barAction = { FriendsAction(onViewFriendsClick) }
) { ) {
BasicButton(R.string.start_session, Modifier.basicButton()) { Feed(feedUiState, continueTask, onEmptyFeedHelp)
onStartSessionClick()
}
} }
} }
@Composable @Composable
fun FriendsAction() { fun FriendsAction(
IconButton(onClick = { /*TODO*/ }) { onClick: () -> Unit
) {
IconButton(onClick = onClick) {
Icon( Icon(
imageVector = Icons.Default.Person, imageVector = Icons.Default.Person,
contentDescription = resources().getString(R.string.friends) contentDescription = resources().getString(R.string.friends)
@ -61,8 +72,40 @@ fun FriendsAction() {
@Composable @Composable
fun HomeScreenPreview() { fun HomeScreenPreview() {
HomeScreen( HomeScreen(
onStartSessionClick = {}, onViewFriendsClick = {},
drawerActions = DrawerActions({}, {}, {}, {}, {}), drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}) navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
feedUiState = FeedUiState.Succes(
mapOf(
"08 May 2023" to listOf(
FeedEntry(
argb_color = 0xFFABD200,
subJectName = "Test Subject",
taskName = "Test Task",
totalStudyTime = 600,
),
FeedEntry(
argb_color = 0xFFFFD200,
subJectName = "Test Subject",
taskName = "Test Task",
totalStudyTime = 20,
),
),
"09 May 2023" to listOf(
FeedEntry(
argb_color = 0xFFFD1200,
subJectName = "Test Subject",
taskName = "Test Task",
),
FeedEntry(
argb_color = 0xFFFF5C89,
subJectName = "Test Subject",
taskName = "Test Task",
),
)
)
),
continueTask = { _, _ -> run {} },
onEmptyFeedHelp = {}
) )
} }

View file

@ -1,6 +1,4 @@
package be.ugent.sel.studeez.screens.home package be.ugent.sel.studeez.screens.home
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.navigation.StudeezDestinations import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel import be.ugent.sel.studeez.screens.StudeezViewModel
@ -9,11 +7,11 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor( class HomeViewModel @Inject constructor(
private val accountDAO: AccountDAO,
logService: LogService logService: LogService
) : StudeezViewModel(logService) { ) : StudeezViewModel(logService) {
fun onStartSessionClick(open: (String) -> Unit) {
open(StudeezDestinations.TIMER_SELECTION_SCREEN) fun onViewFriendsClick(open: (String) -> Unit) {
open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN)
} }
} }

View file

@ -1,5 +0,0 @@
package be.ugent.sel.studeez.screens.profile
data class ProfileEditUiState (
val username: String = ""
)

View file

@ -1,37 +1,50 @@
package be.ugent.sel.studeez.screens.profile package be.ugent.sel.studeez.screens.profile
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment
import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier
import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.style.TextAlign
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.R import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.composable.Headline import be.ugent.sel.studeez.common.composable.Headline
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.common.ext.defaultButtonShape
import be.ugent.sel.studeez.resources import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.ui.theme.StudeezTheme
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import be.ugent.sel.studeez.R.string as AppText import be.ugent.sel.studeez.R.string as AppText
data class ProfileActions( data class ProfileActions(
val getUsername: suspend CoroutineScope.() -> String?, val getUsername: suspend CoroutineScope.() -> String?,
val getBiography: suspend CoroutineScope.() -> String?,
val getAmountOfFriends: () -> Flow<Int>,
val onEditProfileClick: () -> Unit, val onEditProfileClick: () -> Unit,
val onViewFriendsClick: () -> Unit
) )
fun getProfileActions( fun getProfileActions(
viewModel: ProfileViewModel, viewModel: ProfileViewModel,
open: (String) -> Unit, open: (String) -> Unit
): ProfileActions { ): ProfileActions {
return ProfileActions( return ProfileActions(
getUsername = { viewModel.getUsername() }, getUsername = { viewModel.getUsername() },
getBiography = { viewModel.getBiography() },
getAmountOfFriends = { viewModel.getAmountOfFriends() },
onEditProfileClick = { viewModel.onEditProfileClick(open) }, onEditProfileClick = { viewModel.onEditProfileClick(open) },
onViewFriendsClick = { viewModel.onViewFriendsClick(open) }
) )
} }
@ -56,8 +69,12 @@ fun ProfileScreen(
navigationBarActions: NavigationBarActions, navigationBarActions: NavigationBarActions,
) { ) {
var username: String? by remember { mutableStateOf("") } var username: String? by remember { mutableStateOf("") }
var biography: String? by remember { mutableStateOf("") }
val amountOfFriends = profileActions.getAmountOfFriends().collectAsState(initial = 0)
LaunchedEffect(key1 = Unit) { LaunchedEffect(key1 = Unit) {
username = profileActions.getUsername(this) username = profileActions.getUsername(this)
biography = profileActions.getBiography(this)
} }
PrimaryScreenTemplate( PrimaryScreenTemplate(
title = resources().getString(AppText.profile), title = resources().getString(AppText.profile),
@ -65,7 +82,35 @@ fun ProfileScreen(
navigationBarActions = navigationBarActions, navigationBarActions = navigationBarActions,
barAction = { EditAction(onClick = profileActions.onEditProfileClick) } barAction = { EditAction(onClick = profileActions.onEditProfileClick) }
) { ) {
Headline(text = (username ?: resources().getString(R.string.no_username))) LazyColumn(
verticalArrangement = Arrangement.spacedBy(15.dp)
) {
item {
Headline(text = username ?: resources().getString(AppText.no_username))
}
item {
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.fillMaxWidth()
.wrapContentWidth(align = Alignment.CenterHorizontally)
) {
AmountOfFriendsButton(
amountOfFriends = amountOfFriends.value
) {
profileActions.onViewFriendsClick()
}
}
}
item {
Text(
text = biography ?: "",
textAlign = TextAlign.Center,
modifier = Modifier.padding(48.dp, 0.dp)
)
}
}
} }
} }
@ -78,7 +123,6 @@ fun EditAction(
imageVector = Icons.Default.Edit, imageVector = Icons.Default.Edit,
contentDescription = resources().getString(AppText.edit_profile) contentDescription = resources().getString(AppText.edit_profile)
) )
} }
} }
@ -86,8 +130,38 @@ fun EditAction(
@Composable @Composable
fun ProfileScreenPreview() { fun ProfileScreenPreview() {
ProfileScreen( ProfileScreen(
profileActions = ProfileActions({ null }, {}), profileActions = ProfileActions({ null }, { null }, { emptyFlow() }, {}, {}),
drawerActions = DrawerActions({}, {}, {}, {}, {}), drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}) navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {})
) )
}
@Composable
fun AmountOfFriendsButton(
amountOfFriends: Int,
onClick: () -> Unit
){
Button(
onClick = onClick,
shape = defaultButtonShape()
) {
Text(
text = resources().getQuantityString(
/* id = */ R.plurals.friends_amount,
/* quantity = */ amountOfFriends,
/* ...formatArgs = */ amountOfFriends
)
)
}
}
@Preview
@Composable
fun AmountOfFriendsButtonPreview() {
StudeezTheme {
Column {
AmountOfFriendsButton(amountOfFriends = 1) { }
AmountOfFriendsButton(amountOfFriends = 100) { }
}
}
} }

View file

@ -1,24 +1,39 @@
package be.ugent.sel.studeez.screens.profile package be.ugent.sel.studeez.screens.profile
import be.ugent.sel.studeez.domain.FriendshipDAO
import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.UserDAO import be.ugent.sel.studeez.domain.UserDAO
import be.ugent.sel.studeez.navigation.StudeezDestinations import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ProfileViewModel @Inject constructor( class ProfileViewModel @Inject constructor(
private val userDAO: UserDAO, private val userDAO: UserDAO,
private val friendshipDAO: FriendshipDAO,
logService: LogService logService: LogService
) : StudeezViewModel(logService) { ) : StudeezViewModel(logService) {
suspend fun getUsername(): String? { suspend fun getUsername(): String {
return userDAO.getUsername() return userDAO.getLoggedInUser().username
}
suspend fun getBiography(): String {
return userDAO.getLoggedInUser().biography
}
fun getAmountOfFriends(): Flow<Int> {
return friendshipDAO.getFriendshipCount(userDAO.getCurrentUserId())
} }
fun onEditProfileClick(open: (String) -> Unit) { fun onEditProfileClick(open: (String) -> Unit) {
open(StudeezDestinations.EDIT_PROFILE_SCREEN) open(StudeezDestinations.EDIT_PROFILE_SCREEN)
} }
fun onViewFriendsClick(open: (String) -> Unit) {
open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN)
}
} }

View file

@ -1,20 +1,21 @@
package be.ugent.sel.studeez.screens.profile package be.ugent.sel.studeez.screens.profile.edit_profile
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.composable.BasicTextButton import be.ugent.sel.studeez.common.composable.BasicTextButton
import be.ugent.sel.studeez.common.composable.LabelledInputField import be.ugent.sel.studeez.common.composable.LabelledInputField
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
import be.ugent.sel.studeez.common.ext.textButton import be.ugent.sel.studeez.common.ext.textButton
import be.ugent.sel.studeez.resources import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.ui.theme.StudeezTheme import be.ugent.sel.studeez.ui.theme.StudeezTheme
import be.ugent.sel.studeez.R.string as AppText
data class EditProfileActions( data class EditProfileActions(
val onUserNameChange: (String) -> Unit, val onUserNameChange: (String) -> Unit,
val onBiographyChange: (String) -> Unit,
val onSaveClick: () -> Unit, val onSaveClick: () -> Unit,
val onDeleteClick: () -> Unit val onDeleteClick: () -> Unit
) )
@ -25,6 +26,7 @@ fun getEditProfileActions(
): EditProfileActions { ): EditProfileActions {
return EditProfileActions( return EditProfileActions(
onUserNameChange = { viewModel.onUsernameChange(it) }, onUserNameChange = { viewModel.onUsernameChange(it) },
onBiographyChange = { viewModel.onBiographyChange(it) },
onSaveClick = { viewModel.onSaveClick() }, onSaveClick = { viewModel.onSaveClick() },
onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) }, onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) },
) )
@ -51,28 +53,41 @@ fun EditProfileScreen(
editProfileActions: EditProfileActions, editProfileActions: EditProfileActions,
) { ) {
SecondaryScreenTemplate( SecondaryScreenTemplate(
title = resources().getString(R.string.editing_profile), title = resources().getString(AppText.editing_profile),
popUp = goBack popUp = goBack
) { ) {
Column { LazyColumn {
LabelledInputField( item {
value = uiState.username, LabelledInputField(
onNewValue = editProfileActions.onUserNameChange, value = uiState.username,
label = R.string.username onNewValue = editProfileActions.onUserNameChange,
) label = AppText.username
BasicTextButton( )
text = R.string.save, }
Modifier.textButton(), item {
action = { LabelledInputField(
editProfileActions.onSaveClick() value = uiState.biography,
goBack() onNewValue = editProfileActions.onBiographyChange,
} label = AppText.biography
) )
BasicTextButton( }
text = R.string.delete_profile, item {
Modifier.textButton(), BasicTextButton(
action = editProfileActions.onDeleteClick text = AppText.save,
) Modifier.textButton(),
action = {
editProfileActions.onSaveClick()
goBack()
}
)
}
item {
BasicTextButton(
text = AppText.delete_profile,
Modifier.textButton(),
action = editProfileActions.onDeleteClick
)
}
} }
} }
} }
@ -81,6 +96,6 @@ fun EditProfileScreen(
@Composable @Composable
fun EditProfileScreenComposable() { fun EditProfileScreenComposable() {
StudeezTheme { StudeezTheme {
EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {})) EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {}, {}))
} }
} }

View file

@ -0,0 +1,6 @@
package be.ugent.sel.studeez.screens.profile.edit_profile
data class ProfileEditUiState (
val username: String = "",
val biography: String = ""
)

View file

@ -1,8 +1,9 @@
package be.ugent.sel.studeez.screens.profile package be.ugent.sel.studeez.screens.profile.edit_profile
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import be.ugent.sel.studeez.R import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.snackbar.SnackbarManager import be.ugent.sel.studeez.common.snackbar.SnackbarManager
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.domain.AccountDAO import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.UserDAO import be.ugent.sel.studeez.domain.UserDAO
@ -23,7 +24,11 @@ class ProfileEditViewModel @Inject constructor(
init { init {
launchCatching { launchCatching {
uiState.value = uiState.value.copy(username = userDAO.getUsername()!!) val user: User = userDAO.getLoggedInUser()
uiState.value = uiState.value.copy(
username = user.username,
biography = user.biography
)
} }
} }
@ -31,16 +36,23 @@ class ProfileEditViewModel @Inject constructor(
uiState.value = uiState.value.copy(username = newValue) uiState.value = uiState.value.copy(username = newValue)
} }
fun onBiographyChange(newValue: String) {
uiState.value = uiState.value.copy(biography = newValue)
}
fun onSaveClick() { fun onSaveClick() {
launchCatching { launchCatching {
userDAO.save(uiState.value.username) userDAO.saveLoggedInUser(
newUsername = uiState.value.username,
newBiography = uiState.value.biography
)
SnackbarManager.showMessage(R.string.success) SnackbarManager.showMessage(R.string.success)
} }
} }
fun onDeleteClick(openAndPopUp: (String, String) -> Unit) { fun onDeleteClick(openAndPopUp: (String, String) -> Unit) {
launchCatching { launchCatching {
userDAO.deleteUserReferences() // Delete references userDAO.deleteLoggedInUserReferences() // Delete references
accountDAO.deleteAccount() // Delete authentication accountDAO.deleteAccount() // Delete authentication
} }
openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN) openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN)

View file

@ -0,0 +1,179 @@
package be.ugent.sel.studeez.screens.profile.public_profile
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MailOutline
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
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.Headline
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.screens.profile.AmountOfFriendsButton
import be.ugent.sel.studeez.ui.theme.StudeezTheme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import be.ugent.sel.studeez.R.string as AppText
data class PublicProfileActions(
val getUserDetails: () -> Flow<User>,
val getAmountOfFriends: () -> Flow<Int>,
val onViewFriendsClick: () -> Unit,
val sendFriendRequest: () -> Unit
)
fun getPublicProfileActions(
viewModel: PublicProfileViewModel,
open: (String) -> Unit
): PublicProfileActions {
return PublicProfileActions(
getUserDetails = { viewModel.getUserDetails(viewModel.uiState.value.userId) },
getAmountOfFriends = { viewModel.getAmountOfFriends(
userId = viewModel.uiState.value.userId
) },
onViewFriendsClick = { viewModel.onViewFriendsClick(open) },
sendFriendRequest = {
viewModel.sendFriendRequest(
userId = viewModel.uiState.value.userId
)
}
)
}
@Composable
fun PublicProfileRoute(
popUp: () -> Unit,
open: (String) -> Unit,
viewModel: PublicProfileViewModel
) {
PublicProfileScreen(
publicProfileActions = getPublicProfileActions(
viewModel = viewModel,
open = open
),
popUp = popUp
)
}
@Composable
fun PublicProfileScreen(
publicProfileActions: PublicProfileActions,
popUp: () -> Unit
) {
val user = publicProfileActions.getUserDetails().collectAsState(initial = User())
val amountOfFriends = publicProfileActions.getAmountOfFriends().collectAsState(initial = 0)
SecondaryScreenTemplate(
title = stringResource(id = AppText.profile),
popUp = popUp,
barAction = {
PublicProfileEllipsis(
sendFriendRequest = publicProfileActions.sendFriendRequest
)
}
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(15.dp)
) {
item {
Headline(text = user.value.username)
}
item {
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(align = Alignment.CenterHorizontally)
) {
AmountOfFriendsButton(
amountOfFriends = amountOfFriends.value
) {
publicProfileActions.onViewFriendsClick()
}
}
}
item {
Text(
text = user.value.biography,
textAlign = TextAlign.Center,
modifier = Modifier.padding(48.dp, 0.dp)
)
}
}
}
}
@Preview
@Composable
fun PublicProfilePreview() {
StudeezTheme {
PublicProfileScreen(
publicProfileActions = PublicProfileActions(
getUserDetails = {
flowOf(User(
id = "someid",
username = "Maxime De Poorter",
biography = "I am a different student and this is my public profile"
))
},
getAmountOfFriends = { flowOf(113) },
onViewFriendsClick = {},
sendFriendRequest = {}
),
popUp = {}
)
}
}
@Composable
fun PublicProfileEllipsis(
sendFriendRequest: () -> Unit
) {
var expanded by remember { mutableStateOf(false) }
IconButton(
onClick = { expanded = true }
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal),
contentDescription = resources().getString(AppText.view_more)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = { expanded = false }) {
DrawerEntry(
icon = Icons.Default.MailOutline,
text = stringResource(id = AppText.send_friend_request)
) {
sendFriendRequest()
}
}
}
}
@Preview
@Composable
fun PublicProfileEllipsisPreview() {
StudeezTheme {
PublicProfileEllipsis(
sendFriendRequest = {}
)
}
}

View file

@ -0,0 +1,5 @@
package be.ugent.sel.studeez.screens.profile.public_profile
data class PublicProfileUiState(
var userId: String = ""
)

View file

@ -0,0 +1,60 @@
package be.ugent.sel.studeez.screens.profile.public_profile
import androidx.compose.runtime.mutableStateOf
import be.ugent.sel.studeez.data.SelectedUserId
import be.ugent.sel.studeez.data.local.models.User
import be.ugent.sel.studeez.domain.FriendshipDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.UserDAO
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@HiltViewModel
class PublicProfileViewModel @Inject constructor(
private val userDAO: UserDAO,
private val friendshipDAO: FriendshipDAO,
selectedUserIdState: SelectedUserId,
logService: LogService
): StudeezViewModel(logService) {
val uiState = mutableStateOf(
PublicProfileUiState(
userId = selectedUserIdState.value
)
)
fun getUserDetails(
userId: String
): Flow<User> {
uiState.value = uiState.value.copy(
userId = userId
)
return userDAO.getUserDetails(
userId = uiState.value.userId
)
}
fun getAmountOfFriends(
userId: String
): Flow<Int> {
return friendshipDAO.getFriendshipCount(
userId = userId
)
}
fun onViewFriendsClick(
open: (String) -> Unit
) {
open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN)
}
fun sendFriendRequest(
userId: String
) {
friendshipDAO.sendFriendshipRequest(userId)
}
}

View file

@ -10,9 +10,9 @@ object InvisibleSessionManager {
private var viewModel: SessionViewModel? = null private var viewModel: SessionViewModel? = null
private lateinit var mediaPlayer: MediaPlayer private lateinit var mediaPlayer: MediaPlayer
fun setParameters(viewModel: SessionViewModel, mediaplayer: MediaPlayer) { fun setParameters(viewModel: SessionViewModel, mediaPlayer: MediaPlayer) {
this.mediaPlayer = mediaPlayer
this.viewModel = viewModel this.viewModel = viewModel
this.mediaPlayer = mediaplayer
} }
suspend fun updateTimer() { suspend fun updateTimer() {

View file

@ -6,28 +6,22 @@ import android.net.Uri
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.screens.session.sessionScreens.AbstractSessionScreen import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreenComposable
import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreen
data class SessionActions( data class SessionActions(
val getTimer: () -> FunctionalTimer, val getTimer: () -> FunctionalTimer,
val getTask: () -> String, val getTask: () -> String,
val startMediaPlayer: () -> Unit,
val releaseMediaPlayer: () -> Unit,
val endSession: () -> Unit val endSession: () -> Unit
) )
private fun getSessionActions( private fun getSessionActions(
viewModel: SessionViewModel, viewModel: SessionViewModel,
openAndPopUp: (String, String) -> Unit, openAndPopUp: (String, String) -> Unit,
mediaplayer: MediaPlayer,
): SessionActions { ): SessionActions {
return SessionActions( return SessionActions(
getTimer = viewModel::getTimer, getTimer = viewModel::getTimer,
getTask = viewModel::getTask, getTask = viewModel::getTask,
endSession = { viewModel.endSession(openAndPopUp) }, endSession = { viewModel.endSession(openAndPopUp) },
startMediaPlayer = mediaplayer::start,
releaseMediaPlayer = mediaplayer::release,
) )
} }
@ -37,20 +31,15 @@ fun SessionRoute(
openAndPopUp: (String, String) -> Unit, openAndPopUp: (String, String) -> Unit,
viewModel: SessionViewModel, viewModel: SessionViewModel,
) { ) {
val context = LocalContext.current
val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val mediaplayer = MediaPlayer.create(context, uri) val mediaPlayer = MediaPlayer.create(LocalContext.current, uri)
mediaplayer.isLooping = false mediaPlayer.isLooping = false
InvisibleSessionManager.setParameters( InvisibleSessionManager.setParameters(viewModel = viewModel, mediaPlayer = mediaPlayer)
viewModel = viewModel,
mediaplayer = mediaplayer
)
val sessionScreen: AbstractSessionScreen = viewModel.getTimer().accept(GetSessionScreen(mediaplayer)) val soundPlayer = SoundPlayer(LocalContext.current)
val sessionActions = getSessionActions(viewModel, openAndPopUp)
val sessionScreen = viewModel.getTimer().accept(GetSessionScreenComposable(soundPlayer, open, sessionActions))
sessionScreen( sessionScreen()
open = open,
sessionActions = getSessionActions(viewModel, openAndPopUp, mediaplayer)
)
} }

View file

@ -1,7 +1,8 @@
package be.ugent.sel.studeez.screens.session package be.ugent.sel.studeez.screens.session
import be.ugent.sel.studeez.data.SelectedTimerState import be.ugent.sel.studeez.data.SelectedSessionReport
import be.ugent.sel.studeez.data.SessionReportState import be.ugent.sel.studeez.data.SelectedTask
import be.ugent.sel.studeez.data.SelectedTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.navigation.StudeezDestinations import be.ugent.sel.studeez.navigation.StudeezDestinations
@ -11,23 +12,21 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SessionViewModel @Inject constructor( class SessionViewModel @Inject constructor(
private val selectedTimerState: SelectedTimerState, private val selectedTimer: SelectedTimer,
private val sessionReportState: SessionReportState, private val sessionReport: SelectedSessionReport,
private val selectedTask: SelectedTask,
logService: LogService logService: LogService
) : StudeezViewModel(logService) { ) : StudeezViewModel(logService) {
fun getTimer(): FunctionalTimer {
private val task : String = "No task selected" // placeholder for tasks implementation return selectedTimer()
fun getTimer() : FunctionalTimer {
return selectedTimerState.selectedTimer!!
} }
fun getTask(): String { fun getTask(): String {
return task return selectedTask().name
} }
fun endSession(openAndPopUp: (String, String) -> Unit) { fun endSession(openAndPopUp: (String, String) -> Unit) {
sessionReportState.sessionReport = getTimer().getSessionReport() sessionReport.set(getTimer().getSessionReport(selectedTask().subjectId, selectedTask().id))
openAndPopUp(StudeezDestinations.SESSION_RECAP, StudeezDestinations.SESSION_SCREEN) openAndPopUp(StudeezDestinations.SESSION_RECAP, StudeezDestinations.SESSION_SCREEN)
} }
} }

View file

@ -0,0 +1,29 @@
package be.ugent.sel.studeez.screens.session
import android.content.Context
import android.media.MediaPlayer
import android.media.RingtoneManager
class SoundPlayer(private val context: Context) {
var oldValue: Boolean = false
var mediaPlayer: MediaPlayer = initPlayer()
fun playOn(newValue: Boolean) {
if (oldValue != newValue) {
mediaPlayer.start()
mediaPlayer.setOnCompletionListener {
mediaPlayer = initPlayer()
}
oldValue = newValue
}
}
private fun initPlayer(): MediaPlayer {
return MediaPlayer.create(
context,
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
)
}
}

View file

@ -1,143 +0,0 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
import be.ugent.sel.studeez.screens.session.SessionActions
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
abstract class AbstractSessionScreen {
@Composable
operator fun invoke(
open: (String) -> Unit,
sessionActions: SessionActions,
) {
Column(
modifier = Modifier.padding(10.dp)
) {
Timer(
sessionActions = sessionActions,
)
Box(
contentAlignment = Alignment.Center, modifier = Modifier
.fillMaxWidth()
.padding(50.dp)
) {
TextButton(
onClick = {
sessionActions.releaseMediaPlayer
sessionActions.endSession()
},
modifier = Modifier
.padding(horizontal = 20.dp)
.border(1.dp, Color.Red, RoundedCornerShape(32.dp))
.background(Color.Transparent)
) {
Text(
text = "End session",
color = Color.Red,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
modifier = Modifier.padding(1.dp)
)
}
}
}
}
@Composable
fun Timer(
sessionActions: SessionActions,
) {
var tikker by remember { mutableStateOf(false) }
LaunchedEffect(tikker) {
delay(1.seconds)
sessionActions.getTimer().tick()
callMediaPlayer()
tikker = !tikker
}
val hms = sessionActions.getTimer().getHoursMinutesSeconds()
Column {
Text(
text = hms.toString(),
modifier = Modifier
.fillMaxWidth()
.padding(50.dp),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 40.sp,
)
Text(
text = motivationString(),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
fontSize = 30.sp
)
Box(
contentAlignment = Alignment.Center, modifier = Modifier
.fillMaxWidth()
.padding(50.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(16.dp)
.background(Color.Blue, RoundedCornerShape(32.dp))
) {
Text(
text = sessionActions.getTask(),
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 20.dp)
)
}
}
}
}
@Composable
abstract fun motivationString(): String
abstract fun callMediaPlayer()
}
@Preview
@Composable
fun TimerPreview() {
val sessionScreen = object : AbstractSessionScreen() {
@Composable
override fun motivationString(): String = "Test"
override fun callMediaPlayer() {}
}
sessionScreen.Timer(sessionActions = SessionActions({ FunctionalEndlessTimer() }, { "Preview" }, {}, {}, {}))
}

View file

@ -1,45 +0,0 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import android.media.MediaPlayer
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.R.string as AppText
class BreakSessionScreen(
private val funPomoDoroTimer: FunctionalPomodoroTimer,
private var mediaplayer: MediaPlayer?
): AbstractSessionScreen() {
@Composable
override fun motivationString(): String {
if (funPomoDoroTimer.isInBreak) {
return resources().getString(AppText.state_take_a_break)
}
if (funPomoDoroTimer.hasEnded()) {
return resources().getString(AppText.state_done)
}
return resources().getQuantityString(
R.plurals.state_focus_remaining,
funPomoDoroTimer.breaksRemaining,
funPomoDoroTimer.breaksRemaining
)
}
override fun callMediaPlayer() {
if (funPomoDoroTimer.hasEnded()) {
mediaplayer?.let { it: MediaPlayer ->
it.setOnCompletionListener {
it.release()
mediaplayer = null
}
it.start()
}
} else if (funPomoDoroTimer.hasCurrentCountdownEnded()) {
mediaplayer?.start()
}
}
}

View file

@ -0,0 +1,79 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
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.unit.dp
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.screens.session.SessionActions
import be.ugent.sel.studeez.screens.session.SoundPlayer
@Composable
fun BreakSessionScreenComposable(
open: (String) -> Unit,
sessionActions: SessionActions,
pomodoroTimer: FunctionalPomodoroTimer,
soundPlayer: SoundPlayer,
) {
SessionScreen(
open = open,
sessionActions = sessionActions,
midSection = { Dots(pomodoroTimer = pomodoroTimer) },
callMediaPlayer = { soundPlayer.playOn(pomodoroTimer.hasCurrentCountdownEnded()) },
motivationString = { motivationString (pomodoroTimer = pomodoroTimer) }
)
}
@Composable
private fun Dots(pomodoroTimer: FunctionalPomodoroTimer): Int {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
if (pomodoroTimer.hasEnded()) {
repeat(pomodoroTimer.repeats) {
Dot(Color.Green)
}
} else {
repeat(pomodoroTimer.repeats - pomodoroTimer.breaksRemaining - 1) {
Dot(color = Color.DarkGray)
}
if (!pomodoroTimer.isInBreak) Dot(Color.Green) else Dot(Color.DarkGray)
repeat(pomodoroTimer.breaksRemaining) {
Dot(color = Color.Gray)
}
}
}
return pomodoroTimer.breaksRemaining
}
@Composable
private fun Dot(color: Color) {
Box(modifier = Modifier
.padding(5.dp)
.size(10.dp)
.clip(CircleShape)
.background(color))
}
@Composable
private fun motivationString(pomodoroTimer: FunctionalPomodoroTimer): String {
if (pomodoroTimer.isInBreak) {
return resources().getString(R.string.state_take_a_break)
}
if (pomodoroTimer.hasEnded()) {
return resources().getString(R.string.state_done)
}
return resources().getString(R.string.state_focus)
}

View file

@ -1,35 +0,0 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import android.media.MediaPlayer
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.R.string as AppText
class CustomSessionScreen(
private val functionalTimer: FunctionalCustomTimer,
private var mediaplayer: MediaPlayer?
): AbstractSessionScreen() {
@Composable
override fun motivationString(): String {
if (functionalTimer.hasEnded()) {
return resources().getString(AppText.state_done)
}
return resources().getString(AppText.state_focus)
}
override fun callMediaPlayer() {
if (functionalTimer.hasEnded()) {
mediaplayer?.let { it: MediaPlayer ->
it.setOnCompletionListener {
it.release()
mediaplayer = null
}
it.start()
}
}
}
}

View file

@ -0,0 +1,32 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.screens.session.SessionActions
import be.ugent.sel.studeez.screens.session.SoundPlayer
@Composable
fun CustomTimerSessionScreenComposable(
open: (String) -> Unit,
sessionActions: SessionActions,
customTimer: FunctionalCustomTimer,
soundPlayer: SoundPlayer
) {
SessionScreen(
open = open,
callMediaPlayer = { soundPlayer.playOn(customTimer.hasEnded()) },
sessionActions = sessionActions
) {
motivationString(customTimer = customTimer)
}
}
@Composable
private fun motivationString(customTimer: FunctionalCustomTimer): String {
if (customTimer.hasEnded()) {
return resources().getString(R.string.state_done)
}
return resources().getString(R.string.state_focus)
}

View file

@ -1,16 +0,0 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.R.string as AppText
class EndlessSessionScreen : AbstractSessionScreen() {
@Composable
override fun motivationString(): String {
return resources().getString(AppText.state_focus)
}
override fun callMediaPlayer() {}
}

View file

@ -0,0 +1,24 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.screens.session.SessionActions
@Composable
fun EndlessTimerSessionScreenComposable(
open: (String) -> Unit,
sessionActions: SessionActions,
) {
SessionScreen(
open = open,
sessionActions = sessionActions
) {
motivationString()
}
}
@Composable
private fun motivationString(): String {
return resources().getString(R.string.state_focus)
}

View file

@ -1,18 +0,0 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import android.media.MediaPlayer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor
class GetSessionScreen(private val mediaplayer: MediaPlayer?) : FunctionalTimerVisitor<AbstractSessionScreen> {
override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): AbstractSessionScreen =
CustomSessionScreen(functionalCustomTimer, mediaplayer)
override fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): AbstractSessionScreen =
EndlessSessionScreen()
override fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): AbstractSessionScreen =
BreakSessionScreen(functionalPomodoroTimer, mediaplayer)
}

View file

@ -0,0 +1,47 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor
import be.ugent.sel.studeez.screens.session.SessionActions
import be.ugent.sel.studeez.screens.session.SoundPlayer
class GetSessionScreenComposable(
private val soundPlayer: SoundPlayer,
private val open: (String) -> Unit,
private val sessionActions: SessionActions
) :
FunctionalTimerVisitor<@Composable () -> Unit> {
override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): @Composable () -> Unit {
return { CustomTimerSessionScreenComposable(
open = open,
sessionActions = sessionActions,
soundPlayer = soundPlayer,
customTimer = functionalCustomTimer,
)
}
}
override fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): @Composable () -> Unit {
return {
EndlessTimerSessionScreenComposable(
open = open,
sessionActions = sessionActions,
)
}
}
override fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): @Composable () -> Unit {
return {
BreakSessionScreenComposable(
open = open,
sessionActions = sessionActions,
soundPlayer = soundPlayer,
pomodoroTimer = functionalPomodoroTimer
)
}
}
}

Some files were not shown because too many files have changed in this diff Show more