= _signUiState
28 |
29 | val currentScreen = mutableStateOf(Screen.LoginScreen.route)
30 |
31 | fun logInWith(email: String, password: String) {
32 | _logUiState.value = UIState.Loading
33 | auth.signInWithEmailAndPassword(email, password)
34 | .addOnCompleteListener { task ->
35 | if (task.isSuccessful) {
36 | if (currentUser?.isEmailVerified == false) {
37 | currentUser?.sendEmailVerification()
38 | auth.signOut()
39 | _logUiState.value = UIState.Empty("verification")
40 | } else {
41 | _logUiState.value = UIState.Success(currentUser)
42 | visibleState.targetState = false
43 | }
44 | } else {
45 | _logUiState.value = UIState.Empty(task.exception?.localizedMessage)
46 | }
47 | }
48 | }
49 |
50 | fun signInWith(email: String, password: String) {
51 | if (currentUser == null) {
52 | _signUiState.value = UIState.Loading
53 | auth.createUserWithEmailAndPassword(email, password)
54 | .addOnCompleteListener { task ->
55 | if (task.isSuccessful) {
56 | currentUser?.sendEmailVerification()
57 | _signUiState.value = UIState.Success(currentUser)
58 | auth.signOut()
59 | } else {
60 | _signUiState.value = UIState.Empty(task.exception?.localizedMessage)
61 | }
62 | }
63 | }
64 | }
65 |
66 | fun sendResetPasswordLink(email: String) {
67 | auth.sendPasswordResetEmail(email)
68 | }
69 |
70 | fun resetState() {
71 | _signUiState.value = UIState.Empty()
72 | _logUiState.value = UIState.Empty()
73 | }
74 |
75 | fun goTo(screen: Screen) {
76 | currentScreen.value = screen.route
77 | }
78 |
79 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Firenote
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Smart and lightweight notepad, that allows you to manage and create goals, notes with different colors and store them in the cloud.It will look fantastic on each device, because its layout adapt to every screen size.
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## Download
20 | Go to the [Releases](https://github.com/t8rin/Firenote/releases) to download the latest APK.
21 |
22 |
23 | ## Tech stack & Open-source libraries
24 | - Minimum SDK level 21
25 |
26 | - [Kotlin](https://kotlinlang.org/) based
27 |
28 | - [Coroutines](https://github.com/Kotlin/kotlinx.coroutines) for asynchronous work
29 |
30 | - [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/) to emit values from database directly to compose state.
31 |
32 | - [Accompanist](https://github.com/google/accompanist) to expand jetpcak compose opportunities.
33 |
34 | - [Firebase](https://github.com/firebase/FirebaseUI-Android) for registering/signing in and storing data in the cloud.
35 |
36 | - [Hilt](https://dagger.dev/hilt/) for dependency injection.
37 |
38 | - JetPack
39 | - Lifecycle - Observe Android lifecycles and handle UI states upon the lifecycle changes.
40 | - ViewModel - Manages UI-related data holder and lifecycle aware. Allows data to survive configuration changes such as screen rotations.
41 | - Compose - Modern Declarative UI style framework.
42 |
43 | - Architecture
44 | - MVVM Architecture (View - DataBinding - ViewModel - Model)
45 | - Repository Pattern
46 |
47 | - [Coil](https://github.com/coil-kt/coil) - loading images.
48 |
49 | - [Material-Components](https://github.com/material-components/material-components-android) - Material You components with dynamic colors.
50 |
51 | ## Find this repository useful? :heart:
52 | Support it by joining __[stargazers](https://github.com/t8rin/Firenote/stargazers)__ for this repository. :star:
53 | And __[follow](https://github.com/t8rin)__ me for my next creations! 🤩
54 |
55 | # License
56 | ```xml
57 | Designed and developed by 2022 T8RIN
58 |
59 | Licensed under the Apache License, Version 2.0 (the "License");
60 | you may not use this file except in compliance with the License.
61 | You may obtain a copy of the License at
62 |
63 | http://www.apache.org/licenses/LICENSE-2.0
64 |
65 | Unless required by applicable law or agreed to in writing, software
66 | distributed under the License is distributed on an "AS IS" BASIS,
67 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
68 | See the License for the specific language governing permissions and
69 | limitations under the License.
70 | ```
71 |
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/single/text/MaterialTextField.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.single.text
2 |
3 | import androidx.compose.foundation.interaction.MutableInteractionSource
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.text.KeyboardActions
7 | import androidx.compose.foundation.text.KeyboardOptions
8 | import androidx.compose.material.*
9 | import androidx.compose.runtime.*
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.graphics.Shape
13 | import androidx.compose.ui.text.TextStyle
14 | import androidx.compose.ui.text.input.TextFieldValue
15 | import androidx.compose.ui.text.input.VisualTransformation
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.material3.MaterialTheme as M3
18 |
19 | @Composable
20 | fun MaterialTextField(
21 | value: String,
22 | onValueChange: (String) -> Unit,
23 | modifier: Modifier = Modifier,
24 | enabled: Boolean = true,
25 | readOnly: Boolean = false,
26 | textStyle: TextStyle = LocalTextStyle.current,
27 | label: @Composable (() -> Unit)? = null,
28 | placeholder: @Composable (() -> Unit)? = null,
29 | leadingIcon: @Composable (() -> Unit)? = null,
30 | trailingIcon: @Composable (() -> Unit)? = null,
31 | isError: Boolean = false,
32 | errorText: String = "",
33 | visualTransformation: VisualTransformation = VisualTransformation.None,
34 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
35 | keyboardActions: KeyboardActions = KeyboardActions.Default,
36 | singleLine: Boolean = false,
37 | maxLines: Int = Int.MAX_VALUE,
38 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
39 | shape: Shape = MaterialTheme.shapes.medium,
40 | ) {
41 | var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
42 | val textFieldValue = textFieldValueState.copy(text = value)
43 |
44 | val colors: TextFieldColors = TextFieldDefaults.textFieldColors(
45 | textColor = M3.colorScheme.onBackground,
46 | backgroundColor = Color.Transparent,
47 | unfocusedIndicatorColor = if (isError) M3.colorScheme.error else M3.colorScheme.onPrimaryContainer,
48 | focusedIndicatorColor = if (isError) M3.colorScheme.error else M3.colorScheme.primary,
49 | cursorColor = if (isError) M3.colorScheme.error else M3.colorScheme.primary
50 | )
51 |
52 | Column {
53 | OutlinedTextField(
54 | enabled = enabled,
55 | readOnly = readOnly,
56 | value = textFieldValue,
57 | onValueChange = {
58 | textFieldValueState = it
59 | if (value != it.text) {
60 | onValueChange(it.text)
61 | }
62 | },
63 | modifier = modifier,
64 | singleLine = singleLine,
65 | textStyle = textStyle,
66 | label = label,
67 | placeholder = placeholder,
68 | leadingIcon = leadingIcon,
69 | trailingIcon = trailingIcon,
70 | visualTransformation = visualTransformation,
71 | keyboardOptions = keyboardOptions,
72 | keyboardActions = keyboardActions,
73 | maxLines = maxLines,
74 | interactionSource = interactionSource,
75 | shape = shape,
76 | colors = colors
77 | )
78 | if (isError) Text(
79 | errorText,
80 | color = M3.colorScheme.error,
81 | modifier = Modifier.padding(8.dp)
82 | )
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/screen/auth/AuthScreen.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.screen.auth
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.fadeIn
5 | import androidx.compose.animation.fadeOut
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material3.*
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.MutableState
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.painterResource
16 | import androidx.compose.ui.unit.dp
17 | import androidx.lifecycle.viewmodel.compose.viewModel
18 | import ru.tech.firenote.R
19 | import ru.tech.firenote.ui.composable.provider.LocalWindowSize
20 | import ru.tech.firenote.ui.composable.utils.WindowSize
21 | import ru.tech.firenote.ui.route.Screen
22 | import ru.tech.firenote.viewModel.auth.AuthViewModel
23 |
24 | @ExperimentalMaterial3Api
25 | @Composable
26 | fun AuthScreen(visible: MutableState, viewModel: AuthViewModel = viewModel()) {
27 |
28 | if (viewModel.currentUser == null) {
29 | viewModel.visibleState.targetState = true
30 | viewModel.resetState()
31 | }
32 | visible.value = viewModel.visibleState.targetState
33 | AnimatedVisibility(
34 | visibleState = viewModel.visibleState,
35 | enter = fadeIn(),
36 | exit = fadeOut()
37 | ) {
38 | Surface(
39 | modifier = Modifier
40 | .fillMaxSize()
41 | .background(MaterialTheme.colorScheme.background)
42 | .systemBarsPadding()
43 | ) {
44 | Column(
45 | Modifier
46 | .fillMaxSize(),
47 | horizontalAlignment = Alignment.CenterHorizontally,
48 | verticalArrangement = Arrangement.Top
49 | ) {
50 | Image(
51 | painter = painterResource(R.drawable.ic_fire_144),
52 | contentDescription = null,
53 | modifier = Modifier
54 | .weight(0.6f)
55 | )
56 | Card(
57 | Modifier
58 | .weight(
59 | when (LocalWindowSize.current) {
60 | WindowSize.Compact -> 2f
61 | else -> 1f
62 | }
63 | )
64 | .padding(
65 | when (LocalWindowSize.current) {
66 | WindowSize.Compact -> 12.dp
67 | WindowSize.Medium -> 48.dp
68 | else -> 96.dp
69 | }
70 | ),
71 | shape = RoundedCornerShape(24.dp),
72 | containerColor = MaterialTheme.colorScheme.secondaryContainer,
73 | elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
74 | ) {
75 | Box {
76 | when (viewModel.currentScreen.value) {
77 | Screen.LoginScreen.route -> LoginScreen(viewModel)
78 | Screen.RegistrationScreen.route -> RegistrationScreen(viewModel)
79 | Screen.ForgotPasswordScreen.route -> ForgotPasswordScreen(viewModel)
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/screen/creation/CreationContainer.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.screen.creation
2 |
3 | import androidx.activity.compose.BackHandler
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.ExperimentalAnimationApi
6 | import androidx.compose.animation.fadeIn
7 | import androidx.compose.animation.fadeOut
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.fillMaxHeight
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.twotone.FactCheck
14 | import androidx.compose.material.icons.twotone.StickyNote2
15 | import androidx.compose.material3.Divider
16 | import androidx.compose.material3.ExperimentalMaterial3Api
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.LaunchedEffect
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.saveable.rememberSaveable
21 | import androidx.compose.ui.Alignment
22 | import androidx.compose.ui.Modifier
23 | import androidx.compose.ui.unit.dp
24 | import ru.tech.firenote.R
25 | import ru.tech.firenote.ui.composable.single.placeholder.Placeholder
26 | import ru.tech.firenote.viewModel.main.MainViewModel
27 |
28 | @ExperimentalMaterial3Api
29 | @ExperimentalAnimationApi
30 | @Composable
31 | fun CreationContainer(viewModel: MainViewModel, splitScreen: Boolean) {
32 | Box(Modifier.fillMaxSize()) {
33 |
34 | if (!viewModel.showNoteCreation.currentState) {
35 | viewModel.clearGlobalNote()
36 | if (splitScreen && viewModel.selectedItem.value == 0) {
37 | Placeholder(icon = Icons.TwoTone.StickyNote2, textRes = R.string.selectNote)
38 | }
39 | }
40 |
41 | if (!viewModel.showGoalCreation.currentState) {
42 | viewModel.clearGlobalGoal()
43 | if (splitScreen && viewModel.selectedItem.value in 1..2) {
44 | Placeholder(icon = Icons.TwoTone.FactCheck, textRes = R.string.selectGoal)
45 | }
46 | }
47 |
48 | val resetGoal = rememberSaveable { mutableStateOf(false) }
49 | val resetNote = rememberSaveable { mutableStateOf(false) }
50 |
51 | AnimatedVisibility(
52 | visibleState = viewModel.showNoteCreation,
53 | enter = fadeIn(),
54 | exit = fadeOut()
55 | ) {
56 | BackHandler { viewModel.showNoteCreation.targetState = false }
57 |
58 | NoteCreationScreen(
59 | state = viewModel.showNoteCreation,
60 | globalNote = viewModel.globalNote,
61 | reset = resetNote
62 | )
63 |
64 | LaunchedEffect(Unit) {
65 | viewModel.showGoalCreation.targetState = false
66 | resetGoal.value = true
67 | }
68 | }
69 |
70 | AnimatedVisibility(
71 | visibleState = viewModel.showGoalCreation,
72 | enter = fadeIn(),
73 | exit = fadeOut()
74 | ) {
75 | BackHandler { viewModel.showGoalCreation.targetState = false }
76 |
77 | GoalCreationScreen(
78 | state = viewModel.showGoalCreation,
79 | globalGoal = viewModel.globalGoal,
80 | reset = resetGoal
81 | )
82 |
83 | LaunchedEffect(Unit) {
84 | viewModel.showNoteCreation.targetState = false
85 | resetNote.value = true
86 | }
87 | }
88 |
89 | if (splitScreen) {
90 | Divider(
91 | Modifier
92 | .fillMaxHeight()
93 | .width(1.dp)
94 | .align(Alignment.CenterStart)
95 | )
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/single/text/EditText.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.single.text
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.text.BasicTextField
8 | import androidx.compose.foundation.text.KeyboardActions
9 | import androidx.compose.foundation.text.KeyboardOptions
10 | import androidx.compose.material3.LocalTextStyle
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.MutableState
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.geometry.Offset
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.graphics.Shadow
21 | import androidx.compose.ui.graphics.SolidColor
22 | import androidx.compose.ui.graphics.toArgb
23 | import androidx.compose.ui.platform.LocalFocusManager
24 | import androidx.compose.ui.text.TextStyle
25 | import androidx.compose.ui.text.input.KeyboardType
26 | import androidx.compose.ui.text.style.TextAlign
27 | import androidx.compose.ui.unit.Dp
28 | import androidx.compose.ui.unit.dp
29 | import androidx.compose.ui.unit.sp
30 |
31 | @Composable
32 | fun EditText(
33 | modifier: Modifier = Modifier,
34 | hintText: String = "",
35 | textFieldState: MutableState = mutableStateOf(""),
36 | cursorColor: Color = Color.Black,
37 | singleLine: Boolean = true,
38 | color: Color = MaterialTheme.colorScheme.onBackground,
39 | errorEnabled: Boolean = true,
40 | shadowColor: Color = Color.DarkGray,
41 | topPadding: Dp = 0.dp,
42 | enabled: Boolean = true,
43 | errorColor: Int = MaterialTheme.colorScheme.error.toArgb(),
44 | onValueChange: (String) -> Unit = {}
45 | ) {
46 | val localFocusManager = LocalFocusManager.current
47 |
48 | Box(Modifier.padding(top = topPadding)) {
49 | BasicTextField(
50 | modifier = modifier,
51 | value = textFieldState.value,
52 | onValueChange = {
53 | onValueChange(it)
54 | textFieldState.value = it
55 | },
56 | cursorBrush = SolidColor(cursorColor),
57 | textStyle = TextStyle(
58 | fontSize = 22.sp,
59 | color = color,
60 | textAlign = TextAlign.Start,
61 | ),
62 | keyboardOptions = KeyboardOptions(
63 | keyboardType = KeyboardType.Text
64 | ),
65 | keyboardActions = KeyboardActions(
66 | onDone = { localFocusManager.clearFocus() }
67 | ),
68 | singleLine = singleLine,
69 | enabled = enabled
70 | )
71 |
72 | if (textFieldState.value.isEmpty()) {
73 | val localColor =
74 | if (errorEnabled) Color(errorColor)
75 | else MaterialTheme.colorScheme.onSurfaceVariant
76 | Row(
77 | horizontalArrangement = Arrangement.SpaceBetween,
78 | verticalAlignment = Alignment.CenterVertically
79 | ) {
80 | Text(
81 | hintText,
82 | Modifier
83 | .weight(1f)
84 | .padding(start = 40.dp),
85 | fontSize = 22.sp,
86 | color = localColor,
87 | style = LocalTextStyle.current.copy(
88 | shadow = Shadow(
89 | color = shadowColor,
90 | offset = Offset(4f, 4f),
91 | blurRadius = 8f
92 | )
93 | )
94 | )
95 | }
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/single/toast/FancyToast.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.single.toast
2 |
3 | import android.widget.Toast
4 | import androidx.compose.animation.*
5 | import androidx.compose.animation.core.MutableTransitionState
6 | import androidx.compose.foundation.BorderStroke
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material3.*
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.LaunchedEffect
12 | import androidx.compose.runtime.MutableState
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.alpha
17 | import androidx.compose.ui.graphics.vector.ImageVector
18 | import androidx.compose.ui.platform.LocalConfiguration
19 | import androidx.compose.ui.text.style.TextAlign
20 | import androidx.compose.ui.unit.dp
21 | import kotlinx.coroutines.delay
22 | import kotlin.math.max
23 | import kotlin.math.min
24 |
25 | @ExperimentalAnimationApi
26 | @ExperimentalMaterial3Api
27 | @Composable
28 | fun FancyToast(
29 | icon: ImageVector,
30 | message: String = "",
31 | changed: MutableState,
32 | length: Int = Toast.LENGTH_LONG
33 | ) {
34 | val showToast = remember {
35 | MutableTransitionState(false).apply {
36 | targetState = false
37 | }
38 | }
39 | val conf = LocalConfiguration.current
40 | val sizeMin = min(conf.screenWidthDp, conf.screenHeightDp).dp
41 | val sizeMax = max(conf.screenWidthDp, conf.screenHeightDp).dp
42 |
43 | Box(
44 | modifier = Modifier.fillMaxSize()
45 | ) {
46 | AnimatedVisibility(
47 | modifier = Modifier
48 | .align(Alignment.BottomCenter)
49 | .padding(bottom = sizeMax * 0.15f),
50 | visibleState = showToast,
51 | enter = fadeIn() + scaleIn(),
52 | exit = fadeOut() + scaleOut()
53 | ) {
54 | Card(
55 | border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiaryContainer),
56 | contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
57 | modifier = Modifier
58 | .alpha(0.98f)
59 | .heightIn(48.dp)
60 | .widthIn(0.dp, (sizeMin * 0.7f)),
61 | shape = RoundedCornerShape(24.dp)
62 | ) {
63 | Row(
64 | Modifier.padding(15.dp),
65 | verticalAlignment = Alignment.CenterVertically,
66 | horizontalArrangement = Arrangement.Center
67 | ) {
68 | Icon(icon, null)
69 | Spacer(modifier = Modifier.size(8.dp))
70 | Text(
71 | style = MaterialTheme.typography.bodySmall,
72 | text = message,
73 | textAlign = TextAlign.Center,
74 | modifier = Modifier.padding(end = 5.dp)
75 | )
76 | }
77 | }
78 | }
79 | }
80 |
81 | LaunchedEffect(changed.value) {
82 | changed.value = false
83 | if (message != "") {
84 | if (showToast.currentState) {
85 | showToast.targetState = false
86 | delay(1000L)
87 | }
88 | showToast.targetState = true
89 | delay(if (length == Toast.LENGTH_LONG) 5000L else 2500L)
90 | showToast.targetState = false
91 | }
92 | }
93 | }
94 |
95 | fun FancyToastValues.sendToast(icon: ImageVector, text: String) {
96 | this.text?.value = text
97 | this.icon?.value = icon
98 | this.changed?.value = true
99 | }
100 |
101 | data class FancyToastValues(
102 | val icon: MutableState? = null,
103 | val text: MutableState? = null,
104 | val changed: MutableState? = null
105 | )
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/viewModel/creation/GoalCreationViewModel.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.viewModel.creation
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.compose.ui.graphics.toArgb
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.launch
9 | import ru.tech.firenote.model.Goal
10 | import ru.tech.firenote.model.GoalData
11 | import ru.tech.firenote.repository.NoteRepository
12 | import ru.tech.firenote.ui.theme.GoalGreen
13 | import ru.tech.firenote.utils.GlobalUtils.blend
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | class GoalCreationViewModel @Inject constructor(
18 | private val repository: NoteRepository
19 | ) : ViewModel() {
20 |
21 | var goal: Goal? = null
22 |
23 | val goalColor = mutableStateOf(GoalGreen.toArgb())
24 | val appBarColor = mutableStateOf(goalColor.value.blend())
25 |
26 | val goalLabel = mutableStateOf("")
27 | val goalContent = mutableStateOf(listOf(GoalData(done = false)))
28 |
29 | fun saveGoal() {
30 | viewModelScope.launch {
31 | repository.insertGoal(
32 | Goal(
33 | goalLabel.value,
34 | goalContent.value.filter {
35 | !it.content?.trim().isNullOrEmpty()
36 | }.map {
37 | it.copy(content = it.content?.trim())
38 | },
39 | System.currentTimeMillis(),
40 | goalColor.value,
41 | appBarColor.value
42 | )
43 | )
44 | resetValues()
45 | }
46 | }
47 |
48 | fun updateGoal(goal: Goal) {
49 | viewModelScope.launch {
50 | repository.insertGoal(
51 | Goal(
52 | goalLabel.value,
53 | goalContent.value.filter {
54 | !it.content?.trim().isNullOrEmpty()
55 | }.map {
56 | it.copy(content = it.content?.trim())
57 | },
58 | System.currentTimeMillis(),
59 | goalColor.value,
60 | appBarColor.value,
61 | goal.id
62 | )
63 | )
64 | resetValues()
65 | }
66 | }
67 |
68 | fun parseGoalData(goal: Goal?) {
69 | this.goal = goal
70 | goalLabel.value = goal?.title ?: ""
71 | goalContent.value = goal?.content ?: listOf(GoalData(done = false))
72 | goalColor.value = goal?.color ?: GoalGreen.toArgb()
73 | appBarColor.value = goal?.appBarColor ?: goalColor.value.blend()
74 | }
75 |
76 | fun resetValues() {
77 | goal = null
78 | goalColor.value = GoalGreen.toArgb()
79 | appBarColor.value = goalColor.value.blend()
80 |
81 | goalLabel.value = ""
82 | goalContent.value = listOf(GoalData(done = false))
83 | }
84 |
85 | fun removeFromContent(index: Int) {
86 | goalContent.value = goalContent.value.filterIndexed { index1, _ -> index1 != index }
87 | }
88 |
89 | fun updateContent(index: Int, content: String) {
90 | goalContent.value = goalContent.value.mapIndexed { index1, goalData ->
91 | if (index1 == index) {
92 | goalData.copy(content = content)
93 | } else goalData
94 | }
95 | }
96 |
97 | fun updateDone(index: Int, done: Boolean) {
98 | goalContent.value = goalContent.value.mapIndexed { index1, goalData ->
99 | if (index1 == index) {
100 | goalData.copy(done = done)
101 | } else goalData
102 | }
103 | viewModelScope.launch {
104 | goal?.copy(content = goalContent.value)?.let {
105 | repository.insertGoal(it)
106 | }
107 | }
108 | }
109 |
110 | fun addContent(item: GoalData) {
111 | goalContent.value = goalContent.value + item
112 | }
113 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.*
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.SideEffect
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.platform.LocalContext
10 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
11 |
12 | private val LightColorScheme = lightColorScheme(
13 | primary = md_theme_light_primary,
14 | onPrimary = md_theme_light_onPrimary,
15 | primaryContainer = md_theme_light_primaryContainer,
16 | onPrimaryContainer = md_theme_light_onPrimaryContainer,
17 | secondary = md_theme_light_secondary,
18 | onSecondary = md_theme_light_onSecondary,
19 | secondaryContainer = md_theme_light_secondaryContainer,
20 | onSecondaryContainer = md_theme_light_onSecondaryContainer,
21 | tertiary = md_theme_light_tertiary,
22 | onTertiary = md_theme_light_onTertiary,
23 | tertiaryContainer = md_theme_light_tertiaryContainer,
24 | onTertiaryContainer = md_theme_light_onTertiaryContainer,
25 | error = md_theme_light_error,
26 | errorContainer = md_theme_light_errorContainer,
27 | onError = md_theme_light_onError,
28 | onErrorContainer = md_theme_light_onErrorContainer,
29 | background = md_theme_light_background,
30 | onBackground = md_theme_light_onBackground,
31 | surface = md_theme_light_surface,
32 | onSurface = md_theme_light_onSurface,
33 | surfaceVariant = md_theme_light_surfaceVariant,
34 | onSurfaceVariant = md_theme_light_onSurfaceVariant,
35 | outline = md_theme_light_outline,
36 | inverseOnSurface = md_theme_light_inverseOnSurface,
37 | inverseSurface = md_theme_light_inverseSurface,
38 | inversePrimary = md_theme_light_inversePrimary,
39 | )
40 | private val DarkColorScheme = darkColorScheme(
41 | primary = md_theme_dark_primary,
42 | onPrimary = md_theme_dark_onPrimary,
43 | primaryContainer = md_theme_dark_primaryContainer,
44 | onPrimaryContainer = md_theme_dark_onPrimaryContainer,
45 | secondary = md_theme_dark_secondary,
46 | onSecondary = md_theme_dark_onSecondary,
47 | secondaryContainer = md_theme_dark_secondaryContainer,
48 | onSecondaryContainer = md_theme_dark_onSecondaryContainer,
49 | tertiary = md_theme_dark_tertiary,
50 | onTertiary = md_theme_dark_onTertiary,
51 | tertiaryContainer = md_theme_dark_tertiaryContainer,
52 | onTertiaryContainer = md_theme_dark_onTertiaryContainer,
53 | error = md_theme_dark_error,
54 | errorContainer = md_theme_dark_errorContainer,
55 | onError = md_theme_dark_onError,
56 | onErrorContainer = md_theme_dark_onErrorContainer,
57 | background = md_theme_dark_background,
58 | onBackground = md_theme_dark_onBackground,
59 | surface = md_theme_dark_surface,
60 | onSurface = md_theme_dark_onSurface,
61 | surfaceVariant = md_theme_dark_surfaceVariant,
62 | onSurfaceVariant = md_theme_dark_onSurfaceVariant,
63 | outline = md_theme_dark_outline,
64 | inverseOnSurface = md_theme_dark_inverseOnSurface,
65 | inverseSurface = md_theme_dark_inverseSurface,
66 | inversePrimary = md_theme_dark_inversePrimary,
67 | )
68 |
69 | @Composable
70 | fun FirenoteTheme(
71 | darkTheme: Boolean = isSystemInDarkTheme(),
72 | dynamicColor: Boolean = true,
73 | content: @Composable () -> Unit
74 | ) {
75 | val colorScheme = when {
76 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
77 | val context = LocalContext.current
78 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
79 | }
80 | darkTheme -> DarkColorScheme
81 | else -> LightColorScheme
82 | }
83 |
84 | val systemUiController = rememberSystemUiController()
85 | val useDarkIcons = !isSystemInDarkTheme()
86 |
87 | SideEffect {
88 | systemUiController.setSystemBarsColor(
89 | color = Color.Transparent,
90 | darkIcons = useDarkIcons
91 | )
92 | }
93 |
94 | MaterialTheme(
95 | colorScheme = colorScheme,
96 | typography = Typography,
97 | content = content
98 | )
99 | }
100 |
101 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("kotlin-android")
4 | id("kotlin-kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("com.google.gms.google-services")
7 | id("com.google.firebase.crashlytics")
8 | }
9 |
10 | android {
11 | namespace = "ru.tech.firenote"
12 | compileSdk = 32
13 |
14 | defaultConfig {
15 | applicationId = "ru.tech.firenote"
16 | minSdk = 21
17 | targetSdk = 32
18 | versionCode = 8
19 | versionName = "1.1.3"
20 | }
21 |
22 | buildTypes {
23 | release {
24 | isMinifyEnabled = true
25 | isShrinkResources = true
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android-optimize.txt"),
28 | "proguard-rules.pro"
29 | )
30 | }
31 | }
32 | compileOptions {
33 | isCoreLibraryDesugaringEnabled = true
34 | sourceCompatibility = JavaVersion.VERSION_1_8
35 | targetCompatibility = JavaVersion.VERSION_1_8
36 | }
37 | kotlinOptions {
38 | jvmTarget = "1.8"
39 | }
40 | buildFeatures {
41 | compose = true
42 | }
43 | composeOptions {
44 | kotlinCompilerExtensionVersion = "1.2.0-alpha06"
45 | }
46 | packagingOptions {
47 | resources {
48 | excludes += ("/META-INF/{AL2.0,LGPL2.1}")
49 | }
50 | }
51 | }
52 |
53 | dependencies {
54 |
55 | //Android Essentials
56 | implementation("androidx.core:core-ktx:1.7.0")
57 | implementation("androidx.appcompat:appcompat:1.4.1")
58 | implementation("com.google.android.material:material:1.6.0-beta01")
59 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
60 | implementation("androidx.window:window:1.0.0")
61 | implementation("androidx.navigation:navigation-fragment-ktx:2.4.1")
62 | implementation("androidx.navigation:navigation-ui-ktx:2.4.1")
63 |
64 | // Coroutines
65 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
66 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
67 |
68 | // Coroutine Lifecycle Scopes
69 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1")
70 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
71 |
72 | //Dagger - Hilt
73 | implementation("com.google.dagger:hilt-android:2.39.1")
74 | implementation("androidx.hilt:hilt-navigation-fragment:1.0.0")
75 | implementation("androidx.lifecycle:lifecycle-service:2.4.1")
76 | kapt("com.google.dagger:hilt-android-compiler:2.38.1")
77 | implementation("androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03")
78 | kapt("androidx.hilt:hilt-compiler:1.0.0")
79 | implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
80 |
81 | //Desugaring
82 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
83 |
84 | //Compose
85 | implementation("androidx.activity:activity-compose:1.4.0")
86 | implementation("androidx.compose.ui:ui:1.2.0-alpha06")
87 | implementation("androidx.compose.ui:ui-tooling-preview:1.2.0-alpha06")
88 | implementation("androidx.compose.material3:material3:1.0.0-alpha08")
89 | implementation("androidx.compose.material:material:1.2.0-alpha06")
90 | implementation("androidx.compose.material:material-icons-core:1.1.1")
91 | implementation("androidx.compose.material:material-icons-extended:1.1.1")
92 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha05")
93 | implementation("androidx.navigation:navigation-compose:2.5.0-alpha03")
94 | implementation("androidx.constraintlayout:constraintlayout-compose:1.0.0")
95 | implementation("androidx.compose.foundation:foundation:1.2.0-alpha06")
96 |
97 | //Accompanist
98 | implementation("com.google.accompanist:accompanist-systemuicontroller:0.24.2-alpha")
99 | implementation("com.google.accompanist:accompanist-flowlayout:0.24.2-alpha")
100 |
101 | //Coil
102 | implementation("io.coil-kt:coil:2.0.0-rc01")
103 | implementation("io.coil-kt:coil-compose:2.0.0-rc01")
104 |
105 | //Firebase
106 | implementation("com.google.firebase:firebase-auth-ktx:21.0.3")
107 | implementation("com.google.android.gms:play-services-auth:20.1.0")
108 | implementation("com.google.firebase:firebase-database-ktx:20.0.4")
109 | implementation("com.google.firebase:firebase-storage-ktx:20.0.1")
110 | implementation("com.google.firebase:firebase-crashlytics-ktx:18.2.9")
111 | implementation("com.google.firebase:firebase-analytics-ktx:20.1.2")
112 |
113 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/single/lazyitem/ProfileNoteItem.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.single.lazyitem
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.text.BasicTextField
9 | import androidx.compose.foundation.text.KeyboardActions
10 | import androidx.compose.foundation.text.KeyboardOptions
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.geometry.CornerRadius
17 | import androidx.compose.ui.geometry.Offset
18 | import androidx.compose.ui.geometry.Size
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.graphics.Path
21 | import androidx.compose.ui.graphics.drawscope.clipPath
22 | import androidx.compose.ui.graphics.toArgb
23 | import androidx.compose.ui.platform.LocalFocusManager
24 | import androidx.compose.ui.res.stringResource
25 | import androidx.compose.ui.text.TextStyle
26 | import androidx.compose.ui.text.input.ImeAction
27 | import androidx.compose.ui.text.style.TextAlign
28 | import androidx.compose.ui.text.style.TextOverflow
29 | import androidx.compose.ui.unit.Dp
30 | import androidx.compose.ui.unit.dp
31 | import androidx.compose.ui.unit.sp
32 | import ru.tech.firenote.R
33 | import ru.tech.firenote.utils.GlobalUtils.blend
34 |
35 | @Composable
36 | fun ProfileNoteItem(
37 | pair: Pair,
38 | typeText: String,
39 | onValueChange: (String) -> Unit,
40 | modifier: Modifier = Modifier,
41 | cornerRadius: Dp = 10.dp,
42 | cutCornerSize: Dp = 30.dp
43 | ) {
44 | val localFocusManager = LocalFocusManager.current
45 | Box(
46 | modifier = modifier,
47 | ) {
48 | Canvas(modifier = Modifier.matchParentSize()) {
49 | val clipPath = Path().apply {
50 | lineTo(size.width - cutCornerSize.toPx(), 0f)
51 | lineTo(size.width, cutCornerSize.toPx())
52 | lineTo(size.width, size.height)
53 | lineTo(0f, size.height)
54 | close()
55 | }
56 |
57 | clipPath(clipPath) {
58 | drawRoundRect(
59 | color = pair.first,
60 | size = size,
61 | cornerRadius = CornerRadius(cornerRadius.toPx())
62 | )
63 | drawRoundRect(
64 | color = Color(pair.first.toArgb().blend()),
65 | topLeft = Offset(size.width - cutCornerSize.toPx(), -100f),
66 | size = Size(cutCornerSize.toPx() + 100f, cutCornerSize.toPx() + 100f),
67 | cornerRadius = CornerRadius(cornerRadius.toPx())
68 | )
69 | }
70 | }
71 | Spacer(
72 | modifier = Modifier
73 | .fillMaxSize()
74 | .padding(top = 50.dp, bottom = 40.dp, end = 40.dp, start = 40.dp)
75 | )
76 | Text(
77 | text = pair.second.toString(),
78 | style = MaterialTheme.typography.bodyLarge,
79 | color = Color.Black,
80 | maxLines = 1,
81 | overflow = TextOverflow.Ellipsis,
82 | modifier = Modifier
83 | .align(Alignment.TopStart)
84 | .padding(start = 10.dp, top = 10.dp, end = cutCornerSize)
85 | )
86 | BasicTextField(
87 | value = typeText,
88 | onValueChange = { onValueChange(it) },
89 | textStyle = TextStyle(
90 | textAlign = TextAlign.Center,
91 | fontSize = 11.sp
92 | ),
93 | keyboardOptions = KeyboardOptions(
94 | imeAction = ImeAction.Done
95 | ),
96 | keyboardActions = KeyboardActions(
97 | onDone = { localFocusManager.clearFocus() }
98 | ),
99 | modifier = Modifier
100 | .align(Alignment.BottomCenter)
101 | .padding(end = 5.dp, start = 5.dp, bottom = 5.dp),
102 | maxLines = 3
103 | )
104 |
105 | if (typeText.isEmpty()) {
106 | Text(
107 | text = stringResource(R.string.noteType),
108 | modifier = Modifier
109 | .align(Alignment.BottomCenter)
110 | .padding(end = 5.dp, start = 5.dp, bottom = 5.dp),
111 | style = TextStyle(
112 | textAlign = TextAlign.Center,
113 | color = Color.DarkGray,
114 | fontSize = 11.sp
115 | )
116 | )
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Firenote
3 | Notes
4 | Note creation
5 | Note
6 | App closing
7 | The app wil be closed, but it still needs to be in background to send you alarms
8 | Add note
9 | Color
10 | Date
11 | Enter note label
12 | Stay
13 | Close
14 | Save
15 | Enter note description
16 | Fill all fields first!
17 | Note saving
18 | You started creating/editing note, do you want to save it or exit without saving?
19 | Discard changes
20 | Email
21 | Password
22 | Sign Up
23 | Log In
24 | Forgot Password?
25 | Welcome Back!
26 | Password too short
27 | Email is not valid
28 | Confirm password
29 | Passwords are different
30 | Auth
31 | Registration
32 | Reset password
33 | Send email
34 | Check your email and follow instructions in order to reset your password
35 | Nice to see you!
36 | Email not verified, check email to complete this step
37 | We send a verification link to your email
38 | You have no notes
39 | Delete note
40 | Are you really want to delete this note? This action cannot be undone!
41 | Delete
42 | Title
43 | Profile
44 | Note \"*\" deleted
45 | Undo
46 | Log out
47 | You will be logged out, but all your notes will be saved in the cloud
48 | No internet connection
49 | Change
50 | \ Reset\
51 | We will be glad to see you again!
52 | Pick image
53 | If you would like to reset your password, we will send you a confirmation email
54 | Email changing
55 | New email
56 | Email changed
57 | Username changing
58 | Username
59 | Edit
60 | Create or select note to edit/view it here
61 | Goals
62 | Make goal
63 | You have no goals
64 | Delete goal
65 | Are you really want to delete this goal? This action cannot be undone!
66 | Goal \"*\" deleted
67 | Create or select goal to edit/view it here
68 | Goal saving
69 | You started creating/editing goal, do you want to save it or exit without saving?
70 | Enter goal label
71 | Add subgoal
72 | Enter your subgoal here
73 | Completion
74 | Search here
75 | Nothing found, try to change your search query
76 | "Username changed to "
77 | Image picked successfully
78 | Note type
79 | Image
80 | Document
81 | Audio
82 | File
83 |
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/screen/auth/ForgotPasswordScreen.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.screen.auth
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.foundation.text.KeyboardActions
7 | import androidx.compose.foundation.text.KeyboardOptions
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Clear
10 | import androidx.compose.material.icons.outlined.AlternateEmail
11 | import androidx.compose.material3.*
12 | import androidx.compose.runtime.*
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.platform.LocalFocusManager
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.text.font.FontWeight
18 | import androidx.compose.ui.text.input.ImeAction
19 | import androidx.compose.ui.text.input.KeyboardType
20 | import androidx.compose.ui.text.style.TextAlign
21 | import androidx.compose.ui.unit.dp
22 | import androidx.compose.ui.unit.sp
23 | import ru.tech.firenote.R
24 | import ru.tech.firenote.ui.composable.provider.LocalToastHost
25 | import ru.tech.firenote.ui.composable.single.text.MaterialTextField
26 | import ru.tech.firenote.ui.composable.single.toast.sendToast
27 | import ru.tech.firenote.ui.route.Screen
28 | import ru.tech.firenote.viewModel.auth.AuthViewModel
29 |
30 | @ExperimentalMaterial3Api
31 | @Composable
32 | fun ForgotPasswordScreen(viewModel: AuthViewModel) {
33 | var email by remember { mutableStateOf("") }
34 |
35 | val isFormValid by derivedStateOf { email.isValid() }
36 |
37 | val emailError by derivedStateOf {
38 | !email.isValid() && email.isNotEmpty()
39 | }
40 |
41 | val toastHost = LocalToastHost.current
42 | val focusManager = LocalFocusManager.current
43 |
44 | LazyColumn(
45 | Modifier
46 | .fillMaxSize(),
47 | verticalArrangement = Arrangement.Bottom
48 | ) {
49 | item {
50 | Text(
51 | text = stringResource(R.string.resetPassword),
52 | fontWeight = FontWeight.Bold,
53 | fontSize = 32.sp,
54 | textAlign = TextAlign.Center,
55 | modifier = Modifier
56 | .fillMaxWidth()
57 | .padding(32.dp)
58 | )
59 | Column(
60 | Modifier
61 | .fillMaxSize()
62 | .padding(32.dp),
63 | horizontalAlignment = Alignment.CenterHorizontally,
64 | verticalArrangement = Arrangement.Center
65 | ) {
66 | MaterialTextField(
67 | modifier = Modifier.fillMaxWidth(),
68 | value = email,
69 | onValueChange = { email = it },
70 | label = { Text(text = stringResource(R.string.email)) },
71 | singleLine = true,
72 | isError = emailError,
73 | errorText = stringResource(R.string.emailIsNotValid),
74 | keyboardOptions = KeyboardOptions(
75 | keyboardType = KeyboardType.Email,
76 | imeAction = ImeAction.Done
77 | ),
78 | keyboardActions = KeyboardActions(onDone = {
79 | focusManager.clearFocus()
80 | if (isFormValid) viewModel.sendResetPasswordLink(email)
81 | }),
82 | trailingIcon = {
83 | if (email.isNotBlank())
84 | IconButton(onClick = { email = "" }) {
85 | Icon(Icons.Filled.Clear, null)
86 | }
87 | }
88 | )
89 | }
90 |
91 | val txt = stringResource(R.string.checkYourEmail)
92 |
93 | Button(
94 | onClick = {
95 | viewModel.goTo(Screen.LoginScreen)
96 | viewModel.sendResetPasswordLink(email)
97 | toastHost.sendToast(Icons.Outlined.AlternateEmail, txt)
98 | },
99 | enabled = isFormValid,
100 | modifier = Modifier
101 | .fillMaxWidth()
102 | .padding(32.dp),
103 | shape = RoundedCornerShape(16.dp)
104 | ) {
105 | Text(text = stringResource(R.string.sendEmail))
106 | }
107 | Spacer(modifier = Modifier.height(32.dp))
108 | Row(
109 | modifier = Modifier
110 | .fillMaxWidth()
111 | .padding(32.dp),
112 | horizontalArrangement = Arrangement.Start
113 | ) {
114 | TextButton(onClick = {
115 | viewModel.goTo(Screen.LoginScreen)
116 | }) {
117 | Text(text = stringResource(R.string.logIn))
118 | }
119 | }
120 | }
121 | }
122 |
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/single/lazyitem/NoteItem.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.single.lazyitem
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.outlined.Delete
7 | import androidx.compose.material3.*
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.CompositionLocalProvider
10 | import androidx.compose.runtime.derivedStateOf
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.geometry.CornerRadius
15 | import androidx.compose.ui.geometry.Offset
16 | import androidx.compose.ui.geometry.Size
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.graphics.Path
19 | import androidx.compose.ui.graphics.drawscope.clipPath
20 | import androidx.compose.ui.platform.LocalLayoutDirection
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.text.style.TextOverflow
23 | import androidx.compose.ui.unit.Dp
24 | import androidx.compose.ui.unit.LayoutDirection
25 | import androidx.compose.ui.unit.dp
26 | import ru.tech.firenote.model.Note
27 | import ru.tech.firenote.ui.composable.provider.LocalWindowSize
28 | import ru.tech.firenote.ui.composable.utils.WindowSize
29 | import ru.tech.firenote.utils.GlobalUtils.blend
30 | import java.text.SimpleDateFormat
31 | import java.util.*
32 |
33 | @Composable
34 | fun NoteItem(
35 | note: Note,
36 | modifier: Modifier = Modifier,
37 | cornerRadius: Dp = 10.dp,
38 | cutCornerSize: Dp = 30.dp,
39 | onDeleteClick: () -> Unit
40 | ) {
41 | Box(
42 | modifier = modifier,
43 | ) {
44 | Canvas(modifier = Modifier.matchParentSize()) {
45 | val clipPath = Path().apply {
46 | lineTo(size.width - cutCornerSize.toPx(), 0f)
47 | lineTo(size.width, cutCornerSize.toPx())
48 | lineTo(size.width, size.height)
49 | lineTo(0f, size.height)
50 | close()
51 | }
52 |
53 | clipPath(clipPath) {
54 | drawRoundRect(
55 | color = Color(note.color ?: 0),
56 | size = size,
57 | cornerRadius = CornerRadius(cornerRadius.toPx())
58 | )
59 | drawRoundRect(
60 | color = Color(
61 | (note.color ?: 0).blend()
62 | ),
63 | topLeft = Offset(size.width - cutCornerSize.toPx(), -100f),
64 | size = Size(cutCornerSize.toPx() + 100f, cutCornerSize.toPx() + 100f),
65 | cornerRadius = CornerRadius(cornerRadius.toPx())
66 | )
67 | }
68 | }
69 | Column(
70 | modifier = Modifier
71 | .fillMaxSize()
72 | .padding(16.dp)
73 | .padding(end = 32.dp)
74 | ) {
75 | val convertTime by derivedStateOf {
76 | SimpleDateFormat("dd/MM/yyyy\nHH:mm", Locale.getDefault()).format(
77 | note.timestamp ?: 0L
78 | )
79 | }
80 |
81 | Row(modifier = Modifier.fillMaxWidth()) {
82 | Text(
83 | modifier = Modifier.weight(2f),
84 | text = note.title ?: "",
85 | style = MaterialTheme.typography.bodyLarge,
86 | color = Color.Black,
87 | maxLines = 1,
88 | overflow = TextOverflow.Ellipsis
89 | )
90 | CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
91 | Text(
92 | modifier = Modifier.weight(1f),
93 | text = convertTime,
94 | style = MaterialTheme.typography.bodySmall,
95 | color = Color.DarkGray,
96 | textAlign = TextAlign.Justify,
97 | overflow = TextOverflow.Ellipsis
98 | )
99 | }
100 |
101 | }
102 | Spacer(modifier = Modifier.height(8.dp))
103 | Text(
104 | text = note.content ?: "",
105 | style = MaterialTheme.typography.bodySmall,
106 | color = Color.Black,
107 | maxLines = when (LocalWindowSize.current) {
108 | WindowSize.Compact -> 10
109 | WindowSize.Medium -> 20
110 | else -> 30
111 | },
112 | overflow = TextOverflow.Ellipsis
113 | )
114 | }
115 | IconButton(
116 | onClick = onDeleteClick,
117 | modifier = Modifier.align(Alignment.BottomEnd)
118 | ) {
119 | Icon(
120 | imageVector = Icons.Outlined.Delete,
121 | contentDescription = "Delete note",
122 | tint = darkColorScheme().onTertiary
123 | )
124 | }
125 | }
126 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-ru-rRU/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Заметки
4 | Создание заметки
5 | Заметка
6 | Закрытие приложения
7 | Приложение будет закрыто, но оно будет оставаться в фоне, чтобы присылать вам напоминания
8 | Добавить заметку
9 | Цвет
10 | Дата
11 | Введите название
12 | Остаться
13 | Закрыть
14 | Сохранить
15 | Введите текст заметки
16 | Заполните все поля!
17 | Сохранение заметки
18 | Не применять изменения
19 | Почта
20 | Пароль
21 | Зарегистрироваться
22 | Войти
23 | Забыли пароль?
24 | Добро пожаловать!
25 | Пароль слишком короткий
26 | Почта введена некорректно
27 | Подвердите
28 | Пароли отличаются
29 | Аутентификация
30 | Регистрация
31 | Сбросить пароль
32 | Отправить письмо
33 | Проверьте почту и следуйте инструкциям, чтобы восстановть пароль
34 | Добро пожаловать!
35 | Почта не подтверждена, проверьте почту, чтобы пройти верификацию
36 | Мы отправили ссылку для подтвержедния на вашу почту
37 | У вас нет заметок
38 | Удалить заметку
39 | Вы действительно хотите удалить эту заметку? Это действие не может быть отменено!
40 | Удалить
41 | Заголовок
42 | Профиль
43 | Заметка \"*\" удалена
44 | Отменить
45 | Выйти
46 | Вы выйдите из аккаунта, но все ваши заметки будут сохранены в облаке
47 | Нет интернет соединения
48 | Изменить
49 | Сбросить
50 | Рады будем видеть вас снова!
51 | Выбрать изображение
52 | Если вы хотите сбросить пароль, мы вышлем вам письмо с подтверждением
53 | Смена почты
54 | Новая почта
55 | Почта изменена
56 | Изменение никнейма
57 | Никнейм
58 | Редактировать
59 | Выберите или создайте заметку, чтобы увидеть ее тут
60 | Цели
61 | Поставить цель
62 | У вас нет целей
63 | Удалить цель
64 | Вы действительно хотите удалить эту цель? Это действие не может быть отменено!
65 | Цель \"*\" удалена
66 | Выберите или создайте цель, чтобы увидеть ее тут
67 | Сохранение цели
68 | Вы начали создание/редактирование заметки,хотите ее сохранить, или не применять изменения?
69 | Вы начали создание/редактирование цели,хотите ее сохранить, или не применять изменения?
70 | Введите заголовок
71 | Добавить подзадачу
72 | Введите подзадачу
73 | Выполненность
74 | Ищите тут
75 | Ничего не найдено по вашему запросу
76 | "Никнейм изменен на "
77 | Успешно выбрано изображение
78 | Тип заметки
79 | Изображение
80 | Документ
81 | Музыка
82 | Файл
83 |
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.graphics.toArgb
5 |
6 | val NoteYellow = Color(0xFFFFF389)
7 | val NotePink = Color(0xFFE2648C)
8 | val NoteRed = Color(0xFFE76A6A)
9 | val NoteBlue = Color(0xFF81DBDF)
10 | val NoteOrange = Color(0xFFE68049)
11 | val NoteMint = Color(0xFF3ECC89)
12 | val NoteViolet = Color(0xFFF0A2FF)
13 | val NoteIndigo = Color(0xFFA7ABE9)
14 | val NoteGreen = Color(0xFF8DDF69)
15 | val NoteWhite = Color(0xFFFFE2EB)
16 | val NoteBrown = Color(0xFFBD7857)
17 | val NoteGray = Color(0xFFA5969B)
18 |
19 | val GoalGreen = Color(0xFF86C768)
20 | val GoalYellow = Color(0xFFD8D76A)
21 | val GoalCarrot = Color(0xFFD89D53)
22 | val GoalRed = Color(0xFFD57171)
23 |
24 | val md_theme_light_primary = Color(0xFF984065)
25 | val md_theme_light_onPrimary = Color(0xFFffffff)
26 | val md_theme_light_primaryContainer = Color(0xFFffd8e5)
27 | val md_theme_light_onPrimaryContainer = Color(0xFF3e001f)
28 | val md_theme_light_secondary = Color(0xFF735760)
29 | val md_theme_light_onSecondary = Color(0xFFffffff)
30 | val md_theme_light_secondaryContainer = Color(0xFFffd8e3)
31 | val md_theme_light_onSecondaryContainer = Color(0xFF2b151d)
32 | val md_theme_light_tertiary = Color(0xFF7d5636)
33 | val md_theme_light_onTertiary = Color(0xFFffffff)
34 | val md_theme_light_tertiaryContainer = Color(0xFFffdcc1)
35 | val md_theme_light_onTertiaryContainer = Color(0xFF2f1500)
36 | val md_theme_light_error = Color(0xFFba1b1b)
37 | val md_theme_light_errorContainer = Color(0xFFffdad4)
38 | val md_theme_light_onError = Color(0xFFffffff)
39 | val md_theme_light_onErrorContainer = Color(0xFF410001)
40 | val md_theme_light_background = Color(0xFFfcfcfc)
41 | val md_theme_light_onBackground = Color(0xFF1f1a1b)
42 | val md_theme_light_surface = Color(0xFFfcfcfc)
43 | val md_theme_light_onSurface = Color(0xFF1f1a1b)
44 | val md_theme_light_surfaceVariant = Color(0xFFf2dde2)
45 | val md_theme_light_onSurfaceVariant = Color(0xFF514347)
46 | val md_theme_light_outline = Color(0xFF827377)
47 | val md_theme_light_inverseOnSurface = Color(0xFFfaeef0)
48 | val md_theme_light_inverseSurface = Color(0xFF352f30)
49 | val md_theme_light_inversePrimary = Color(0xFFffb0cd)
50 |
51 | val md_theme_dark_primary = Color(0xFFffb0cd)
52 | val md_theme_dark_onPrimary = Color(0xFF5d1136)
53 | val md_theme_dark_primaryContainer = Color(0xFF7a294d)
54 | val md_theme_dark_onPrimaryContainer = Color(0xFFffd8e5)
55 | val md_theme_dark_secondary = Color(0xFFe1bdc7)
56 | val md_theme_dark_onSecondary = Color(0xFF422932)
57 | val md_theme_dark_secondaryContainer = Color(0xFF5a3f48)
58 | val md_theme_dark_onSecondaryContainer = Color(0xFFffd8e3)
59 | val md_theme_dark_tertiary = Color(0xFFf0bc95)
60 | val md_theme_dark_onTertiary = Color(0xFF48290d)
61 | val md_theme_dark_tertiaryContainer = Color(0xFF623f21)
62 | val md_theme_dark_onTertiaryContainer = Color(0xFFffdcc1)
63 | val md_theme_dark_error = Color(0xFFffb4a9)
64 | val md_theme_dark_errorContainer = Color(0xFF930006)
65 | val md_theme_dark_onError = Color(0xFF680003)
66 | val md_theme_dark_onErrorContainer = Color(0xFFffdad4)
67 | val md_theme_dark_background = Color(0xFF1f1a1b)
68 | val md_theme_dark_onBackground = Color(0xFFebdfe1)
69 | val md_theme_dark_surface = Color(0xFF1f1a1b)
70 | val md_theme_dark_onSurface = Color(0xFFebdfe1)
71 | val md_theme_dark_surfaceVariant = Color(0xFF514347)
72 | val md_theme_dark_onSurfaceVariant = Color(0xFFd5c1c6)
73 | val md_theme_dark_outline = Color(0xFF9d8c90)
74 | val md_theme_dark_inverseOnSurface = Color(0xFF1f1a1b)
75 | val md_theme_dark_inverseSurface = Color(0xFFebdfe1)
76 | val md_theme_dark_inversePrimary = Color(0xFF984065)
77 |
78 |
79 | val noteColors =
80 | listOf(
81 | NoteYellow,
82 | NoteGreen,
83 | NoteMint,
84 | NoteBlue,
85 | NoteIndigo,
86 | NoteViolet,
87 | NoteOrange,
88 | NoteRed,
89 | NotePink,
90 | NoteWhite,
91 | NoteGray,
92 | NoteBrown
93 | )
94 |
95 | val goalColors =
96 | listOf(
97 | GoalGreen,
98 | GoalYellow,
99 | GoalCarrot,
100 | GoalRed
101 | )
102 |
103 | val Int.priority
104 | get() = when (this) {
105 | NoteWhite.toArgb() -> -3
106 | NoteGray.toArgb() -> -2
107 | NoteBrown.toArgb() -> -1
108 | NoteYellow.toArgb() -> 0
109 | NoteGreen.toArgb() -> 1
110 | NoteMint.toArgb() -> 2
111 | NoteBlue.toArgb() -> 3
112 | NoteIndigo.toArgb() -> 4
113 | NoteViolet.toArgb() -> 5
114 | NoteOrange.toArgb() -> 6
115 | NoteRed.toArgb() -> 7
116 | else -> 8
117 | }
118 |
119 | val Int.position
120 | get() = when (this) {
121 | NoteWhite.toArgb() -> 9
122 | NoteGray.toArgb() -> 10
123 | NoteBrown.toArgb() -> 11
124 | NoteYellow.toArgb() -> 0
125 | NoteGreen.toArgb() -> 1
126 | NoteMint.toArgb() -> 2
127 | NoteBlue.toArgb() -> 3
128 | NoteIndigo.toArgb() -> 4
129 | NoteViolet.toArgb() -> 5
130 | NoteOrange.toArgb() -> 6
131 | NoteRed.toArgb() -> 7
132 | else -> 8
133 | }
134 |
135 | val Int.priorityGoal
136 | get() = when (this) {
137 | GoalGreen.toArgb() -> 0
138 | GoalYellow.toArgb() -> 1
139 | GoalCarrot.toArgb() -> 2
140 | else -> 3
141 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/app/FirenoteApp.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.app
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.pm.ActivityInfo
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.BackHandler
7 | import androidx.compose.animation.ExperimentalAnimationApi
8 | import androidx.compose.foundation.ExperimentalFoundationApi
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.lazy.rememberLazyListState
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.filled.Error
13 | import androidx.compose.material.icons.filled.ExitToApp
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.SnackbarHostState
16 | import androidx.compose.material3.Surface
17 | import androidx.compose.runtime.*
18 | import androidx.compose.runtime.saveable.rememberSaveable
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.draw.alpha
21 | import androidx.lifecycle.viewmodel.compose.viewModel
22 | import androidx.navigation.NavHostController
23 | import ru.tech.firenote.R
24 | import ru.tech.firenote.ui.composable.provider.LocalLazyListStateProvider
25 | import ru.tech.firenote.ui.composable.provider.LocalSnackbarHost
26 | import ru.tech.firenote.ui.composable.provider.LocalToastHost
27 | import ru.tech.firenote.ui.composable.provider.LocalWindowSize
28 | import ru.tech.firenote.ui.composable.screen.auth.AuthScreen
29 | import ru.tech.firenote.ui.composable.screen.creation.CreationContainer
30 | import ru.tech.firenote.ui.composable.single.dialog.MaterialDialog
31 | import ru.tech.firenote.ui.composable.single.scaffold.FirenoteScaffold
32 | import ru.tech.firenote.ui.composable.single.toast.FancyToast
33 | import ru.tech.firenote.ui.composable.single.toast.FancyToastValues
34 | import ru.tech.firenote.ui.composable.utils.WindowSize
35 | import ru.tech.firenote.ui.theme.FirenoteTheme
36 | import ru.tech.firenote.viewModel.main.MainViewModel
37 |
38 | @ExperimentalFoundationApi
39 | @SuppressLint("SourceLockedOrientationActivity")
40 | @ExperimentalMaterial3Api
41 | @ExperimentalAnimationApi
42 | @Composable
43 | fun FirenoteApp(
44 | context: ComponentActivity,
45 | windowSize: WindowSize,
46 | splitScreen: Boolean,
47 | navController: NavHostController,
48 | viewModel: MainViewModel = viewModel()
49 | ) {
50 |
51 | val isScaffoldVisible by derivedStateOf {
52 | (!viewModel.showNoteCreation.currentState or !viewModel.showNoteCreation.targetState)
53 | .and(!viewModel.showGoalCreation.currentState or !viewModel.showGoalCreation.targetState)
54 | }
55 |
56 | FirenoteTheme {
57 | MaterialDialog(
58 | showDialog = rememberSaveable { mutableStateOf(false) },
59 | icon = Icons.Filled.ExitToApp,
60 | title = R.string.exitApp,
61 | message = R.string.exitAppMessage,
62 | confirmText = R.string.stay,
63 | dismissText = R.string.close,
64 | dismissAction = { context.finishAffinity() }
65 | )
66 | if (viewModel.searchMode.value) BackHandler {
67 | viewModel.searchMode.value = false
68 | viewModel.updateSearch()
69 | }
70 |
71 | val icon = remember { mutableStateOf(Icons.Default.Error) }
72 | val text = remember { mutableStateOf("") }
73 | val changed = remember { mutableStateOf(false) }
74 |
75 | val snackbarHostState = remember { SnackbarHostState() }
76 |
77 | val lazyListState = rememberLazyListState()
78 |
79 | CompositionLocalProvider(
80 | LocalSnackbarHost provides snackbarHostState,
81 | LocalWindowSize provides windowSize,
82 | LocalToastHost provides FancyToastValues(icon, text, changed),
83 | LocalLazyListStateProvider provides lazyListState
84 | ) {
85 | if (viewModel.isAuth.value) {
86 | AuthScreen(viewModel.isAuth)
87 | context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
88 | } else {
89 | context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
90 | if (splitScreen) {
91 | Row {
92 | FirenoteScaffold(
93 | modifier = Modifier.weight(1f),
94 | viewModel = viewModel,
95 | navController = navController,
96 | context = context
97 | )
98 | Surface(modifier = Modifier.weight(1.5f)) {
99 | CreationContainer(viewModel, splitScreen)
100 | }
101 | }
102 | } else {
103 | FirenoteScaffold(
104 | modifier = Modifier.alpha(if (isScaffoldVisible) 1f else 0f),
105 | viewModel = viewModel,
106 | navController = navController,
107 | context = context
108 | )
109 | CreationContainer(viewModel, splitScreen)
110 | }
111 | }
112 | }
113 |
114 | FancyToast(icon = icon.value, message = text.value, changed = changed)
115 |
116 | }
117 |
118 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/single/lazyitem/GoalItem.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.single.lazyitem
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.outlined.Delete
7 | import androidx.compose.material3.*
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.CompositionLocalProvider
10 | import androidx.compose.runtime.derivedStateOf
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.geometry.CornerRadius
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.graphics.Path
17 | import androidx.compose.ui.graphics.drawscope.clipPath
18 | import androidx.compose.ui.platform.LocalLayoutDirection
19 | import androidx.compose.ui.text.style.TextAlign
20 | import androidx.compose.ui.text.style.TextDecoration
21 | import androidx.compose.ui.text.style.TextOverflow
22 | import androidx.compose.ui.unit.Dp
23 | import androidx.compose.ui.unit.LayoutDirection
24 | import androidx.compose.ui.unit.dp
25 | import ru.tech.firenote.model.Goal
26 | import ru.tech.firenote.ui.composable.provider.LocalWindowSize
27 | import ru.tech.firenote.ui.composable.utils.WindowSize
28 | import java.text.SimpleDateFormat
29 | import java.util.*
30 |
31 | @Composable
32 | fun GoalItem(
33 | goal: Goal,
34 | modifier: Modifier = Modifier,
35 | cornerRadius: Dp = 10.dp,
36 | onDeleteClick: () -> Unit
37 | ) {
38 | var doneAll = true
39 | val mapped = goal.content?.mapIndexed { index, item ->
40 | var text = ""
41 | text += ("• ${item.content}")
42 | if (index != goal.content.lastIndex) text += "\n"
43 | if (item.done == false) doneAll = false
44 | item.copy(content = text)
45 | }
46 |
47 | Box(
48 | modifier = modifier,
49 | ) {
50 | Canvas(modifier = Modifier.matchParentSize()) {
51 | val clipPath = Path().apply {
52 | lineTo(size.width, 0f)
53 | lineTo(size.width, 0f)
54 | lineTo(size.width, size.height)
55 | lineTo(0f, size.height)
56 | close()
57 | }
58 |
59 | clipPath(clipPath) {
60 | drawRoundRect(
61 | color = Color(goal.color ?: 0),
62 | size = size,
63 | cornerRadius = CornerRadius(cornerRadius.toPx())
64 | )
65 | }
66 | }
67 | Column(
68 | modifier = Modifier
69 | .fillMaxSize()
70 | .padding(16.dp)
71 | ) {
72 |
73 | val convertTime by derivedStateOf {
74 | SimpleDateFormat("dd/MM/yyyy\nHH:mm", Locale.getDefault()).format(
75 | goal.timestamp ?: 0L
76 | )
77 | }
78 |
79 | Row(modifier = Modifier.fillMaxWidth()) {
80 | Text(
81 | modifier = Modifier.weight(2f),
82 | text = goal.title ?: "",
83 | style = MaterialTheme.typography.bodyLarge,
84 | color = if (doneAll) Color.DarkGray else Color.Black,
85 | textDecoration = if (doneAll) TextDecoration.LineThrough else TextDecoration.None,
86 | maxLines = 1,
87 | overflow = TextOverflow.Ellipsis
88 | )
89 | CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
90 | Text(
91 | modifier = Modifier.weight(1f),
92 | text = convertTime,
93 | style = MaterialTheme.typography.bodySmall,
94 | color = Color.DarkGray,
95 | textAlign = TextAlign.Justify,
96 | overflow = TextOverflow.Ellipsis
97 | )
98 | }
99 |
100 | }
101 | Spacer(modifier = Modifier.height(8.dp))
102 | Column(Modifier.padding(end = 32.dp)) {
103 | mapped?.let {
104 | it.forEachIndexed { index, item ->
105 | if (index <= when (LocalWindowSize.current) {
106 | WindowSize.Compact -> 10
107 | WindowSize.Medium -> 20
108 | else -> 30
109 | }
110 | ) {
111 | Text(
112 | text = item.content ?: "",
113 | style = MaterialTheme.typography.bodySmall,
114 | color = if (item.done == true) Color.DarkGray else Color.Black,
115 | textDecoration = if (item.done == true) TextDecoration.LineThrough else TextDecoration.None,
116 | overflow = TextOverflow.Ellipsis,
117 | maxLines = 5
118 | )
119 | }
120 | }
121 | }
122 | }
123 | }
124 | IconButton(
125 | onClick = onDeleteClick,
126 | modifier = Modifier.align(Alignment.BottomEnd)
127 | ) {
128 | Icon(
129 | imageVector = Icons.Outlined.Delete,
130 | contentDescription = "Delete note",
131 | tint = darkColorScheme().onTertiary
132 | )
133 | }
134 | }
135 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/viewModel/navigation/ProfileViewModel.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.viewModel.navigation
2 |
3 | import android.net.Uri
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.toArgb
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import dagger.hilt.android.lifecycle.HiltViewModel
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.StateFlow
13 | import kotlinx.coroutines.launch
14 | import ru.tech.firenote.model.Type
15 | import ru.tech.firenote.repository.NoteRepository
16 | import ru.tech.firenote.ui.state.UIState
17 | import ru.tech.firenote.ui.theme.noteColors
18 | import ru.tech.firenote.ui.theme.position
19 | import javax.inject.Inject
20 |
21 | @HiltViewModel
22 | class ProfileViewModel @Inject constructor(
23 | private val repository: NoteRepository
24 | ) : ViewModel() {
25 |
26 | val email get() = repository.auth.currentUser?.email ?: ""
27 |
28 | private val _photoState = MutableStateFlow(UIState.Empty())
29 | val photoState: StateFlow = _photoState
30 |
31 | private val _updateState = MutableStateFlow(UIState.Empty())
32 | val updateState: StateFlow = _updateState
33 |
34 | private val _noteCountState = MutableStateFlow(UIState.Empty())
35 | val noteCountState: StateFlow = _noteCountState
36 |
37 | private val _username = MutableStateFlow(UIState.Success(email.split("@")[0]))
38 | var username: StateFlow = _username
39 |
40 | private val _typeState = mutableStateOf>(listOf())
41 | val typeState: State> = _typeState
42 |
43 | init {
44 | loadUsername()
45 | loadProfileImage()
46 | getNotes()
47 | getTypes()
48 | }
49 |
50 | private fun getNotes() {
51 | viewModelScope.launch {
52 | _noteCountState.value = UIState.Loading
53 | repository.getNotes().collect {
54 | if (it.isSuccess) {
55 | val tempList = ArrayList(List(noteColors.size) { 0 })
56 | val list = it.getOrNull()
57 | if (list.isNullOrEmpty()) _noteCountState.value = UIState.Success(tempList)
58 | else {
59 | for (note in list) {
60 | tempList[(note.color ?: 0).position]++
61 | }
62 | _noteCountState.value = UIState.Success(tempList)
63 | }
64 | } else {
65 | _noteCountState.value = UIState.Empty(it.exceptionOrNull()?.localizedMessage)
66 | }
67 | }
68 | }
69 | }
70 |
71 | private fun getTypes() {
72 | viewModelScope.launch {
73 | repository.getTypes().collect {
74 | if (it.isSuccess) {
75 | val list = it.getOrNull()
76 | _typeState.value = list ?: listOf()
77 | } else {
78 | _typeState.value = listOf()
79 | }
80 | }
81 | }
82 | }
83 |
84 | private fun loadProfileImage() {
85 | viewModelScope.launch {
86 | _photoState.value = UIState.Loading
87 | repository.getProfileUri().collect {
88 | val profileImageUri = it.getOrNull()
89 |
90 | if (profileImageUri != null) _photoState.value = UIState.Success(profileImageUri)
91 | else _photoState.value = UIState.Empty(it.exceptionOrNull()?.localizedMessage)
92 | }
93 | }
94 | }
95 |
96 | fun updateProfile(uri: Uri?) {
97 | if (uri != null) {
98 | viewModelScope.launch {
99 | _photoState.value = UIState.Loading
100 | repository.setProfileUri(uri)
101 | }
102 | }
103 | }
104 |
105 | private fun loadUsername() {
106 | viewModelScope.launch {
107 | _username.value = UIState.Loading
108 | repository.getUsername().collect {
109 | val username = it.getOrNull()
110 |
111 | if (username != null) _username.value = UIState.Success(username)
112 | else _username.value = UIState.Empty(it.exceptionOrNull()?.localizedMessage)
113 | }
114 | }
115 | }
116 |
117 | fun sendResetPasswordLink() {
118 | repository.auth.sendPasswordResetEmail(email)
119 | }
120 |
121 | fun sendVerifyEmail() {
122 | repository.auth.currentUser?.sendEmailVerification()
123 | }
124 |
125 | fun signOut() {
126 | repository.auth.signOut()
127 | }
128 |
129 | fun updateUsername(username: String) {
130 | viewModelScope.launch {
131 | _username.value = UIState.Loading
132 | repository.setUsername(username)
133 | }
134 | }
135 |
136 | fun changeEmail(oldEmail: String, password: String, newEmail: String) {
137 | _updateState.value = UIState.Loading
138 | repository.auth.signInWithEmailAndPassword(oldEmail, password)
139 | .addOnCompleteListener { task ->
140 | if (task.isSuccessful) {
141 | repository.auth.currentUser?.updateEmail(newEmail)
142 | ?.addOnCompleteListener { emailTask ->
143 | if (emailTask.isSuccessful) {
144 | _updateState.value = UIState.Success(newEmail)
145 | } else {
146 | _updateState.value =
147 | UIState.Empty(emailTask.exception?.localizedMessage)
148 | }
149 | }
150 | } else {
151 | _updateState.value = UIState.Empty(task.exception?.localizedMessage)
152 | }
153 | }
154 | }
155 |
156 | fun updateType(color: Color, type: String) {
157 | viewModelScope.launch {
158 | repository.updateType(color.toArgb(), type)
159 | }
160 | }
161 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/screen/navigation/NoteListScreen.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.screen.navigation
2 |
3 | import androidx.compose.animation.core.MutableTransitionState
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.interaction.MutableInteractionSource
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.PaddingValues
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.lazy.LazyColumn
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.outlined.Delete
14 | import androidx.compose.material.icons.outlined.Error
15 | import androidx.compose.material.icons.twotone.Cloud
16 | import androidx.compose.material.icons.twotone.FindInPage
17 | import androidx.compose.material3.CircularProgressIndicator
18 | import androidx.compose.material3.SnackbarResult
19 | import androidx.compose.runtime.*
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.compose.ui.unit.dp
24 | import androidx.hilt.navigation.compose.hiltViewModel
25 | import ru.tech.firenote.R
26 | import ru.tech.firenote.model.Note
27 | import ru.tech.firenote.ui.composable.provider.LocalLazyListStateProvider
28 | import ru.tech.firenote.ui.composable.provider.LocalSnackbarHost
29 | import ru.tech.firenote.ui.composable.provider.LocalToastHost
30 | import ru.tech.firenote.ui.composable.provider.showSnackbar
31 | import ru.tech.firenote.ui.composable.single.dialog.MaterialDialog
32 | import ru.tech.firenote.ui.composable.single.lazyitem.NoteItem
33 | import ru.tech.firenote.ui.composable.single.placeholder.Placeholder
34 | import ru.tech.firenote.ui.composable.single.toast.sendToast
35 | import ru.tech.firenote.ui.state.UIState
36 | import ru.tech.firenote.ui.theme.priority
37 | import ru.tech.firenote.viewModel.navigation.NoteListViewModel
38 |
39 | @Suppress("UNCHECKED_CAST")
40 | @ExperimentalFoundationApi
41 | @Composable
42 | fun NoteListScreen(
43 | showNoteCreation: MutableTransitionState,
44 | globalNote: MutableState = mutableStateOf(null),
45 | filterType: MutableState,
46 | isDescendingFilter: MutableState,
47 | searchString: MutableState,
48 | viewModel: NoteListViewModel = hiltViewModel()
49 | ) {
50 | val notePaddingValues = PaddingValues(top = 10.dp, start = 10.dp, end = 10.dp, bottom = 140.dp)
51 | val needToShowDeleteDialog = remember { mutableStateOf(false) }
52 | var note by remember { mutableStateOf(Note()) }
53 | val scope = rememberCoroutineScope()
54 | val host = LocalSnackbarHost.current
55 |
56 | val message = stringResource(R.string.noteDeleted)
57 | val action = stringResource(R.string.undo)
58 |
59 | when (val state = viewModel.uiState.collectAsState().value) {
60 | is UIState.Loading -> {
61 | Column(
62 | modifier = Modifier
63 | .fillMaxSize(),
64 | verticalArrangement = Arrangement.Center,
65 | horizontalAlignment = Alignment.CenterHorizontally
66 | ) {
67 | CircularProgressIndicator()
68 | }
69 | }
70 | is UIState.Success<*> -> {
71 | val repoList = state.data as List
72 | var data = if (isDescendingFilter.value) {
73 | when (filterType.value) {
74 | 0 -> repoList.sortedBy { it.title }
75 | 1 -> repoList.sortedBy { (it.color ?: 0).priority }
76 | else -> repoList.sortedBy { it.timestamp }
77 | }
78 | } else {
79 | when (filterType.value) {
80 | 0 -> repoList.sortedByDescending { it.title }
81 | 1 -> repoList.sortedByDescending { (it.color ?: 0).priority }
82 | else -> repoList.sortedByDescending { it.timestamp }
83 | }
84 | }
85 |
86 | if (searchString.value.isNotEmpty()) {
87 | data = repoList.filter {
88 | it.content?.lowercase()?.contains(searchString.value)
89 | ?.or(
90 | it.title?.lowercase()?.contains(searchString.value) ?: false
91 | ) ?: false
92 | }
93 | if (data.isEmpty()) {
94 | Placeholder(icon = Icons.TwoTone.FindInPage, textRes = R.string.nothingFound)
95 | }
96 | }
97 |
98 | LazyColumn(
99 | state = LocalLazyListStateProvider.current,
100 | verticalArrangement = Arrangement.spacedBy(8.dp),
101 | contentPadding = notePaddingValues
102 | ) {
103 | items(data.size) { index ->
104 | val locNote = data[index]
105 | NoteItem(
106 | note = locNote,
107 | onDeleteClick = {
108 | note = locNote
109 | needToShowDeleteDialog.value = true
110 | },
111 | modifier = Modifier
112 | .clickable(remember { MutableInteractionSource() }, null) {
113 | globalNote.value = locNote
114 | showNoteCreation.targetState = true
115 | }
116 | )
117 | }
118 | }
119 | }
120 | is UIState.Empty -> {
121 | state.message?.let {
122 | LocalToastHost.current.sendToast(Icons.Outlined.Error, it)
123 | }
124 | Placeholder(icon = Icons.TwoTone.Cloud, textRes = R.string.noNotes)
125 | }
126 | }
127 |
128 | MaterialDialog(
129 | showDialog = needToShowDeleteDialog,
130 | icon = Icons.Outlined.Delete,
131 | title = R.string.deleteNote,
132 | message = R.string.deleteNoteMessage,
133 | confirmText = R.string.close,
134 | dismissText = R.string.delete,
135 | dismissAction = {
136 | viewModel.deleteNote(note) { note ->
137 | var temp = note.title.toString().take(30)
138 | if (note.title.toString().length > 30) temp += "..."
139 | val messageNew = message.replace("*", temp)
140 |
141 | showSnackbar(
142 | scope,
143 | host,
144 | messageNew,
145 | action
146 | ) {
147 | if (it == SnackbarResult.ActionPerformed) {
148 | viewModel.insertNote(note)
149 | }
150 | }
151 | }
152 | },
153 | backHandler = { }
154 | )
155 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/screen/navigation/GoalListScreen.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.screen.navigation
2 |
3 | import androidx.compose.animation.core.MutableTransitionState
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.interaction.MutableInteractionSource
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.PaddingValues
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.outlined.Delete
13 | import androidx.compose.material.icons.outlined.Error
14 | import androidx.compose.material.icons.twotone.Cloud
15 | import androidx.compose.material.icons.twotone.FindInPage
16 | import androidx.compose.material3.CircularProgressIndicator
17 | import androidx.compose.material3.SnackbarResult
18 | import androidx.compose.runtime.*
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.res.stringResource
22 | import androidx.compose.ui.unit.dp
23 | import androidx.hilt.navigation.compose.hiltViewModel
24 | import ru.tech.firenote.R
25 | import ru.tech.firenote.model.Goal
26 | import ru.tech.firenote.ui.composable.provider.LocalLazyListStateProvider
27 | import ru.tech.firenote.ui.composable.provider.LocalSnackbarHost
28 | import ru.tech.firenote.ui.composable.provider.LocalToastHost
29 | import ru.tech.firenote.ui.composable.provider.showSnackbar
30 | import ru.tech.firenote.ui.composable.single.dialog.MaterialDialog
31 | import ru.tech.firenote.ui.composable.single.lazyitem.GoalItem
32 | import ru.tech.firenote.ui.composable.single.placeholder.Placeholder
33 | import ru.tech.firenote.ui.composable.single.toast.sendToast
34 | import ru.tech.firenote.ui.state.UIState
35 | import ru.tech.firenote.ui.theme.priorityGoal
36 | import ru.tech.firenote.viewModel.navigation.GoalListViewModel
37 |
38 | @Suppress("UNCHECKED_CAST")
39 | @Composable
40 | fun GoalListScreen(
41 | showGoalCreation: MutableTransitionState,
42 | globalGoal: MutableState = mutableStateOf(null),
43 | filterType: MutableState,
44 | isDescendingFilter: MutableState,
45 | searchString: MutableState,
46 | viewModel: GoalListViewModel = hiltViewModel()
47 | ) {
48 | val paddingValues = PaddingValues(top = 10.dp, start = 10.dp, end = 10.dp, bottom = 140.dp)
49 | val needToShowDeleteDialog = remember { mutableStateOf(false) }
50 | var goal by remember { mutableStateOf(Goal()) }
51 | val scope = rememberCoroutineScope()
52 | val host = LocalSnackbarHost.current
53 |
54 | val message = stringResource(R.string.goalDeleted)
55 | val action = stringResource(R.string.undo)
56 |
57 | when (val state = viewModel.uiState.collectAsState().value) {
58 | is UIState.Loading -> {
59 | Column(
60 | modifier = Modifier
61 | .fillMaxSize(),
62 | verticalArrangement = Arrangement.Center,
63 | horizontalAlignment = Alignment.CenterHorizontally
64 | ) {
65 | CircularProgressIndicator()
66 | }
67 | }
68 | is UIState.Success<*> -> {
69 | val repoList = state.data as List
70 | var data = if (isDescendingFilter.value) {
71 | when (filterType.value) {
72 | 1 -> repoList.sortedBy { (it.color ?: 0).priorityGoal }
73 | 3 -> repoList.sortedBy { it.timestamp }
74 | 2 -> repoList.sortedByDescending {
75 | var cnt = 0f
76 | it.content?.forEach { data ->
77 | if (data.done == true) cnt++
78 | }
79 | cnt / (it.content?.size ?: 1)
80 | }
81 | else -> repoList.sortedBy { it.title }
82 | }
83 | } else {
84 | when (filterType.value) {
85 | 1 -> repoList.sortedByDescending { (it.color ?: 0).priorityGoal }
86 | 3 -> repoList.sortedByDescending { it.timestamp }
87 | 2 -> repoList.sortedBy {
88 | var cnt = 0f
89 | it.content?.forEach { data ->
90 | if (data.done == true) cnt++
91 | }
92 | cnt / (it.content?.size ?: 1)
93 | }
94 | else -> repoList.sortedByDescending { it.title }
95 | }
96 | }
97 |
98 | if (searchString.value.isNotEmpty()) {
99 | data = repoList.filter {
100 | val statement1 =
101 | it.title?.lowercase()?.contains(searchString.value) ?: false
102 | var statement2 = false
103 | it.content?.forEach { data ->
104 | if (data.content?.lowercase()
105 | ?.contains(searchString.value) == true
106 | ) statement2 = true
107 | }
108 |
109 | statement1 or statement2
110 | }
111 | if (data.isEmpty()) {
112 | Placeholder(icon = Icons.TwoTone.FindInPage, textRes = R.string.nothingFound)
113 | }
114 | }
115 |
116 | LazyColumn(
117 | state = LocalLazyListStateProvider.current,
118 | verticalArrangement = Arrangement.spacedBy(8.dp),
119 | contentPadding = paddingValues
120 | ) {
121 | items(data.size) { index ->
122 | val locGoal = data[index]
123 | GoalItem(
124 | goal = locGoal,
125 | onDeleteClick = {
126 | goal = locGoal
127 | needToShowDeleteDialog.value = true
128 | },
129 | modifier = Modifier
130 | .clickable(remember { MutableInteractionSource() }, null) {
131 | globalGoal.value = locGoal
132 | showGoalCreation.targetState = true
133 | }
134 | )
135 | }
136 | }
137 | }
138 | is UIState.Empty -> {
139 | state.message?.let {
140 | LocalToastHost.current.sendToast(Icons.Outlined.Error, it)
141 | }
142 | Placeholder(icon = Icons.TwoTone.Cloud, textRes = R.string.noGoals)
143 | }
144 | }
145 |
146 | MaterialDialog(
147 | showDialog = needToShowDeleteDialog,
148 | icon = Icons.Outlined.Delete,
149 | title = R.string.deleteGoal,
150 | message = R.string.deleteGoalMessage,
151 | confirmText = R.string.close,
152 | dismissText = R.string.delete,
153 | dismissAction = {
154 | viewModel.deleteGoal(goal) { goal1 ->
155 | var temp = goal1.title.toString().take(30)
156 | if (goal1.title.toString().length > 30) temp += "..."
157 | val messageNew = message.replace("*", temp)
158 |
159 | showSnackbar(
160 | scope,
161 | host,
162 | messageNew,
163 | action
164 | ) {
165 | if (it == SnackbarResult.ActionPerformed) {
166 | viewModel.insertGoal(goal1)
167 | }
168 | }
169 | }
170 | },
171 | backHandler = { }
172 | )
173 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/repository/impl/NoteRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.repository.impl
2 |
3 | import android.net.Uri
4 | import com.google.firebase.auth.ktx.auth
5 | import com.google.firebase.database.DataSnapshot
6 | import com.google.firebase.database.DatabaseError
7 | import com.google.firebase.database.DatabaseReference
8 | import com.google.firebase.database.ValueEventListener
9 | import com.google.firebase.ktx.Firebase
10 | import com.google.firebase.storage.StorageReference
11 | import dagger.hilt.android.scopes.ActivityScoped
12 | import kotlinx.coroutines.channels.awaitClose
13 | import kotlinx.coroutines.channels.trySendBlocking
14 | import kotlinx.coroutines.flow.Flow
15 | import kotlinx.coroutines.flow.callbackFlow
16 | import ru.tech.firenote.model.*
17 | import ru.tech.firenote.repository.NoteRepository
18 | import javax.inject.Inject
19 |
20 |
21 | @ActivityScoped
22 | class NoteRepositoryImpl @Inject constructor(
23 | private val database: DatabaseReference,
24 | private val storage: StorageReference
25 | ) : NoteRepository {
26 |
27 | override val auth get() = Firebase.auth
28 |
29 | private val path get() = Firebase.auth.uid.toString() + "/"
30 | private val notesChild = "notes/"
31 | private val imageChild = "image/"
32 | private val usernameChild = "username/"
33 | private val goalsChild = "goals/"
34 | private val typesChild = "types/"
35 |
36 | override suspend fun getNotes(): Flow>> {
37 | return callbackFlow {
38 | val postListener = object : ValueEventListener {
39 | override fun onCancelled(error: DatabaseError) {
40 | this@callbackFlow.trySendBlocking(Result.failure(error.toException()))
41 | }
42 |
43 | override fun onDataChange(dataSnapshot: DataSnapshot) {
44 | val items = dataSnapshot.children.map { ds ->
45 | ds.getValue(Note::class.java)
46 | }
47 | this@callbackFlow.trySendBlocking(Result.success(items.filterNotNull()))
48 | }
49 | }
50 | database.child(path).child(notesChild).addValueEventListener(postListener)
51 |
52 | awaitClose {
53 | database.child(path).child(notesChild).removeEventListener(postListener)
54 | }
55 | }
56 | }
57 |
58 | override suspend fun insertNote(note: Note) {
59 | if (note.id == null) {
60 | val id = database.child(path).child(notesChild).push().key
61 | note.id = id
62 | }
63 | database.child(path).child(notesChild + note.id).setValue(note)
64 | }
65 |
66 | override suspend fun deleteNote(note: Note) {
67 | note.id?.let { database.child(path).child(notesChild + it).removeValue() }
68 | }
69 |
70 | override suspend fun getProfileUri(): Flow> {
71 | return callbackFlow {
72 | val postListener = object : ValueEventListener {
73 | override fun onCancelled(error: DatabaseError) {
74 | this@callbackFlow.trySendBlocking(Result.failure(error.toException()))
75 | }
76 |
77 | override fun onDataChange(dataSnapshot: DataSnapshot) {
78 | val imageUri = dataSnapshot.getValue(ImageUri::class.java)?.uri
79 | val uri = if (imageUri == null) null else Uri.parse(imageUri)
80 | this@callbackFlow.trySendBlocking(Result.success(uri))
81 | }
82 | }
83 | database.child(path).child(imageChild).addValueEventListener(postListener)
84 |
85 | awaitClose {
86 | database.child(path).child(imageChild).removeEventListener(postListener)
87 | }
88 | }
89 | }
90 |
91 | override suspend fun setProfileUri(uri: Uri) {
92 | storage.child(path).child(imageChild).child("profileImage").putFile(uri)
93 | .addOnSuccessListener { taskSnapshot ->
94 | taskSnapshot.metadata?.reference?.downloadUrl?.addOnSuccessListener { uri ->
95 | database.child(path).child(imageChild).setValue(ImageUri(uri.toString()))
96 | }
97 | }
98 | }
99 |
100 | override suspend fun getUsername(): Flow> {
101 | return callbackFlow {
102 | val postListener = object : ValueEventListener {
103 | override fun onCancelled(error: DatabaseError) {
104 | this@callbackFlow.trySendBlocking(Result.failure(error.toException()))
105 | }
106 |
107 | override fun onDataChange(dataSnapshot: DataSnapshot) {
108 | val tempUsername =
109 | dataSnapshot.getValue(Username::class.java)?.username
110 | ?: (auth.currentUser?.email ?: "").split("@")[0]
111 | this@callbackFlow.trySendBlocking(Result.success(tempUsername))
112 | }
113 | }
114 | database.child(path).child(usernameChild).addValueEventListener(postListener)
115 |
116 | awaitClose {
117 | database.child(path).child(usernameChild).removeEventListener(postListener)
118 | }
119 | }
120 | }
121 |
122 | override suspend fun setUsername(username: String) {
123 | database.child(path).child(usernameChild).setValue(Username(username))
124 | }
125 |
126 | override suspend fun getGoals(): Flow>> {
127 | return callbackFlow {
128 | val postListener = object : ValueEventListener {
129 | override fun onCancelled(error: DatabaseError) {
130 | this@callbackFlow.trySendBlocking(Result.failure(error.toException()))
131 | }
132 |
133 | override fun onDataChange(dataSnapshot: DataSnapshot) {
134 | val items = dataSnapshot.children.map { ds ->
135 | ds.getValue(Goal::class.java)
136 | }
137 | this@callbackFlow.trySendBlocking(Result.success(items.filterNotNull()))
138 | }
139 | }
140 | database.child(path).child(goalsChild).addValueEventListener(postListener)
141 |
142 | awaitClose {
143 | database.child(path).child(goalsChild).removeEventListener(postListener)
144 | }
145 | }
146 | }
147 |
148 | override suspend fun insertGoal(goal: Goal) {
149 | if (goal.id == null) {
150 | val id = database.child(path).child(goalsChild).push().key
151 | goal.id = id
152 | }
153 | database.child(path).child(goalsChild + goal.id).setValue(goal)
154 | }
155 |
156 | override suspend fun deleteGoal(goal: Goal) {
157 | goal.id?.let { database.child(path).child(goalsChild + it).removeValue() }
158 | }
159 |
160 | override suspend fun updateType(color: Int, type: String) {
161 | database.child(path).child(typesChild + color).setValue(Type(color, type))
162 | }
163 |
164 | override suspend fun getTypes(): Flow>> {
165 | return callbackFlow {
166 | val postListener = object : ValueEventListener {
167 | override fun onCancelled(error: DatabaseError) {
168 | this@callbackFlow.trySendBlocking(Result.failure(error.toException()))
169 | }
170 |
171 | override fun onDataChange(dataSnapshot: DataSnapshot) {
172 | val items = dataSnapshot.children.map { ds ->
173 | ds.getValue(Type::class.java)
174 | }
175 | this@callbackFlow.trySendBlocking(Result.success(items.filterNotNull()))
176 | }
177 | }
178 | database.child(path).child(typesChild).addValueEventListener(postListener)
179 |
180 | awaitClose {
181 | database.child(path).child(typesChild).removeEventListener(postListener)
182 | }
183 | }
184 | }
185 |
186 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/single/bar/AppBarActions.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.single.bar
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.shape.CircleShape
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.CalendarToday
8 | import androidx.compose.material.icons.filled.CheckCircle
9 | import androidx.compose.material.icons.filled.Palette
10 | import androidx.compose.material.icons.filled.TextSnippet
11 | import androidx.compose.material.icons.outlined.*
12 | import androidx.compose.material3.*
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.runtime.saveable.rememberSaveable
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.platform.LocalContext
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.unit.dp
21 | import ru.tech.firenote.R
22 | import ru.tech.firenote.ui.composable.provider.LocalToastHost
23 | import ru.tech.firenote.ui.composable.single.dialog.MaterialDialog
24 | import ru.tech.firenote.ui.composable.single.toast.sendToast
25 | import ru.tech.firenote.utils.GlobalUtils.isOnline
26 | import ru.tech.firenote.viewModel.main.MainViewModel
27 |
28 | @Composable
29 | fun NoteActions(
30 | viewModel: MainViewModel
31 | ) {
32 | val selectedModifier =
33 | Modifier
34 | .padding(5.dp)
35 | .background(MaterialTheme.colorScheme.primaryContainer, CircleShape)
36 | val unselectedModifier = Modifier.padding(5.dp)
37 | val showFilter = remember { mutableStateOf(false) }
38 |
39 | IconButton(onClick = {
40 | viewModel.isDescendingFilter.value = !viewModel.isDescendingFilter.value
41 | }) {
42 | Icon(
43 | if (viewModel.isDescendingFilter.value) Icons.Outlined.ArrowDropDown else Icons.Outlined.ArrowDropUp,
44 | "filter"
45 | )
46 | }
47 | IconButton(onClick = { showFilter.value = true }) {
48 | Icon(Icons.Outlined.FilterAlt, null)
49 | }
50 |
51 | DropdownMenu(
52 | expanded = showFilter.value,
53 | onDismissRequest = { showFilter.value = false }
54 | ) {
55 | DropdownMenuItem(
56 | onClick = {
57 | viewModel.filterType.value = 0
58 | showFilter.value = false
59 | },
60 | text = { Text(stringResource(R.string.title)) },
61 | leadingIcon = {
62 | Icon(
63 | if (viewModel.filterType.value == 0) Icons.Filled.TextSnippet else Icons.Outlined.TextSnippet,
64 | null
65 | )
66 | },
67 | modifier = if (viewModel.filterType.value == 0) selectedModifier else unselectedModifier
68 | )
69 | DropdownMenuItem(
70 | onClick = {
71 | viewModel.filterType.value = 1
72 | showFilter.value = false
73 | },
74 | text = { Text(stringResource(R.string.color)) },
75 | leadingIcon = {
76 | Icon(
77 | if (viewModel.filterType.value == 1) Icons.Filled.Palette else Icons.Outlined.Palette,
78 | null
79 | )
80 | },
81 | modifier = if (viewModel.filterType.value == 1) selectedModifier else unselectedModifier
82 | )
83 | DropdownMenuItem(
84 | onClick = {
85 | viewModel.filterType.value = 2
86 | showFilter.value = false
87 | },
88 | text = { Text(stringResource(R.string.date)) },
89 | leadingIcon = {
90 | Icon(
91 | if (viewModel.filterType.value in 2..3) Icons.Filled.CalendarToday else Icons.Outlined.CalendarToday,
92 | null
93 | )
94 | },
95 | modifier = if (viewModel.filterType.value in 2..3) selectedModifier else unselectedModifier
96 | )
97 | }
98 | }
99 |
100 | @Composable
101 | fun GoalActions(viewModel: MainViewModel) {
102 | val selectedModifier =
103 | Modifier
104 | .padding(5.dp)
105 | .background(MaterialTheme.colorScheme.primaryContainer, CircleShape)
106 | val unselectedModifier = Modifier.padding(5.dp)
107 | val showFilter = remember { mutableStateOf(false) }
108 |
109 | IconButton(onClick = {
110 | viewModel.isDescendingFilter.value = !viewModel.isDescendingFilter.value
111 | }) {
112 | Icon(
113 | if (viewModel.isDescendingFilter.value) Icons.Outlined.ArrowDropDown else Icons.Outlined.ArrowDropUp,
114 | null
115 | )
116 | }
117 | IconButton(onClick = { showFilter.value = true }) {
118 | Icon(Icons.Outlined.FilterAlt, null)
119 | }
120 |
121 | DropdownMenu(
122 | expanded = showFilter.value,
123 | onDismissRequest = { showFilter.value = false }
124 | ) {
125 | DropdownMenuItem(
126 | onClick = {
127 | viewModel.filterType.value = 0
128 | showFilter.value = false
129 | },
130 | text = { Text(stringResource(R.string.title)) },
131 | leadingIcon = {
132 | Icon(
133 | if (viewModel.filterType.value == 0) Icons.Filled.TextSnippet else Icons.Outlined.TextSnippet,
134 | null
135 | )
136 | },
137 | modifier = if (viewModel.filterType.value == 0) selectedModifier else unselectedModifier
138 | )
139 | DropdownMenuItem(
140 | onClick = {
141 | viewModel.filterType.value = 1
142 | showFilter.value = false
143 | },
144 | text = { Text(stringResource(R.string.color)) },
145 | leadingIcon = {
146 | Icon(
147 | if (viewModel.filterType.value == 1) Icons.Filled.Palette else Icons.Outlined.Palette,
148 | null
149 | )
150 | },
151 | modifier = if (viewModel.filterType.value == 1) selectedModifier else unselectedModifier
152 | )
153 | DropdownMenuItem(
154 | onClick = {
155 | viewModel.filterType.value = 3
156 | showFilter.value = false
157 | },
158 | text = { Text(stringResource(R.string.date)) },
159 | leadingIcon = {
160 | Icon(
161 | if (viewModel.filterType.value == 3) Icons.Filled.CalendarToday else Icons.Outlined.CalendarToday,
162 | null
163 | )
164 | },
165 | modifier = if (viewModel.filterType.value == 3) selectedModifier else unselectedModifier
166 | )
167 | DropdownMenuItem(
168 | onClick = {
169 | viewModel.filterType.value = 2
170 | showFilter.value = false
171 | },
172 | text = { Text(stringResource(R.string.completion)) },
173 | leadingIcon = {
174 | Icon(
175 | if (viewModel.filterType.value == 2) Icons.Filled.CheckCircle else Icons.Outlined.CheckCircle,
176 | null
177 | )
178 | },
179 | modifier = if (viewModel.filterType.value == 2) selectedModifier else unselectedModifier
180 | )
181 | }
182 | }
183 |
184 | @Composable
185 | fun ProfileActions(onClick: () -> Unit) {
186 | val showDialog = rememberSaveable { mutableStateOf(false) }
187 | val context = LocalContext.current
188 | val toastHost = LocalToastHost.current
189 | val txt = stringResource(R.string.noInternet)
190 |
191 | IconButton(onClick = {
192 | if (context.isOnline()) showDialog.value = true
193 | else toastHost.sendToast(Icons.Outlined.SignalWifiOff, txt)
194 | }) {
195 | Icon(Icons.Outlined.Logout, null)
196 | }
197 |
198 | MaterialDialog(
199 | showDialog = showDialog,
200 | icon = Icons.Outlined.Logout,
201 | title = R.string.logOut,
202 | message = R.string.logoutMessage,
203 | confirmText = R.string.stay,
204 | dismissText = R.string.logOut,
205 | dismissAction = onClick,
206 | backHandler = {}
207 | )
208 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/screen/auth/LoginScreen.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.screen.auth
2 |
3 | import android.util.Patterns
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.lazy.LazyColumn
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.foundation.text.KeyboardActions
8 | import androidx.compose.foundation.text.KeyboardOptions
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.filled.Clear
11 | import androidx.compose.material.icons.filled.Visibility
12 | import androidx.compose.material.icons.filled.VisibilityOff
13 | import androidx.compose.material.icons.outlined.DoneOutline
14 | import androidx.compose.material.icons.outlined.Error
15 | import androidx.compose.material.icons.outlined.HelpOutline
16 | import androidx.compose.material3.*
17 | import androidx.compose.runtime.*
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.platform.LocalFocusManager
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.text.input.ImeAction
25 | import androidx.compose.ui.text.input.KeyboardType
26 | import androidx.compose.ui.text.input.PasswordVisualTransformation
27 | import androidx.compose.ui.text.input.VisualTransformation
28 | import androidx.compose.ui.text.style.TextAlign
29 | import androidx.compose.ui.unit.dp
30 | import androidx.compose.ui.unit.sp
31 | import ru.tech.firenote.R
32 | import ru.tech.firenote.ui.composable.provider.LocalToastHost
33 | import ru.tech.firenote.ui.composable.single.text.MaterialTextField
34 | import ru.tech.firenote.ui.composable.single.toast.sendToast
35 | import ru.tech.firenote.ui.route.Screen
36 | import ru.tech.firenote.ui.state.UIState
37 | import ru.tech.firenote.viewModel.auth.AuthViewModel
38 |
39 |
40 | @ExperimentalMaterial3Api
41 | @Composable
42 | fun LoginScreen(viewModel: AuthViewModel) {
43 | var email by remember {
44 | mutableStateOf("")
45 | }
46 | var password by remember {
47 | mutableStateOf("")
48 | }
49 | var isPasswordVisible by remember {
50 | mutableStateOf(false)
51 | }
52 | val isFormValid by derivedStateOf {
53 | email.isValid() && password.isNotEmpty()
54 | }
55 |
56 | val emailError by derivedStateOf {
57 | !email.isValid() && email.isNotEmpty()
58 | }
59 |
60 | val focusManager = LocalFocusManager.current
61 |
62 | LazyColumn(
63 | Modifier
64 | .fillMaxSize(),
65 | verticalArrangement = Arrangement.Bottom
66 | ) {
67 | item {
68 | Text(
69 | text = stringResource(R.string.welcomeBack),
70 | fontWeight = FontWeight.Bold,
71 | fontSize = 32.sp,
72 | textAlign = TextAlign.Center,
73 | modifier = Modifier
74 | .fillMaxWidth()
75 | .padding(32.dp)
76 | )
77 |
78 | when (val state = viewModel.logUiState.collectAsState().value) {
79 | is UIState.Loading ->
80 | Column(
81 | modifier = Modifier
82 | .fillMaxSize()
83 | .padding(60.dp),
84 | verticalArrangement = Arrangement.Center,
85 | horizontalAlignment = Alignment.CenterHorizontally
86 | ) {
87 | CircularProgressIndicator()
88 | }
89 | is UIState.Success<*> -> {
90 |
91 | LocalToastHost.current.sendToast(
92 | Icons.Outlined.DoneOutline,
93 | stringResource(R.string.niceToSeeYou)
94 | )
95 |
96 | Column(
97 | modifier = Modifier
98 | .fillMaxSize()
99 | .padding(60.dp),
100 | verticalArrangement = Arrangement.Center,
101 | horizontalAlignment = Alignment.CenterHorizontally
102 | ) {
103 | CircularProgressIndicator()
104 | }
105 | }
106 | is UIState.Empty -> {
107 | if (state.message == "verification") {
108 | LocalToastHost.current.sendToast(
109 | Icons.Outlined.HelpOutline,
110 | stringResource(R.string.notVerified)
111 | )
112 | viewModel.resetState()
113 | } else {
114 | state.message?.let {
115 | LocalToastHost.current.sendToast(Icons.Outlined.Error, it)
116 | viewModel.resetState()
117 | }
118 | }
119 |
120 | Column(
121 | Modifier
122 | .fillMaxSize()
123 | .padding(32.dp),
124 | horizontalAlignment = Alignment.CenterHorizontally,
125 | verticalArrangement = Arrangement.Center
126 | ) {
127 | MaterialTextField(
128 | modifier = Modifier.fillMaxWidth(),
129 | value = email,
130 | onValueChange = { email = it },
131 | label = { Text(text = stringResource(R.string.email)) },
132 | singleLine = true,
133 | isError = emailError,
134 | errorText = stringResource(R.string.emailIsNotValid),
135 | keyboardOptions = KeyboardOptions(
136 | keyboardType = KeyboardType.Email,
137 | imeAction = ImeAction.Next
138 | ),
139 | trailingIcon = {
140 | if (email.isNotBlank())
141 | IconButton(onClick = { email = "" }) {
142 | Icon(Icons.Filled.Clear, null)
143 | }
144 | }
145 | )
146 | Spacer(modifier = Modifier.height(8.dp))
147 | MaterialTextField(
148 | modifier = Modifier.fillMaxWidth(),
149 | value = password,
150 | onValueChange = { password = it },
151 | label = { Text(text = stringResource(R.string.password)) },
152 | singleLine = true,
153 | keyboardOptions = KeyboardOptions(
154 | keyboardType = KeyboardType.Password,
155 | imeAction = ImeAction.Done
156 | ),
157 | keyboardActions = KeyboardActions(onDone = {
158 | focusManager.clearFocus()
159 | if (isFormValid) viewModel.logInWith(email, password)
160 | }),
161 | visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
162 | trailingIcon = {
163 | IconButton(onClick = {
164 | isPasswordVisible = !isPasswordVisible
165 | }) {
166 | Icon(
167 | if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
168 | null
169 | )
170 | }
171 | }
172 | )
173 | }
174 | }
175 | }
176 | Button(
177 | onClick = { viewModel.logInWith(email, password) },
178 | enabled = isFormValid,
179 | modifier = Modifier
180 | .fillMaxWidth()
181 | .padding(32.dp),
182 | shape = RoundedCornerShape(16.dp)
183 | ) {
184 | Text(text = stringResource(R.string.logIn))
185 | }
186 | Spacer(modifier = Modifier.height(32.dp))
187 | Row(
188 | modifier = Modifier
189 | .fillMaxWidth()
190 | .padding(32.dp),
191 | horizontalArrangement = Arrangement.SpaceBetween
192 | ) {
193 | TextButton(onClick = {
194 | viewModel.goTo(Screen.RegistrationScreen)
195 | }) {
196 | Text(text = stringResource(R.string.signUp))
197 | }
198 | TextButton(onClick = {
199 | viewModel.goTo(Screen.ForgotPasswordScreen)
200 | }) {
201 | Text(
202 | text = stringResource(R.string.forgotPassword),
203 | color = Color.Gray
204 | )
205 | }
206 | }
207 | }
208 | }
209 |
210 | }
211 |
212 | fun String.isValid(): Boolean {
213 | return Patterns.EMAIL_ADDRESS.matcher(this).matches()
214 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/screen/auth/RegistrationScreen.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.screen.auth
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.foundation.text.KeyboardActions
7 | import androidx.compose.foundation.text.KeyboardOptions
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Clear
10 | import androidx.compose.material.icons.filled.Visibility
11 | import androidx.compose.material.icons.filled.VisibilityOff
12 | import androidx.compose.material.icons.outlined.AlternateEmail
13 | import androidx.compose.material.icons.outlined.Error
14 | import androidx.compose.material3.*
15 | import androidx.compose.runtime.*
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.platform.LocalFocusManager
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.text.input.ImeAction
22 | import androidx.compose.ui.text.input.KeyboardType
23 | import androidx.compose.ui.text.input.PasswordVisualTransformation
24 | import androidx.compose.ui.text.input.VisualTransformation
25 | import androidx.compose.ui.text.style.TextAlign
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import ru.tech.firenote.R
29 | import ru.tech.firenote.ui.composable.provider.LocalToastHost
30 | import ru.tech.firenote.ui.composable.single.text.MaterialTextField
31 | import ru.tech.firenote.ui.composable.single.toast.sendToast
32 | import ru.tech.firenote.ui.route.Screen
33 | import ru.tech.firenote.ui.state.UIState
34 | import ru.tech.firenote.viewModel.auth.AuthViewModel
35 |
36 |
37 | @ExperimentalMaterial3Api
38 | @Composable
39 | fun RegistrationScreen(viewModel: AuthViewModel) {
40 | var email by remember {
41 | mutableStateOf("")
42 | }
43 | var password by remember {
44 | mutableStateOf("")
45 | }
46 | var passwordConfirm by remember {
47 | mutableStateOf("")
48 | }
49 | var isPasswordVisible by remember {
50 | mutableStateOf(false)
51 | }
52 | val isFormValid by derivedStateOf {
53 | email.isValid() && password.length >= 7 && passwordConfirm == password
54 | }
55 |
56 | val emailError by derivedStateOf {
57 | !email.isValid() && email.isNotEmpty()
58 | }
59 | val passwordError by derivedStateOf {
60 | password.length in 1..6
61 | }
62 | val confirmPasswordError by derivedStateOf {
63 | password != passwordConfirm
64 | }
65 |
66 | val focusManager = LocalFocusManager.current
67 |
68 | LazyColumn(
69 | Modifier
70 | .fillMaxSize(),
71 | verticalArrangement = Arrangement.Bottom
72 | ) {
73 | item {
74 | Text(
75 | text = stringResource(R.string.registration),
76 | fontWeight = FontWeight.Bold,
77 | fontSize = 32.sp,
78 | textAlign = TextAlign.Center,
79 | modifier = Modifier
80 | .fillMaxWidth()
81 | .padding(32.dp)
82 | )
83 |
84 | when (val state = viewModel.signUiState.collectAsState().value) {
85 | is UIState.Loading ->
86 | Column(
87 | modifier = Modifier
88 | .fillMaxSize()
89 | .padding(60.dp),
90 | verticalArrangement = Arrangement.Center,
91 | horizontalAlignment = Alignment.CenterHorizontally
92 | ) {
93 | CircularProgressIndicator()
94 | }
95 | is UIState.Success<*> -> {
96 | viewModel.goTo(Screen.LoginScreen)
97 | LocalToastHost.current.sendToast(
98 | Icons.Outlined.AlternateEmail,
99 | stringResource(R.string.emailToVerify)
100 | )
101 | viewModel.resetState()
102 | }
103 | is UIState.Empty -> {
104 | state.message?.let {
105 | LocalToastHost.current.sendToast(Icons.Outlined.Error, it)
106 | viewModel.resetState()
107 | }
108 | Column(
109 | Modifier
110 | .fillMaxSize()
111 | .padding(32.dp),
112 | horizontalAlignment = Alignment.CenterHorizontally,
113 | verticalArrangement = Arrangement.Center
114 | ) {
115 | MaterialTextField(
116 | modifier = Modifier.fillMaxWidth(),
117 | value = email,
118 | onValueChange = { email = it },
119 | label = { Text(text = stringResource(R.string.email)) },
120 | singleLine = true,
121 | isError = emailError,
122 | errorText = stringResource(R.string.emailIsNotValid),
123 | keyboardOptions = KeyboardOptions(
124 | keyboardType = KeyboardType.Email,
125 | imeAction = ImeAction.Next
126 | ),
127 | trailingIcon = {
128 | if (email.isNotBlank())
129 | IconButton(onClick = { email = "" }) {
130 | Icon(Icons.Filled.Clear, null)
131 | }
132 | }
133 | )
134 | Spacer(modifier = Modifier.height(8.dp))
135 | MaterialTextField(
136 | modifier = Modifier.fillMaxWidth(),
137 | value = password,
138 | onValueChange = { password = it },
139 | label = { Text(text = stringResource(R.string.password)) },
140 | singleLine = true,
141 | isError = passwordError,
142 | errorText = stringResource(R.string.passwordTooShort),
143 | keyboardOptions = KeyboardOptions(
144 | keyboardType = KeyboardType.Password,
145 | imeAction = ImeAction.Next
146 | ),
147 | visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
148 | trailingIcon = {
149 | IconButton(onClick = {
150 | isPasswordVisible = !isPasswordVisible
151 | }) {
152 | Icon(
153 | if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
154 | null
155 | )
156 | }
157 | }
158 | )
159 | Spacer(modifier = Modifier.height(8.dp))
160 | MaterialTextField(
161 | modifier = Modifier.fillMaxWidth(),
162 | value = passwordConfirm,
163 | onValueChange = { passwordConfirm = it },
164 | label = { Text(text = stringResource(R.string.passwordConfirm)) },
165 | singleLine = true,
166 | isError = confirmPasswordError,
167 | errorText = stringResource(R.string.passwordsDifferent),
168 | keyboardOptions = KeyboardOptions(
169 | keyboardType = KeyboardType.Password,
170 | imeAction = ImeAction.Done
171 | ),
172 | keyboardActions = KeyboardActions(onDone = {
173 | focusManager.clearFocus()
174 | if (isFormValid) viewModel.signInWith(email, password)
175 | }),
176 | visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
177 | trailingIcon = {
178 | IconButton(onClick = {
179 | isPasswordVisible = !isPasswordVisible
180 | }) {
181 | Icon(
182 | if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
183 | null
184 | )
185 | }
186 | }
187 | )
188 | }
189 | }
190 | }
191 |
192 | Button(
193 | onClick = { viewModel.signInWith(email, password) },
194 | enabled = isFormValid,
195 | modifier = Modifier
196 | .fillMaxWidth()
197 | .padding(32.dp),
198 | shape = RoundedCornerShape(16.dp)
199 | ) {
200 | Text(text = stringResource(R.string.signUp))
201 | }
202 | Spacer(modifier = Modifier.height(32.dp))
203 | Row(
204 | modifier = Modifier
205 | .fillMaxWidth()
206 | .padding(32.dp),
207 | horizontalArrangement = Arrangement.Start
208 | ) {
209 | TextButton(onClick = {
210 | viewModel.goTo(Screen.LoginScreen)
211 | }) {
212 | Text(text = stringResource(R.string.logIn))
213 | }
214 | }
215 | }
216 | }
217 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/tech/firenote/ui/composable/single/scaffold/FirenoteScaffold.kt:
--------------------------------------------------------------------------------
1 | package ru.tech.firenote.ui.composable.single.scaffold
2 |
3 | import android.content.Context
4 | import androidx.compose.animation.*
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.outlined.*
11 | import androidx.compose.material.icons.rounded.ArrowBack
12 | import androidx.compose.material.icons.rounded.Close
13 | import androidx.compose.material.icons.rounded.Search
14 | import androidx.compose.material3.*
15 | import androidx.compose.runtime.*
16 | import androidx.compose.runtime.saveable.rememberSaveable
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.input.nestedscroll.nestedScroll
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.text.style.TextAlign
21 | import androidx.compose.ui.text.style.TextOverflow
22 | import androidx.compose.ui.unit.dp
23 | import androidx.navigation.NavHostController
24 | import ru.tech.firenote.R
25 | import ru.tech.firenote.ui.composable.navigation.Navigation
26 | import ru.tech.firenote.ui.composable.provider.LocalLazyListStateProvider
27 | import ru.tech.firenote.ui.composable.provider.LocalSnackbarHost
28 | import ru.tech.firenote.ui.composable.provider.LocalToastHost
29 | import ru.tech.firenote.ui.composable.single.ExtendableFloatingActionButton
30 | import ru.tech.firenote.ui.composable.single.bar.*
31 | import ru.tech.firenote.ui.composable.single.toast.sendToast
32 | import ru.tech.firenote.ui.route.Screen
33 | import ru.tech.firenote.utils.GlobalUtils.isOnline
34 | import ru.tech.firenote.viewModel.main.MainViewModel
35 |
36 | @ExperimentalFoundationApi
37 | @ExperimentalAnimationApi
38 | @ExperimentalMaterial3Api
39 | @Composable
40 | fun FirenoteScaffold(
41 | modifier: Modifier = Modifier,
42 | viewModel: MainViewModel,
43 | navController: NavHostController,
44 | context: Context
45 | ) {
46 | Scaffold(
47 | topBar = {
48 | AppBarWithInsets(
49 | type = APP_BAR_CENTER,
50 | navigationIcon = {
51 |
52 | AnimatedVisibility(
53 | viewModel.selectedItem.value in 0..1 && !viewModel.searchMode.value,
54 | enter = fadeIn() + scaleIn(),
55 | exit = fadeOut() + scaleOut()
56 | ) {
57 | IconButton(onClick = {
58 | viewModel.dispatchSearch()
59 | }) {
60 | Icon(
61 | Icons.Rounded.Search,
62 | null
63 | )
64 | }
65 | }
66 |
67 | AnimatedVisibility(
68 | viewModel.selectedItem.value in 0..1 && viewModel.searchMode.value,
69 | enter = fadeIn() + scaleIn(),
70 | exit = fadeOut() + scaleOut()
71 | ) {
72 | IconButton(onClick = {
73 | viewModel.dispatchSearch()
74 | }) {
75 | Icon(
76 | Icons.Rounded.ArrowBack,
77 | null
78 | )
79 | }
80 | }
81 |
82 | AnimatedVisibility(
83 | viewModel.selectedItem.value == 2,
84 | enter = fadeIn() + scaleIn(),
85 | exit = fadeOut() + scaleOut()
86 | )
87 | {
88 | val toastHost = LocalToastHost.current
89 | val txt = stringResource(R.string.noInternet)
90 | Row {
91 | IconButton(onClick = {
92 | if (context.isOnline()) viewModel.resultLauncher.value?.launch("image/*")
93 | else toastHost.sendToast(Icons.Outlined.SignalWifiOff, txt)
94 | }) {
95 | Icon(
96 | Icons.Outlined.AddPhotoAlternate,
97 | null
98 | )
99 | }
100 |
101 | IconButton(onClick = {
102 | viewModel.showUsernameDialog.value = true
103 | }) {
104 | Icon(
105 | Icons.Outlined.Edit,
106 | null
107 | )
108 | }
109 | }
110 | }
111 | },
112 | scrollBehavior = viewModel.scrollBehavior.value,
113 | title = {
114 | AnimatedVisibility(
115 | !viewModel.searchMode.value,
116 | enter = fadeIn() + scaleIn(),
117 | exit = fadeOut() + scaleOut()
118 | ) {
119 | Text(
120 | modifier = Modifier
121 | .fillMaxWidth()
122 | .padding(horizontal = 15.dp),
123 | text = if (viewModel.selectedItem.value == 2) viewModel.profileTitle.value
124 | else stringResource(viewModel.title.value),
125 | maxLines = 1,
126 | textAlign = TextAlign.Center,
127 | overflow = TextOverflow.Ellipsis
128 | )
129 | }
130 |
131 | AnimatedVisibility(
132 | viewModel.searchMode.value,
133 | enter = fadeIn() + scaleIn(),
134 | exit = fadeOut() + scaleOut()
135 | ) {
136 | SearchBar(searchString = viewModel.searchString.value) {
137 | viewModel.updateSearch(it)
138 | }
139 | }
140 | },
141 | actions = {
142 | if (!viewModel.searchMode.value) {
143 | val toastHost = LocalToastHost.current
144 | val txt = stringResource(R.string.seeYouAgain)
145 |
146 | when (viewModel.selectedItem.value) {
147 | 0 -> NoteActions(viewModel)
148 | 1 -> GoalActions(viewModel)
149 | 2 -> ProfileActions {
150 | navController.navigate(Screen.NoteListScreen.route) {
151 | navController.popBackStack()
152 | launchSingleTop = true
153 | }
154 |
155 | toastHost.sendToast(Icons.Outlined.TagFaces, txt)
156 |
157 | viewModel.signOut()
158 | }
159 | }
160 | } else {
161 | IconButton(onClick = {
162 | viewModel.updateSearch()
163 | }) {
164 | Icon(Icons.Rounded.Close, null)
165 | }
166 | }
167 | }
168 | )
169 | },
170 | floatingActionButton = {
171 |
172 | val lazyListState = LocalLazyListStateProvider.current
173 | var fabExtended by rememberSaveable { mutableStateOf(false) }
174 |
175 | LaunchedEffect(lazyListState) {
176 | var prev = 0
177 | snapshotFlow { lazyListState.firstVisibleItemIndex }
178 | .collect {
179 | fabExtended = it <= prev
180 | prev = it
181 | }
182 | }
183 |
184 | AnimatedVisibility(
185 | visible = viewModel.selectedItem.value in 0..1,
186 | enter = fadeIn() + scaleIn(),
187 | exit = fadeOut() + scaleOut()
188 | ) {
189 | var icon by remember { mutableStateOf(Icons.Outlined.Edit) }
190 | var text by remember { mutableStateOf("") }
191 |
192 | when (viewModel.selectedItem.value) {
193 | 0 -> {
194 | icon = Icons.Outlined.Edit
195 | text = stringResource(R.string.addNote)
196 | }
197 | 1 -> {
198 | icon = Icons.Outlined.AddTask
199 | text = stringResource(R.string.makeGoal)
200 | }
201 | }
202 |
203 | ExtendableFloatingActionButton(
204 | onClick = {
205 | when (viewModel.selectedItem.value) {
206 | 0 -> {
207 | viewModel.showNoteCreation.targetState = true
208 | viewModel.clearGlobalNote()
209 | }
210 | 1 -> {
211 | viewModel.showGoalCreation.targetState = true
212 | viewModel.clearGlobalGoal()
213 | }
214 | }
215 | },
216 | icon = { Icon(icon, null) },
217 | text = { Text(text) },
218 | extended = fabExtended
219 | )
220 | }
221 | },
222 | bottomBar = {
223 | BottomNavigationBar(
224 | title = viewModel.title,
225 | selectedItem = viewModel.selectedItem,
226 | searchMode = viewModel.searchMode,
227 | searchString = viewModel.searchString,
228 | navController = navController,
229 | items = listOf(
230 | Screen.NoteListScreen,
231 | Screen.GoalsScreen,
232 | Screen.ProfileScreen
233 | ),
234 | alwaysShowLabel = false
235 | )
236 | },
237 | snackbarHost = { SnackbarHost(LocalSnackbarHost.current) },
238 | modifier = modifier.nestedScroll(viewModel.scrollBehavior.value.nestedScrollConnection)
239 | ) { contentPadding ->
240 | Navigation(navController, contentPadding, viewModel)
241 | }
242 | }
--------------------------------------------------------------------------------