Merge commit '629137249e
'
This commit is contained in:
commit
43838f5d6d
84 changed files with 3324 additions and 524 deletions
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
@ -1,6 +1,6 @@
|
|||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
|
@ -147,4 +147,4 @@ protobuf {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
app/src/androidTest/java/be/ugent/sel/studeez/FabTest.kt
Normal file
74
app/src/androidTest/java/be/ugent/sel/studeez/FabTest.kt
Normal file
|
@ -0,0 +1,74 @@
|
|||
package be.ugent.sel.studeez
|
||||
|
||||
import androidx.compose.material.FloatingActionButton
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.performClick
|
||||
import be.ugent.sel.studeez.common.composable.AddButtonActions
|
||||
import be.ugent.sel.studeez.common.composable.ExpandedAddButton
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class FabTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun expandFabTest() {
|
||||
var expand = false
|
||||
|
||||
composeTestRule.setContent {
|
||||
FloatingActionButton(
|
||||
onClick = {expand = true}
|
||||
) {}
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
composeTestRule
|
||||
.onNode(hasClickAction())
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
assert(expand)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fabTest() {
|
||||
var task = false
|
||||
var session = false
|
||||
var friend = false
|
||||
|
||||
composeTestRule.setContent {
|
||||
ExpandedAddButton(
|
||||
addButtonActions = AddButtonActions(
|
||||
{task = true},
|
||||
{friend = true},
|
||||
{session = true}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Session")
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Task")
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Friend")
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
assert(task)
|
||||
assert(session)
|
||||
assert(friend)
|
||||
}
|
||||
}
|
207
app/src/androidTest/java/be/ugent/sel/studeez/HomeScreenTest.kt
Normal file
207
app/src/androidTest/java/be/ugent/sel/studeez/HomeScreenTest.kt
Normal file
|
@ -0,0 +1,207 @@
|
|||
package be.ugent.sel.studeez
|
||||
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
|
||||
import be.ugent.sel.studeez.common.composable.feed.FeedUiState
|
||||
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
|
||||
import be.ugent.sel.studeez.data.local.models.FeedEntry
|
||||
import be.ugent.sel.studeez.screens.home.HomeScreen
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class HomeScreenTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun homeScreenTest() {
|
||||
var continueTask = false
|
||||
|
||||
composeTestRule.setContent {
|
||||
HomeScreen(
|
||||
drawerActions = DrawerActions({}, {}, {}, {}, {}),
|
||||
navigationBarActions = NavigationBarActions({false}, {}, {}, {}, {}, {}, {}, {}),
|
||||
feedUiState = FeedUiState.Succes(mapOf(
|
||||
"08 May 2023" to listOf(
|
||||
FeedEntry(
|
||||
argb_color = 0xFFABD200,
|
||||
subJectName = "Test Subject",
|
||||
taskName = "Test Task",
|
||||
totalStudyTime = 600,
|
||||
)
|
||||
)
|
||||
)),
|
||||
continueTask = {_, _ -> continueTask = true },
|
||||
onEmptyFeedHelp = {},
|
||||
onViewFriendsClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
"continue",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
Assert.assertTrue(continueTask)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun drawerTest() {
|
||||
var homebuttontest = false
|
||||
var timersbuttontest = false
|
||||
var settingsbuttontest = false
|
||||
var logoutbuttontest = false
|
||||
var aboutbuttontest = false
|
||||
|
||||
composeTestRule.setContent {
|
||||
HomeScreen(
|
||||
drawerActions = DrawerActions(
|
||||
{homebuttontest = true},
|
||||
{timersbuttontest = true},
|
||||
{settingsbuttontest = true},
|
||||
{logoutbuttontest = true},
|
||||
{aboutbuttontest = true}
|
||||
),
|
||||
navigationBarActions = NavigationBarActions({false}, {}, {}, {}, {}, {}, {}, {}),
|
||||
feedUiState = FeedUiState.Succes(mapOf()),
|
||||
continueTask = {_, _ -> },
|
||||
onEmptyFeedHelp = {},
|
||||
onViewFriendsClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
"home",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)[2] // Third node has the button
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
"timer",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
"settings",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
"log out",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
"about",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
Assert.assertTrue(homebuttontest)
|
||||
Assert.assertTrue(timersbuttontest)
|
||||
Assert.assertTrue(settingsbuttontest)
|
||||
Assert.assertTrue(logoutbuttontest)
|
||||
Assert.assertTrue(aboutbuttontest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun navigationbarTest() {
|
||||
var hometest = false
|
||||
var tasktest = false
|
||||
var sessiontest = false
|
||||
var profiletest = false
|
||||
|
||||
composeTestRule.setContent {
|
||||
HomeScreen(
|
||||
drawerActions = DrawerActions({}, {}, {}, {}, {}),
|
||||
navigationBarActions = NavigationBarActions(
|
||||
{false},
|
||||
{hometest = true},
|
||||
{tasktest = true},
|
||||
{sessiontest = true},
|
||||
{profiletest = true},
|
||||
{}, {}, {}
|
||||
),
|
||||
feedUiState = FeedUiState.Succes(mapOf()),
|
||||
continueTask = {_, _ -> },
|
||||
onEmptyFeedHelp = {},
|
||||
onViewFriendsClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithContentDescription(
|
||||
"Home",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)[0] // Third node has the button
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(
|
||||
"tasks",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(
|
||||
"session",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(
|
||||
"profile",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
Assert.assertTrue(hometest)
|
||||
Assert.assertTrue(tasktest)
|
||||
Assert.assertTrue(sessiontest)
|
||||
Assert.assertTrue(profiletest)
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
160
app/src/androidTest/java/be/ugent/sel/studeez/TaskScreenTest.kt
Normal file
160
app/src/androidTest/java/be/ugent/sel/studeez/TaskScreenTest.kt
Normal file
|
@ -0,0 +1,160 @@
|
|||
package be.ugent.sel.studeez
|
||||
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import be.ugent.sel.studeez.common.composable.DeleteButton
|
||||
import be.ugent.sel.studeez.data.local.models.task.Subject
|
||||
import be.ugent.sel.studeez.data.local.models.task.Task
|
||||
import be.ugent.sel.studeez.screens.tasks.TaskActions
|
||||
import be.ugent.sel.studeez.screens.tasks.TaskScreen
|
||||
import be.ugent.sel.studeez.screens.tasks.form.TaskForm
|
||||
import be.ugent.sel.studeez.screens.tasks.form.TaskFormUiState
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class TaskScreenTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun addTaskScreenTest() {
|
||||
var confirm = false
|
||||
var goback = false
|
||||
|
||||
composeTestRule.setContent {
|
||||
TaskForm(
|
||||
title = R.string.new_task,
|
||||
goBack = {goback = true},
|
||||
uiState = TaskFormUiState(),
|
||||
onConfirm = {confirm = true},
|
||||
onNameChange = {},
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
text = "confirm",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(
|
||||
label = "go back",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
assert(confirm)
|
||||
assert(goback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun editTaskScreenTest() {
|
||||
var confirm = false
|
||||
var delete = false
|
||||
|
||||
composeTestRule.setContent {
|
||||
TaskForm(
|
||||
title = R.string.edit_task,
|
||||
goBack = {},
|
||||
uiState = TaskFormUiState(
|
||||
name = "Test Task",
|
||||
),
|
||||
onConfirm = {confirm = true},
|
||||
onNameChange = {},
|
||||
) {
|
||||
DeleteButton(text = R.string.delete_task) {
|
||||
delete = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
text = "confirm",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
text = "delete",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
assert(confirm)
|
||||
assert(delete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun taskScreenTest() {
|
||||
var add = false
|
||||
var edit = false
|
||||
var start = false
|
||||
|
||||
composeTestRule.setContent {
|
||||
TaskScreen(
|
||||
goBack = {},
|
||||
taskActions = TaskActions(
|
||||
{add = true},
|
||||
{ Subject(name = "Test Subject") },
|
||||
{ flowOf(listOf(Task())) },
|
||||
{ _, _ -> run {} },
|
||||
{edit = true},
|
||||
{start = true},
|
||||
{},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(
|
||||
label = "edit",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
text = "new",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
text = "start",
|
||||
substring = true,
|
||||
ignoreCase = true
|
||||
)
|
||||
.assertExists()
|
||||
.performClick()
|
||||
|
||||
assert(add)
|
||||
assert(edit)
|
||||
assert(start)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -33,7 +33,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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
package be.ugent.sel.studeez.common.composable
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
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,6 +23,7 @@ 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 com.google.android.material.color.MaterialColors
|
||||
import kotlin.math.sin
|
||||
import be.ugent.sel.studeez.R.drawable as AppIcon
|
||||
import be.ugent.sel.studeez.R.string as AppText
|
||||
|
@ -47,7 +49,7 @@ fun LabelledInputField(
|
|||
value: String,
|
||||
onNewValue: (String) -> Unit,
|
||||
@StringRes label: Int,
|
||||
singleLine: Boolean = false
|
||||
singleLine: Boolean = true
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
|
@ -119,7 +121,9 @@ fun LabeledErrorTextField(
|
|||
initialValue: String,
|
||||
@StringRes label: Int,
|
||||
singleLine: Boolean = false,
|
||||
errorText: Int,
|
||||
isValid: MutableState<Boolean> = remember { mutableStateOf(true) },
|
||||
isFirst: MutableState<Boolean> = remember { mutableStateOf(false) },
|
||||
@StringRes errorText: Int,
|
||||
keyboardType: KeyboardType,
|
||||
predicate: (String) -> Boolean,
|
||||
onNewCorrectValue: (String) -> Unit
|
||||
|
@ -128,31 +132,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 +219,34 @@ 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
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = modifier,
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
|
@ -5,6 +5,7 @@ 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
|
||||
|
||||
|
@ -42,4 +43,11 @@ class SelectedSubject @Inject constructor() : SelectedState<Subject>() {
|
|||
@Singleton
|
||||
class SelectedTimerInfo @Inject constructor() : SelectedState<TimerInfo>() {
|
||||
override lateinit var value: TimerInfo
|
||||
}
|
||||
|
||||
@Singleton
|
||||
class SelectedUserId @Inject constructor(
|
||||
userDAO: UserDAO
|
||||
): SelectedState<String>() {
|
||||
override var value: String = userDAO.getCurrentUserId()
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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 = ""
|
||||
)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package be.ugent.sel.studeez.data.local.models.task
|
||||
|
||||
import com.google.firebase.firestore.DocumentId
|
||||
import com.google.firebase.firestore.Exclude
|
||||
|
||||
data class Subject(
|
||||
@DocumentId val id: String = "",
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package be.ugent.sel.studeez.data.remote
|
||||
|
||||
object FirebaseSessionReport {
|
||||
const val STUDYTIME: String = "studyTime"
|
||||
const val ENDTIME: String = "endTime"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package be.ugent.sel.studeez.data.remote
|
||||
|
||||
object FirebaseUser {
|
||||
const val USERNAME: String = "username"
|
||||
const val BIOGRAPHY: String = "biography"
|
||||
}
|
|
@ -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,13 +29,13 @@ 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
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package be.ugent.sel.studeez.domain
|
||||
|
||||
import be.ugent.sel.studeez.data.local.models.Friendship
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Should be used for interactions between friends.
|
||||
*/
|
||||
interface FriendshipDAO {
|
||||
|
||||
/**
|
||||
* @return all friendships of a chosen user.
|
||||
*/
|
||||
fun getAllFriendships(
|
||||
userId: String
|
||||
): Flow<List<Friendship>>
|
||||
|
||||
/**
|
||||
* @return the amount of friends of a chosen user.
|
||||
* This method should be faster than just counting the length of getAllFriends()
|
||||
*/
|
||||
fun getFriendshipCount(
|
||||
userId: String
|
||||
): Flow<Int>
|
||||
|
||||
/**
|
||||
* @param id the id of the friendship that you want details of
|
||||
* @return the details of a Friendship
|
||||
*/
|
||||
fun getFriendshipDetails(id: String): Friendship
|
||||
|
||||
/**
|
||||
* Send a friend request to a user.
|
||||
* @param id of the user that you want to add as a friend
|
||||
* @return Success/faillure of transaction
|
||||
*/
|
||||
fun sendFriendshipRequest(id: String): Boolean
|
||||
|
||||
/**
|
||||
* Accept a friend request that has already been sent.
|
||||
* @param id of the friendship that you want to update
|
||||
* @return: Success/faillure of transaction
|
||||
*/
|
||||
fun acceptFriendship(id: String): Boolean
|
||||
|
||||
/**
|
||||
* Remove a friend or decline a friendrequest.
|
||||
* @param friendship the one you want to remove
|
||||
* @return: Success/faillure of transaction
|
||||
*/
|
||||
fun removeFriendship(
|
||||
friendship: Friendship
|
||||
): Boolean
|
||||
}
|
|
@ -1,12 +1,19 @@
|
|||
package be.ugent.sel.studeez.domain
|
||||
|
||||
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.timer_info.TimerInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SessionDAO {
|
||||
|
||||
fun getSessions(): Flow<List<SessionReport>>
|
||||
suspend fun getSessionsOfUser(userId: String): List<SessionReport>
|
||||
|
||||
/**
|
||||
* Return a list of pairs, containing the username and all the studysessions of that user.
|
||||
*/
|
||||
fun getFriendsSessions(): Flow<List<Pair<String,List<SessionReport>>>>
|
||||
|
||||
fun saveSession(newSessionReport: SessionReport)
|
||||
|
||||
|
|
|
@ -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<List<User>>
|
||||
|
||||
/**
|
||||
* @return all users based on a query, a trimmed down version of getAllUsers()
|
||||
*/
|
||||
fun getUsersWithQuery(
|
||||
fieldName: String,
|
||||
value: String
|
||||
): Flow<List<User>>
|
||||
|
||||
/**
|
||||
* Request information about a user
|
||||
*/
|
||||
fun getUserDetails(
|
||||
userId: String
|
||||
): Flow<User>
|
||||
|
||||
suspend fun getUsername(
|
||||
userId: String
|
||||
): String
|
||||
|
||||
/**
|
||||
* @return information on the currently logged in user.
|
||||
*/
|
||||
suspend fun getLoggedInUser(): User
|
||||
// TODO Should be refactored to fun getLoggedInUser(): Flow<User>, without suspend.
|
||||
|
||||
suspend fun saveLoggedInUser(
|
||||
newUsername: String,
|
||||
newBiography: String = ""
|
||||
)
|
||||
// TODO Should be refactored to fun saveLoggedInUser(...): Boolean, without suspend.
|
||||
|
||||
/**
|
||||
* Delete all references to the logged in user in the database. Similar to the deleteCascade in
|
||||
* relational databases.
|
||||
*/
|
||||
suspend fun deleteUserReferences()
|
||||
suspend fun deleteLoggedInUserReferences()
|
||||
// TODO Should be refactored to fun deleteLoggedInUserReferences(): Boolean, without suspend.
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package be.ugent.sel.studeez.domain.implementation
|
||||
|
||||
import be.ugent.sel.studeez.data.local.models.SessionReport
|
||||
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
|
||||
import be.ugent.sel.studeez.domain.AccountDAO
|
||||
import be.ugent.sel.studeez.domain.SessionDAO
|
||||
import com.google.firebase.firestore.CollectionReference
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import com.google.firebase.firestore.ktx.snapshots
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class FireBaseSessionDAO @Inject constructor(
|
||||
private val firestore: FirebaseFirestore,
|
||||
private val auth: AccountDAO
|
||||
) : SessionDAO {
|
||||
|
||||
override fun getSessions(): Flow<List<SessionReport>> {
|
||||
return currentUserSessionsCollection()
|
||||
.snapshots()
|
||||
.map { it.toObjects(SessionReport::class.java) }
|
||||
}
|
||||
|
||||
override fun saveSession(newSessionReport: SessionReport) {
|
||||
currentUserSessionsCollection().add(newSessionReport)
|
||||
}
|
||||
|
||||
override fun deleteSession(newTimer: TimerInfo) {
|
||||
currentUserSessionsCollection().document(newTimer.id).delete()
|
||||
}
|
||||
|
||||
private fun currentUserSessionsCollection(): CollectionReference =
|
||||
firestore.collection(FireBaseCollections.USER_COLLECTION)
|
||||
.document(auth.currentUserId)
|
||||
.collection(FireBaseCollections.SESSION_COLLECTION)
|
||||
}
|
|
@ -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"
|
|
@ -0,0 +1,134 @@
|
|||
package be.ugent.sel.studeez.domain.implementation
|
||||
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.FRIENDSSINCE
|
||||
import be.ugent.sel.studeez.data.remote.FirebaseFriendship.FRIENDID
|
||||
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 javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import be.ugent.sel.studeez.R.string as AppText
|
||||
|
||||
class FirebaseFriendshipDAO @Inject constructor(
|
||||
private val firestore: FirebaseFirestore,
|
||||
private val auth: AccountDAO
|
||||
): FriendshipDAO {
|
||||
|
||||
private fun currentUserDocument(): DocumentReference = firestore
|
||||
.collection(USER_COLLECTION)
|
||||
.document(auth.currentUserId)
|
||||
|
||||
override fun getAllFriendships(
|
||||
userId: String
|
||||
): Flow<List<Friendship>> {
|
||||
return firestore
|
||||
.collection(USER_COLLECTION)
|
||||
.document(userId)
|
||||
.collection(FRIENDS_COLLECTION)
|
||||
.snapshots()
|
||||
.map { it.toObjects(Friendship::class.java) }
|
||||
}
|
||||
|
||||
override fun getFriendshipCount(
|
||||
userId: String
|
||||
): Flow<Int> {
|
||||
return flow {
|
||||
val friendshipCount = suspendCoroutine { continuation ->
|
||||
firestore
|
||||
.collection(USER_COLLECTION)
|
||||
.document(userId)
|
||||
.collection(FRIENDS_COLLECTION)
|
||||
.get()
|
||||
.addOnSuccessListener { querySnapshot ->
|
||||
continuation.resume(querySnapshot.size())
|
||||
}
|
||||
.addOnFailureListener { exception ->
|
||||
continuation.resumeWithException(exception)
|
||||
}
|
||||
}
|
||||
emit(friendshipCount)
|
||||
}.catch {
|
||||
SnackbarManager.showMessage(AppText.generic_error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFriendshipDetails(id: String): Friendship {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun sendFriendshipRequest(id: String): Boolean {
|
||||
val currentUserId: String = auth.currentUserId
|
||||
val otherUserId: String = id
|
||||
|
||||
// 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()
|
||||
))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package be.ugent.sel.studeez.domain.implementation
|
||||
|
||||
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.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.AccountDAO
|
||||
import be.ugent.sel.studeez.domain.FriendshipDAO
|
||||
import be.ugent.sel.studeez.domain.SessionDAO
|
||||
import be.ugent.sel.studeez.domain.UserDAO
|
||||
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 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,
|
||||
private val userDAO: UserDAO,
|
||||
private val friendshipDAO: FriendshipDAO
|
||||
) : SessionDAO {
|
||||
|
||||
override fun getSessions(): Flow<List<SessionReport>> {
|
||||
return currentUserSessionsCollection()
|
||||
.snapshots()
|
||||
.map { it.toObjects(SessionReport::class.java) }
|
||||
}
|
||||
|
||||
override suspend fun getSessionsOfUser(userId: String): List<SessionReport> {
|
||||
val collection = firestore.collection(USER_COLLECTION)
|
||||
.document(userId)
|
||||
.collection(SESSION_COLLECTION)
|
||||
.get().await()
|
||||
val list: MutableList<SessionReport> = mutableListOf()
|
||||
for (document in collection) {
|
||||
val id = document.id
|
||||
val studyTime: Int = document.getField<Int>(STUDYTIME)!!
|
||||
val endTime: Timestamp = document.getField<Timestamp>(ENDTIME)!!
|
||||
list.add(SessionReport(id, studyTime, endTime))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
override fun getFriendsSessions(): Flow<List<Pair<String, List<SessionReport>>>> {
|
||||
return friendshipDAO.getAllFriendships(auth.currentUserId)
|
||||
.map { friendships ->
|
||||
friendships.map { friendship ->
|
||||
val userId: String = friendship.friendId
|
||||
val username = userDAO.getUsername(userId)
|
||||
val userSessions = getSessionsOfUser(userId)
|
||||
|
||||
Pair(username, userSessions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
|
@ -19,7 +19,7 @@ import kotlinx.coroutines.tasks.await
|
|||
import javax.inject.Inject
|
||||
import kotlin.collections.count
|
||||
|
||||
class FireBaseSubjectDAO @Inject constructor(
|
||||
class FirebaseSubjectDAO @Inject constructor(
|
||||
private val firestore: FirebaseFirestore,
|
||||
private val auth: AccountDAO,
|
||||
private val taskDAO: TaskDAO,
|
||||
|
@ -50,7 +50,7 @@ class FireBaseSubjectDAO @Inject constructor(
|
|||
override suspend fun archiveSubject(subject: Subject) {
|
||||
currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true)
|
||||
currentUserSubjectsCollection().document(subject.id)
|
||||
.collection(FireBaseCollections.TASK_COLLECTION)
|
||||
.collection(FirebaseCollections.TASK_COLLECTION)
|
||||
.taskNotArchived()
|
||||
.get().await()
|
||||
.documents
|
||||
|
@ -75,20 +75,20 @@ class FireBaseSubjectDAO @Inject constructor(
|
|||
}
|
||||
|
||||
private fun currentUserSubjectsCollection(): CollectionReference =
|
||||
firestore.collection(FireBaseCollections.USER_COLLECTION)
|
||||
firestore.collection(FirebaseCollections.USER_COLLECTION)
|
||||
.document(auth.currentUserId)
|
||||
.collection(FireBaseCollections.SUBJECT_COLLECTION)
|
||||
.collection(FirebaseCollections.SUBJECT_COLLECTION)
|
||||
|
||||
private fun subjectTasksCollection(subject: Subject): CollectionReference =
|
||||
firestore.collection(FireBaseCollections.USER_COLLECTION)
|
||||
firestore.collection(FirebaseCollections.USER_COLLECTION)
|
||||
.document(auth.currentUserId)
|
||||
.collection(FireBaseCollections.SUBJECT_COLLECTION)
|
||||
.collection(FirebaseCollections.SUBJECT_COLLECTION)
|
||||
.document(subject.id)
|
||||
.collection(FireBaseCollections.TASK_COLLECTION)
|
||||
.collection(FirebaseCollections.TASK_COLLECTION)
|
||||
|
||||
fun CollectionReference.subjectNotArchived(): Query =
|
||||
this.whereEqualTo(SubjectDocument.archived, false)
|
||||
|
||||
fun Query.subjectNotArchived(): Query =
|
||||
this.whereEqualTo(SubjectDocument.archived, false)
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.tasks.await
|
||||
import javax.inject.Inject
|
||||
|
||||
class FireBaseTaskDAO @Inject constructor(
|
||||
class FirebaseTaskDAO @Inject constructor(
|
||||
private val firestore: FirebaseFirestore,
|
||||
private val auth: AccountDAO,
|
||||
) : TaskDAO {
|
||||
|
@ -45,12 +45,11 @@ class FireBaseTaskDAO @Inject constructor(
|
|||
}
|
||||
|
||||
private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference =
|
||||
firestore.collection(FireBaseCollections.USER_COLLECTION)
|
||||
firestore.collection(FirebaseCollections.USER_COLLECTION)
|
||||
.document(auth.currentUserId)
|
||||
.collection(FireBaseCollections.SUBJECT_COLLECTION)
|
||||
.collection(FirebaseCollections.SUBJECT_COLLECTION)
|
||||
.document(subjectId)
|
||||
.collection(FireBaseCollections.TASK_COLLECTION)
|
||||
|
||||
.collection(FirebaseCollections.TASK_COLLECTION)
|
||||
}
|
||||
|
||||
// Extend CollectionReference and Query with some filters
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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<List<User>> {
|
||||
return firestore
|
||||
.collection(USER_COLLECTION)
|
||||
.snapshots()
|
||||
.map { it.toObjects(User::class.java) }
|
||||
}
|
||||
|
||||
override suspend fun deleteUserReferences() {
|
||||
override fun getUsersWithQuery(
|
||||
fieldName: String,
|
||||
value: String
|
||||
): Flow<List<User>> {
|
||||
return firestore
|
||||
.collection(USER_COLLECTION)
|
||||
.whereEqualTo(fieldName, value)
|
||||
.snapshots()
|
||||
.map { it.toObjects(User::class.java) }
|
||||
}
|
||||
|
||||
override fun getUserDetails(userId: String): Flow<User> {
|
||||
return flow {
|
||||
val snapshot = firestore
|
||||
.collection(USER_COLLECTION)
|
||||
.document(userId)
|
||||
.get()
|
||||
.await()
|
||||
val user = snapshot.toObject(User::class.java)!!
|
||||
emit(user)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUsername(userId: String): String {
|
||||
val user = firestore.collection(USER_COLLECTION)
|
||||
.document(userId)
|
||||
.get().await()
|
||||
return user.getString(USERNAME)!!
|
||||
}
|
||||
|
||||
override suspend fun getLoggedInUser(): User {
|
||||
val userDocument = currentUserDocument().get().await()
|
||||
return User(
|
||||
username = userDocument.getString(USERNAME) ?: "",
|
||||
biography = userDocument.getString(BIOGRAPHY) ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun saveLoggedInUser(
|
||||
newUsername: String,
|
||||
newBiography: String
|
||||
) {
|
||||
currentUserDocument().set(mapOf(
|
||||
USERNAME to newUsername,
|
||||
BIOGRAPHY to newBiography
|
||||
))
|
||||
}
|
||||
|
||||
override suspend fun deleteLoggedInUserReferences() {
|
||||
currentUserDocument().delete()
|
||||
.addOnSuccessListener { SnackbarManager.showMessage(R.string.success) }
|
||||
.addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) }
|
||||
|
|
|
@ -31,7 +31,9 @@ object StudeezDestinations {
|
|||
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"
|
||||
|
|
|
@ -14,10 +14,13 @@ 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.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.edit_profile.EditProfileRoute
|
||||
import be.ugent.sel.studeez.screens.profile.ProfileRoute
|
||||
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
|
||||
|
@ -70,6 +73,7 @@ fun StudeezNavGraph(
|
|||
drawerActions = drawerActions,
|
||||
navigationBarActions = navigationBarActions,
|
||||
feedViewModel = hiltViewModel(),
|
||||
viewModel = hiltViewModel()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -230,8 +234,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
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
package be.ugent.sel.studeez.screens.friends.friends_overview
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.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.SearchField
|
||||
import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry
|
||||
import be.ugent.sel.studeez.common.ext.basicButton
|
||||
import be.ugent.sel.studeez.data.local.models.Friendship
|
||||
import be.ugent.sel.studeez.data.local.models.User
|
||||
import be.ugent.sel.studeez.resources
|
||||
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||
import com.google.firebase.Timestamp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import be.ugent.sel.studeez.R.string as AppText
|
||||
|
||||
data class FriendsOverviewActions(
|
||||
val getFriendsFlow: () -> Flow<List<Pair<User, Friendship>>>,
|
||||
val searchFriends: () -> Unit,
|
||||
val onQueryStringChange: (String) -> Unit,
|
||||
val onSubmit: () -> Unit,
|
||||
val viewProfile: (String) -> Unit,
|
||||
val removeFriend: (Friendship) -> Unit
|
||||
)
|
||||
|
||||
fun getFriendsOverviewActions(
|
||||
viewModel: FriendsOverviewViewModel,
|
||||
open: (String) -> Unit
|
||||
): FriendsOverviewActions {
|
||||
return FriendsOverviewActions(
|
||||
getFriendsFlow = viewModel::getAllFriends,
|
||||
searchFriends = { viewModel.searchFriends(open) },
|
||||
onQueryStringChange = viewModel::onQueryStringChange,
|
||||
onSubmit = { viewModel.onSubmit(open) },
|
||||
viewProfile = { userId ->
|
||||
viewModel.viewProfile(userId, open)
|
||||
},
|
||||
removeFriend = viewModel::removeFriend
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FriendsOveriewRoute(
|
||||
open: (String) -> Unit,
|
||||
popUp: () -> Unit,
|
||||
viewModel: FriendsOverviewViewModel
|
||||
) {
|
||||
val uiState by viewModel.uiState
|
||||
FriendsOverviewScreen(
|
||||
popUp = popUp,
|
||||
uiState = uiState,
|
||||
friendsOverviewActions = getFriendsOverviewActions(
|
||||
viewModel = viewModel,
|
||||
open = open
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FriendsOverviewScreen(
|
||||
popUp: () -> Unit,
|
||||
uiState: FriendsOverviewUiState,
|
||||
friendsOverviewActions: FriendsOverviewActions
|
||||
) {
|
||||
val friends = friendsOverviewActions.getFriendsFlow().collectAsState(initial = emptyList())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
// TODO Link to each other
|
||||
SearchField(
|
||||
value = uiState.queryString,
|
||||
onValueChange = friendsOverviewActions.onQueryStringChange,
|
||||
onSubmit = friendsOverviewActions.onSubmit,
|
||||
label = AppText.search_friends
|
||||
)
|
||||
},
|
||||
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
|
||||
) {
|
||||
Row (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 15.dp, vertical = 7.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 = { }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package be.ugent.sel.studeez.screens.friends.friends_overview
|
||||
|
||||
data class FriendsOverviewUiState(
|
||||
val userId: String,
|
||||
val queryString: String = ""
|
||||
)
|
|
@ -0,0 +1,78 @@
|
|||
package be.ugent.sel.studeez.screens.friends.friends_overview
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import be.ugent.sel.studeez.data.SelectedUserId
|
||||
import be.ugent.sel.studeez.data.local.models.Friendship
|
||||
import be.ugent.sel.studeez.data.local.models.User
|
||||
import be.ugent.sel.studeez.domain.FriendshipDAO
|
||||
import be.ugent.sel.studeez.domain.LogService
|
||||
import be.ugent.sel.studeez.domain.UserDAO
|
||||
import be.ugent.sel.studeez.navigation.StudeezDestinations
|
||||
import be.ugent.sel.studeez.screens.StudeezViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class FriendsOverviewViewModel @Inject constructor(
|
||||
private val userDAO: UserDAO,
|
||||
private val friendshipDAO: FriendshipDAO,
|
||||
private val selectedUserIdState: SelectedUserId,
|
||||
logService: LogService
|
||||
) : StudeezViewModel(logService) {
|
||||
|
||||
var uiState = mutableStateOf(FriendsOverviewUiState(
|
||||
userId = selectedUserIdState.value
|
||||
))
|
||||
private set
|
||||
|
||||
fun getAllFriends(): Flow<List<Pair<User, Friendship>>> {
|
||||
return friendshipDAO.getAllFriendships(
|
||||
userId = uiState.value.userId
|
||||
)
|
||||
.flatMapConcat { friendships ->
|
||||
val userFlows = friendships.map { friendship ->
|
||||
userDAO.getUserDetails(friendship.friendId)
|
||||
}
|
||||
combine(userFlows) { users ->
|
||||
friendships.zip(users) { friendship, user ->
|
||||
Pair(user, friendship)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchFriends(open: (String) -> Unit) {
|
||||
open(StudeezDestinations.SEARCH_FRIENDS_SCREEN)
|
||||
}
|
||||
|
||||
fun onQueryStringChange(newValue: String) {
|
||||
uiState.value = uiState.value.copy(
|
||||
queryString = newValue
|
||||
)
|
||||
}
|
||||
|
||||
fun onSubmit(open: (String) -> Unit) {
|
||||
val query = uiState.value.queryString // TODO Pass as argument
|
||||
open(StudeezDestinations.SEARCH_FRIENDS_SCREEN)
|
||||
}
|
||||
|
||||
fun viewProfile(
|
||||
userId: String,
|
||||
open: (String) -> Unit
|
||||
) {
|
||||
selectedUserIdState.value = userId
|
||||
open(StudeezDestinations.PUBLIC_PROFILE_SCREEN)
|
||||
}
|
||||
|
||||
fun removeFriend(
|
||||
friendship: Friendship
|
||||
) {
|
||||
friendshipDAO.removeFriendship(
|
||||
friendship = friendship
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package be.ugent.sel.studeez.screens.friends.friends_search
|
||||
|
||||
import be.ugent.sel.studeez.data.local.models.User
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
data class SearchFriendUiState(
|
||||
val queryString: String = "",
|
||||
val searchResults: Flow<List<User>> = emptyFlow()
|
||||
)
|
|
@ -0,0 +1,265 @@
|
|||
package be.ugent.sel.studeez.screens.friends.friends_search
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.SearchField
|
||||
import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry
|
||||
import be.ugent.sel.studeez.data.local.models.User
|
||||
import be.ugent.sel.studeez.resources
|
||||
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import be.ugent.sel.studeez.R.string as AppText
|
||||
|
||||
data class SearchFriendsActions(
|
||||
val onQueryStringChange: (String) -> Unit,
|
||||
val getUsersWithUsername: (String) -> Unit,
|
||||
val getAllUsers: () -> Flow<List<User>>,
|
||||
val goToProfile: (String) -> Unit
|
||||
)
|
||||
|
||||
fun getSearchFriendsActions(
|
||||
viewModel: SearchFriendsViewModel,
|
||||
open: (String) -> Unit
|
||||
): SearchFriendsActions {
|
||||
return SearchFriendsActions(
|
||||
onQueryStringChange = viewModel::onQueryStringChange,
|
||||
getUsersWithUsername = viewModel::getUsersWithUsername,
|
||||
getAllUsers = { viewModel.getAllUsers() },
|
||||
goToProfile = { userId -> viewModel.goToProfile(userId, open) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchFriendsRoute(
|
||||
popUp: () -> Unit,
|
||||
open: (String) -> Unit,
|
||||
viewModel: SearchFriendsViewModel
|
||||
) {
|
||||
val uiState by viewModel.uiState
|
||||
|
||||
SearchFriendsScreen(
|
||||
popUp = popUp,
|
||||
uiState = uiState,
|
||||
searchFriendsActions = getSearchFriendsActions(
|
||||
viewModel = viewModel,
|
||||
open = open
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchFriendsScreen(
|
||||
popUp: () -> Unit,
|
||||
uiState: SearchFriendUiState,
|
||||
searchFriendsActions: SearchFriendsActions
|
||||
) {
|
||||
var query by remember { mutableStateOf(uiState.queryString) }
|
||||
val searchResults = searchFriendsActions.getAllUsers().collectAsState(
|
||||
initial = emptyList()
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
SearchField(
|
||||
value = query,
|
||||
onValueChange = { newValue ->
|
||||
searchFriendsActions.onQueryStringChange(newValue)
|
||||
query = newValue
|
||||
},
|
||||
onSubmit = { },
|
||||
label = AppText.search_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 = { }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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.filter
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchFriendsViewModel @Inject constructor(
|
||||
private val userDAO: UserDAO,
|
||||
private val selectedProfileState: SelectedUserId,
|
||||
logService: LogService
|
||||
): StudeezViewModel(logService) {
|
||||
|
||||
var uiState = mutableStateOf(SearchFriendUiState())
|
||||
private set
|
||||
|
||||
fun onQueryStringChange(newValue: String) {
|
||||
uiState.value = uiState.value.copy(
|
||||
queryString = newValue
|
||||
)
|
||||
uiState.value = uiState.value.copy(
|
||||
searchResults = userDAO.getUsersWithQuery(
|
||||
fieldName = FirebaseUser.USERNAME,
|
||||
value = uiState.value.queryString
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun getUsersWithUsername(
|
||||
value: String
|
||||
): Flow<List<User>> {
|
||||
return userDAO.getUsersWithQuery(
|
||||
fieldName = FirebaseUser.USERNAME,
|
||||
value = value
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users, except for the current user.
|
||||
*/
|
||||
fun getAllUsers(): Flow<List<User>> {
|
||||
return userDAO.getAllUsers()
|
||||
.filter { users ->
|
||||
users.any { user ->
|
||||
user.id != userDAO.getCurrentUserId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun goToProfile(
|
||||
userId: String,
|
||||
open: (String) -> Unit
|
||||
) {
|
||||
selectedProfileState.value = userId
|
||||
open(StudeezDestinations.PUBLIC_PROFILE_SCREEN)
|
||||
}
|
||||
}
|
|
@ -21,14 +21,15 @@ import be.ugent.sel.studeez.resources
|
|||
@Composable
|
||||
fun HomeRoute(
|
||||
open: (String) -> Unit,
|
||||
viewModel: HomeViewModel,
|
||||
drawerActions: DrawerActions,
|
||||
navigationBarActions: NavigationBarActions,
|
||||
feedViewModel: FeedViewModel,
|
||||
) {
|
||||
val feedUiState by feedViewModel.uiState.collectAsState()
|
||||
HomeScreen(
|
||||
onViewFriendsClick = { viewModel.onViewFriendsClick(open) },
|
||||
drawerActions = drawerActions,
|
||||
open = open,
|
||||
navigationBarActions = navigationBarActions,
|
||||
feedUiState = feedUiState,
|
||||
continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) },
|
||||
|
@ -38,7 +39,7 @@ fun HomeRoute(
|
|||
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
open: (String) -> Unit,
|
||||
onViewFriendsClick: () -> Unit,
|
||||
drawerActions: DrawerActions,
|
||||
navigationBarActions: NavigationBarActions,
|
||||
feedUiState: FeedUiState,
|
||||
|
@ -49,15 +50,17 @@ fun HomeScreen(
|
|||
title = resources().getString(R.string.home),
|
||||
drawerActions = drawerActions,
|
||||
navigationBarActions = navigationBarActions,
|
||||
// TODO barAction = { FriendsAction() }
|
||||
barAction = { FriendsAction(onViewFriendsClick) }
|
||||
) {
|
||||
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)
|
||||
|
@ -69,9 +72,9 @@ fun FriendsAction() {
|
|||
@Composable
|
||||
fun HomeScreenPreview() {
|
||||
HomeScreen(
|
||||
onViewFriendsClick = {},
|
||||
drawerActions = DrawerActions({}, {}, {}, {}, {}),
|
||||
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
|
||||
open = {},
|
||||
feedUiState = FeedUiState.Succes(
|
||||
mapOf(
|
||||
"08 May 2023" to listOf(
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package be.ugent.sel.studeez.screens.home
|
||||
import be.ugent.sel.studeez.domain.LogService
|
||||
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 HomeViewModel @Inject constructor(
|
||||
logService: LogService
|
||||
) : StudeezViewModel(logService) {
|
||||
|
||||
|
||||
fun onViewFriendsClick(open: (String) -> Unit) {
|
||||
open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN)
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package be.ugent.sel.studeez.screens.profile
|
||||
|
||||
data class ProfileEditUiState (
|
||||
val username: String = ""
|
||||
)
|
|
@ -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<Int>,
|
||||
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) { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +1,40 @@
|
|||
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 com.google.firebase.auth.FirebaseAuth
|
||||
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<Int> {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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({}, {}, {}, {}))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package be.ugent.sel.studeez.screens.profile.edit_profile
|
||||
|
||||
data class ProfileEditUiState (
|
||||
val username: String = "",
|
||||
val biography: String = ""
|
||||
)
|
|
@ -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)
|
|
@ -0,0 +1,178 @@
|
|||
package be.ugent.sel.studeez.screens.profile.public_profile
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MailOutline
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import be.ugent.sel.studeez.R
|
||||
import be.ugent.sel.studeez.common.composable.Headline
|
||||
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
|
||||
import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry
|
||||
import be.ugent.sel.studeez.data.local.models.User
|
||||
import be.ugent.sel.studeez.resources
|
||||
import be.ugent.sel.studeez.screens.profile.AmountOfFriendsButton
|
||||
import be.ugent.sel.studeez.ui.theme.StudeezTheme
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import be.ugent.sel.studeez.R.string as AppText
|
||||
|
||||
data class PublicProfileActions(
|
||||
val getUserDetails: () -> Flow<User>,
|
||||
val getAmountOfFriends: () -> Flow<Int>,
|
||||
val onViewFriendsClick: () -> Unit,
|
||||
val sendFriendRequest: () -> Boolean
|
||||
)
|
||||
|
||||
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 = { true }
|
||||
),
|
||||
popUp = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PublicProfileEllipsis(
|
||||
sendFriendRequest: () -> Boolean
|
||||
) {
|
||||
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 }
|
||||
) {
|
||||
DropdownMenuItem(onClick = { expanded = false }) {
|
||||
DrawerEntry(
|
||||
icon = Icons.Default.MailOutline,
|
||||
text = stringResource(id = AppText.send_friend_request)
|
||||
) {
|
||||
sendFriendRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PublicProfileEllipsisPreview() {
|
||||
StudeezTheme {
|
||||
PublicProfileEllipsis(
|
||||
sendFriendRequest = { true }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package be.ugent.sel.studeez.screens.profile.public_profile
|
||||
|
||||
data class PublicProfileUiState(
|
||||
var userId: String = ""
|
||||
)
|
|
@ -0,0 +1,60 @@
|
|||
package be.ugent.sel.studeez.screens.profile.public_profile
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import be.ugent.sel.studeez.data.SelectedUserId
|
||||
import be.ugent.sel.studeez.data.local.models.User
|
||||
import be.ugent.sel.studeez.domain.FriendshipDAO
|
||||
import be.ugent.sel.studeez.domain.LogService
|
||||
import be.ugent.sel.studeez.domain.UserDAO
|
||||
import be.ugent.sel.studeez.navigation.StudeezDestinations
|
||||
import be.ugent.sel.studeez.screens.StudeezViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PublicProfileViewModel @Inject constructor(
|
||||
private val userDAO: UserDAO,
|
||||
private val friendshipDAO: FriendshipDAO,
|
||||
selectedUserIdState: SelectedUserId,
|
||||
logService: LogService
|
||||
): StudeezViewModel(logService) {
|
||||
|
||||
val uiState = mutableStateOf(
|
||||
PublicProfileUiState(
|
||||
userId = selectedUserIdState.value
|
||||
)
|
||||
)
|
||||
|
||||
fun getUserDetails(
|
||||
userId: String
|
||||
): Flow<User> {
|
||||
uiState.value = uiState.value.copy(
|
||||
userId = userId
|
||||
)
|
||||
return userDAO.getUserDetails(
|
||||
userId = uiState.value.userId
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountOfFriends(
|
||||
userId: String
|
||||
): Flow<Int> {
|
||||
return friendshipDAO.getFriendshipCount(
|
||||
userId = userId
|
||||
)
|
||||
}
|
||||
|
||||
fun onViewFriendsClick(
|
||||
open: (String) -> Unit
|
||||
) {
|
||||
open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN)
|
||||
}
|
||||
|
||||
fun sendFriendRequest(
|
||||
userId: String
|
||||
): Boolean {
|
||||
return friendshipDAO.sendFriendshipRequest(userId)
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,150 +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
|
||||
)
|
||||
|
||||
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))
|
||||
) {
|
||||
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
|
||||
|
||||
@Composable
|
||||
open fun MidSection() {
|
||||
// Default has no midsection, unless overwritten.
|
||||
}
|
||||
|
||||
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" }, {}, {}, {}))
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
package be.ugent.sel.studeez.screens.session.sessionScreens
|
||||
|
||||
import android.media.MediaPlayer
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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 MidSection() {
|
||||
Dots()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Dots() {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
if (funPomoDoroTimer.hasEnded()) {
|
||||
repeat(funPomoDoroTimer.repeats) {
|
||||
Dot(Color.Green)
|
||||
}
|
||||
} else {
|
||||
repeat(funPomoDoroTimer.repeats - funPomoDoroTimer.breaksRemaining - 1) {
|
||||
Dot(color = Color.DarkGray)
|
||||
}
|
||||
if (!funPomoDoroTimer.isInBreak) Dot(Color.Green) else Dot(Color.DarkGray)
|
||||
repeat(funPomoDoroTimer.breaksRemaining) {
|
||||
Dot(color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Dot(color: Color) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
)
|
||||
}
|
||||
|
||||
@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().getString(AppText.state_focus)
|
||||
}
|
||||
|
||||
override fun callMediaPlayer() {
|
||||
if (funPomoDoroTimer.hasEnded()) {
|
||||
mediaplayer?.let { it: MediaPlayer ->
|
||||
it.setOnCompletionListener {
|
||||
it.release()
|
||||
mediaplayer = null
|
||||
}
|
||||
it.start()
|
||||
}
|
||||
} else if (funPomoDoroTimer.hasCurrentCountdownEnded()) {
|
||||
mediaplayer?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MidsectionPreview() {
|
||||
val funPomoDoroTimer = FunctionalPomodoroTimer(15, 60, 5)
|
||||
val breakSessionScreen = BreakSessionScreen(funPomoDoroTimer, MediaPlayer())
|
||||
breakSessionScreen.MidSection()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package be.ugent.sel.studeez.screens.session.sessionScreens
|
||||
|
||||
import android.media.MediaPlayer
|
||||
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
|
||||
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
|
||||
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
|
||||
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor
|
||||
|
||||
class GetSessionScreen(private val mediaplayer: MediaPlayer?) : FunctionalTimerVisitor<AbstractSessionScreen> {
|
||||
override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): AbstractSessionScreen =
|
||||
CustomSessionScreen(functionalCustomTimer, mediaplayer)
|
||||
|
||||
override fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): AbstractSessionScreen =
|
||||
EndlessSessionScreen()
|
||||
|
||||
override fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): AbstractSessionScreen =
|
||||
BreakSessionScreen(functionalPomodoroTimer, mediaplayer)
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import be.ugent.sel.studeez.common.composable.BasicButton
|
|||
import be.ugent.sel.studeez.common.composable.DeleteButton
|
||||
import be.ugent.sel.studeez.common.composable.FormComposable
|
||||
import be.ugent.sel.studeez.common.composable.LabelledInputField
|
||||
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
|
||||
import be.ugent.sel.studeez.common.ext.basicButton
|
||||
import be.ugent.sel.studeez.common.ext.fieldModifier
|
||||
import be.ugent.sel.studeez.common.ext.generateRandomArgb
|
||||
|
|
|
@ -2,8 +2,6 @@ package be.ugent.sel.studeez.screens.subjects.form
|
|||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import be.ugent.sel.studeez.common.ext.generateRandomArgb
|
||||
import be.ugent.sel.studeez.data.SelectedSubject
|
||||
import be.ugent.sel.studeez.data.local.models.task.Subject
|
||||
import be.ugent.sel.studeez.domain.LogService
|
||||
|
|
|
@ -3,6 +3,7 @@ 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
|
||||
|
@ -12,8 +13,16 @@ fun TimerAddRoute(
|
|||
popUp: () -> Unit,
|
||||
viewModel: TimerFormViewModel
|
||||
) {
|
||||
TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.add_timer) {
|
||||
viewModel.saveTimer(it, goBack = popUp)
|
||||
|
||||
|
||||
TimerFormScreen(
|
||||
popUp = popUp,
|
||||
getTimerInfo = viewModel::getTimerInfo,
|
||||
extraButton= { },
|
||||
AppText.add_timer
|
||||
) {
|
||||
viewModel.saveTimer(it, goBack = {popUp(); popUp()})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +31,20 @@ fun TimerEditRoute(
|
|||
popUp: () -> Unit,
|
||||
viewModel: TimerFormViewModel
|
||||
) {
|
||||
TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.edit_timer) {
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +53,7 @@ fun TimerEditRoute(
|
|||
fun TimerFormScreen(
|
||||
popUp: () -> Unit,
|
||||
getTimerInfo: () -> TimerInfo,
|
||||
extraButton: @Composable () -> Unit,
|
||||
@StringRes label: Int,
|
||||
onConfirmClick: (TimerInfo) -> Unit
|
||||
) {
|
||||
|
@ -40,6 +63,6 @@ fun TimerFormScreen(
|
|||
title = stringResource(id = label),
|
||||
popUp = popUp
|
||||
) {
|
||||
timerFormScreen(onConfirmClick)
|
||||
timerFormScreen(onConfirmClick, extraButton)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,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()
|
||||
|
|
|
@ -3,49 +3,82 @@ package be.ugent.sel.studeez.screens.timer_form.form_screens
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.*
|
||||
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) {
|
||||
|
||||
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
|
||||
operator fun invoke(
|
||||
onSaveClick: (TimerInfo) -> Unit,
|
||||
extraButton: @Composable () -> Unit = {},
|
||||
) {
|
||||
|
||||
Column {
|
||||
|
||||
// Fields that every timer shares (ommited id)
|
||||
LabelledInputField(
|
||||
value = name,
|
||||
onNewValue = { name = it },
|
||||
label = R.string.name
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
LabelledInputField(
|
||||
value = description,
|
||||
onNewValue = { description = it },
|
||||
label = AppText.description,
|
||||
singleLine = false
|
||||
)
|
||||
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.
|
||||
|
|
|
@ -15,6 +15,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 +28,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 +46,10 @@ class BreakTimerFormScreen(
|
|||
}
|
||||
}
|
||||
|
||||
fun isNumber(text: String): Boolean {
|
||||
return text.matches(Regex("[1-9]+\\d*"))
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun BreakEditScreenPreview() {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
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.*
|
||||
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.*
|
||||
|
@ -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 = {}
|
||||
)
|
||||
}
|
5
app/src/main/res/drawable/ic_more_horizontal.xml
Normal file
5
app/src/main/res/drawable/ic_more_horizontal.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
|
@ -16,11 +16,12 @@
|
|||
<string name="go_back">Go back</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="view_more">View more</string>
|
||||
|
||||
<!-- Messages -->
|
||||
<string name="success">Success!</string>
|
||||
<string name="try_again">Try again</string>
|
||||
<string name="generic_error">Something wrong happened. Please try again.</string>
|
||||
<string name="generic_error">Something went wrong. Please try again.</string>
|
||||
<string name="email_error">Please insert a valid email.</string>
|
||||
|
||||
<!-- ========== NavBar ========== -->
|
||||
|
@ -63,6 +64,7 @@
|
|||
<string name="edit_profile">Edit profile</string>
|
||||
<string name="editing_profile">Editing profile</string>
|
||||
<string name="delete_profile">Delete profile</string>
|
||||
<string name="biography">Bio</string>
|
||||
|
||||
<!-- ========== Drawer ========== -->
|
||||
|
||||
|
@ -72,8 +74,15 @@
|
|||
|
||||
<!-- Timers -->
|
||||
<string name="timers">Timers</string>
|
||||
<string name="delete_timer">Delete Timer</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="add_timer">Add timer</string>
|
||||
|
||||
<string name="name_error">Name should not be blank</string>
|
||||
<string name="description_error">Description should not be blank</string>
|
||||
<string name="fill_out_error">Fill out all the fields correctly!</string>
|
||||
|
||||
|
||||
<string name="pick_time">Select time</string>
|
||||
<string name="state_focus">Focus!</string>
|
||||
<plurals name="state_focus_remaining">
|
||||
|
@ -115,7 +124,16 @@
|
|||
|
||||
<string name="friends">Friends</string>
|
||||
<string name="friend">Friend</string>
|
||||
<plurals name="friends_amount">
|
||||
<item quantity="one">%d Friend</item>
|
||||
<item quantity="other">%d Friends</item>
|
||||
</plurals>
|
||||
<string name="add_friend_not_possible_yet">Adding friends still needs to be implemented. Hang on tight!</string> <!-- TODO Remove this description line once implemented. -->
|
||||
<string name="no_friends">You don\'t have any friends yet. Add one!</string>
|
||||
<string name="search_friends">Search friends</string>
|
||||
<string name="send_friend_request">Send friend request</string>
|
||||
<string name="remove_friend">Remove as friend</string>
|
||||
<string name="show_profile">Show profile</string>
|
||||
|
||||
<!-- ========== Create & edit screens ========== -->
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
package be.ugent.sel.studeez.timer_functional
|
||||
|
||||
import android.media.MediaPlayer
|
||||
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.LogService
|
||||
import be.ugent.sel.studeez.domain.implementation.LogServiceImpl
|
||||
import be.ugent.sel.studeez.screens.session.InvisibleSessionManager
|
||||
import be.ugent.sel.studeez.screens.session.SessionViewModel
|
||||
|
@ -22,13 +21,13 @@ import org.mockito.kotlin.mock
|
|||
class InvisibleSessionManagerTest {
|
||||
private var selectedTimer: SelectedTimer = SelectedTimer()
|
||||
private lateinit var viewModel: SessionViewModel
|
||||
private var mediaPlayer: MediaPlayer = mock()
|
||||
|
||||
|
||||
@Test
|
||||
fun InvisibleEndlessTimerTest() = runTest {
|
||||
selectedTimer.set(FunctionalEndlessTimer())
|
||||
viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), mock(), LogServiceImpl())
|
||||
InvisibleSessionManager.setParameters(viewModel, mediaPlayer)
|
||||
viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl())
|
||||
InvisibleSessionManager.setParameters(viewModel, mock())
|
||||
|
||||
val test = launch {
|
||||
InvisibleSessionManager.updateTimer()
|
||||
|
@ -47,10 +46,10 @@ class InvisibleSessionManagerTest {
|
|||
fun InvisiblePomodoroTimerTest() = runTest {
|
||||
val studyTime = 10
|
||||
val breakTime = 5
|
||||
val repeats = 1
|
||||
val repeats = 2
|
||||
selectedTimer.set(FunctionalPomodoroTimer(studyTime, breakTime, repeats))
|
||||
viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), mock(), LogServiceImpl())
|
||||
InvisibleSessionManager.setParameters(viewModel, mediaPlayer)
|
||||
viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl())
|
||||
InvisibleSessionManager.setParameters(viewModel, mock())
|
||||
|
||||
val test = launch {
|
||||
InvisibleSessionManager.updateTimer()
|
||||
|
@ -82,8 +81,8 @@ class InvisibleSessionManagerTest {
|
|||
@Test
|
||||
fun InvisibleCustomTimerTest() = runTest {
|
||||
selectedTimer.set(FunctionalCustomTimer(5))
|
||||
viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), mock(), LogServiceImpl())
|
||||
InvisibleSessionManager.setParameters(viewModel, mediaPlayer)
|
||||
viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl())
|
||||
InvisibleSessionManager.setParameters(viewModel, mock())
|
||||
|
||||
val test = launch {
|
||||
InvisibleSessionManager.updateTimer()
|
||||
|
|
|
@ -21,4 +21,3 @@ plugins {
|
|||
// Hilt
|
||||
id 'com.google.dagger.hilt.android' version '2.44' apply false
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
android.nonTransitiveRClass=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonFinalResIds=false
|
Reference in a new issue