diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cb..773fe0f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,6 @@ - - + diff --git a/app/build.gradle b/app/build.gradle index a19cbd7..fc2bd08 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -123,9 +123,6 @@ dependencies { implementation 'com.google.firebase:firebase-firestore-ktx' implementation 'com.google.firebase:firebase-perf-ktx' implementation 'com.google.firebase:firebase-config-ktx' - - // Colorpicker - implementation 'com.github.skydoves:colorpicker-compose:1.0.2' } // Allow references to generate code @@ -150,4 +147,4 @@ protobuf { } } } -} +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/FabTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/FabTest.kt new file mode 100644 index 0000000..fbd6968 --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/FabTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/HomeScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/HomeScreenTest.kt new file mode 100644 index 0000000..6906683 --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/HomeScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/ExampleInstrumentedTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/InstrumentedTest.kt similarity index 95% rename from app/src/androidTest/java/be/ugent/sel/studeez/ExampleInstrumentedTest.kt rename to app/src/androidTest/java/be/ugent/sel/studeez/InstrumentedTest.kt index 06f9435..d6a1522 100644 --- a/app/src/androidTest/java/be/ugent/sel/studeez/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/be/ugent/sel/studeez/InstrumentedTest.kt @@ -14,7 +14,7 @@ import org.junit.Assert.* * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { +class InstrumentedTest { @Test fun useAppContext() { // Context of the app under test. diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/LoginScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/LoginScreenTest.kt new file mode 100644 index 0000000..9498241 --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/LoginScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/ProfileEditScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/ProfileEditScreenTest.kt new file mode 100644 index 0000000..43c8240 --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/ProfileEditScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/ProfileScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/ProfileScreenTest.kt new file mode 100644 index 0000000..14e077d --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/ProfileScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/SessionRecapScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/SessionRecapScreenTest.kt new file mode 100644 index 0000000..829152b --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/SessionRecapScreenTest.kt @@ -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) + } + + +} diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/SignUpScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/SignUpScreenTest.kt new file mode 100644 index 0000000..c7b5f41 --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/SignUpScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/SplashScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/SplashScreenTest.kt new file mode 100644 index 0000000..6a43119 --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/SplashScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/SubjectScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/SubjectScreenTest.kt new file mode 100644 index 0000000..d4b5c68 --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/SubjectScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/TaskScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/TaskScreenTest.kt new file mode 100644 index 0000000..0f7a8b8 --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/TaskScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/TimerOverviewScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/TimerOverviewScreenTest.kt new file mode 100644 index 0000000..357bed4 --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/TimerOverviewScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/TimerSelectionScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/TimerSelectionScreenTest.kt new file mode 100644 index 0000000..f055daa --- /dev/null +++ b/app/src/androidTest/java/be/ugent/sel/studeez/TimerSelectionScreenTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt index 73ae1b5..be799d2 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt @@ -2,19 +2,9 @@ package be.ugent.sel.studeez.common.composable import androidx.annotation.StringRes import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonColors -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.TextButton +import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add 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 @Composable -fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { +fun BasicTextButton( + @StringRes text: Int, + modifier: Modifier, + action: () -> Unit +) { TextButton( onClick = action, modifier = modifier @@ -48,6 +42,7 @@ fun BasicButton( modifier: Modifier = Modifier, colors: ButtonColors = ButtonDefaults.buttonColors(), border: BorderStroke? = null, + enabled: Boolean = true, onClick: () -> Unit, ) { Button( @@ -56,6 +51,7 @@ fun BasicButton( shape = defaultButtonShape(), colors = colors, border = border, + enabled = enabled, ) { Text( text = stringResource(text), @@ -74,17 +70,22 @@ fun BasicButtonPreview() { fun StealthButton( @StringRes text: Int, modifier: Modifier = Modifier.card(), + enabled: Boolean = true, onClick: () -> Unit, ) { + //val clickablemodifier = if (disabled) Modifier.clickable(indication = null) else modifier + val borderColor = if (enabled) MaterialTheme.colors.primary + else MaterialTheme.colors.onSurface.copy(alpha = 0.3f) BasicButton( text = text, onClick = onClick, modifier = modifier, + enabled = enabled, colors = ButtonDefaults.buttonColors( backgroundColor = MaterialTheme.colors.surface, - contentColor = 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) ) } diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt index bc40ead..ea2b52d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt @@ -2,7 +2,6 @@ package be.ugent.sel.studeez.common.composable import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.updateTransition -import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.material.FloatingActionButton import androidx.compose.material.Icon diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/FormComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/FormComposable.kt new file mode 100644 index 0000000..1fbcfb2 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/FormComposable.kt @@ -0,0 +1,22 @@ +package be.ugent.sel.studeez.common.composable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun FormComposable( + title: String, + popUp: () -> Unit, + content: @Composable () -> Unit, +) { + SecondaryScreenTemplate(title = title, popUp = popUp) { + Box( + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/ImageComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/ImageComposable.kt new file mode 100644 index 0000000..39e7272 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/ImageComposable.kt @@ -0,0 +1,39 @@ +package be.ugent.sel.studeez.common.composable + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp + +@Composable +fun ImageBackgroundButton( + paint: Painter, + str: String, + background2: Color, + setBackground1: (Color) -> Unit, + setBackground2: (Color) -> Unit +) { + Image( + painter = paint, + str, + modifier = Modifier + .clickable { + if (background2 == Color.Transparent) { + setBackground1(Color.LightGray) + setBackground2(Color.Transparent) + } else { + setBackground2(Color.Transparent) + } + } + .border( + width = 2.dp, + color = background2, + shape = RoundedCornerShape(16.dp) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/ProfilePictureComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/ProfilePictureComposable.kt new file mode 100644 index 0000000..c214088 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/ProfilePictureComposable.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextComposable.kt index 1b921a9..425d8f6 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextComposable.kt @@ -3,10 +3,13 @@ package be.ugent.sel.studeez.common.composable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable @@ -23,4 +26,14 @@ fun Headline( fontSize = 34.sp ) } +} + +@Composable +fun DateText(date: String) { + Text( + text = date, + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + modifier = Modifier.padding(horizontal = 10.dp) + ) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt index aadcee3..880157f 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt @@ -3,13 +3,13 @@ package be.ugent.sel.studeez.common.composable import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import be.ugent.sel.studeez.common.ext.fieldModifier import be.ugent.sel.studeez.resources -import kotlin.math.sin import be.ugent.sel.studeez.R.drawable as AppIcon import be.ugent.sel.studeez.R.string as AppText @@ -47,7 +46,7 @@ fun LabelledInputField( value: String, onNewValue: (String) -> Unit, @StringRes label: Int, - singleLine: Boolean = false + singleLine: Boolean = true ) { OutlinedTextField( value = value, @@ -119,7 +118,9 @@ fun LabeledErrorTextField( initialValue: String, @StringRes label: Int, singleLine: Boolean = false, - errorText: Int, + isValid: MutableState = remember { mutableStateOf(true) }, + isFirst: MutableState = remember { mutableStateOf(false) }, + @StringRes errorText: Int, keyboardType: KeyboardType, predicate: (String) -> Boolean, onNewCorrectValue: (String) -> Unit @@ -128,31 +129,28 @@ fun LabeledErrorTextField( mutableStateOf(initialValue) } - var isValid by remember { - mutableStateOf(predicate(value)) - } - Column { OutlinedTextField( modifier = modifier.fieldModifier(), value = value, onValueChange = { newText -> + isFirst.value = false value = newText - isValid = predicate(value) - if (isValid) { + isValid.value = predicate(value) + if (isValid.value) { onNewCorrectValue(newText) } }, singleLine = singleLine, label = { Text(text = stringResource(id = label)) }, - isError = !isValid, + isError = !isValid.value && !isFirst.value, keyboardOptions = KeyboardOptions( keyboardType = keyboardType, imeAction = ImeAction.Done ) ) - if (!isValid) { + if (!isValid.value && !isFirst.value) { Text( modifier = Modifier.padding(start = 16.dp), text = stringResource(id = errorText), @@ -218,4 +216,36 @@ private fun PasswordField( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), 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 + ) + ) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/Feed.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/Feed.kt new file mode 100644 index 0000000..fb18f02 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/Feed.kt @@ -0,0 +1,160 @@ +package be.ugent.sel.studeez.common.composable.feed + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import be.ugent.sel.studeez.common.composable.BasicTextButton +import be.ugent.sel.studeez.common.composable.DateText +import be.ugent.sel.studeez.common.composable.Headline +import be.ugent.sel.studeez.common.ext.textButton +import be.ugent.sel.studeez.data.local.models.FeedEntry +import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds +import be.ugent.sel.studeez.R.string as AppText + +@Composable +fun Feed( + uiState: FeedUiState, + continueTask: (String, String) -> Unit, + onEmptyFeedHelp: () -> Unit +) { + when (uiState) { + FeedUiState.Loading -> LoadingFeed() + is FeedUiState.Succes -> LoadedFeed( + uiState = uiState, + continueTask = continueTask, + onEmptyFeedHelp = onEmptyFeedHelp + ) + } +} + +@Composable +fun LoadedFeed( + uiState: FeedUiState.Succes, + continueTask: (String, String) -> Unit, + onEmptyFeedHelp: () -> Unit, +) { + if (uiState.feedEntries.isEmpty()) EmptyFeed(onEmptyFeedHelp) + else FeedWithElements(uiState = uiState, continueTask = continueTask) +} + +@Composable +fun LoadingFeed() { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator(color = MaterialTheme.colors.onBackground) + } +} + +@Composable +fun FeedWithElements( + uiState: FeedUiState.Succes, + continueTask: (String, String) -> Unit, +) { + val feedEntries = uiState.feedEntries + LazyColumn { + items(feedEntries.toList()) { (date, feedEntries) -> + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val totalDayStudyTime: Int = feedEntries.sumOf { it.totalStudyTime } + DateText(date = date) + Text( + text = "${HoursMinutesSeconds(totalDayStudyTime)}", + fontSize = 15.sp, + fontWeight = FontWeight.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 {} }, {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedEntry.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedEntry.kt new file mode 100644 index 0000000..016090d --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedEntry.kt @@ -0,0 +1,116 @@ +package be.ugent.sel.studeez.common.composable.feed + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import be.ugent.sel.studeez.common.composable.StealthButton +import be.ugent.sel.studeez.data.local.models.FeedEntry +import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds +import be.ugent.sel.studeez.R.string as AppText + +@Composable +fun FeedEntry( + feedEntry: FeedEntry, + continueWithTask: () -> Unit, +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp, vertical = 5.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(start = 10.dp) + .weight(11f) + ) { + Box( + modifier = Modifier + .size(20.dp) + .clip(CircleShape) + .background(Color(feedEntry.argb_color)), + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp) + ) { + Text( + text = feedEntry.subJectName, + fontWeight = FontWeight.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, + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedUiState.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedUiState.kt new file mode 100644 index 0000000..1b938ca --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedUiState.kt @@ -0,0 +1,8 @@ +package be.ugent.sel.studeez.common.composable.feed + +import be.ugent.sel.studeez.data.local.models.FeedEntry + +sealed interface FeedUiState { + object Loading : FeedUiState + data class Succes(val feedEntries: Map>) : FeedUiState +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedViewModel.kt new file mode 100644 index 0000000..b5e2b1a --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/feed/FeedViewModel.kt @@ -0,0 +1,45 @@ +package be.ugent.sel.studeez.common.composable.feed + +import androidx.lifecycle.viewModelScope +import be.ugent.sel.studeez.data.SelectedTask +import be.ugent.sel.studeez.domain.FeedDAO +import be.ugent.sel.studeez.domain.LogService +import be.ugent.sel.studeez.domain.TaskDAO +import be.ugent.sel.studeez.navigation.StudeezDestinations +import be.ugent.sel.studeez.screens.StudeezViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class FeedViewModel @Inject constructor( + feedDAO: FeedDAO, + private val taskDAO: TaskDAO, + private val selectedTask: SelectedTask, + logService: LogService +) : StudeezViewModel(logService) { + + val uiState: StateFlow = feedDAO.getFeedEntries() + .map { FeedUiState.Succes(it) } + .stateIn( + scope = viewModelScope, + initialValue = FeedUiState.Loading, + started = SharingStarted.Eagerly, + ) + + fun continueTask(open: (String) -> Unit, subjectId: String, taskId: String) { + viewModelScope.launch { + val task = taskDAO.getTask(subjectId, taskId) + selectedTask.set(task) + open(StudeezDestinations.TIMER_SELECTION_SCREEN) + } + } + + fun onEmptyFeedHelp(open: (String) -> Unit) { + open(StudeezDestinations.ADD_SUBJECT_FORM) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarComposable.kt index c4d6e33..ecf26b0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarComposable.kt @@ -8,13 +8,15 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.DateRange +import androidx.compose.material.icons.outlined.Face import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import be.ugent.sel.studeez.navigation.StudeezDestinations.FRIENDS_FEED import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN -import be.ugent.sel.studeez.navigation.StudeezDestinations.SESSIONS_SCREEN import be.ugent.sel.studeez.navigation.StudeezDestinations.SUBJECT_SCREEN import be.ugent.sel.studeez.resources import be.ugent.sel.studeez.ui.theme.StudeezTheme @@ -99,11 +101,11 @@ fun NavigationBar( BottomNavigationItem( 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)) }, - selected = navigationBarActions.isSelectedTab(SESSIONS_SCREEN), + label = { Text(text = resources().getString(AppText.friends_feed)) }, + selected = navigationBarActions.isSelectedTab(FRIENDS_FEED), onClick = navigationBarActions.onSessionsClick ) diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarViewModel.kt index 07a5bf9..336ad69 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/navbar/NavigationBarViewModel.kt @@ -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.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.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.screens.StudeezViewModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -25,7 +27,7 @@ class NavigationBarViewModel @Inject constructor( } fun onSessionsClick(open: (String) -> Unit) { - open(SESSIONS_SCREEN) + open(FRIENDS_FEED) } fun onProfileClick(open: (String) -> Unit) { @@ -33,13 +35,11 @@ class NavigationBarViewModel @Inject constructor( } fun onAddTaskClick(open: (String) -> Unit) { - // TODO open(CREATE_TASK_SCREEN) - SnackbarManager.showMessage(AppText.create_task_not_possible_yet) // TODO Remove + open(SELECT_SUBJECT) } fun onAddFriendClick(open: (String) -> Unit) { - // TODO open(SEARCH_FRIENDS_SCREEN) - SnackbarManager.showMessage(AppText.add_friend_not_possible_yet) // TODO Remove + open(SEARCH_FRIENDS_SCREEN) } fun onAddSessionClick(open: (String) -> Unit) { diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt index 8655ba3..1c98af2 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt @@ -1,20 +1,17 @@ package be.ugent.sel.studeez.common.composable.tasks import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Card import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.List import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -24,16 +21,24 @@ 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.R.string as AppText import be.ugent.sel.studeez.common.composable.StealthButton import be.ugent.sel.studeez.data.local.models.task.Subject import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import be.ugent.sel.studeez.R.string as AppText @Composable fun SubjectEntry( subject: Subject, - onViewSubject: () -> Unit, + getTaskCount: () -> Flow, + getCompletedTaskCount: () -> Flow, + getStudyTime: () -> Flow, + selectButton: @Composable (RowScope) -> Unit, ) { + val studytime by getStudyTime().collectAsState(initial = 0) + val taskCount by getTaskCount().collectAsState(initial = 0) + val completedTaskCount by getCompletedTaskCount().collectAsState(initial = 0) Card( modifier = Modifier .fillMaxWidth() @@ -61,16 +66,17 @@ fun SubjectEntry( ) { Text( text = subject.name, - fontWeight = FontWeight.Bold, overflow = TextOverflow.Ellipsis, maxLines = 1, + fontWeight = FontWeight.Medium ) Row( horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Text( - text = HoursMinutesSeconds(subject.time).toString(), + text = HoursMinutesSeconds(studytime).toString(), + color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f) ) Row( verticalAlignment = Alignment.CenterVertically, @@ -78,21 +84,18 @@ fun SubjectEntry( ) { Icon( 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( - text = AppText.view_tasks, - modifier = Modifier - .padding(start = 10.dp, end = 5.dp) - .weight(1f) - ) { - onViewSubject() - } + selectButton(this) } } } @@ -104,9 +107,17 @@ fun SubjectEntryPreview() { subject = Subject( name = "Test Subject", 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 @@ -116,7 +127,9 @@ fun OverflowSubjectEntryPreview() { subject = Subject( name = "Testttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt", argb_color = 0xFFFFD200, - time = 60 ), + getTaskCount = { flowOf() }, + getCompletedTaskCount = { flowOf() }, + getStudyTime = { flowOf() }, ) {} } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/TaskEntry.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/TaskEntry.kt index fefb924..35e7a44 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/TaskEntry.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/TaskEntry.kt @@ -1,17 +1,7 @@ package be.ugent.sel.studeez.common.composable.tasks -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -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.foundation.layout.* +import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.runtime.Composable @@ -31,7 +21,8 @@ import be.ugent.sel.studeez.resources fun TaskEntry( task: Task, onCheckTask: (Boolean) -> Unit, - onDeleteTask: () -> Unit, + onArchiveTask: () -> Unit, + onStartTask: () -> Unit ) { Card( modifier = Modifier @@ -80,7 +71,7 @@ fun TaskEntry( Box(modifier = Modifier.weight(7f)) { if (task.completed) { IconButton( - onClick = onDeleteTask, + onClick = onArchiveTask, modifier = Modifier .padding(start = 20.dp) ) { @@ -95,6 +86,7 @@ fun TaskEntry( modifier = Modifier .padding(end = 5.dp), ) { + onStartTask() } } } @@ -110,7 +102,7 @@ fun TaskEntryPreview() { name = "Test Task", completed = false, ), - {}, {}, + {}, {}, {} ) } @@ -122,7 +114,7 @@ fun CompletedTaskEntryPreview() { name = "Test Task", completed = true, ), - {}, {}, + {}, {}, {}, ) } @@ -134,6 +126,6 @@ fun OverflowTaskEntryPreview() { name = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkkkk", completed = false, ), - {}, {}, + {}, {}, {} ) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/ext/ColorExt.kt b/app/src/main/java/be/ugent/sel/studeez/common/ext/ColorExt.kt new file mode 100644 index 0000000..87ce226 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/ext/ColorExt.kt @@ -0,0 +1,10 @@ +package be.ugent.sel.studeez.common.ext + +import androidx.compose.ui.graphics.Color +import kotlin.random.Random + +fun Color.Companion.generateRandomArgb(): Long { + val random = Random + val mask: Long = (0x000000FFL shl random.nextInt(0, 3)).inv() + return random.nextLong(0xFF000000L, 0xFFFFFFFFL) and mask +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/EditTimerState.kt b/app/src/main/java/be/ugent/sel/studeez/data/EditTimerState.kt deleted file mode 100644 index dceec8c..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/data/EditTimerState.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt b/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt new file mode 100644 index 0000000..ebe8589 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt @@ -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 { + abstract var value: T + operator fun invoke() = value + fun set(newValue: T) { + this.value = newValue + } +} + +@Singleton +class SelectedSessionReport @Inject constructor() : SelectedState() { + override lateinit var value: SessionReport +} + +@Singleton +class SelectedTask @Inject constructor() : SelectedState() { + override lateinit var value: Task +} + +@Singleton +class SelectedTimer @Inject constructor() : SelectedState() { + override lateinit var value: FunctionalTimer +} + +@Singleton +class SelectedSubject @Inject constructor() : SelectedState() { + override lateinit var value: Subject +} + +@Singleton +class SelectedTimerInfo @Inject constructor() : SelectedState() { + override lateinit var value: TimerInfo +} + +@Singleton +class SelectedUserId @Inject constructor( + userDAO: UserDAO +): SelectedState() { + override var value: String = userDAO.getCurrentUserId() +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/SelectedSubject.kt b/app/src/main/java/be/ugent/sel/studeez/data/SelectedSubject.kt deleted file mode 100644 index fbc7e48..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/data/SelectedSubject.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/SelectedTask.kt b/app/src/main/java/be/ugent/sel/studeez/data/SelectedTask.kt deleted file mode 100644 index 9c3f042..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/data/SelectedTask.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/SelectedTimerState.kt b/app/src/main/java/be/ugent/sel/studeez/data/SelectedTimerState.kt deleted file mode 100644 index f8fcebd..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/data/SelectedTimerState.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/SessionReportState.kt b/app/src/main/java/be/ugent/sel/studeez/data/SessionReportState.kt deleted file mode 100644 index 47770d0..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/data/SessionReportState.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/FeedEntry.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/FeedEntry.kt new file mode 100644 index 0000000..8733c48 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/FeedEntry.kt @@ -0,0 +1,14 @@ +package be.ugent.sel.studeez.data.local.models + +import com.google.firebase.Timestamp + +data class FeedEntry( + val argb_color: Long = 0, + val subJectName: String = "", + val taskName: String = "", + val taskId: String = "", // Name of task is not unique + val subjectId: String = "", + val totalStudyTime: Int = 0, + val endTime: Timestamp = Timestamp(0, 0), + val isArchived: Boolean = false +) diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/Friendship.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/Friendship.kt new file mode 100644 index 0000000..98aa9a5 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/Friendship.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/SessionReport.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/SessionReport.kt index 20a44a8..5835538 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/local/models/SessionReport.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/SessionReport.kt @@ -6,5 +6,7 @@ import com.google.firebase.firestore.DocumentId data class SessionReport( @DocumentId val id: String = "", val studyTime: Int = 0, - val endTime: Timestamp = Timestamp(0, 0) + val endTime: Timestamp = Timestamp(0, 0), + val taskId: String = "", + val subjectId: String = "" ) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt index 2fba2ce..a92bebb 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/User.kt @@ -1,3 +1,9 @@ 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 = "" +) diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt index e84c2bb..88c48c0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt @@ -5,6 +5,13 @@ import com.google.firebase.firestore.DocumentId data class Subject( @DocumentId val id: String = "", val name: String = "", - val time: Int = 0, val argb_color: Long = 0, -) \ No newline at end of file + var archived: Boolean = false, +) + +object SubjectDocument { + const val id = "id" + const val name = "name" + const val archived = "archived" + const val argb_color = "argb_color" +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Task.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Task.kt index f2618db..ff2748d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Task.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Task.kt @@ -5,9 +5,10 @@ import com.google.firebase.firestore.DocumentId data class Task( @DocumentId val id: String = "", val name: String = "", - val completed: Boolean = false, + var completed: Boolean = false, val time: Int = 0, val subjectId: String = "", + var archived: Boolean = false, ) object TaskDocument { @@ -16,4 +17,5 @@ object TaskDocument { const val completed = "completed" const val time = "time" const val subjectId = "subjectId" + const val archived = "archived" } diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt index 6d4f868..f5237d6 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt @@ -2,17 +2,17 @@ package be.ugent.sel.studeez.data.local.models.timer_functional class FunctionalPomodoroTimer( private var studyTime: Int, - private var breakTime: Int, repeats: Int + private var breakTime: Int, + val repeats: Int ) : FunctionalTimer(studyTime) { - var breaksRemaining = repeats + var breaksRemaining = repeats - 1 var isInBreak = false override fun tick() { if (hasEnded()) { return } - if (hasCurrentCountdownEnded()) { if (isInBreak) { breaksRemaining-- diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimer.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimer.kt index 1f4231a..be057b8 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimer.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalTimer.kt @@ -17,10 +17,12 @@ abstract class FunctionalTimer(initialValue: Int) { abstract fun hasCurrentCountdownEnded(): Boolean - fun getSessionReport(): SessionReport { + fun getSessionReport(subjectId: String, taskId: String): SessionReport { return SessionReport( studyTime = totalStudyTime, - endTime = Timestamp.now() + endTime = Timestamp.now(), + taskId = taskId, + subjectId = subjectId ) } diff --git a/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseFriendship.kt b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseFriendship.kt new file mode 100644 index 0000000..fb2af4b --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseFriendship.kt @@ -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" +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseSessionReport.kt b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseSessionReport.kt new file mode 100644 index 0000000..f33718f --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseSessionReport.kt @@ -0,0 +1,6 @@ +package be.ugent.sel.studeez.data.remote + +object FirebaseSessionReport { + const val STUDYTIME: String = "studyTime" + const val ENDTIME: String = "endTime" +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseUser.kt b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseUser.kt new file mode 100644 index 0000000..9ee5aa2 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/data/remote/FirebaseUser.kt @@ -0,0 +1,6 @@ +package be.ugent.sel.studeez.data.remote + +object FirebaseUser { + const val USERNAME: String = "username" + const val BIOGRAPHY: String = "biography" +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt b/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt index 7ee4992..33bf73b 100644 --- a/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt +++ b/app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt @@ -16,6 +16,9 @@ abstract class DatabaseModule { @Binds abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO + @Binds + abstract fun provideFriendshipDAO(impl: FirebaseFriendshipDAO): FriendshipDAO + @Binds abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO @@ -26,11 +29,14 @@ abstract class DatabaseModule { abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService @Binds - abstract fun provideSessionDAO(impl: FireBaseSessionDAO): SessionDAO + abstract fun provideSessionDAO(impl: FirebaseSessionDAO): SessionDAO @Binds - abstract fun provideSubjectDAO(impl: FireBaseSubjectDAO): SubjectDAO + abstract fun provideSubjectDAO(impl: FirebaseSubjectDAO): SubjectDAO @Binds - abstract fun provideTaskDAO(impl: FireBaseTaskDAO): TaskDAO + abstract fun provideTaskDAO(impl: FirebaseTaskDAO): TaskDAO + + @Binds + abstract fun provideFeedDAO(impl: FirebaseFeedDAO): FeedDAO } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt new file mode 100644 index 0000000..e9e95b9 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt @@ -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>> + + suspend fun getFeedEntriesFromUser(id: String): Map> + + fun getFriendsSessions(): Flow>>> +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/FriendshipDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/FriendshipDAO.kt new file mode 100644 index 0000000..0beb01a --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/FriendshipDAO.kt @@ -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> + + /** + * @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 + + /** + * @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 +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt index 77087d2..4ac2891 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/SessionDAO.kt @@ -1,12 +1,16 @@ 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.User +import be.ugent.sel.studeez.data.local.models.task.Task import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo import kotlinx.coroutines.flow.Flow interface SessionDAO { fun getSessions(): Flow> + suspend fun getSessionsOfUser(userId: String): List fun saveSession(newSessionReport: SessionReport) diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt index 2749fac..7b89bba 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt @@ -12,4 +12,13 @@ interface SubjectDAO { fun deleteSubject(oldSubject: Subject) fun updateSubject(newSubject: Subject) + + suspend fun archiveSubject(subject: Subject) + + fun getTaskCount(subject: Subject): Flow + fun getCompletedTaskCount(subject: Subject): Flow + fun getStudyTime(subject: Subject): Flow + + suspend fun getSubject(subjectId: String): Subject? + suspend fun getSubjectOfUSer(subjectId: String, userId: String): Subject } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/TaskDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/TaskDAO.kt index 0f629ea..c27cf7d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/TaskDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/TaskDAO.kt @@ -14,5 +14,7 @@ interface TaskDAO { 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 } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt index b96cf17..80a7689 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt @@ -1,13 +1,52 @@ package be.ugent.sel.studeez.domain +import be.ugent.sel.studeez.data.local.models.User +import kotlinx.coroutines.flow.Flow + interface UserDAO { - suspend fun getUsername(): String? - suspend fun save(newUsername: String) + fun getCurrentUserId(): String /** - * Delete all references to this user in the database. Similar to the deleteCascade in + * @return all users + */ + fun getAllUsers(): Flow> + + /** + * @return all users based on a query, a trimmed down version of getAllUsers() + */ + fun getUsersWithQuery( + fieldName: String, + value: String + ): Flow> + + /** + * Request information about a user + */ + fun getUserDetails( + userId: String + ): Flow + + 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, 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. */ - suspend fun deleteUserReferences() + suspend fun deleteLoggedInUserReferences() + // TODO Should be refactored to fun deleteLoggedInUserReferences(): Boolean, without suspend. } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSessionDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSessionDAO.kt deleted file mode 100644 index a818236..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSessionDAO.kt +++ /dev/null @@ -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> { - 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) -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt deleted file mode 100644 index 7d90fbf..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt +++ /dev/null @@ -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> { - 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) -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseTaskDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseTaskDAO.kt deleted file mode 100644 index b8855e6..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseTaskDAO.kt +++ /dev/null @@ -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> { - 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) -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseCollections.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseCollections.kt similarity index 78% rename from app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseCollections.kt rename to app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseCollections.kt index 78867c9..042c0f0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseCollections.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseCollections.kt @@ -1,8 +1,9 @@ package be.ugent.sel.studeez.domain.implementation -object FireBaseCollections { +object FirebaseCollections { const val SESSION_COLLECTION = "sessions" const val USER_COLLECTION = "users" + const val FRIENDS_COLLECTION = "friends" const val TIMER_COLLECTION = "timers" const val SUBJECT_COLLECTION = "subjects" const val TASK_COLLECTION = "tasks" diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseFeedDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseFeedDAO.kt new file mode 100644 index 0000000..be4893d --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseFeedDAO.kt @@ -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>> { + 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> { + 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>>> { + 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>>>): Map>> { + val new: MutableMap>> = mutableMapOf() + for ((name, map) in l) { + for ((day, feedEntries: List) 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 = + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseFriendshipDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseFriendshipDAO.kt new file mode 100644 index 0000000..2583bed --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseFriendshipDAO.kt @@ -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> { + return firestore + .collection(USER_COLLECTION) + .document(userId) + .collection(FRIENDS_COLLECTION) + .snapshots() + .map { it.toObjects(Friendship::class.java) } + } + + override fun getFriendshipCount( + userId: String + ): Flow { + 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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSessionDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSessionDAO.kt new file mode 100644 index 0000000..df271c6 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSessionDAO.kt @@ -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> { + return currentUserSessionsCollection() + .snapshots() + .map { it.toObjects(SessionReport::class.java) } + } + + override suspend fun getSessionsOfUser(userId: String): List { + 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) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSubjectDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSubjectDAO.kt new file mode 100644 index 0000000..1b8d7da --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseSubjectDAO.kt @@ -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> { + 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 { + return taskDAO.getTasks(subject) + .map(List::count) + } + + override fun getCompletedTaskCount(subject: Subject): Flow { + return taskDAO.getTasks(subject) + .map { tasks -> tasks.count { it.completed && !it.archived } } + } + + override fun getStudyTime(subject: Subject): Flow { + 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) +} diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTaskDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTaskDAO.kt new file mode 100644 index 0000000..1ca60e4 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTaskDAO.kt @@ -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> { + 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) diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTimerDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTimerDAO.kt index 1f37a18..dad7047 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTimerDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseTimerDAO.kt @@ -48,8 +48,8 @@ class FirebaseTimerDAO @Inject constructor( } private fun currentUserTimersCollection(): CollectionReference = - firestore.collection(FireBaseCollections.USER_COLLECTION) + firestore.collection(FirebaseCollections.USER_COLLECTION) .document(auth.currentUserId) - .collection(FireBaseCollections.TIMER_COLLECTION) + .collection(FirebaseCollections.TIMER_COLLECTION) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt index 3158b88..04239c0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt @@ -2,34 +2,91 @@ package be.ugent.sel.studeez.domain.implementation import be.ugent.sel.studeez.R import be.ugent.sel.studeez.common.snackbar.SnackbarManager +import be.ugent.sel.studeez.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.UserDAO +import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION 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.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.tasks.await import javax.inject.Inject class FirebaseUserDAO @Inject constructor( private val firestore: FirebaseFirestore, private val auth: AccountDAO - ) : UserDAO { +) : UserDAO { - override suspend fun getUsername(): String? { - return currentUserDocument().get().await().getString("username") - } - - override suspend fun save(newUsername: String) { - currentUserDocument().set(mapOf("username" to newUsername)) + override fun getCurrentUserId(): String { + return auth.currentUserId } private fun currentUserDocument(): DocumentReference = - firestore.collection(USER_COLLECTION).document(auth.currentUserId) + firestore + .collection(USER_COLLECTION) + .document(auth.currentUserId) - companion object { - private const val USER_COLLECTION = "users" + override fun getAllUsers(): Flow> { + return firestore + .collection(USER_COLLECTION) + .snapshots() + .map { it.toObjects(User::class.java) } } - override suspend fun deleteUserReferences() { + override fun getUsersWithQuery( + fieldName: String, + value: String + ): Flow> { + return firestore + .collection(USER_COLLECTION) + .whereEqualTo(fieldName, value) + .snapshots() + .map { it.toObjects(User::class.java) } + } + + override fun getUserDetails(userId: String): Flow { + 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() .addOnSuccessListener { SnackbarManager.showMessage(R.string.success) } .addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) } diff --git a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt index 49856c9..1ab69be 100644 --- a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt +++ b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt @@ -4,7 +4,7 @@ object StudeezDestinations { // NavBar const val HOME_SCREEN = "home" const val SUBJECT_SCREEN = "subjects" - const val SESSIONS_SCREEN = "sessions" + const val FRIENDS_FEED = "friends_feed" const val PROFILE_SCREEN = "profile" // Drawer @@ -27,10 +27,13 @@ object StudeezDestinations { const val EDIT_SUBJECT_FORM = "edit_subject" const val TASKS_SCREEN = "tasks" const val ADD_TASK_FORM = "add_task" + const val SELECT_SUBJECT = "select_subject" const val EDIT_TASK_FORM = "edit_task" // Friends flow + const val FRIENDS_OVERVIEW_SCREEN = "friends_overview" const val SEARCH_FRIENDS_SCREEN = "search_friends" + const val PUBLIC_PROFILE_SCREEN = "public_profile" // Create & edit screens const val CREATE_TASK_SCREEN = "create_task" diff --git a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt index a09846a..6dd57d5 100644 --- a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt +++ b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt @@ -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.NavigationBarViewModel 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.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.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_recap.SessionRecapRoute -import be.ugent.sel.studeez.screens.sessions.SessionsRoute import be.ugent.sel.studeez.screens.settings.SettingsRoute import be.ugent.sel.studeez.screens.sign_up.SignUpRoute import be.ugent.sel.studeez.screens.splash.SplashRoute -import be.ugent.sel.studeez.screens.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.forms.SubjectAddRoute -import be.ugent.sel.studeez.screens.tasks.forms.SubjectEditRoute -import be.ugent.sel.studeez.screens.tasks.forms.TaskAddRoute -import be.ugent.sel.studeez.screens.tasks.forms.TaskEditRoute +import be.ugent.sel.studeez.screens.tasks.form.TaskCreateRoute +import be.ugent.sel.studeez.screens.tasks.form.TaskEditRoute import be.ugent.sel.studeez.screens.timer_form.TimerAddRoute import be.ugent.sel.studeez.screens.timer_form.TimerEditRoute import be.ugent.sel.studeez.screens.timer_form.timer_type_select.TimerTypeSelectScreen @@ -51,6 +55,7 @@ fun StudeezNavGraph( val open: (String) -> Unit = { appState.navigate(it) } val openAndPopUp: (String, String) -> Unit = { route, popUp -> appState.navigateAndPopUp(route, popUp) } + val clearAndNavigate: (route: String) -> Unit = { route -> appState.clearAndNavigate(route) } val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp) val navigationBarActions: NavigationBarActions = @@ -64,10 +69,11 @@ fun StudeezNavGraph( // NavBar composable(StudeezDestinations.HOME_SCREEN) { HomeRoute( - open, - viewModel = hiltViewModel(), + open = open, 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) { - SubjectAddRoute( + SubjectCreateRoute( goBack = goBack, openAndPopUp = openAndPopUp, viewModel = hiltViewModel(), @@ -98,14 +112,14 @@ fun StudeezNavGraph( composable(StudeezDestinations.TASKS_SCREEN) { TaskRoute( - goBack = goBack, + goBack = { openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.TASKS_SCREEN) }, open = open, viewModel = hiltViewModel(), ) } composable(StudeezDestinations.ADD_TASK_FORM) { - TaskAddRoute( + TaskCreateRoute( goBack = goBack, openAndPopUp = openAndPopUp, viewModel = hiltViewModel(), @@ -121,10 +135,11 @@ fun StudeezNavGraph( } - composable(StudeezDestinations.SESSIONS_SCREEN) { - SessionsRoute( + composable(StudeezDestinations.FRIENDS_FEED) { + FriendsFeedRoute( drawerActions = drawerActions, - navigationBarActions = navigationBarActions + navigationBarActions = navigationBarActions, + viewModel = hiltViewModel() ) } @@ -200,7 +215,7 @@ fun StudeezNavGraph( composable(StudeezDestinations.SESSION_RECAP) { SessionRecapRoute( - openAndPopUp = openAndPopUp, + clearAndNavigate = clearAndNavigate, viewModel = hiltViewModel() ) } @@ -220,8 +235,28 @@ fun StudeezNavGraph( } // Friends flow + composable(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) { + FriendsOveriewRoute( + open = open, + popUp = goBack, + viewModel = hiltViewModel() + ) + } + 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 diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewScreen.kt new file mode 100644 index 0000000..a1960d8 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewScreen.kt @@ -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>>, + 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 = { } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewUiState.kt new file mode 100644 index 0000000..8672814 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewUiState.kt @@ -0,0 +1,6 @@ +package be.ugent.sel.studeez.screens.friends.friends_overview + +data class FriendsOverviewUiState( + val userId: String, + val queryString: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewViewModel.kt new file mode 100644 index 0000000..556e435 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_overview/FriendsOverviewViewModel.kt @@ -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>> { + 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 + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendUiState.kt new file mode 100644 index 0000000..0a5a10f --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendUiState.kt @@ -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> = emptyFlow() +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsScreen.kt new file mode 100644 index 0000000..508f7ed --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsScreen.kt @@ -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>, + 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 = { } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsViewModel.kt new file mode 100644 index 0000000..05bf6d6 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends/friends_search/SearchFriendsViewModel.kt @@ -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> { + return userDAO.getUsersWithQuery( + fieldName = FirebaseUser.USERNAME, + value = value + ) + } + + /** + * Get all users, except for the current user. + */ + fun getAllUsers(): Flow> { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends_feed/FriendsFeedScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends_feed/FriendsFeedScreen.kt index b5d0ca1..7aab92a 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/friends_feed/FriendsFeedScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends_feed/FriendsFeedScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.material.Card import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -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.PrimaryScreenTemplate 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.data.local.models.FeedEntry import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds @@ -33,11 +31,10 @@ fun FriendsFeedRoute( drawerActions: DrawerActions, navigationBarActions: NavigationBarActions ) { - val friendsFeedUiState by viewModel.uiState.collectAsState() FriendsFeedScreen( drawerActions = drawerActions, navigationBarActions = navigationBarActions, - uiState = friendsFeedUiState, + viewModel = viewModel ) } @@ -45,30 +42,31 @@ fun FriendsFeedRoute( fun FriendsFeedScreen( drawerActions: DrawerActions, navigationBarActions: NavigationBarActions, - uiState: FriendsFeedUiState, + viewModel: FriendsFeedViewModel ) { PrimaryScreenTemplate( title = resources().getString(AppText.friends_feed), drawerActions = drawerActions, navigationBarActions = navigationBarActions ) { - when (uiState) { - FriendsFeedUiState.Loading -> LoadingFeed() - is FriendsFeedUiState.Succes -> { - val friendsSessions = uiState.friendSessions - LazyColumn() { - // Default Timers, cannot be edited - items(friendsSessions) { - val (day, feedEntries) = it - DateText(date = day) - feedEntries.forEach { (name, feedEntry) -> - FriendsFeedEntry(name = name, feedEntry = feedEntry) - } - Spacer(modifier = Modifier.height(10.dp)) - } + + val friendsSessions = viewModel.getFriendsSessions().collectAsState(initial = emptyList()) + + + + + LazyColumn() { + // Default Timers, cannot be edited + items(friendsSessions.value) { + val (day, feedEntries) = it + DateText(date = day) + feedEntries.forEach { (name, feedEntry) -> + FriendsFeedEntry(name = name, feedEntry = feedEntry) } + Spacer(modifier = Modifier.height(10.dp)) } } + } } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/friends_feed/FriendsFeedViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/friends_feed/FriendsFeedViewModel.kt index 5f01c40..d4ae2dc 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/friends_feed/FriendsFeedViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/friends_feed/FriendsFeedViewModel.kt @@ -1,14 +1,15 @@ 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.LogService +import be.ugent.sel.studeez.domain.SessionDAO 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.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.toList import javax.inject.Inject @HiltViewModel @@ -17,13 +18,10 @@ class FriendsFeedViewModel @Inject constructor( logService: LogService ) : StudeezViewModel(logService) { - val uiState: StateFlow = feedDAO.getFriendsSessions() - .map { it.toList() } - .map { FriendsFeedUiState.Succes(it) } - .stateIn( - scope = viewModelScope, - initialValue = FriendsFeedUiState.Loading, - started = SharingStarted.Eagerly, - ) + fun getFriendsSessions(): Flow>>>> { + return feedDAO.getFriendsSessions().map { it.toList() } + } + + } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt index f02852e..7b46c7d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt @@ -5,14 +5,17 @@ import androidx.compose.material.IconButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.tooling.preview.Preview import be.ugent.sel.studeez.R -import be.ugent.sel.studeez.common.composable.BasicButton import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate import be.ugent.sel.studeez.common.composable.drawer.DrawerActions +import be.ugent.sel.studeez.common.composable.feed.Feed +import be.ugent.sel.studeez.common.composable.feed.FeedUiState +import be.ugent.sel.studeez.common.composable.feed.FeedViewModel import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions -import be.ugent.sel.studeez.common.ext.basicButton +import be.ugent.sel.studeez.data.local.models.FeedEntry import be.ugent.sel.studeez.resources @Composable @@ -21,35 +24,43 @@ fun HomeRoute( viewModel: HomeViewModel, drawerActions: DrawerActions, navigationBarActions: NavigationBarActions, + feedViewModel: FeedViewModel, ) { + val feedUiState by feedViewModel.uiState.collectAsState() HomeScreen( - onStartSessionClick = { viewModel.onStartSessionClick(open) }, + onViewFriendsClick = { viewModel.onViewFriendsClick(open) }, drawerActions = drawerActions, navigationBarActions = navigationBarActions, + feedUiState = feedUiState, + continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) }, + onEmptyFeedHelp = { feedViewModel.onEmptyFeedHelp(open) } ) } @Composable fun HomeScreen( - onStartSessionClick: () -> Unit, + onViewFriendsClick: () -> Unit, drawerActions: DrawerActions, - navigationBarActions: NavigationBarActions + navigationBarActions: NavigationBarActions, + feedUiState: FeedUiState, + continueTask: (String, String) -> Unit, + onEmptyFeedHelp: () -> Unit, ) { PrimaryScreenTemplate( title = resources().getString(R.string.home), drawerActions = drawerActions, navigationBarActions = navigationBarActions, - // TODO barAction = { FriendsAction() } + barAction = { FriendsAction(onViewFriendsClick) } ) { - BasicButton(R.string.start_session, Modifier.basicButton()) { - onStartSessionClick() - } + Feed(feedUiState, continueTask, onEmptyFeedHelp) } } @Composable -fun FriendsAction() { - IconButton(onClick = { /*TODO*/ }) { +fun FriendsAction( + onClick: () -> Unit +) { + IconButton(onClick = onClick) { Icon( imageVector = Icons.Default.Person, contentDescription = resources().getString(R.string.friends) @@ -61,8 +72,40 @@ fun FriendsAction() { @Composable fun HomeScreenPreview() { HomeScreen( - onStartSessionClick = {}, + onViewFriendsClick = {}, 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 = {} ) } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeViewModel.kt index b27f995..5a9407a 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeViewModel.kt @@ -1,6 +1,4 @@ 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.navigation.StudeezDestinations import be.ugent.sel.studeez.screens.StudeezViewModel @@ -9,11 +7,11 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - private val accountDAO: AccountDAO, logService: LogService ) : StudeezViewModel(logService) { - fun onStartSessionClick(open: (String) -> Unit) { - open(StudeezDestinations.TIMER_SELECTION_SCREEN) + + fun onViewFriendsClick(open: (String) -> Unit) { + open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) } -} \ No newline at end of file +} diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditUiState.kt deleted file mode 100644 index 9ecaba3..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditUiState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package be.ugent.sel.studeez.screens.profile - -data class ProfileEditUiState ( - val username: String = "" -) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt index 9c76337..ca59fba 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt @@ -1,37 +1,50 @@ 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.IconButton +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.PrimaryScreenTemplate import be.ugent.sel.studeez.common.composable.drawer.DrawerActions 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.ui.theme.StudeezTheme import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import be.ugent.sel.studeez.R.string as AppText data class ProfileActions( val getUsername: suspend CoroutineScope.() -> String?, + val getBiography: suspend CoroutineScope.() -> String?, + val getAmountOfFriends: () -> Flow, val onEditProfileClick: () -> Unit, + val onViewFriendsClick: () -> Unit ) fun getProfileActions( viewModel: ProfileViewModel, - open: (String) -> Unit, + open: (String) -> Unit ): ProfileActions { return ProfileActions( getUsername = { viewModel.getUsername() }, + getBiography = { viewModel.getBiography() }, + getAmountOfFriends = { viewModel.getAmountOfFriends() }, onEditProfileClick = { viewModel.onEditProfileClick(open) }, + onViewFriendsClick = { viewModel.onViewFriendsClick(open) } ) } @@ -56,8 +69,12 @@ fun ProfileScreen( navigationBarActions: NavigationBarActions, ) { var username: String? by remember { mutableStateOf("") } + var biography: String? by remember { mutableStateOf("") } + val amountOfFriends = profileActions.getAmountOfFriends().collectAsState(initial = 0) + LaunchedEffect(key1 = Unit) { username = profileActions.getUsername(this) + biography = profileActions.getBiography(this) } PrimaryScreenTemplate( title = resources().getString(AppText.profile), @@ -65,7 +82,35 @@ fun ProfileScreen( navigationBarActions = navigationBarActions, 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, contentDescription = resources().getString(AppText.edit_profile) ) - } } @@ -86,8 +130,38 @@ fun EditAction( @Composable fun ProfileScreenPreview() { ProfileScreen( - profileActions = ProfileActions({ null }, {}), + profileActions = ProfileActions({ null }, { null }, { emptyFlow() }, {}, {}), drawerActions = DrawerActions({}, {}, {}, {}, {}), 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) { } + } + } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt index e24defd..3231add 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt @@ -1,24 +1,39 @@ 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.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 ProfileViewModel @Inject constructor( private val userDAO: UserDAO, + private val friendshipDAO: FriendshipDAO, logService: LogService ) : StudeezViewModel(logService) { - suspend fun getUsername(): String? { - return userDAO.getUsername() + suspend fun getUsername(): String { + return userDAO.getLoggedInUser().username + } + + suspend fun getBiography(): String { + return userDAO.getLoggedInUser().biography + } + + fun getAmountOfFriends(): Flow { + return friendshipDAO.getFriendshipCount(userDAO.getCurrentUserId()) } fun onEditProfileClick(open: (String) -> Unit) { open(StudeezDestinations.EDIT_PROFILE_SCREEN) } + fun onViewFriendsClick(open: (String) -> Unit) { + open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) + } + } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditScreen.kt similarity index 54% rename from app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditScreen.kt rename to app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditScreen.kt index c6fcbaf..31dcb9d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditScreen.kt @@ -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.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import be.ugent.sel.studeez.R import be.ugent.sel.studeez.common.composable.BasicTextButton import be.ugent.sel.studeez.common.composable.LabelledInputField import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate import be.ugent.sel.studeez.common.ext.textButton import be.ugent.sel.studeez.resources import be.ugent.sel.studeez.ui.theme.StudeezTheme +import be.ugent.sel.studeez.R.string as AppText data class EditProfileActions( val onUserNameChange: (String) -> Unit, + val onBiographyChange: (String) -> Unit, val onSaveClick: () -> Unit, val onDeleteClick: () -> Unit ) @@ -25,6 +26,7 @@ fun getEditProfileActions( ): EditProfileActions { return EditProfileActions( onUserNameChange = { viewModel.onUsernameChange(it) }, + onBiographyChange = { viewModel.onBiographyChange(it) }, onSaveClick = { viewModel.onSaveClick() }, onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) }, ) @@ -51,28 +53,41 @@ fun EditProfileScreen( editProfileActions: EditProfileActions, ) { SecondaryScreenTemplate( - title = resources().getString(R.string.editing_profile), + title = resources().getString(AppText.editing_profile), popUp = goBack ) { - Column { - LabelledInputField( - value = uiState.username, - onNewValue = editProfileActions.onUserNameChange, - label = R.string.username - ) - BasicTextButton( - text = R.string.save, - Modifier.textButton(), - action = { - editProfileActions.onSaveClick() - goBack() - } - ) - BasicTextButton( - text = R.string.delete_profile, - Modifier.textButton(), - action = editProfileActions.onDeleteClick - ) + LazyColumn { + item { + LabelledInputField( + value = uiState.username, + onNewValue = editProfileActions.onUserNameChange, + label = AppText.username + ) + } + item { + LabelledInputField( + value = uiState.biography, + onNewValue = editProfileActions.onBiographyChange, + label = AppText.biography + ) + } + item { + BasicTextButton( + 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 fun EditProfileScreenComposable() { StudeezTheme { - EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {})) + EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {}, {})) } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditUiState.kt new file mode 100644 index 0000000..911df68 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditUiState.kt @@ -0,0 +1,6 @@ +package be.ugent.sel.studeez.screens.profile.edit_profile + +data class ProfileEditUiState ( + val username: String = "", + val biography: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditViewModel.kt similarity index 66% rename from app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditViewModel.kt rename to app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditViewModel.kt index cb270be..57bbbc0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/edit_profile/ProfileEditViewModel.kt @@ -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 be.ugent.sel.studeez.R 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.LogService import be.ugent.sel.studeez.domain.UserDAO @@ -23,7 +24,11 @@ class ProfileEditViewModel @Inject constructor( init { 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) } + fun onBiographyChange(newValue: String) { + uiState.value = uiState.value.copy(biography = newValue) + } + fun onSaveClick() { launchCatching { - userDAO.save(uiState.value.username) + userDAO.saveLoggedInUser( + newUsername = uiState.value.username, + newBiography = uiState.value.biography + ) SnackbarManager.showMessage(R.string.success) } } fun onDeleteClick(openAndPopUp: (String, String) -> Unit) { launchCatching { - userDAO.deleteUserReferences() // Delete references + userDAO.deleteLoggedInUserReferences() // Delete references accountDAO.deleteAccount() // Delete authentication } openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN) diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileScreen.kt new file mode 100644 index 0000000..4fff845 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileScreen.kt @@ -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, + val getAmountOfFriends: () -> Flow, + 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 = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileUiState.kt new file mode 100644 index 0000000..537fed9 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileUiState.kt @@ -0,0 +1,5 @@ +package be.ugent.sel.studeez.screens.profile.public_profile + +data class PublicProfileUiState( + var userId: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileViewModel.kt new file mode 100644 index 0000000..6958b88 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/public_profile/PublicProfileViewModel.kt @@ -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 { + uiState.value = uiState.value.copy( + userId = userId + ) + return userDAO.getUserDetails( + userId = uiState.value.userId + ) + } + + fun getAmountOfFriends( + userId: String + ): Flow { + return friendshipDAO.getFriendshipCount( + userId = userId + ) + } + + fun onViewFriendsClick( + open: (String) -> Unit + ) { + open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) + } + + fun sendFriendRequest( + userId: String + ) { + friendshipDAO.sendFriendshipRequest(userId) + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/InvisibleSessionManager.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/InvisibleSessionManager.kt index 9051fa8..8a7c405 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/InvisibleSessionManager.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/InvisibleSessionManager.kt @@ -10,9 +10,9 @@ object InvisibleSessionManager { private var viewModel: SessionViewModel? = null private lateinit var mediaPlayer: MediaPlayer - fun setParameters(viewModel: SessionViewModel, mediaplayer: MediaPlayer) { + fun setParameters(viewModel: SessionViewModel, mediaPlayer: MediaPlayer) { + this.mediaPlayer = mediaPlayer this.viewModel = viewModel - this.mediaPlayer = mediaplayer } suspend fun updateTimer() { diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionRoute.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionRoute.kt index 084ff43..6ca8a96 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionRoute.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionRoute.kt @@ -6,28 +6,22 @@ import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer -import be.ugent.sel.studeez.screens.session.sessionScreens.AbstractSessionScreen -import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreen +import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreenComposable data class SessionActions( val getTimer: () -> FunctionalTimer, val getTask: () -> String, - val startMediaPlayer: () -> Unit, - val releaseMediaPlayer: () -> Unit, val endSession: () -> Unit ) private fun getSessionActions( viewModel: SessionViewModel, openAndPopUp: (String, String) -> Unit, - mediaplayer: MediaPlayer, ): SessionActions { return SessionActions( getTimer = viewModel::getTimer, getTask = viewModel::getTask, endSession = { viewModel.endSession(openAndPopUp) }, - startMediaPlayer = mediaplayer::start, - releaseMediaPlayer = mediaplayer::release, ) } @@ -37,20 +31,15 @@ fun SessionRoute( openAndPopUp: (String, String) -> Unit, viewModel: SessionViewModel, ) { - val context = LocalContext.current val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - val mediaplayer = MediaPlayer.create(context, uri) - mediaplayer.isLooping = false + val mediaPlayer = MediaPlayer.create(LocalContext.current, uri) + mediaPlayer.isLooping = false - InvisibleSessionManager.setParameters( - viewModel = viewModel, - mediaplayer = mediaplayer - ) + InvisibleSessionManager.setParameters(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( - open = open, - sessionActions = getSessionActions(viewModel, openAndPopUp, mediaplayer) - ) + sessionScreen() } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionViewModel.kt index d5e2bab..0be4147 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionViewModel.kt @@ -1,7 +1,8 @@ package be.ugent.sel.studeez.screens.session -import be.ugent.sel.studeez.data.SelectedTimerState -import be.ugent.sel.studeez.data.SessionReportState +import be.ugent.sel.studeez.data.SelectedSessionReport +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.domain.LogService import be.ugent.sel.studeez.navigation.StudeezDestinations @@ -11,23 +12,21 @@ import javax.inject.Inject @HiltViewModel class SessionViewModel @Inject constructor( - private val selectedTimerState: SelectedTimerState, - private val sessionReportState: SessionReportState, + private val selectedTimer: SelectedTimer, + private val sessionReport: SelectedSessionReport, + private val selectedTask: SelectedTask, logService: LogService ) : StudeezViewModel(logService) { - - private val task : String = "No task selected" // placeholder for tasks implementation - - fun getTimer() : FunctionalTimer { - return selectedTimerState.selectedTimer!! + fun getTimer(): FunctionalTimer { + return selectedTimer() } fun getTask(): String { - return task + return selectedTask().name } 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) } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/SoundPlayer.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/SoundPlayer.kt new file mode 100644 index 0000000..14fae19 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/SoundPlayer.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/AbstractSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/AbstractSessionScreen.kt deleted file mode 100644 index 65f5d24..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/AbstractSessionScreen.kt +++ /dev/null @@ -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" }, {}, {}, {})) -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakSessionScreen.kt deleted file mode 100644 index 8fa45ff..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakSessionScreen.kt +++ /dev/null @@ -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() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakTimerScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakTimerScreenComposable.kt new file mode 100644 index 0000000..42ec4f7 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakTimerScreenComposable.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomSessionScreen.kt deleted file mode 100644 index 7fc60bc..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomSessionScreen.kt +++ /dev/null @@ -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() - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomTimerSessionScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomTimerSessionScreenComposable.kt new file mode 100644 index 0000000..a0c385c --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomTimerSessionScreenComposable.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessSessionScreen.kt deleted file mode 100644 index be67cff..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessSessionScreen.kt +++ /dev/null @@ -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() {} -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessTimerSessionScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessTimerSessionScreenComposable.kt new file mode 100644 index 0000000..4f1dbe3 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessTimerSessionScreenComposable.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreen.kt deleted file mode 100644 index 98b2d5e..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreen.kt +++ /dev/null @@ -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 { - 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) -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreenComposable.kt new file mode 100644 index 0000000..47ca52e --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreenComposable.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/SessionScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/SessionScreenComposable.kt new file mode 100644 index 0000000..c94d2a5 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/SessionScreenComposable.kt @@ -0,0 +1,73 @@ +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import be.ugent.sel.studeez.screens.session.SessionActions + +@Composable +fun SessionScreen( + open: (String) -> Unit, + sessionActions: SessionActions, + callMediaPlayer: () -> Unit = {}, + midSection: @Composable () -> Int = {0}, + motivationString: @Composable () -> String, + +) { + Column( + modifier = Modifier.padding(10.dp) + ) { + Timer( + sessionActions = sessionActions, + callMediaPlayer = callMediaPlayer, + motivationString = motivationString, + MidSection = midSection + ) + Box( + contentAlignment = Alignment.Center, modifier = Modifier + .fillMaxWidth() + .padding(50.dp) + ) { + EndSessionButton(sessionActions = sessionActions) + } + } +} + +@Composable +fun EndSessionButton(sessionActions: SessionActions) { + TextButton( + onClick = { + sessionActions.endSession() + }, + modifier = Modifier + .padding(horizontal = 20.dp) + .border(1.dp, Color.Red, RoundedCornerShape(32.dp)) + .background(Color.Transparent) + ) { + EndsessionText() + } +} + +@Composable +fun EndsessionText() { + Text( + text = "End session", + color = Color.Red, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + modifier = Modifier.padding(1.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/TimerComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/TimerComposable.kt new file mode 100644 index 0000000..2a29403 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/TimerComposable.kt @@ -0,0 +1,95 @@ +package be.ugent.sel.studeez.screens.session.sessionScreens + +import androidx.compose.foundation.background +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.runtime.* +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.unit.dp +import androidx.compose.ui.unit.sp +import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds +import be.ugent.sel.studeez.screens.session.SessionActions +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.seconds + +@Composable +fun Timer( + sessionActions: SessionActions, + callMediaPlayer: () -> Unit, + motivationString: @Composable () -> String, + MidSection: @Composable () -> Int +) { + var tikker by remember { mutableStateOf(false) } + LaunchedEffect(tikker) { + delay(1.seconds) + sessionActions.getTimer().tick() + callMediaPlayer() + tikker = !tikker + } + + val hms = sessionActions.getTimer().getHoursMinutesSeconds() + Column { + + TimerClock(hms) + MotivationText(text = motivationString()) + MidSection() + + 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)) + ) { + TaskText(taskName = sessionActions.getTask()) + } + } + } +} + +@Composable +fun TimerClock(hms: HoursMinutesSeconds) { + Text( + text = hms.toString(), + modifier = Modifier + .fillMaxWidth() + .padding(50.dp), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 40.sp, + ) +} + +@Composable +fun MotivationText(text: String) { + Text( + text = text, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Light, + fontSize = 30.sp + ) +} + +@Composable +fun TaskText(taskName: String) { + Text( + text = taskName, + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 4.dp, horizontal = 20.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapScreen.kt index 2d06e0b..3a1e85f 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapScreen.kt @@ -1,13 +1,24 @@ package be.ugent.sel.studeez.screens.session_recap -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.* import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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.R import be.ugent.sel.studeez.common.composable.BasicButton +import be.ugent.sel.studeez.common.composable.ImageBackgroundButton import be.ugent.sel.studeez.common.ext.basicButton import be.ugent.sel.studeez.data.local.models.SessionReport import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds @@ -21,24 +32,24 @@ data class SessionRecapActions( fun getSessionRecapActions( viewModel: SessionRecapViewModel, - openAndPopUp: (String, String) -> Unit, + clearAndNavigate: (String) -> Unit, ): SessionRecapActions { return SessionRecapActions( viewModel::getSessionReport, - {viewModel.saveSession(openAndPopUp)}, - {viewModel.discardSession(openAndPopUp)} + { viewModel.saveSession(clearAndNavigate) }, + { viewModel.discardSession(clearAndNavigate) } ) } @Composable fun SessionRecapRoute( - openAndPopUp: (String, String) -> Unit, + clearAndNavigate: (String) -> Unit, modifier: Modifier = Modifier, viewModel: SessionRecapViewModel, ) { SessionRecapScreen( modifier = modifier, - getSessionRecapActions(viewModel, openAndPopUp) + getSessionRecapActions(viewModel, clearAndNavigate) ) } @@ -47,21 +58,88 @@ fun SessionRecapScreen(modifier: Modifier, sessionRecapActions: SessionRecapActi val sessionReport: SessionReport = sessionRecapActions.getSessionReport() val studyTime: Int = sessionReport.studyTime val hms: HoursMinutesSeconds = Time(studyTime).getAsHMS() + val (background1, setBackground1) = remember { mutableStateOf(Color.Transparent) } + val (background2, setBackground2) = remember { mutableStateOf(Color.Transparent) } Column( modifier = modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween ) { - Text(text = "You studied: $hms") + Text( + text = stringResource(R.string.congrats, hms), + modifier = Modifier + .fillMaxWidth(), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Light, + fontSize = 30.sp, - BasicButton( - R.string.save, Modifier.basicButton() + ) + + Column( + modifier = Modifier.fillMaxWidth() ) { - sessionRecapActions.saveSession() + Text( + text = stringResource(R.string.how_did_it_go), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Light, + fontSize = 30.sp + ) + + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + ) { + ImageBackgroundButton( + paint = painterResource(id = R.drawable.mood_1), + str = stringResource(id = R.string.good), + background2 = background2, + setBackground1 = setBackground2, + setBackground2 = setBackground1 + ) + + ImageBackgroundButton( + paint = painterResource(id = R.drawable.mood_2), + str = stringResource(id = R.string.bad), + background2 = background1, + setBackground1 = setBackground1, + setBackground2 = setBackground2 + ) + } } - BasicButton( - R.string.discard, Modifier.basicButton(), - colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) - ) { - sessionRecapActions.discardSession() + + Column { + BasicButton( + R.string.save, Modifier.basicButton() + ) { + sessionRecapActions.saveSession() + } + BasicButton( + R.string.discard, Modifier.basicButton(), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) + ) { + sessionRecapActions.discardSession() + } } } } + +@Preview +@Composable +fun SessionRecapScreenPreview() { + SessionRecapScreen( + modifier = Modifier, + sessionRecapActions = SessionRecapActions( + { SessionReport( + studyTime = 100, + ) }, + {}, + {}, + ) + ) +} diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapViewModel.kt index 5fb4943..bf11b93 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapViewModel.kt @@ -1,9 +1,11 @@ package be.ugent.sel.studeez.screens.session_recap -import be.ugent.sel.studeez.data.SessionReportState +import be.ugent.sel.studeez.data.SelectedSessionReport +import be.ugent.sel.studeez.data.SelectedTask import be.ugent.sel.studeez.data.local.models.SessionReport import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.SessionDAO +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 @@ -11,23 +13,26 @@ import javax.inject.Inject @HiltViewModel class SessionRecapViewModel @Inject constructor( - sessionReportState: SessionReportState, + private val selectedSessionReport: SelectedSessionReport, private val sessionDAO: SessionDAO, + private val taskDAO: TaskDAO, + private val selectedTask: SelectedTask, logService: LogService ) : StudeezViewModel(logService) { - private val report: SessionReport = sessionReportState.sessionReport!! - fun getSessionReport(): SessionReport { - return report + return selectedSessionReport() } - fun saveSession(open: (String, String) -> Unit) { + fun saveSession(open: (String) -> Unit) { sessionDAO.saveSession(getSessionReport()) - open(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SESSION_RECAP) + val newTask = + selectedTask().copy(time = selectedTask().time + selectedSessionReport().studyTime) + taskDAO.updateTask(newTask) + open(StudeezDestinations.HOME_SCREEN) } - fun discardSession(open: (String, String) -> Unit) { - open(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SESSION_RECAP) + fun discardSession(open: (String) -> Unit) { + open(StudeezDestinations.HOME_SCREEN) } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/sessions/SessionsScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/sessions/SessionsScreen.kt deleted file mode 100644 index fe60ca8..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/sessions/SessionsScreen.kt +++ /dev/null @@ -1,42 +0,0 @@ -package be.ugent.sel.studeez.screens.sessions - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate -import be.ugent.sel.studeez.common.composable.drawer.DrawerActions -import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions -import be.ugent.sel.studeez.resources -import be.ugent.sel.studeez.R.string as AppText - -@Composable -fun SessionsRoute( - // viewModel: SessionsViewModel, - drawerActions: DrawerActions, - navigationBarActions: NavigationBarActions -) { - SessionsScreen( - drawerActions = drawerActions, - navigationBarActions = navigationBarActions - ) -} - -@Composable -fun SessionsScreen( - drawerActions: DrawerActions, - navigationBarActions: NavigationBarActions -) { - PrimaryScreenTemplate( - title = resources().getString(AppText.upcoming_sessions), - drawerActions = drawerActions, - navigationBarActions = navigationBarActions - ) { - Text( - text = resources().getString(AppText.sessions_temp_description), - modifier = Modifier.fillMaxSize(), - textAlign = TextAlign.Center - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt index a08d063..4cfa6a9 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt @@ -66,7 +66,7 @@ class SignUpViewModel @Inject constructor( launchCatching { accountDAO.signUpWithEmailAndPassword(email, password) accountDAO.signInWithEmailAndPassword(email, password) - userDAO.save(username) + userDAO.saveLoggedInUser(username) openAndPopUp(HOME_SCREEN, SIGN_UP_SCREEN) } } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectScreen.kt new file mode 100644 index 0000000..accb7de --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectScreen.kt @@ -0,0 +1,140 @@ +package be.ugent.sel.studeez.screens.subjects + +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.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import be.ugent.sel.studeez.common.composable.NewTaskSubjectButton +import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate +import be.ugent.sel.studeez.common.composable.StealthButton +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.tasks.SubjectEntry +import be.ugent.sel.studeez.data.local.models.task.Subject +import be.ugent.sel.studeez.navigation.StudeezDestinations +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import be.ugent.sel.studeez.R.string as AppText + +@Composable +fun SubjectRoute( + open: (String) -> Unit, + viewModel: SubjectViewModel, + drawerActions: DrawerActions, + navigationBarActions: NavigationBarActions, +) { + val uiState by viewModel.uiState.collectAsState() + SubjectScreen( + drawerActions = drawerActions, + navigationBarActions = navigationBarActions, + onAddSubject = { viewModel.onAddSubject(open) }, + onViewSubject = { viewModel.onSelectSubject(it) { open(StudeezDestinations.TASKS_SCREEN) } }, + getTaskCount = viewModel::getTaskCount, + getCompletedTaskCount = viewModel::getCompletedTaskCount, + getStudyTime = viewModel::getStudyTime, + uiState = uiState, + ) +} + +@Composable +fun SubjectScreen( + drawerActions: DrawerActions, + navigationBarActions: NavigationBarActions, + onAddSubject: () -> Unit, + onViewSubject: (Subject) -> Unit, + getTaskCount: (Subject) -> Flow, + getCompletedTaskCount: (Subject) -> Flow, + getStudyTime: (Subject) -> Flow, + uiState: SubjectUiState, +) { + PrimaryScreenTemplate( + title = stringResource(AppText.my_subjects), + drawerActions = drawerActions, + navigationBarActions = navigationBarActions, + barAction = {}, + ) { + when (uiState) { + SubjectUiState.Loading -> Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator(color = MaterialTheme.colors.onBackground) + } + is SubjectUiState.Succes -> { + Column( + modifier = Modifier.padding(top = 5.dp) + ) { + NewTaskSubjectButton(onClick = onAddSubject, AppText.new_subject) + LazyColumn { + items(uiState.subjects) { subject -> + SubjectEntry( + subject = subject, + getTaskCount = { getTaskCount(subject) }, + getCompletedTaskCount = { getCompletedTaskCount(subject) }, + getStudyTime = { getStudyTime(subject) }, + ) { + StealthButton( + text = AppText.view_tasks, + modifier = Modifier + .padding(start = 10.dp, end = 5.dp) + .weight(1f) + ) { + onViewSubject(subject) + } + } + } + } + } + } + } + } +} + +@Preview +@Composable +fun SubjectScreenPreview() { + SubjectScreen( + drawerActions = DrawerActions({}, {}, {}, {}, {}), + navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), + onAddSubject = {}, + onViewSubject = {}, + getTaskCount = { flowOf() }, + getCompletedTaskCount = { flowOf() }, + getStudyTime = { flowOf() }, + uiState = SubjectUiState.Succes( + listOf( + Subject( + name = "Test Subject", + argb_color = 0xFFFFD200, + ) + ) + ) + ) +} + +@Preview +@Composable +fun SubjectScreenLoadingPreview() { + SubjectScreen( + drawerActions = DrawerActions({}, {}, {}, {}, {}), + navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), + onAddSubject = {}, + onViewSubject = {}, + getTaskCount = { flowOf() }, + getCompletedTaskCount = { flowOf() }, + getStudyTime = { flowOf() }, + uiState = SubjectUiState.Loading, + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectUiState.kt new file mode 100644 index 0000000..2e44e27 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectUiState.kt @@ -0,0 +1,8 @@ +package be.ugent.sel.studeez.screens.subjects + +import be.ugent.sel.studeez.data.local.models.task.Subject + +sealed interface SubjectUiState { + object Loading : SubjectUiState + data class Succes(val subjects: List) : SubjectUiState +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectViewModel.kt new file mode 100644 index 0000000..0c5b354 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectViewModel.kt @@ -0,0 +1,49 @@ +package be.ugent.sel.studeez.screens.subjects + +import androidx.lifecycle.viewModelScope +import be.ugent.sel.studeez.data.SelectedSubject +import be.ugent.sel.studeez.data.local.models.task.Subject +import be.ugent.sel.studeez.domain.LogService +import be.ugent.sel.studeez.domain.SubjectDAO +import be.ugent.sel.studeez.navigation.StudeezDestinations +import be.ugent.sel.studeez.screens.StudeezViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@HiltViewModel +class SubjectViewModel @Inject constructor( + private val subjectDAO: SubjectDAO, + private val selectedSubject: SelectedSubject, + logService: LogService, +) : StudeezViewModel(logService) { + + val uiState: StateFlow = subjectDAO.getSubjects() + .map { SubjectUiState.Succes(it) } + .stateIn( + scope = viewModelScope, + initialValue = SubjectUiState.Loading, + started = SharingStarted.Eagerly, + ) + + fun onAddSubject(open: (String) -> Unit) { + open(StudeezDestinations.ADD_SUBJECT_FORM) + } + + fun getTaskCount(subject: Subject): Flow { + return subjectDAO.getTaskCount(subject) + } + + fun getCompletedTaskCount(subject: Subject): Flow { + return subjectDAO.getCompletedTaskCount(subject) + } + + fun getStudyTime(subject: Subject): Flow { + return subjectDAO.getStudyTime(subject) + } + + fun onSelectSubject(subject: Subject, open: () -> Unit) { + selectedSubject.set(subject) + open() + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/SubjectFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormScreen.kt similarity index 63% rename from app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/SubjectFormScreen.kt rename to app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormScreen.kt index 74bc7d2..196ad3f 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/SubjectFormScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormScreen.kt @@ -1,28 +1,35 @@ -package be.ugent.sel.studeez.screens.tasks.forms +package be.ugent.sel.studeez.screens.subjects.form import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column -import androidx.compose.material.OutlinedTextField +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import be.ugent.sel.studeez.common.composable.BasicButton import be.ugent.sel.studeez.common.composable.DeleteButton -import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate +import be.ugent.sel.studeez.common.composable.FormComposable +import be.ugent.sel.studeez.common.composable.LabelledInputField import be.ugent.sel.studeez.common.ext.basicButton import be.ugent.sel.studeez.common.ext.fieldModifier +import be.ugent.sel.studeez.common.ext.generateRandomArgb import be.ugent.sel.studeez.resources +import kotlinx.coroutines.launch import be.ugent.sel.studeez.R.string as AppText @Composable -fun SubjectAddRoute( +fun SubjectCreateRoute( goBack: () -> Unit, openAndPopUp: (String, String) -> Unit, - viewModel: SubjectFormViewModel, + viewModel: SubjectCreateFormViewModel, ) { val uiState by viewModel.uiState SubjectForm( @@ -31,7 +38,7 @@ fun SubjectAddRoute( uiState = uiState, onConfirm = { viewModel.onCreate(openAndPopUp) }, onNameChange = viewModel::onNameChange, - onColorChange = {}, + onColorChange = viewModel::onColorChange, ) } @@ -39,19 +46,22 @@ fun SubjectAddRoute( fun SubjectEditRoute( goBack: () -> Unit, openAndPopUp: (String, String) -> Unit, - viewModel: SubjectFormViewModel, + viewModel: SubjectEditFormViewModel, ) { val uiState by viewModel.uiState + val coroutineScope = rememberCoroutineScope() SubjectForm( title = AppText.edit_subject, goBack = goBack, uiState = uiState, onConfirm = { viewModel.onEdit(openAndPopUp) }, onNameChange = viewModel::onNameChange, - onColorChange = {}, + onColorChange = viewModel::onColorChange, ) { DeleteButton(text = AppText.delete_subject) { - viewModel.onDelete(openAndPopUp) + coroutineScope.launch { + viewModel.onDelete(openAndPopUp) + } } } } @@ -63,21 +73,21 @@ fun SubjectForm( uiState: SubjectFormUiState, onConfirm: () -> Unit, onNameChange: (String) -> Unit, - onColorChange: (Color) -> Unit, + onColorChange: (Long) -> Unit, extraButton: @Composable () -> Unit = {}, ) { - SecondaryScreenTemplate( + FormComposable( title = resources().getString(title), popUp = goBack, ) { Column { - OutlinedTextField( + LabelledInputField( singleLine = true, value = uiState.name, - onValueChange = onNameChange, - placeholder = { Text(stringResource(id = AppText.name)) }, - modifier = Modifier.fieldModifier(), + onNewValue = onNameChange, + label = AppText.name, ) + ColorPicker(onColorChange, uiState) BasicButton( text = AppText.confirm, modifier = Modifier.basicButton(), @@ -88,6 +98,24 @@ fun SubjectForm( } } +@Composable +fun ColorPicker( + onColorChange: (Long) -> Unit, + uiState: SubjectFormUiState, +) { + Button( + onClick = { onColorChange(Color.generateRandomArgb()) }, + modifier = Modifier.fieldModifier(), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(uiState.color), + contentColor = Color.White, + ), + shape = RoundedCornerShape(4.dp), + ) { + Text(text = stringResource(id = AppText.regenerate_color)) + } +} + @Preview @Composable fun AddSubjectFormPreview() { diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormUiState.kt new file mode 100644 index 0000000..10a18e8 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormUiState.kt @@ -0,0 +1,9 @@ +package be.ugent.sel.studeez.screens.subjects.form + +import androidx.compose.ui.graphics.Color +import be.ugent.sel.studeez.common.ext.generateRandomArgb + +data class SubjectFormUiState( + val name: String = "", + val color: Long = Color.generateRandomArgb(), +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/SubjectFormViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormViewModel.kt similarity index 55% rename from app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/SubjectFormViewModel.kt rename to app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormViewModel.kt index 68ebd3e..84162d0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/SubjectFormViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormViewModel.kt @@ -1,34 +1,28 @@ -package be.ugent.sel.studeez.screens.tasks.forms +package be.ugent.sel.studeez.screens.subjects.form +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import be.ugent.sel.studeez.data.SelectedSubject import be.ugent.sel.studeez.data.local.models.task.Subject import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.SubjectDAO +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 javax.inject.Inject -@HiltViewModel -class SubjectFormViewModel @Inject constructor( - private val subjectDAO: SubjectDAO, - private val selectedSubject: SelectedSubject, +abstract class SubjectFormViewModel( + protected val subjectDAO: SubjectDAO, + protected val selectedSubject: SelectedSubject, logService: LogService, ) : StudeezViewModel(logService) { - var uiState = mutableStateOf( - if (selectedSubject.isSet()) SubjectFormUiState( - name = selectedSubject().name, - color = selectedSubject().argb_color - ) - else SubjectFormUiState() - ) - private set + abstract val uiState: MutableState - private val name: String + protected val name: String get() = uiState.value.name - private val color: Long + protected val color: Long get() = uiState.value.color fun onNameChange(newValue: String) { @@ -38,11 +32,15 @@ class SubjectFormViewModel @Inject constructor( fun onColorChange(newValue: Long) { uiState.value = uiState.value.copy(color = newValue) } +} - fun onDelete(openAndPopUp: (String, String) -> Unit) { - subjectDAO.deleteSubject(selectedSubject()) - openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.EDIT_SUBJECT_FORM) - } +@HiltViewModel +class SubjectCreateFormViewModel @Inject constructor( + subjectDAO: SubjectDAO, + selectedSubject: SelectedSubject, + logService: LogService, +) : SubjectFormViewModel(subjectDAO, selectedSubject, logService) { + override val uiState = mutableStateOf(SubjectFormUiState()) fun onCreate(openAndPopUp: (String, String) -> Unit) { val newSubject = Subject( @@ -57,13 +55,35 @@ class SubjectFormViewModel @Inject constructor( // open(StudeezDestinations.TASKS_SCREEN) openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.ADD_SUBJECT_FORM) } +} + +@HiltViewModel +class SubjectEditFormViewModel @Inject constructor( + subjectDAO: SubjectDAO, + private val taskDAO: TaskDAO, + selectedSubject: SelectedSubject, + logService: LogService, +) : SubjectFormViewModel(subjectDAO, selectedSubject, logService) { + override val uiState = mutableStateOf( + SubjectFormUiState( + name = selectedSubject().name, + color = selectedSubject().argb_color + ) + ) + + suspend fun onDelete(openAndPopUp: (String, String) -> Unit) { + subjectDAO.archiveSubject(selectedSubject()) + openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.EDIT_SUBJECT_FORM) + } fun onEdit(openAndPopUp: (String, String) -> Unit) { - val newSubject = selectedSubject().copy( - name = name, - argb_color = color, + selectedSubject.set( + selectedSubject().copy( + name = name, + argb_color = color, + ) ) - subjectDAO.updateSubject(newSubject) + subjectDAO.updateSubject(selectedSubject()) openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.EDIT_SUBJECT_FORM) } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/select/SubjectSelectionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/select/SubjectSelectionScreen.kt new file mode 100644 index 0000000..099786e --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/select/SubjectSelectionScreen.kt @@ -0,0 +1,128 @@ +package be.ugent.sel.studeez.screens.subjects.select + +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.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.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.common.composable.SecondaryScreenTemplate +import be.ugent.sel.studeez.common.composable.StealthButton +import be.ugent.sel.studeez.common.composable.tasks.SubjectEntry +import be.ugent.sel.studeez.data.local.models.task.Subject +import be.ugent.sel.studeez.navigation.StudeezDestinations +import be.ugent.sel.studeez.screens.subjects.SubjectUiState +import be.ugent.sel.studeez.screens.subjects.SubjectViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +@Composable +fun SubjectSelectionRoute( + open: (String) -> Unit, + goBack: () -> Unit, + viewModel: SubjectViewModel, +) { + val uiState by viewModel.uiState.collectAsState() + SubjectSelectionScreen( + onViewSubject = { viewModel.onSelectSubject(it) { open(StudeezDestinations.ADD_TASK_FORM) } }, + getTaskCount = viewModel::getTaskCount, + getCompletedTaskCount = viewModel::getCompletedTaskCount, + getStudyTime = viewModel::getStudyTime, + goBack = goBack, + uiState = uiState, + ) +} + +@Composable +fun SubjectSelectionScreen( + goBack: () -> Unit, + onViewSubject: (Subject) -> Unit, + getTaskCount: (Subject) -> Flow, + getCompletedTaskCount: (Subject) -> Flow, + getStudyTime: (Subject) -> Flow, + uiState: SubjectUiState, +) { + SecondaryScreenTemplate( + title = stringResource(R.string.select_subject_title), + barAction = {}, + popUp = goBack, + ) { + when (uiState) { + SubjectUiState.Loading -> Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator(color = MaterialTheme.colors.onBackground) + } + is SubjectUiState.Succes -> { + Column( + modifier = Modifier.padding(top = 5.dp) + ) { + LazyColumn { + items(uiState.subjects) { subject -> + SubjectEntry( + subject = subject, + getTaskCount = { getTaskCount(subject) }, + getCompletedTaskCount = { getCompletedTaskCount(subject) }, + getStudyTime = { getStudyTime(subject) }, + ) { + StealthButton( + text = R.string.select_subject, + modifier = Modifier + .padding(start = 4.dp, end = 4.dp) + .weight(1f) + ) { + onViewSubject(subject) + } + } + } + } + } + } + } + } +} + +@Preview +@Composable +fun SubjectScreenPreview() { + SubjectSelectionScreen( + goBack = {}, + onViewSubject = {}, + getTaskCount = { flowOf() }, + getCompletedTaskCount = { flowOf() }, + getStudyTime = { flowOf() }, + uiState = SubjectUiState.Succes( + listOf( + Subject( + name = "Test Subject", + argb_color = 0xFFFFD200, + ) + ) + ) + ) +} + +@Preview +@Composable +fun SubjectScreenLoadingPreview() { + SubjectSelectionScreen( + goBack = {}, + onViewSubject = {}, + getTaskCount = { flowOf() }, + getCompletedTaskCount = { flowOf() }, + getStudyTime = { flowOf() }, + uiState = SubjectUiState.Loading, + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/SubjectScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/SubjectScreen.kt deleted file mode 100644 index 15a3925..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/SubjectScreen.kt +++ /dev/null @@ -1,80 +0,0 @@ -package be.ugent.sel.studeez.screens.tasks - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -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.common.composable.NewTaskSubjectButton -import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate -import be.ugent.sel.studeez.common.composable.drawer.DrawerActions -import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions -import be.ugent.sel.studeez.common.composable.tasks.SubjectEntry -import be.ugent.sel.studeez.data.local.models.task.Subject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import be.ugent.sel.studeez.R.string as AppText - -@Composable -fun SubjectRoute( - open: (String) -> Unit, - viewModel: SubjectViewModel, - drawerActions: DrawerActions, - navigationBarActions: NavigationBarActions, -) { - SubjectScreen( - drawerActions = drawerActions, - navigationBarActions = navigationBarActions, - addSubject = { viewModel.addSubject(open) }, - getSubjects = viewModel::getSubjects, - onViewSubject = { viewModel.onViewSubject(it, open) }, - ) -} - -@Composable -fun SubjectScreen( - drawerActions: DrawerActions, - navigationBarActions: NavigationBarActions, - addSubject: () -> Unit, - getSubjects: () -> Flow>, - onViewSubject: (Subject) -> Unit, -) { - PrimaryScreenTemplate( - title = stringResource(AppText.my_subjects), - drawerActions = drawerActions, - navigationBarActions = navigationBarActions, - barAction = {}, - ) { - val subjects = getSubjects().collectAsState(initial = emptyList()) - Column( - modifier = Modifier.padding(top = 5.dp) - ) { - LazyColumn { - items(subjects.value) { - SubjectEntry( - subject = it, - onViewSubject = { onViewSubject(it) }, - ) - } - } - NewTaskSubjectButton(onClick = addSubject, AppText.new_subject) - } - } -} - -@Preview -@Composable -fun SubjectScreenPreview() { - SubjectScreen( - drawerActions = DrawerActions({}, {}, {}, {}, {}), - navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), - addSubject = {}, - getSubjects = { flowOf() }, - onViewSubject = {}, - ) -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/SubjectViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/SubjectViewModel.kt deleted file mode 100644 index f1d6071..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/SubjectViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -package be.ugent.sel.studeez.screens.tasks - -import be.ugent.sel.studeez.data.SelectedSubject -import be.ugent.sel.studeez.data.local.models.task.Subject -import be.ugent.sel.studeez.domain.LogService -import be.ugent.sel.studeez.domain.SubjectDAO -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 SubjectViewModel @Inject constructor( - private val subjectDAO: SubjectDAO, - private val selectedSubject: SelectedSubject, - logService: LogService, -) : StudeezViewModel(logService) { - fun addSubject(open: (String) -> Unit) { - open(StudeezDestinations.ADD_SUBJECT_FORM) - } - - fun getSubjects(): Flow> { - return subjectDAO.getSubjects() - } - - fun onViewSubject(subject: Subject, open: (String) -> Unit) { - selectedSubject.set(subject) - open(StudeezDestinations.TASKS_SCREEN) - } -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/TaskScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/TaskScreen.kt index 67f0e93..516b836 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/TaskScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/TaskScreen.kt @@ -27,9 +27,10 @@ data class TaskActions( val addTask: () -> Unit, val getSubject: () -> Subject, val getTasks: () -> Flow>, - val deleteTask: (Task) -> Unit, val onCheckTask: (Task, Boolean) -> Unit, val editSubject: () -> Unit, + val startTask: (Task) -> Unit, + val archiveTask: (Task) -> Unit, ) fun getTaskActions(viewModel: TaskViewModel, open: (String) -> Unit): TaskActions { @@ -37,9 +38,10 @@ fun getTaskActions(viewModel: TaskViewModel, open: (String) -> Unit): TaskAction addTask = { viewModel.addTask(open) }, getTasks = viewModel::getTasks, getSubject = viewModel::getSelectedSubject, - deleteTask = viewModel::deleteTask, onCheckTask = { task, isChecked -> viewModel.toggleTaskCompleted(task, isChecked) }, - editSubject = { viewModel.editSubject(open) } + editSubject = { viewModel.editSubject(open) }, + startTask = { task -> viewModel.startTask(task, open) }, + archiveTask = viewModel::archiveTask ) } @@ -69,16 +71,25 @@ fun TaskScreen( Column( modifier = Modifier.padding(top = 5.dp) ) { + NewTaskSubjectButton(onClick = taskActions.addTask, AppText.new_task) LazyColumn { - items(tasks.value) { + items(tasks.value.filter { !it.completed }) { TaskEntry( task = it, onCheckTask = { isChecked -> taskActions.onCheckTask(it, isChecked) }, - onDeleteTask = { taskActions.deleteTask(it) }, + onArchiveTask = { taskActions.archiveTask(it) }, + onStartTask = { taskActions.startTask(it) } + ) + } + items(tasks.value.filter { it.completed }) { + TaskEntry( + task = it, + onCheckTask = { isChecked -> taskActions.onCheckTask(it, isChecked) }, + onArchiveTask = { taskActions.archiveTask(it) }, + onStartTask = { taskActions.startTask(it) } ) } } - NewTaskSubjectButton(onClick = taskActions.addTask, AppText.new_task) } } } @@ -105,9 +116,10 @@ fun TaskScreenPreview() { {}, { Subject(name = "Test Subject") }, { flowOf() }, - {}, { _, _ -> run {} }, {}, + {}, + {}, ) ) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/TaskViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/TaskViewModel.kt index 138d32c..e2adbc1 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/TaskViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/TaskViewModel.kt @@ -1,10 +1,10 @@ package be.ugent.sel.studeez.screens.tasks import be.ugent.sel.studeez.data.SelectedSubject +import be.ugent.sel.studeez.data.SelectedTask 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.LogService -import be.ugent.sel.studeez.domain.SubjectDAO import be.ugent.sel.studeez.domain.TaskDAO import be.ugent.sel.studeez.navigation.StudeezDestinations import be.ugent.sel.studeez.screens.StudeezViewModel @@ -15,8 +15,8 @@ import javax.inject.Inject @HiltViewModel class TaskViewModel @Inject constructor( private val taskDAO: TaskDAO, - private val subjectDAO: SubjectDAO, private val selectedSubject: SelectedSubject, + private val selectedTask: SelectedTask, logService: LogService, ) : StudeezViewModel(logService) { fun addTask(open: (String) -> Unit) { @@ -27,11 +27,6 @@ class TaskViewModel @Inject constructor( return taskDAO.getTasks(selectedSubject()) } - fun deleteSubject(open: (String) -> Unit) { - subjectDAO.deleteSubject(selectedSubject()) - open(StudeezDestinations.SUBJECT_SCREEN) - } - fun getSelectedSubject(): Subject { return selectedSubject() } @@ -40,11 +35,20 @@ class TaskViewModel @Inject constructor( taskDAO.deleteTask(task) } + fun archiveTask(task: Task) { + taskDAO.updateTask(task.copy(archived = true)) + } + fun toggleTaskCompleted(task: Task, completed: Boolean) { - taskDAO.toggleTaskCompleted(task, completed) + taskDAO.updateTask(task.copy(completed = completed)) } fun editSubject(open: (String) -> Unit) { open(StudeezDestinations.EDIT_SUBJECT_FORM) } + + fun startTask(task: Task, open: (String) -> Unit) { + selectedTask.set(task) + open(StudeezDestinations.TIMER_SELECTION_SCREEN) + } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/TaskFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormScreen.kt similarity index 92% rename from app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/TaskFormScreen.kt rename to app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormScreen.kt index 62b6c6c..79c744d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/TaskFormScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormScreen.kt @@ -1,4 +1,4 @@ -package be.ugent.sel.studeez.screens.tasks.forms +package be.ugent.sel.studeez.screens.tasks.form import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column @@ -11,17 +11,17 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import be.ugent.sel.studeez.common.composable.BasicButton import be.ugent.sel.studeez.common.composable.DeleteButton -import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate +import be.ugent.sel.studeez.common.composable.FormComposable import be.ugent.sel.studeez.common.ext.basicButton import be.ugent.sel.studeez.common.ext.fieldModifier import be.ugent.sel.studeez.resources import be.ugent.sel.studeez.R.string as AppText @Composable -fun TaskAddRoute( +fun TaskCreateRoute( goBack: () -> Unit, openAndPopUp: (String, String) -> Unit, - viewModel: TaskFormViewModel, + viewModel: TaskCreateFormViewModel, ) { val uiState by viewModel.uiState TaskForm( @@ -37,7 +37,7 @@ fun TaskAddRoute( fun TaskEditRoute( goBack: () -> Unit, openAndPopUp: (String, String) -> Unit, - viewModel: TaskFormViewModel, + viewModel: TaskEditFormViewModel, ) { val uiState by viewModel.uiState TaskForm( @@ -62,7 +62,7 @@ fun TaskForm( onNameChange: (String) -> Unit, extraButton: @Composable () -> Unit = {} ) { - SecondaryScreenTemplate( + FormComposable( title = resources().getString(title), popUp = goBack, ) { diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/TaskFormUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormUiState.kt similarity index 53% rename from app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/TaskFormUiState.kt rename to app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormUiState.kt index d967d59..6156fb7 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/TaskFormUiState.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormUiState.kt @@ -1,4 +1,4 @@ -package be.ugent.sel.studeez.screens.tasks.forms +package be.ugent.sel.studeez.screens.tasks.form data class TaskFormUiState( val name: String = "", diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/TaskFormViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormViewModel.kt similarity index 56% rename from app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/TaskFormViewModel.kt rename to app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormViewModel.kt index 03ad32b..07cba5d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/TaskFormViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormViewModel.kt @@ -1,5 +1,6 @@ -package be.ugent.sel.studeez.screens.tasks.forms +package be.ugent.sel.studeez.screens.tasks.form +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import be.ugent.sel.studeez.data.SelectedSubject import be.ugent.sel.studeez.data.SelectedTask @@ -11,39 +12,55 @@ import be.ugent.sel.studeez.screens.StudeezViewModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject -@HiltViewModel -class TaskFormViewModel @Inject constructor( - private val taskDAO: TaskDAO, - private val selectedSubject: SelectedSubject, - private val selectedTask: SelectedTask, +abstract class TaskFormViewModel( + protected val taskDAO: TaskDAO, + protected val selectedSubject: SelectedSubject, + protected val selectedTask: SelectedTask, logService: LogService, ) : StudeezViewModel(logService) { - var uiState = mutableStateOf( - if (selectedTask.isSet()) TaskFormUiState(selectedTask().name) else TaskFormUiState() - ) - private set + abstract val uiState: MutableState - private val name: String + protected val name: String get() = uiState.value.name fun onNameChange(newValue: String) { uiState.value = uiState.value.copy(name = newValue) } +} - fun onDelete(openAndPopUp: (String, String) -> Unit) { - taskDAO.deleteTask(selectedTask()) - openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.EDIT_TASK_FORM) - } +@HiltViewModel +class TaskCreateFormViewModel @Inject constructor( + taskDAO: TaskDAO, + selectedSubject: SelectedSubject, + selectedTask: SelectedTask, + logService: LogService, +) : TaskFormViewModel(taskDAO, selectedSubject, selectedTask, logService) { + override val uiState = mutableStateOf(TaskFormUiState()) fun onCreate(openAndPopUp: (String, String) -> Unit) { val newTask = Task(name = name, subjectId = selectedSubject().id) taskDAO.saveTask(newTask) openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.ADD_TASK_FORM) } +} + +@HiltViewModel +class TaskEditFormViewModel @Inject constructor( + taskDAO: TaskDAO, + selectedSubject: SelectedSubject, + selectedTask: SelectedTask, + logService: LogService, +) : TaskFormViewModel(taskDAO, selectedSubject, selectedTask, logService) { + override val uiState = mutableStateOf(TaskFormUiState()) + + fun onDelete(openAndPopUp: (String, String) -> Unit) { + taskDAO.deleteTask(selectedTask()) + openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.EDIT_TASK_FORM) + } fun onEdit(openAndPopUp: (String, String) -> Unit) { - val newTask = Task(name = name) + val newTask = selectedTask().copy(name = name) taskDAO.updateTask(newTask) openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.EDIT_TASK_FORM) } -} \ No newline at end of file +} diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/SubjectFormUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/SubjectFormUiState.kt deleted file mode 100644 index 5418b74..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/forms/SubjectFormUiState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package be.ugent.sel.studeez.screens.tasks.forms - -data class SubjectFormUiState( - val name: String = "", - val color: Long = 0xFFFFD200, -) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormRoute.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormRoute.kt deleted file mode 100644 index 0323dc2..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormRoute.kt +++ /dev/null @@ -1,42 +0,0 @@ -package be.ugent.sel.studeez.screens.timer_form - -import androidx.annotation.StringRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate -import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo -import be.ugent.sel.studeez.R.string as AppText - -@Composable -fun TimerAddRoute( - popUp: () -> Unit, - viewModel: TimerFormViewModel -) { - TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.add_timer) { - viewModel.saveTimer(it, goBack = popUp) - } -} - -@Composable -fun TimerEditRoute( - popUp: () -> Unit, - viewModel: TimerFormViewModel -) { - TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.edit_timer) { - viewModel.editTimer(it, goBack = popUp) - } -} - -@Composable -fun TimerFormScreen( - popUp: () -> Unit, - getTimerInfo: () -> TimerInfo, - @StringRes label: Int, - onConfirmClick: (TimerInfo) -> Unit -) { - val timerFormScreen = getTimerInfo().accept(GetTimerFormScreen()) - - SecondaryScreenTemplate(title = stringResource(id = label), popUp = popUp) { - timerFormScreen(onConfirmClick) - } -} diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormScreen.kt new file mode 100644 index 0000000..c69e929 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormScreen.kt @@ -0,0 +1,68 @@ +package be.ugent.sel.studeez.screens.timer_form + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import be.ugent.sel.studeez.common.composable.DeleteButton +import be.ugent.sel.studeez.common.composable.FormComposable +import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo +import be.ugent.sel.studeez.R.string as AppText + +@Composable +fun TimerAddRoute( + popUp: () -> Unit, + viewModel: TimerFormViewModel +) { + + + TimerFormScreen( + popUp = popUp, + getTimerInfo = viewModel::getTimerInfo, + extraButton= { }, + AppText.add_timer + ) { + viewModel.saveTimer(it, goBack = {popUp(); popUp()}) + + } +} + +@Composable +fun TimerEditRoute( + popUp: () -> Unit, + viewModel: TimerFormViewModel +) { + + @Composable + fun deleteButton() { + DeleteButton(text = AppText.delete_timer) { + viewModel.deleteTimer(viewModel.getTimerInfo(), popUp) + } + } + + TimerFormScreen( + popUp = popUp, + getTimerInfo = viewModel::getTimerInfo, + extraButton= { deleteButton() }, + AppText.edit_timer + ) { + viewModel.editTimer(it, goBack = popUp) + } +} + +@Composable +fun TimerFormScreen( + popUp: () -> Unit, + getTimerInfo: () -> TimerInfo, + extraButton: @Composable () -> Unit, + @StringRes label: Int, + onConfirmClick: (TimerInfo) -> Unit +) { + val timerFormScreen = getTimerInfo().accept(GetTimerFormScreen()) + + FormComposable( + title = stringResource(id = label), + popUp = popUp + ) { + timerFormScreen(onConfirmClick, extraButton) + } +} diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormViewModel.kt index 4c2079c..c34cd06 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormViewModel.kt @@ -1,6 +1,6 @@ package be.ugent.sel.studeez.screens.timer_form -import be.ugent.sel.studeez.data.EditTimerState +import be.ugent.sel.studeez.data.SelectedTimerInfo import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.TimerDAO @@ -10,15 +10,12 @@ import javax.inject.Inject @HiltViewModel class TimerFormViewModel @Inject constructor( - private val editTimerState: EditTimerState, + private val selectedTimerInfo: SelectedTimerInfo, private val timerDAO: TimerDAO, logService: LogService ) : StudeezViewModel(logService) { - - private val timerInfo: TimerInfo = editTimerState.timerInfo - fun getTimerInfo(): TimerInfo { - return timerInfo + return selectedTimerInfo() } fun editTimer(timerInfo: TimerInfo, goBack: () -> Unit) { @@ -26,6 +23,11 @@ class TimerFormViewModel @Inject constructor( goBack() } + fun deleteTimer(timerInfo: TimerInfo, goBack: () -> Unit) { + timerDAO.deleteTimer(timerInfo) + goBack() + } + fun saveTimer(timerInfo: TimerInfo, goBack: () -> Unit) { timerDAO.saveTimer(timerInfo) goBack() diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/AbstractTimerFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/AbstractTimerFormScreen.kt index 5f4a17b..7f92701 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/AbstractTimerFormScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/AbstractTimerFormScreen.kt @@ -1,69 +1,85 @@ package be.ugent.sel.studeez.screens.timer_form.form_screens -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.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.text.input.KeyboardType import be.ugent.sel.studeez.R import be.ugent.sel.studeez.common.composable.BasicButton -import be.ugent.sel.studeez.common.composable.LabelledInputField +import be.ugent.sel.studeez.common.composable.LabeledErrorTextField import be.ugent.sel.studeez.common.ext.basicButton +import be.ugent.sel.studeez.common.snackbar.SnackbarManager import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo import be.ugent.sel.studeez.R.string as AppText abstract class AbstractTimerFormScreen(private val timerInfo: TimerInfo) { + protected val valids = mutableMapOf( + "name" to mutableStateOf(textPredicate(timerInfo.name)), + "description" to mutableStateOf(textPredicate(timerInfo.description)) + ) + + protected val firsts = mutableMapOf( + "name" to mutableStateOf(true), + "description" to mutableStateOf(true) + ) + + @Composable - operator fun invoke(onSaveClick: (TimerInfo) -> Unit) { + operator fun invoke( + onSaveClick: (TimerInfo) -> Unit, + extraButton: @Composable () -> Unit = {}, + ) { - var name by remember { mutableStateOf(timerInfo.name) } - var description by remember { mutableStateOf(timerInfo.description) } - - // This shall rerun whenever name and description change - timerInfo.name = name - timerInfo.description = description - - Column( - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxHeight().verticalScroll(rememberScrollState()), - ) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - - // Fields that every timer shares (ommited id) - LabelledInputField( - value = name, - onNewValue = { name = it }, - label = R.string.name - ) - - LabelledInputField( - value = description, - onNewValue = { description = it }, - label = AppText.description, - singleLine = false - ) - - ExtraFields() + Column { + // Fields that every timer shares (ommited id) + LabeledErrorTextField( + initialValue = timerInfo.name, + label = R.string.name, + errorText = AppText.name_error, + isValid = valids.getValue("name"), + isFirst = firsts.getValue("name"), + keyboardType = KeyboardType.Text, + predicate = { it.isNotBlank() } + ) { correctName -> + timerInfo.name = correctName } + + LabeledErrorTextField( + initialValue = timerInfo.description, + label = R.string.description, + errorText = AppText.description_error, + isValid = valids.getValue("description"), + isFirst = firsts.getValue("description"), + singleLine = false, + keyboardType = KeyboardType.Text, + predicate = { textPredicate(it) } + ) { correctName -> + timerInfo.description = correctName + } + + ExtraFields() + BasicButton(R.string.save, Modifier.basicButton()) { - onSaveClick(timerInfo) + if (valids.all { it.component2().value }) { // All fields are valid + onSaveClick(timerInfo) + } else { + firsts.map { + it.component2().value = false + } // dont mask error because its not been filled out yet + SnackbarManager.showMessage(AppText.fill_out_error) + } } + extraButton() } } + private fun textPredicate(text: String): Boolean { + return text.isNotBlank() + } + @Composable open fun ExtraFields() { // By default no extra fields, unless overwritten by subclass. diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/BreakTimerFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/BreakTimerFormScreen.kt index 12d07a4..06c1079 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/BreakTimerFormScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/BreakTimerFormScreen.kt @@ -1,6 +1,8 @@ package be.ugent.sel.studeez.screens.timer_form.form_screens -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import be.ugent.sel.studeez.R @@ -15,6 +17,8 @@ class BreakTimerFormScreen( private val breakTimerInfo: PomodoroTimerInfo ): AbstractTimerFormScreen(breakTimerInfo) { + + @Composable override fun ExtraFields() { // If the user presses the OK button on the timepicker, the time in the button should change @@ -26,12 +30,17 @@ class BreakTimerFormScreen( breakTimerInfo.breakTime = newTime } + valids["repeats"] = remember {mutableStateOf(true)} + firsts["repeats"] = remember { mutableStateOf(true) } + LabeledErrorTextField( initialValue = breakTimerInfo.repeats.toString(), label = R.string.repeats, errorText = AppText.repeats_error, + isValid = valids.getValue("repeats"), + isFirst = firsts.getValue("repeats"), keyboardType = KeyboardType.Decimal, - predicate = { it.matches(Regex("[1-9]+\\d*")) } + predicate = { isNumber(it) } ) { correctlyTypedInt -> breakTimerInfo.repeats = correctlyTypedInt.toInt() } @@ -39,6 +48,10 @@ class BreakTimerFormScreen( } } +fun isNumber(text: String): Boolean { + return text.matches(Regex("[1-9]+\\d*")) +} + @Preview @Composable fun BreakEditScreenPreview() { diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectScreen.kt index fa8d650..d4b58f2 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectScreen.kt @@ -2,19 +2,20 @@ package be.ugent.sel.studeez.screens.timer_form.timer_type_select import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.Button 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.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate import be.ugent.sel.studeez.data.local.models.timer_info.* +import be.ugent.sel.studeez.data.local.models.timer_info.TimerType.* import be.ugent.sel.studeez.R.string as AppText -import be.ugent.sel.studeez.data.local.models.timer_info.TimerType.CUSTOM -import be.ugent.sel.studeez.data.local.models.timer_info.TimerType.BREAK -import be.ugent.sel.studeez.data.local.models.timer_info.TimerType.ENDLESS val defaultTimerInfo: Map = mapOf( CUSTOM to CustomTimerInfo("", "", 0), @@ -37,10 +38,22 @@ fun TimerTypeSelectScreen( ) { TimerType.values().forEach { timerType -> val default: TimerInfo = defaultTimerInfo.getValue(timerType) - Button(onClick = { viewModel.onTimerTypeChosen(default, open) }) { + Button( + onClick = { viewModel.onTimerTypeChosen(default, open) }, + modifier = Modifier.fillMaxWidth().padding(5.dp) + ) { Text(text = timerType.name) } } } } +} + +@Preview +@Composable +fun TimerTypeSelectScreenPreview() { + TimerTypeSelectScreen( + open = {}, + popUp = {} + ) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectViewModel.kt index 569a36c..c3ed2c4 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectViewModel.kt @@ -1,6 +1,6 @@ package be.ugent.sel.studeez.screens.timer_form.timer_type_select -import be.ugent.sel.studeez.data.EditTimerState +import be.ugent.sel.studeez.data.SelectedTimerInfo import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.navigation.StudeezDestinations @@ -10,13 +10,13 @@ import javax.inject.Inject @HiltViewModel class TimerTypeSelectViewModel @Inject constructor( - private val editTimerState: EditTimerState, + private val selectedTimerInfo: SelectedTimerInfo, logService: LogService ) : StudeezViewModel(logService) { fun onTimerTypeChosen(timerInfo: TimerInfo, open: (String) -> Unit) { - editTimerState.timerInfo = timerInfo + selectedTimerInfo.set(timerInfo) open(StudeezDestinations.ADD_TIMER_SCREEN) } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_overview/TimerOverviewScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_overview/TimerOverviewScreen.kt index a07dd67..3c25ddf 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_overview/TimerOverviewScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_overview/TimerOverviewScreen.kt @@ -82,12 +82,13 @@ fun TimerOverviewScreen( items(timers.value) { timerInfo -> TimerEntry( timerInfo = timerInfo, - ) { - StealthButton( - text = R.string.edit, - onClick = { timerOverviewActions.onEditClick(timerInfo) } - ) - } + rightButton = { + StealthButton( + text = R.string.edit, + onClick = { timerOverviewActions.onEditClick(timerInfo) } + ) + } + ) } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_overview/TimerOverviewViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_overview/TimerOverviewViewModel.kt index 3e10053..395a155 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_overview/TimerOverviewViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_overview/TimerOverviewViewModel.kt @@ -1,6 +1,6 @@ package be.ugent.sel.studeez.screens.timer_overview -import be.ugent.sel.studeez.data.EditTimerState +import be.ugent.sel.studeez.data.SelectedTimerInfo import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo import be.ugent.sel.studeez.domain.ConfigurationService import be.ugent.sel.studeez.domain.LogService @@ -15,11 +15,11 @@ import javax.inject.Inject class TimerOverviewViewModel @Inject constructor( private val configurationService: ConfigurationService, private val timerDAO: TimerDAO, - private val editTimerState: EditTimerState, + private val selectedTimerInfo: SelectedTimerInfo, logService: LogService ) : StudeezViewModel(logService) { - fun getUserTimers() : Flow> { + fun getUserTimers(): Flow> { return timerDAO.getUserTimers() } @@ -27,8 +27,8 @@ class TimerOverviewViewModel @Inject constructor( return configurationService.getDefaultTimers() } - fun update(timerInfo: TimerInfo, open: (String) -> Unit) { - editTimerState.timerInfo = timerInfo + fun update(timerInfo: TimerInfo, open: (String) -> Unit) { + selectedTimerInfo.set(timerInfo) open(StudeezDestinations.TIMER_EDIT_SCREEN) } @@ -36,7 +36,7 @@ class TimerOverviewViewModel @Inject constructor( open(StudeezDestinations.TIMER_TYPE_CHOOSING_SCREEN) } - fun delete(timerInfo: TimerInfo) =timerDAO.deleteTimer(timerInfo) + fun delete(timerInfo: TimerInfo) = timerDAO.deleteTimer(timerInfo) fun save(timerInfo: TimerInfo) = timerDAO.saveTimer(timerInfo) diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_selection/TimerSelectionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_selection/TimerSelectionScreen.kt index 2f17e65..d78b4bf 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_selection/TimerSelectionScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_selection/TimerSelectionScreen.kt @@ -1,10 +1,13 @@ package be.ugent.sel.studeez.screens.timer_selection +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier 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.SecondaryScreenTemplate import be.ugent.sel.studeez.common.composable.StealthButton @@ -99,7 +102,10 @@ fun CustomTimerEntry( ) }, rightButton = { - TimePickerButton(initialSeconds = hms.getTotalSeconds()) { chosenTime -> + TimePickerButton( + initialSeconds = hms.getTotalSeconds(), + modifier = Modifier.padding(horizontal = 5.dp) + ) { chosenTime -> timerInfo.studyTime = chosenTime } } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_selection/TimerSelectionViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_selection/TimerSelectionViewModel.kt index ab42973..c6c6793 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_selection/TimerSelectionViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_selection/TimerSelectionViewModel.kt @@ -1,10 +1,9 @@ package be.ugent.sel.studeez.screens.timer_selection import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import be.ugent.sel.studeez.data.SelectedTimerState +import be.ugent.sel.studeez.data.SelectedTimer +import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.TimerDAO @@ -17,18 +16,20 @@ import javax.inject.Inject @HiltViewModel class TimerSelectionViewModel @Inject constructor( private val timerDAO: TimerDAO, - private val selectedTimerState: SelectedTimerState, + private val selectedTimer: SelectedTimer, logService: LogService ) : StudeezViewModel(logService) { - var customTimerStudyTime: MutableState = mutableStateOf(0) + var customTimerStudyTime: MutableState = mutableStateOf( + HoursMinutesSeconds(1, 0, 0).getTotalSeconds() + ) - fun getAllTimers() : Flow> { + fun getAllTimers(): Flow> { return timerDAO.getAllTimers() } fun startSession(open: (String) -> Unit, timerInfo: TimerInfo) { - selectedTimerState.selectedTimer = timerInfo.getFunctionalTimer() + selectedTimer.set(timerInfo.getFunctionalTimer()) open(StudeezDestinations.SESSION_SCREEN) } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_more_horizontal.xml b/app/src/main/res/drawable/ic_more_horizontal.xml new file mode 100644 index 0000000..afbe22d --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horizontal.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/mood_1.xml b/app/src/main/res/drawable/mood_1.xml new file mode 100644 index 0000000..bf009f2 --- /dev/null +++ b/app/src/main/res/drawable/mood_1.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/mood_2.xml b/app/src/main/res/drawable/mood_2.xml new file mode 100644 index 0000000..0fd3daa --- /dev/null +++ b/app/src/main/res/drawable/mood_2.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d51259c..691b9f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,11 +16,12 @@ Go back Next Start + View more Success! Try again - Something wrong happened. Please try again. + Something went wrong. Please try again. Please insert a valid email. @@ -29,10 +30,20 @@ Home Start session + + Continue + Deleted + This is your feed + Click here to create you first subject and tasks to get started + + + Feed + Tasks Task My Subjects + Select Subject New Subject New Task Edit Subject @@ -40,6 +51,8 @@ Delete Subject Delete Task View + Select + Regenerate Color Looks like you found the sessions screen! In here, your upcoming studying sessions with friends will be listed. You can accept invites or edit your own. @@ -54,6 +67,7 @@ Edit profile Editing profile Delete profile + Bio @@ -63,8 +77,15 @@ Timers + Delete Timer Edit Add timer + + Name should not be blank + Description should not be blank + Fill out all the fields correctly! + + Select time Focus! @@ -106,7 +127,19 @@ Friends Friend + + %d Friend + %d Friends + Adding friends still needs to be implemented. Hang on tight! + You don\'t have any friends yet. Add one! + Search friends + Send friend request + Remove as friend + Show profile + Click to search friends + Searching friends + You are already befriended with that person. @@ -143,4 +176,11 @@ Break Time Number of Repeats + + "Congratulations! You studied: %s" + How did it go? + Good + Bad + + diff --git a/app/src/test/java/be/ugent/sel/studeez/timer_functional/FunctionalPomodoroTimerUnitTest.kt b/app/src/test/java/be/ugent/sel/studeez/timer_functional/FunctionalPomodoroTimerUnitTest.kt index 4b259c8..89a9b17 100644 --- a/app/src/test/java/be/ugent/sel/studeez/timer_functional/FunctionalPomodoroTimerUnitTest.kt +++ b/app/src/test/java/be/ugent/sel/studeez/timer_functional/FunctionalPomodoroTimerUnitTest.kt @@ -7,13 +7,14 @@ import org.junit.Test class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() { private val breakTime = 10 private val breaks = 2 + private val repeats = 3 // = breaks + 1 override val hours = 0 override val minutes = 0 override val seconds = 10 private lateinit var pomodoroTimer: FunctionalPomodoroTimer override fun setTimer() { - pomodoroTimer = FunctionalPomodoroTimer(time, breakTime, breaks) + pomodoroTimer = FunctionalPomodoroTimer(time, breakTime, repeats) } @Test diff --git a/app/src/test/java/be/ugent/sel/studeez/timer_functional/InvisibleSessionManagerTest.kt b/app/src/test/java/be/ugent/sel/studeez/timer_functional/InvisibleSessionManagerTest.kt index f9e34c3..891c379 100644 --- a/app/src/test/java/be/ugent/sel/studeez/timer_functional/InvisibleSessionManagerTest.kt +++ b/app/src/test/java/be/ugent/sel/studeez/timer_functional/InvisibleSessionManagerTest.kt @@ -1,11 +1,12 @@ package be.ugent.sel.studeez.timer_functional -import android.media.MediaPlayer -import be.ugent.sel.studeez.data.SelectedTimerState -import be.ugent.sel.studeez.data.SessionReportState +import be.ugent.sel.studeez.data.SelectedSessionReport +import be.ugent.sel.studeez.data.SelectedTask +import be.ugent.sel.studeez.data.SelectedTimer 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.domain.implementation.LogServiceImpl import be.ugent.sel.studeez.screens.session.InvisibleSessionManager import be.ugent.sel.studeez.screens.session.SessionViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -18,15 +19,15 @@ import org.mockito.kotlin.mock @ExperimentalCoroutinesApi class InvisibleSessionManagerTest { - private var timerState: SelectedTimerState = SelectedTimerState() + private var selectedTimer: SelectedTimer = SelectedTimer() private lateinit var viewModel: SessionViewModel - private var mediaPlayer: MediaPlayer = mock() + @Test fun InvisibleEndlessTimerTest() = runTest { - timerState.selectedTimer = FunctionalEndlessTimer() - viewModel = SessionViewModel(timerState, SessionReportState(), mock()) - InvisibleSessionManager.setParameters(viewModel, mediaPlayer) + selectedTimer.set(FunctionalEndlessTimer()) + viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl()) + InvisibleSessionManager.setParameters(viewModel, mock()) val test = launch { InvisibleSessionManager.updateTimer() @@ -45,10 +46,10 @@ class InvisibleSessionManagerTest { fun InvisiblePomodoroTimerTest() = runTest { val studyTime = 10 val breakTime = 5 - val repeats = 1 - timerState.selectedTimer = FunctionalPomodoroTimer(studyTime, breakTime, repeats) - viewModel = SessionViewModel(timerState, SessionReportState(), mock()) - InvisibleSessionManager.setParameters(viewModel, mediaPlayer) + val repeats = 2 + selectedTimer.set(FunctionalPomodoroTimer(studyTime, breakTime, repeats)) + viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl()) + InvisibleSessionManager.setParameters(viewModel, mock()) val test = launch { InvisibleSessionManager.updateTimer() @@ -79,9 +80,9 @@ class InvisibleSessionManagerTest { @Test fun InvisibleCustomTimerTest() = runTest { - timerState.selectedTimer = FunctionalCustomTimer(5) - viewModel = SessionViewModel(timerState, SessionReportState(), mock()) - InvisibleSessionManager.setParameters(viewModel, mediaPlayer) + selectedTimer.set(FunctionalCustomTimer(5)) + viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl()) + InvisibleSessionManager.setParameters(viewModel, mock()) val test = launch { InvisibleSessionManager.updateTimer() diff --git a/app/src/test/java/be/ugent/sel/studeez/timer_functional/TimeUnitTest.kt b/app/src/test/java/be/ugent/sel/studeez/timer_functional/TimeUnitTest.kt index 86fdce8..0d9bf4b 100644 --- a/app/src/test/java/be/ugent/sel/studeez/timer_functional/TimeUnitTest.kt +++ b/app/src/test/java/be/ugent/sel/studeez/timer_functional/TimeUnitTest.kt @@ -10,7 +10,7 @@ class TimeUnitTest { private val hours = 4 private val minutes = 20 private val seconds = 39 - private val time: Time = Time(seconds + minutes * 60 + hours * 60 * 60) + private var time: Time = Time(seconds + minutes * 60 + hours * 60 * 60) @Before fun setup() { @@ -21,9 +21,9 @@ class TimeUnitTest { fun formatTime() { Assert.assertEquals( HoursMinutesSeconds( - hours.toString().padStart(2, '0'), - minutes.toString().padStart(2, '0'), - seconds.toString().padStart(2, '0'), + hours, + minutes, + seconds, ), time.getAsHMS(), ) @@ -39,7 +39,11 @@ class TimeUnitTest { @Test fun minOne() { - time.minOne() + Assert.assertEquals( + (seconds + minutes * 60 + hours * 60 * 60), + time.time, + ) + time-- Assert.assertEquals( (seconds + minutes * 60 + hours * 60 * 60) - 1, time.time, @@ -48,7 +52,7 @@ class TimeUnitTest { @Test fun plusOne() { - time.plusOne() + time++ Assert.assertEquals( (seconds + minutes * 60 + hours * 60 * 60) + 1, time.time, @@ -59,7 +63,7 @@ class TimeUnitTest { fun minMultiple() { val n = 10 for (i in 1 .. n) { - time.minOne() + time-- } Assert.assertEquals( (seconds + minutes * 60 + hours * 60 * 60) - n, @@ -71,7 +75,7 @@ class TimeUnitTest { fun plusMultiple() { val n = 10 for (i in 1 .. n) { - time.plusOne() + time++ } Assert.assertEquals( (seconds + minutes * 60 + hours * 60 * 60) + n, diff --git a/build.gradle b/build.gradle index 7f25617..4535dd7 100644 --- a/build.gradle +++ b/build.gradle @@ -21,4 +21,3 @@ plugins { // Hilt id 'com.google.dagger.hilt.android' version '2.44' apply false } - diff --git a/gradle.properties b/gradle.properties index edf11ef..8581bd2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,4 +22,6 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false \ No newline at end of file