├── app ├── .gitignore ├── release │ ├── app-release.apk │ └── output-metadata.json ├── proguard-rules.pro ├── src │ └── main │ │ ├── ic_launcher-playstore.png │ │ ├── res │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── themes.xml │ │ │ └── strings.xml │ │ ├── values-night │ │ │ └── colors.xml │ │ ├── xml │ │ │ ├── data_extraction_rules.xml │ │ │ └── backup_rules.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── drawable │ │ │ ├── launch_screen.xml │ │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-v24 │ │ │ └── ic_fire_144.xml │ │ └── values-ru-rRU │ │ │ └── strings.xml │ │ ├── java │ │ └── ru │ │ │ └── tech │ │ │ └── firenote │ │ │ ├── ui │ │ │ ├── state │ │ │ │ └── UIState.kt │ │ │ ├── composable │ │ │ │ ├── provider │ │ │ │ │ ├── LocalWindowSize.kt │ │ │ │ │ ├── LocalToastHost.kt │ │ │ │ │ ├── LocalLazyListStateProvider.kt │ │ │ │ │ └── LocalSnackbarHost.kt │ │ │ │ ├── single │ │ │ │ │ ├── placeholder │ │ │ │ │ │ └── Placeholder.kt │ │ │ │ │ ├── ExtendableFloatingActionButton.kt │ │ │ │ │ ├── bar │ │ │ │ │ │ ├── SearchBar.kt │ │ │ │ │ │ ├── AppBarWithInsets.kt │ │ │ │ │ │ ├── EditableAppBar.kt │ │ │ │ │ │ ├── BottomNavigationBar.kt │ │ │ │ │ │ └── AppBarActions.kt │ │ │ │ │ ├── dialog │ │ │ │ │ │ └── MaterialDialog.kt │ │ │ │ │ ├── text │ │ │ │ │ │ ├── MaterialTextField.kt │ │ │ │ │ │ └── EditText.kt │ │ │ │ │ ├── toast │ │ │ │ │ │ └── FancyToast.kt │ │ │ │ │ ├── lazyitem │ │ │ │ │ │ ├── ProfileNoteItem.kt │ │ │ │ │ │ ├── NoteItem.kt │ │ │ │ │ │ └── GoalItem.kt │ │ │ │ │ └── scaffold │ │ │ │ │ │ └── FirenoteScaffold.kt │ │ │ │ ├── navigation │ │ │ │ │ └── Navigation.kt │ │ │ │ ├── utils │ │ │ │ │ └── WindowSize.kt │ │ │ │ ├── screen │ │ │ │ │ ├── auth │ │ │ │ │ │ ├── AuthScreen.kt │ │ │ │ │ │ ├── ForgotPasswordScreen.kt │ │ │ │ │ │ ├── LoginScreen.kt │ │ │ │ │ │ └── RegistrationScreen.kt │ │ │ │ │ ├── creation │ │ │ │ │ │ └── CreationContainer.kt │ │ │ │ │ └── navigation │ │ │ │ │ │ ├── NoteListScreen.kt │ │ │ │ │ │ └── GoalListScreen.kt │ │ │ │ └── app │ │ │ │ │ └── FirenoteApp.kt │ │ │ ├── theme │ │ │ │ ├── Type.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Color.kt │ │ │ └── route │ │ │ │ └── Screen.kt │ │ │ ├── model │ │ │ ├── ImageUri.kt │ │ │ ├── Username.kt │ │ │ ├── Type.kt │ │ │ ├── Note.kt │ │ │ ├── Goal.kt │ │ │ └── GoalData.kt │ │ │ ├── application │ │ │ └── FirenoteApplication.kt │ │ │ ├── repository │ │ │ ├── NoteRepository.kt │ │ │ └── impl │ │ │ │ └── NoteRepositoryImpl.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ ├── utils │ │ │ └── GlobalUtils.kt │ │ │ ├── MainActivity.kt │ │ │ └── viewModel │ │ │ ├── navigation │ │ │ ├── NoteListViewModel.kt │ │ │ ├── GoalListViewModel.kt │ │ │ └── ProfileViewModel.kt │ │ │ ├── creation │ │ │ ├── NoteCreationViewModel.kt │ │ │ └── GoalCreationViewModel.kt │ │ │ ├── main │ │ │ └── MainViewModel.kt │ │ │ └── auth │ │ │ └── AuthViewModel.kt │ │ └── AndroidManifest.xml ├── google-services.json └── build.gradle.kts ├── blob └── preview │ └── intro.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── compiler.xml ├── vcs.xml ├── inspectionProfiles │ └── Project_Default.xml ├── gradle.xml └── misc.xml ├── .gitignore ├── settings.gradle ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /blob/preview/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/Firenote/HEAD/blob/preview/intro.png -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/Firenote/HEAD/app/release/app-release.apk -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class com.google.android.gms.** { *; } 2 | -keep class com.google.firebase.** { *; } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/Firenote/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/Firenote/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #F3E8E8 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2F2F2F 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1D1D1D 4 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/state/UIState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.state 2 | 3 | sealed class UIState { 4 | class Empty(var message: String? = null) : UIState() 5 | object Loading : UIState() 6 | class Success(val data: T) : UIState() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/model/ImageUri.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.model 2 | 3 | import androidx.annotation.Keep 4 | import com.google.firebase.database.IgnoreExtraProperties 5 | 6 | @Keep 7 | @IgnoreExtraProperties 8 | data class ImageUri(val uri: String? = null) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/model/Username.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.model 2 | 3 | import androidx.annotation.Keep 4 | import com.google.firebase.database.IgnoreExtraProperties 5 | 6 | @Keep 7 | @IgnoreExtraProperties 8 | data class Username(val username: String? = null) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Feb 16 19:42:38 MSK 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/provider/LocalWindowSize.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.provider 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import ru.tech.firenote.ui.composable.utils.WindowSize 5 | 6 | val LocalWindowSize = compositionLocalOf { WindowSize.Compact } 7 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/model/Type.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.model 2 | 3 | import androidx.annotation.Keep 4 | import com.google.firebase.database.IgnoreExtraProperties 5 | 6 | @Keep 7 | @IgnoreExtraProperties 8 | data class Type( 9 | val color: Int? = null, 10 | val type: String? = null 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/provider/LocalToastHost.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.provider 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import ru.tech.firenote.ui.composable.single.toast.FancyToastValues 5 | 6 | val LocalToastHost = compositionLocalOf { FancyToastValues() } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/provider/LocalLazyListStateProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.provider 2 | 3 | import androidx.compose.foundation.lazy.LazyListState 4 | import androidx.compose.runtime.compositionLocalOf 5 | 6 | val LocalLazyListStateProvider = compositionLocalOf { LazyListState() } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/launch_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { url 'https://jitpack.io' } 14 | } 15 | } 16 | rootProject.name = "Firenote" 17 | include ':app' 18 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/model/Note.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.model 2 | 3 | import androidx.annotation.Keep 4 | import com.google.firebase.database.IgnoreExtraProperties 5 | 6 | @Keep 7 | @IgnoreExtraProperties 8 | data class Note( 9 | val title: String? = null, 10 | val content: String? = null, 11 | val timestamp: Long? = null, 12 | val color: Int? = null, 13 | val appBarColor: Int? = null, 14 | var id: String? = null 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/model/Goal.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.model 2 | 3 | import androidx.annotation.Keep 4 | import com.google.firebase.database.IgnoreExtraProperties 5 | 6 | @Keep 7 | @IgnoreExtraProperties 8 | data class Goal( 9 | val title: String? = null, 10 | val content: List? = null, 11 | val timestamp: Long? = null, 12 | val color: Int? = null, 13 | val appBarColor: Int? = null, 14 | var id: String? = null 15 | ) -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "ru.tech.firenote", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 8, 15 | "versionName": "1.1.3", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/application/FirenoteApplication.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.application 2 | 3 | import android.app.Application 4 | import com.google.firebase.FirebaseApp 5 | import com.google.firebase.database.ktx.database 6 | import com.google.firebase.ktx.Firebase 7 | import dagger.hilt.android.HiltAndroidApp 8 | 9 | @HiltAndroidApp 10 | class FirenoteApplication : Application() { 11 | 12 | override fun onCreate() { 13 | super.onCreate() 14 | FirebaseApp.initializeApp(this) 15 | Firebase.database.setPersistenceEnabled(true) 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/provider/LocalSnackbarHost.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.provider 2 | 3 | import androidx.compose.material3.SnackbarHostState 4 | import androidx.compose.material3.SnackbarResult 5 | import androidx.compose.runtime.compositionLocalOf 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.launch 8 | 9 | val LocalSnackbarHost = compositionLocalOf { SnackbarHostState() } 10 | 11 | fun showSnackbar( 12 | scope: CoroutineScope, 13 | host: SnackbarHostState, 14 | message: String, 15 | action: String, 16 | result: (SnackbarResult) -> Unit 17 | ) { 18 | scope.launch { 19 | result( 20 | host.showSnackbar( 21 | message = message, 22 | actionLabel = action 23 | ) 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | val Typography = Typography( 10 | bodyLarge = TextStyle( 11 | fontFamily = FontFamily.Default, 12 | fontWeight = FontWeight.Normal, 13 | fontSize = 16.sp, 14 | lineHeight = 24.sp, 15 | letterSpacing = 0.5.sp 16 | ), 17 | titleLarge = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.Normal, 20 | fontSize = 22.sp, 21 | lineHeight = 28.sp, 22 | letterSpacing = 0.sp 23 | ), 24 | labelSmall = TextStyle( 25 | fontFamily = FontFamily.Default, 26 | fontWeight = FontWeight.Medium, 27 | fontSize = 11.sp, 28 | lineHeight = 16.sp, 29 | letterSpacing = 0.5.sp 30 | ) 31 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/repository/NoteRepository.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.repository 2 | 3 | import android.net.Uri 4 | import com.google.firebase.auth.FirebaseAuth 5 | import kotlinx.coroutines.flow.Flow 6 | import ru.tech.firenote.model.Goal 7 | import ru.tech.firenote.model.Note 8 | import ru.tech.firenote.model.Type 9 | 10 | interface NoteRepository { 11 | 12 | val auth: FirebaseAuth 13 | 14 | suspend fun getNotes(): Flow>> 15 | 16 | suspend fun insertNote(note: Note) 17 | 18 | suspend fun deleteNote(note: Note) 19 | 20 | suspend fun getProfileUri(): Flow> 21 | 22 | suspend fun setProfileUri(uri: Uri) 23 | 24 | suspend fun getUsername(): Flow> 25 | 26 | suspend fun setUsername(username: String) 27 | 28 | suspend fun getGoals(): Flow>> 29 | 30 | suspend fun insertGoal(goal: Goal) 31 | 32 | suspend fun deleteGoal(goal: Goal) 33 | 34 | suspend fun updateType(color: Int, type: String) 35 | 36 | suspend fun getTypes(): Flow>> 37 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/single/placeholder/Placeholder.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.single.placeholder 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.vector.ImageVector 13 | import androidx.compose.ui.res.stringResource 14 | 15 | @Composable 16 | fun Placeholder(icon: ImageVector, @StringRes textRes: Int) { 17 | Column( 18 | modifier = Modifier 19 | .fillMaxSize(), 20 | verticalArrangement = Arrangement.Center, 21 | horizontalAlignment = Alignment.CenterHorizontally 22 | ) { 23 | Icon(icon, null, modifier = Modifier.fillMaxSize(0.3f)) 24 | Text(stringResource(textRes)) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/model/GoalData.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.model 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import androidx.annotation.Keep 6 | import com.google.firebase.database.IgnoreExtraProperties 7 | 8 | @Keep 9 | @IgnoreExtraProperties 10 | data class GoalData( 11 | val content: String? = null, 12 | val done: Boolean? = null 13 | ) : Parcelable { 14 | 15 | constructor(parcel: Parcel) : this( 16 | parcel.readString(), 17 | parcel.readValue(Boolean::class.java.classLoader) as? Boolean 18 | ) 19 | 20 | override fun writeToParcel(parcel: Parcel, flags: Int) { 21 | parcel.writeString(content) 22 | parcel.writeValue(done) 23 | } 24 | 25 | override fun describeContents(): Int { 26 | return 0 27 | } 28 | 29 | companion object CREATOR : Parcelable.Creator { 30 | override fun createFromParcel(parcel: Parcel): GoalData { 31 | return GoalData(parcel) 32 | } 33 | 34 | override fun newArray(size: Int): Array { 35 | return arrayOfNulls(size) 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "415132711530", 4 | "project_id": "firenote-525f9", 5 | "storage_bucket": "firenote-525f9.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:415132711530:android:3b93cb9371b945a2b56813", 11 | "android_client_info": { 12 | "package_name": "ru.tech.firenote" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "415132711530-916kn84ta7b02e4edgguvogpd9emuvkr.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyAzKdsFxutf-mSpEKEgnr5TO0YCVHofQ7c" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "415132711530-916kn84ta7b02e4edgguvogpd9emuvkr.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | ], 38 | "configuration_version": "1" 39 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.di 2 | 3 | import com.google.firebase.database.DatabaseReference 4 | import com.google.firebase.database.FirebaseDatabase 5 | import com.google.firebase.storage.FirebaseStorage 6 | import com.google.firebase.storage.StorageReference 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import ru.tech.firenote.repository.NoteRepository 12 | import ru.tech.firenote.repository.impl.NoteRepositoryImpl 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object AppModule { 18 | 19 | @Provides 20 | @Singleton 21 | fun provideFirebaseDatabase() = FirebaseDatabase.getInstance().getReference("Users") 22 | 23 | @Provides 24 | @Singleton 25 | fun provideFirebaseStorage() = FirebaseStorage.getInstance().getReference("Users") 26 | 27 | @Provides 28 | @Singleton 29 | fun provideNoteRepository( 30 | database: DatabaseReference, 31 | storage: StorageReference 32 | ): NoteRepository = NoteRepositoryImpl(database, storage) 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_fire_144.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/utils/GlobalUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.utils 2 | 3 | import android.content.Context 4 | import android.content.Context.CONNECTIVITY_SERVICE 5 | import android.net.ConnectivityManager 6 | import android.net.NetworkCapabilities 7 | import android.os.Build 8 | import androidx.core.graphics.ColorUtils 9 | 10 | object GlobalUtils { 11 | 12 | fun Int.blend(ratio: Float = 0.2f, with: Int = 0x000000) = 13 | ColorUtils.blendARGB(this, with, ratio) 14 | 15 | fun Context.isOnline(): Boolean { 16 | val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager 17 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 18 | val capabilities = 19 | connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) 20 | when { 21 | capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> true 22 | capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> true 23 | capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true -> true 24 | else -> false 25 | } 26 | } else @Suppress("DEPRECATION") { 27 | val activeNetworkInfo = connectivityManager.activeNetworkInfo 28 | activeNetworkInfo != null && activeNetworkInfo.isConnected 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/single/ExtendableFloatingActionButton.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.single 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.material3.FloatingActionButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun ExtendableFloatingActionButton( 16 | modifier: Modifier = Modifier, 17 | extended: Boolean, 18 | text: @Composable () -> Unit, 19 | icon: @Composable () -> Unit, 20 | onClick: () -> Unit = {} 21 | ) { 22 | 23 | FloatingActionButton( 24 | modifier = modifier, 25 | onClick = onClick 26 | ) { 27 | Row( 28 | modifier = Modifier.padding( 29 | start = 16.dp, 30 | end = 16.dp 31 | ), verticalAlignment = Alignment.CenterVertically 32 | ) { 33 | icon() 34 | 35 | AnimatedVisibility(visible = extended) { 36 | Row { 37 | Spacer(Modifier.width(12.dp)) 38 | text() 39 | } 40 | } 41 | } 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | -Xopt-in=kotlin.RequiresOptIn -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.animation.ExperimentalAnimationApi 7 | import androidx.compose.foundation.ExperimentalFoundationApi 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.core.view.WindowCompat 10 | import androidx.navigation.compose.rememberNavController 11 | import dagger.hilt.android.AndroidEntryPoint 12 | import ru.tech.firenote.ui.composable.app.FirenoteApp 13 | import ru.tech.firenote.ui.composable.utils.WindowSize 14 | import ru.tech.firenote.ui.composable.utils.rememberWindowSizeClass 15 | 16 | 17 | @ExperimentalFoundationApi 18 | @AndroidEntryPoint 19 | @ExperimentalMaterial3Api 20 | @ExperimentalAnimationApi 21 | class MainActivity : ComponentActivity() { 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | setTheme(R.style.Theme_Firenote) 25 | super.onCreate(savedInstanceState) 26 | WindowCompat.setDecorFitsSystemWindows(window, false) 27 | 28 | setContent { 29 | val navController = rememberNavController() 30 | val windowSize = rememberWindowSizeClass() 31 | val splitScreen = windowSize != WindowSize.Compact 32 | 33 | FirenoteApp( 34 | context = this, 35 | windowSize = windowSize, 36 | splitScreen = splitScreen, 37 | navController = navController 38 | ) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/route/Screen.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.route 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.AccountCircle 6 | import androidx.compose.material.icons.filled.FactCheck 7 | import androidx.compose.material.icons.filled.StickyNote2 8 | import androidx.compose.material.icons.outlined.AccountCircle 9 | import androidx.compose.material.icons.outlined.FactCheck 10 | import androidx.compose.material.icons.outlined.PhoneAndroid 11 | import androidx.compose.material.icons.outlined.StickyNote2 12 | import androidx.compose.material.icons.rounded.PhoneAndroid 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | import ru.tech.firenote.R 15 | 16 | sealed class Screen( 17 | val route: String, 18 | @StringRes val resourceId: Int = R.string.app_name, 19 | val baseIcon: ImageVector = Icons.Outlined.PhoneAndroid, 20 | val selectedIcon: ImageVector = Icons.Rounded.PhoneAndroid 21 | ) { 22 | object NoteListScreen : Screen( 23 | route = "notes", 24 | resourceId = R.string.notes, 25 | baseIcon = Icons.Outlined.StickyNote2, 26 | selectedIcon = Icons.Filled.StickyNote2 27 | ) 28 | 29 | object GoalsScreen : Screen( 30 | route = "goals", 31 | resourceId = R.string.goals, 32 | baseIcon = Icons.Outlined.FactCheck, 33 | selectedIcon = Icons.Filled.FactCheck 34 | ) 35 | 36 | object ProfileScreen : Screen( 37 | route = "profile", 38 | resourceId = R.string.profile, 39 | baseIcon = Icons.Outlined.AccountCircle, 40 | selectedIcon = Icons.Filled.AccountCircle 41 | ) 42 | 43 | object LoginScreen : Screen(route = "login") 44 | object RegistrationScreen : Screen(route = "register") 45 | object ForgotPasswordScreen : Screen(route = "forgot") 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/viewModel/navigation/NoteListViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.viewModel.navigation 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.StateFlow 9 | import kotlinx.coroutines.launch 10 | import ru.tech.firenote.model.Note 11 | import ru.tech.firenote.repository.NoteRepository 12 | import ru.tech.firenote.ui.state.UIState 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class NoteListViewModel @Inject constructor( 17 | private val repository: NoteRepository 18 | ) : ViewModel() { 19 | 20 | private val _uiState = MutableStateFlow(UIState.Empty()) 21 | val uiState: StateFlow = _uiState 22 | 23 | 24 | init { 25 | getNotes() 26 | } 27 | 28 | private fun getNotes() { 29 | viewModelScope.launch { 30 | _uiState.value = UIState.Loading 31 | 32 | while (repository.auth.currentUser == null) { 33 | delay(500) 34 | } 35 | 36 | repository.getNotes().collect { 37 | if (it.isSuccess) { 38 | if (it.getOrNull().isNullOrEmpty()) _uiState.value = UIState.Empty() 39 | else _uiState.value = UIState.Success(it.getOrNull()) 40 | } else { 41 | _uiState.value = UIState.Empty(it.exceptionOrNull()?.localizedMessage) 42 | } 43 | } 44 | } 45 | } 46 | 47 | fun deleteNote( 48 | note: Note, 49 | onDeleted: (Note) -> Unit 50 | ) { 51 | viewModelScope.launch { 52 | repository.deleteNote(note) 53 | onDeleted(note) 54 | } 55 | } 56 | 57 | fun insertNote(note: Note) { 58 | viewModelScope.launch { repository.insertNote(note) } 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/viewModel/navigation/GoalListViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.viewModel.navigation 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.StateFlow 9 | import kotlinx.coroutines.launch 10 | import ru.tech.firenote.model.Goal 11 | import ru.tech.firenote.repository.NoteRepository 12 | import ru.tech.firenote.ui.state.UIState 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class GoalListViewModel @Inject constructor( 17 | private val repository: NoteRepository 18 | ) : ViewModel() { 19 | 20 | 21 | private val _uiState = MutableStateFlow(UIState.Empty()) 22 | val uiState: StateFlow = _uiState 23 | 24 | 25 | init { 26 | getGoals() 27 | } 28 | 29 | private fun getGoals() { 30 | viewModelScope.launch { 31 | _uiState.value = UIState.Loading 32 | 33 | while (repository.auth.currentUser == null) { 34 | delay(500) 35 | } 36 | 37 | repository.getGoals().collect { 38 | if (it.isSuccess) { 39 | if (it.getOrNull().isNullOrEmpty()) _uiState.value = UIState.Empty() 40 | else _uiState.value = UIState.Success(it.getOrNull()) 41 | } else { 42 | _uiState.value = UIState.Empty(it.exceptionOrNull()?.localizedMessage) 43 | } 44 | } 45 | } 46 | } 47 | 48 | fun deleteGoal( 49 | goal: Goal, 50 | onDeleted: (Goal) -> Unit 51 | ) { 52 | viewModelScope.launch { 53 | repository.deleteGoal(goal) 54 | onDeleted(goal) 55 | } 56 | } 57 | 58 | fun insertGoal(goal: Goal) { 59 | viewModelScope.launch { repository.insertGoal(goal) } 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/single/bar/SearchBar.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.single.bar 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.text.BasicTextField 6 | import androidx.compose.foundation.text.KeyboardActions 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.SolidColor 12 | import androidx.compose.ui.platform.LocalFocusManager 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.text.TextStyle 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.unit.sp 18 | import ru.tech.firenote.R 19 | 20 | @Composable 21 | fun SearchBar(searchString: String, onValueChange: (String) -> Unit) { 22 | val localFocusManager = 23 | LocalFocusManager.current 24 | BasicTextField( 25 | modifier = Modifier.fillMaxWidth(), 26 | value = searchString, 27 | textStyle = TextStyle( 28 | fontSize = 22.sp, 29 | color = MaterialTheme.colorScheme.onBackground, 30 | textAlign = TextAlign.Start, 31 | ), 32 | keyboardActions = KeyboardActions( 33 | onDone = { localFocusManager.clearFocus() } 34 | ), 35 | singleLine = true, 36 | cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), 37 | onValueChange = { 38 | onValueChange(it) 39 | }) 40 | if (searchString.isEmpty()) { 41 | Text( 42 | text = stringResource(R.string.searchHere), 43 | modifier = Modifier 44 | .fillMaxWidth() 45 | .padding(start = 12.dp), 46 | style = TextStyle( 47 | fontSize = 22.sp, 48 | color = MaterialTheme.colorScheme.outline, 49 | textAlign = TextAlign.Start, 50 | ) 51 | ) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/single/bar/AppBarWithInsets.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.single.bar 2 | 3 | 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.foundation.layout.statusBarsPadding 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | 11 | @Composable 12 | fun AppBarWithInsets( 13 | modifier: Modifier = Modifier, 14 | title: @Composable () -> Unit = {}, 15 | type: Int = APP_BAR_SIMPLE, 16 | scrollBehavior: TopAppBarScrollBehavior? = null, 17 | navigationIcon: @Composable () -> Unit = {}, 18 | actions: @Composable RowScope.() -> Unit = {}, 19 | ) { 20 | val backgroundColors = TopAppBarDefaults.smallTopAppBarColors() 21 | val backgroundColor = backgroundColors.containerColor( 22 | scrollFraction = scrollBehavior?.scrollFraction ?: 0f 23 | ).value 24 | val foregroundColors = TopAppBarDefaults.smallTopAppBarColors( 25 | containerColor = Color.Transparent, 26 | scrolledContainerColor = Color.Transparent 27 | ) 28 | Surface(color = backgroundColor) { 29 | when (type) { 30 | APP_BAR_CENTER -> { 31 | CenterAlignedTopAppBar( 32 | title = title, 33 | navigationIcon = navigationIcon, 34 | actions = actions, 35 | scrollBehavior = scrollBehavior, 36 | colors = foregroundColors, 37 | modifier = modifier.statusBarsPadding() 38 | ) 39 | } 40 | APP_BAR_SIMPLE -> { 41 | SmallTopAppBar( 42 | title = title, 43 | navigationIcon = navigationIcon, 44 | actions = actions, 45 | scrollBehavior = scrollBehavior, 46 | colors = foregroundColors, 47 | modifier = modifier.statusBarsPadding() 48 | ) 49 | } 50 | } 51 | } 52 | } 53 | 54 | const val APP_BAR_SIMPLE = 0 55 | const val APP_BAR_CENTER = 1 -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/single/dialog/MaterialDialog.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.single.dialog 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.annotation.StringRes 5 | import androidx.compose.material3.AlertDialog 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.runtime.MutableState 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.text.style.TextAlign 16 | 17 | @Composable 18 | fun MaterialDialog( 19 | showDialog: MutableState = mutableStateOf(false), 20 | icon: ImageVector, 21 | @StringRes title: Int, 22 | @StringRes message: Int, 23 | @StringRes confirmText: Int, 24 | confirmAction: () -> Unit = {}, 25 | @StringRes dismissText: Int, 26 | dismissAction: () -> Unit = {}, 27 | onDismiss: () -> Unit = {}, 28 | backHandler: @Composable () -> Unit = { BackHandler { showDialog.value = true } } 29 | ) { 30 | 31 | if (showDialog.value) { 32 | AlertDialog( 33 | icon = { Icon(icon, null) }, 34 | title = { Text(stringResource(title)) }, 35 | text = { Text(stringResource(message), textAlign = TextAlign.Center) }, 36 | confirmButton = { 37 | TextButton(onClick = { 38 | confirmAction() 39 | showDialog.value = false 40 | }) { 41 | Text(stringResource(confirmText)) 42 | } 43 | }, 44 | dismissButton = { 45 | TextButton(onClick = { 46 | dismissAction() 47 | showDialog.value = false 48 | }) { 49 | Text(stringResource(dismissText)) 50 | } 51 | }, 52 | onDismissRequest = { showDialog.value = false } 53 | ) 54 | } else LaunchedEffect(Unit) { onDismiss() } 55 | 56 | backHandler() 57 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/navigation/Navigation.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.navigation 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.navigation.NavHostController 9 | import androidx.navigation.compose.NavHost 10 | import androidx.navigation.compose.composable 11 | import ru.tech.firenote.ui.composable.screen.navigation.GoalListScreen 12 | import ru.tech.firenote.ui.composable.screen.navigation.NoteListScreen 13 | import ru.tech.firenote.ui.composable.screen.navigation.ProfileScreen 14 | import ru.tech.firenote.ui.route.Screen 15 | import ru.tech.firenote.viewModel.main.MainViewModel 16 | 17 | @ExperimentalFoundationApi 18 | @Composable 19 | fun Navigation( 20 | navController: NavHostController, 21 | contentPadding: PaddingValues, 22 | viewModel: MainViewModel, 23 | ) { 24 | NavHost( 25 | navController = navController, 26 | startDestination = Screen.NoteListScreen.route, 27 | Modifier.padding(contentPadding) 28 | ) { 29 | composable(Screen.NoteListScreen.route) { 30 | NoteListScreen( 31 | viewModel.showNoteCreation, 32 | viewModel.globalNote, 33 | viewModel.filterType, 34 | viewModel.isDescendingFilter, 35 | viewModel.searchString 36 | ) 37 | } 38 | composable(Screen.GoalsScreen.route) { 39 | GoalListScreen( 40 | viewModel.showGoalCreation, 41 | viewModel.globalGoal, 42 | viewModel.filterType, 43 | viewModel.isDescendingFilter, 44 | viewModel.searchString 45 | ) 46 | } 47 | composable(Screen.ProfileScreen.route) { 48 | ProfileScreen( 49 | navController, 50 | viewModel.selectedItem, 51 | viewModel.resultLauncher, 52 | viewModel.profileTitle, 53 | viewModel.showUsernameDialog 54 | ) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/single/bar/EditableAppBar.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.single.bar 2 | 3 | import androidx.compose.foundation.layout.RowScope 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.statusBarsPadding 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.MutableState 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.graphics.toArgb 13 | import ru.tech.firenote.ui.composable.single.text.EditText 14 | 15 | @Composable 16 | fun EditableAppBar( 17 | modifier: Modifier = Modifier, 18 | textModifier: Modifier = Modifier, 19 | hint: String, 20 | color: Color, 21 | backgroundColor: Color, 22 | text: MutableState = mutableStateOf(""), 23 | errorColor: Int = MaterialTheme.colorScheme.error.toArgb(), 24 | scrollBehavior: TopAppBarScrollBehavior? = null, 25 | navigationIcon: @Composable () -> Unit = {}, 26 | actions: @Composable RowScope.() -> Unit = {}, 27 | enabled: Boolean = true, 28 | onValueChange: (String) -> Unit = {} 29 | ) { 30 | val foregroundColors = TopAppBarDefaults.smallTopAppBarColors( 31 | containerColor = Color.Transparent, 32 | scrolledContainerColor = Color.Transparent 33 | ) 34 | Surface(color = Color.DarkGray) { 35 | Surface(color = backgroundColor) { 36 | SmallTopAppBar( 37 | title = { 38 | EditText( 39 | modifier = textModifier.fillMaxWidth(), 40 | hintText = hint, 41 | onValueChange = onValueChange, 42 | errorColor = errorColor, 43 | color = color, 44 | enabled = enabled, 45 | textFieldState = text 46 | ) 47 | }, 48 | navigationIcon = navigationIcon, 49 | scrollBehavior = scrollBehavior, 50 | colors = foregroundColors, 51 | modifier = modifier.statusBarsPadding(), 52 | actions = actions 53 | ) 54 | } 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/utils/WindowSize.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.utils 2 | 3 | import android.app.Activity 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.geometry.Size 7 | import androidx.compose.ui.graphics.toComposeRect 8 | import androidx.compose.ui.platform.LocalConfiguration 9 | import androidx.compose.ui.platform.LocalDensity 10 | import androidx.compose.ui.unit.DpSize 11 | import androidx.compose.ui.unit.dp 12 | import androidx.window.layout.WindowMetricsCalculator 13 | 14 | /** 15 | * Opinionated set of viewport breakpoints 16 | * 17 | * - Compact: Most phones in portrait mode 18 | * - Medium: Most fold devices and tablets in portrait mode 19 | * - Expanded: Most tablets in landscape mode 20 | * 21 | */ 22 | enum class WindowSize { Compact, Medium, Expanded } 23 | 24 | @Composable 25 | fun Activity.rememberWindowSizeClass(): WindowSize { 26 | val windowSize = rememberWindowSize() 27 | val windowDpSize = with(LocalDensity.current) { 28 | windowSize.toDpSize() 29 | } 30 | return getWindowSizeClass(windowDpSize) 31 | } 32 | 33 | @Composable 34 | private fun Activity.rememberWindowSize(): Size { 35 | val configuration = LocalConfiguration.current 36 | val windowMetrics = remember(configuration) { 37 | WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this) 38 | } 39 | return windowMetrics.bounds.toComposeRect().size 40 | } 41 | 42 | fun getWindowSizeClass(windowDpSize: DpSize): WindowSize { 43 | val width = when { 44 | windowDpSize.width < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative") 45 | windowDpSize.width < 600.dp -> WindowSize.Compact 46 | windowDpSize.width < 840.dp -> WindowSize.Medium 47 | else -> WindowSize.Expanded 48 | } 49 | val height = when { 50 | windowDpSize.height < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative") 51 | windowDpSize.height < 480.dp -> WindowSize.Compact 52 | windowDpSize.height < 900.dp -> WindowSize.Medium 53 | else -> WindowSize.Expanded 54 | } 55 | return width minOf height 56 | } 57 | 58 | infix fun WindowSize.minOf(size: WindowSize): WindowSize { 59 | return if (size > this) this 60 | else size 61 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/viewModel/creation/NoteCreationViewModel.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.Note 10 | import ru.tech.firenote.repository.NoteRepository 11 | import ru.tech.firenote.ui.theme.NoteYellow 12 | import ru.tech.firenote.utils.GlobalUtils.blend 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class NoteCreationViewModel @Inject constructor( 17 | private val repository: NoteRepository 18 | ) : ViewModel() { 19 | 20 | var note: Note? = null 21 | 22 | val noteColor = mutableStateOf(NoteYellow.toArgb()) 23 | val appBarColor = mutableStateOf(noteColor.value.blend()) 24 | 25 | val noteLabel = mutableStateOf("") 26 | val noteContent = mutableStateOf("") 27 | 28 | fun saveNote() { 29 | viewModelScope.launch { 30 | repository.insertNote( 31 | Note( 32 | noteLabel.value, 33 | noteContent.value, 34 | System.currentTimeMillis(), 35 | noteColor.value, 36 | appBarColor.value 37 | ) 38 | ) 39 | resetValues() 40 | } 41 | } 42 | 43 | fun updateNote(note: Note) { 44 | viewModelScope.launch { 45 | repository.insertNote( 46 | Note( 47 | noteLabel.value, 48 | noteContent.value, 49 | System.currentTimeMillis(), 50 | noteColor.value, 51 | appBarColor.value, 52 | note.id 53 | ) 54 | ) 55 | resetValues() 56 | } 57 | } 58 | 59 | fun parseNoteData(note: Note?) { 60 | this.note = note 61 | noteLabel.value = note?.title ?: "" 62 | noteContent.value = note?.content ?: "" 63 | noteColor.value = note?.color ?: NoteYellow.toArgb() 64 | appBarColor.value = note?.appBarColor ?: noteColor.value.blend() 65 | } 66 | 67 | fun resetValues() { 68 | note = null 69 | noteColor.value = NoteYellow.toArgb() 70 | appBarColor.value = noteColor.value.blend() 71 | 72 | noteLabel.value = "" 73 | noteContent.value = "" 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/ui/composable/single/bar/BottomNavigationBar.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.ui.composable.single.bar 2 | 3 | import androidx.compose.foundation.layout.navigationBarsPadding 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.rememberCoroutineScope 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.navigation.NavHostController 11 | import kotlinx.coroutines.launch 12 | import ru.tech.firenote.ui.composable.provider.LocalLazyListStateProvider 13 | import ru.tech.firenote.ui.route.Screen 14 | 15 | @Composable 16 | fun BottomNavigationBar( 17 | navController: NavHostController, 18 | items: List, 19 | searchMode: MutableState, 20 | searchString: MutableState, 21 | title: MutableState, 22 | selectedItem: MutableState, 23 | alwaysShowLabel: Boolean = true 24 | ) { 25 | Surface(color = TopAppBarDefaults.smallTopAppBarColors().containerColor(100f).value) { 26 | NavigationBar(modifier = Modifier.navigationBarsPadding()) { 27 | items.forEachIndexed { index, screen -> 28 | 29 | selectedItem.value = 30 | items.indexOfFirst { it.route == navController.currentDestination?.route } 31 | 32 | val scrollState = LocalLazyListStateProvider.current 33 | val scope = rememberCoroutineScope() 34 | 35 | NavigationBarItem( 36 | icon = { 37 | Icon( 38 | if (selectedItem.value == index) screen.selectedIcon else screen.baseIcon, 39 | null 40 | ) 41 | }, 42 | alwaysShowLabel = alwaysShowLabel, 43 | label = { Text(stringResource(screen.resourceId)) }, 44 | selected = selectedItem.value == index, 45 | onClick = { 46 | if (selectedItem.value != index) { 47 | title.value = screen.resourceId 48 | selectedItem.value = index 49 | navController.navigate(screen.route) { 50 | navController.popBackStack() 51 | launchSingleTop = true 52 | } 53 | searchMode.value = false 54 | searchString.value = "" 55 | scope.launch { scrollState.scrollToItem(0) } 56 | } 57 | } 58 | ) 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/viewModel/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.viewModel.main 2 | 3 | import android.net.Uri 4 | import androidx.activity.compose.ManagedActivityResultLauncher 5 | import androidx.compose.animation.core.MutableTransitionState 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.TopAppBarDefaults 8 | import androidx.compose.runtime.MutableState 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.lifecycle.ViewModel 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import ru.tech.firenote.model.Goal 13 | import ru.tech.firenote.model.Note 14 | import ru.tech.firenote.repository.NoteRepository 15 | import ru.tech.firenote.ui.route.Screen 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class MainViewModel @Inject constructor( 20 | private val repository: NoteRepository 21 | ) : ViewModel() { 22 | 23 | val title = mutableStateOf(Screen.NoteListScreen.resourceId) 24 | val profileTitle = mutableStateOf("") 25 | 26 | val selectedItem = mutableStateOf(0) 27 | 28 | val globalNote: MutableState = mutableStateOf(null) 29 | val showNoteCreation = MutableTransitionState(false).apply { 30 | targetState = false 31 | } 32 | 33 | val globalGoal: MutableState = mutableStateOf(null) 34 | val showGoalCreation = MutableTransitionState(false).apply { 35 | targetState = false 36 | } 37 | 38 | val searchMode = mutableStateOf(false) 39 | val searchString = mutableStateOf("") 40 | 41 | val resultLauncher = mutableStateOf?>(null) 42 | 43 | val isAuth = mutableStateOf(repository.auth.currentUser == null) 44 | 45 | val showUsernameDialog = mutableStateOf(false) 46 | 47 | @ExperimentalMaterial3Api 48 | val scrollBehavior = mutableStateOf(TopAppBarDefaults.pinnedScrollBehavior()) 49 | 50 | val filterType = mutableStateOf(2) 51 | val isDescendingFilter = mutableStateOf(false) 52 | 53 | init { 54 | repository.auth.addAuthStateListener { 55 | if (it.currentUser == null) isAuth.value = true 56 | } 57 | } 58 | 59 | fun signOut() { 60 | showNoteCreation.targetState = false 61 | showGoalCreation.targetState = false 62 | globalNote.value = null 63 | globalGoal.value = null 64 | 65 | repository.auth.signOut() 66 | } 67 | 68 | fun dispatchSearch() { 69 | searchMode.value = !searchMode.value 70 | updateSearch() 71 | } 72 | 73 | fun updateSearch(newValue: String = "") { 74 | searchString.value = newValue.lowercase().trim() 75 | } 76 | 77 | fun clearGlobalGoal() { 78 | globalGoal.value = null 79 | } 80 | 81 | fun clearGlobalNote() { 82 | globalNote.value = null 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/firenote/viewModel/auth/AuthViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.firenote.viewModel.auth 2 | 3 | import androidx.compose.animation.core.MutableTransitionState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.ViewModel 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.StateFlow 9 | import ru.tech.firenote.repository.NoteRepository 10 | import ru.tech.firenote.ui.route.Screen 11 | import ru.tech.firenote.ui.state.UIState 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class AuthViewModel @Inject constructor( 16 | repository: NoteRepository 17 | ) : ViewModel() { 18 | 19 | private val auth = repository.auth 20 | val currentUser get() = auth.currentUser 21 | val visibleState = MutableTransitionState(currentUser == null) 22 | 23 | private val _logUiState = MutableStateFlow(UIState.Empty()) 24 | val logUiState: StateFlow = _logUiState 25 | 26 | private val _signUiState = MutableStateFlow(UIState.Empty()) 27 | val signUiState: StateFlow = _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 | License 5 | API 6 | Build Status 7 | Profile 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 | } --------------------------------------------------------------------------------