commit 89f3652f554bf4584b2a880c5314ec2a62ed8e2f Author: Andrey Kassaev Date: Wed Mar 20 13:48:00 2024 +0400 code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..4bec4ea --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,117 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..e872d55 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..049e300 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..44ca2d9 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..fdf8d99 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0ad17cb --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..f3d4a2e --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..37d752a --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,92 @@ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsKotlinAndroid) + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + namespace = "com.kassaev.notes" + compileSdk = 34 + + defaultConfig { + applicationId = "com.kassaev.notes" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +kapt { + correctErrorTypes = true +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.lifecycle.runtime.compose) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + //navigation + val nav_version = "2.7.7" + implementation("androidx.navigation:navigation-compose:$nav_version") + + //Hilt + implementation("com.google.dagger:hilt-android:2.51") + kapt("com.google.dagger:hilt-android-compiler:2.51") + implementation("androidx.hilt:hilt-navigation-compose:1.2.0") + + //Room + val room_version = "2.6.1" + implementation("androidx.room:room-runtime:$room_version") + annotationProcessor("androidx.room:room-compiler:$room_version") + kapt("androidx.room:room-compiler:$room_version") + implementation("androidx.room:room-ktx:$room_version") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/release/app-release.apk b/app/release/app-release.apk new file mode 100644 index 0000000..6cc96dc Binary files /dev/null and b/app/release/app-release.apk differ diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm new file mode 100644 index 0000000..b3c58e1 Binary files /dev/null and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm new file mode 100644 index 0000000..08339e2 Binary files /dev/null and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..5e9e863 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,37 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.kassaev.notes", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File", + "baselineProfiles": [ + { + "minApi": 28, + "maxApi": 30, + "baselineProfiles": [ + "baselineProfiles/1/app-release.dm" + ] + }, + { + "minApi": 31, + "maxApi": 2147483647, + "baselineProfiles": [ + "baselineProfiles/0/app-release.dm" + ] + } + ], + "minSdkVersionForDexing": 26 +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/kassaev/notes/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/kassaev/notes/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..5681c35 --- /dev/null +++ b/app/src/androidTest/java/com/kassaev/notes/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.kassaev.notes + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.kassaev.notes", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1e02c77 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/database/note_db.db b/app/src/main/assets/database/note_db.db new file mode 100644 index 0000000..46b8287 Binary files /dev/null and b/app/src/main/assets/database/note_db.db differ diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..d74d2de Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/kassaev/notes/MainActivity.kt b/app/src/main/java/com/kassaev/notes/MainActivity.kt new file mode 100644 index 0000000..168910d --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/MainActivity.kt @@ -0,0 +1,33 @@ +package com.kassaev.notes + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import androidx.room.Room +import com.kassaev.notes.data.NoteDatabase +import com.kassaev.notes.navigation.Navigation +import com.kassaev.notes.ui.theme.NotesTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + println("TIME IS: " + System.currentTimeMillis()) + setContent { + NotesTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Navigation() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/Util.kt b/app/src/main/java/com/kassaev/notes/Util.kt new file mode 100644 index 0000000..eee5b7d --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/Util.kt @@ -0,0 +1,17 @@ +package com.kassaev.notes + +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +object Util { + fun toLocalDateTime(timeStamp: Long): String = + DateTimeFormatter.ofPattern("dd MMM yyyy, HH:mm") + .format( + ZonedDateTime.ofInstant( + Instant.ofEpochMilli(timeStamp), + ZoneId.systemDefault() + ) + ) +} diff --git a/app/src/main/java/com/kassaev/notes/data/FolderEntity.kt b/app/src/main/java/com/kassaev/notes/data/FolderEntity.kt new file mode 100644 index 0000000..17c6bd4 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/data/FolderEntity.kt @@ -0,0 +1,21 @@ +package com.kassaev.notes.data + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.kassaev.notes.domain.FolderModel + +@Entity( + tableName = "folder" +) +data class FolderEntity( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "folder_id") val folderId: Int, + val name: String +){ + fun toModel(): FolderModel { + return FolderModel( + folderId = this.folderId, + name = this.name + ) + } +} diff --git a/app/src/main/java/com/kassaev/notes/data/INoteDao.kt b/app/src/main/java/com/kassaev/notes/data/INoteDao.kt new file mode 100644 index 0000000..4af60f1 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/data/INoteDao.kt @@ -0,0 +1,32 @@ +package com.kassaev.notes.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +@Dao +interface INoteDao { + + @Query("SELECT * FROM note WHERE folder_id=:folderId ORDER BY date DESC") + fun selectAllNotes(folderId: Int): Flow> + + @Upsert + suspend fun upsertNote(noteEntity: NoteEntity) + + @Query("SELECT * FROM note WHERE note_id=:noteId") + suspend fun getNoteById(noteId: Int): NoteEntity? + + @Delete + suspend fun deleteNote(noteEntity: NoteEntity): Int + + @Query("DELETE FROM note WHERE folder_id=1") + suspend fun deleteAllNotesFromRecycleBin() + + @Insert + suspend fun insertNote(noteEntity: NoteEntity): Long + +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/data/NoteDatabase.kt b/app/src/main/java/com/kassaev/notes/data/NoteDatabase.kt new file mode 100644 index 0000000..b04d6a6 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/data/NoteDatabase.kt @@ -0,0 +1,15 @@ +package com.kassaev.notes.data + +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database( + version = 1, + entities = [ + NoteEntity::class, + FolderEntity::class + ], +) +abstract class NoteDatabase: RoomDatabase() { + abstract fun noteDao(): INoteDao +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/data/NoteEntity.kt b/app/src/main/java/com/kassaev/notes/data/NoteEntity.kt new file mode 100644 index 0000000..19ad9c7 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/data/NoteEntity.kt @@ -0,0 +1,35 @@ +package com.kassaev.notes.data + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.kassaev.notes.domain.NoteModel +import com.kassaev.notes.Util + +@Entity( + tableName = "note", + foreignKeys = [ + ForeignKey( + entity = FolderEntity::class, + parentColumns = arrayOf("folder_id"), + childColumns = arrayOf("folder_id") + ) + ] +) +data class NoteEntity( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "note_id") val noteId: Int, + var text: String, + val date: Long, + @ColumnInfo(name = "folder_id") val folderId: Int +){ + + fun toModel(): NoteModel { + return NoteModel( + noteId = this.noteId, + text = this.text, + date = Util.toLocalDateTime(this.date), + folderId = this.folderId + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/data/NoteRepository.kt b/app/src/main/java/com/kassaev/notes/data/NoteRepository.kt new file mode 100644 index 0000000..572a485 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/data/NoteRepository.kt @@ -0,0 +1,32 @@ +package com.kassaev.notes.data + +import com.kassaev.notes.domain.NoteModel +import kotlinx.coroutines.flow.Flow + +class NoteRepository( + private val noteDao: INoteDao +) { + + suspend fun createNote(noteEntity: NoteEntity) = + noteDao.getNoteById(noteDao.insertNote(noteEntity).toInt())?.toModel() + + + fun getAllNotes(folderId: Int): Flow> = + noteDao.selectAllNotes(folderId) + + suspend fun createOrUpdateNote(noteEntity: NoteEntity) = + noteDao.upsertNote(noteEntity) + + + suspend fun getNoteById(noteId: Int): NoteEntity? = + noteDao.getNoteById(noteId) + + suspend fun deleteNote(noteEntity: NoteEntity) = + noteDao.deleteNote(noteEntity) + + + suspend fun deleteAllNotesFromRecycleBin() = + noteDao.deleteAllNotesFromRecycleBin() + + +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/di/App.kt b/app/src/main/java/com/kassaev/notes/di/App.kt new file mode 100644 index 0000000..9026d46 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/di/App.kt @@ -0,0 +1,8 @@ +package com.kassaev.notes.di + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class App: Application() { +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/di/AppModule.kt b/app/src/main/java/com/kassaev/notes/di/AppModule.kt new file mode 100644 index 0000000..53f41a0 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/di/AppModule.kt @@ -0,0 +1,39 @@ +package com.kassaev.notes.di + +import android.content.Context +import androidx.room.Room +import com.kassaev.notes.data.INoteDao +import com.kassaev.notes.data.NoteDatabase +import com.kassaev.notes.data.NoteRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun provideNoteRepository(noteDao: INoteDao): NoteRepository = NoteRepository(noteDao) + + @Provides + @Singleton + fun provideNoteDao(noteDatabase: NoteDatabase): INoteDao = noteDatabase.noteDao() + + @Singleton + @Provides + fun provideNoteDatabase(@ApplicationContext context: Context): NoteDatabase { + return Room + .databaseBuilder( + context = context, + NoteDatabase::class.java, + name = "note_db.db" + ) + .createFromAsset("database/note_db.db") + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/domain/FolderModel.kt b/app/src/main/java/com/kassaev/notes/domain/FolderModel.kt new file mode 100644 index 0000000..f46b698 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/domain/FolderModel.kt @@ -0,0 +1,16 @@ +package com.kassaev.notes.domain + +import com.kassaev.notes.data.FolderEntity + +data class FolderModel( + val folderId: Int, + val name: String +) { + + fun toEntity(): FolderEntity{ + return FolderEntity( + folderId = this.folderId, + name = this.name + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/domain/NoteModel.kt b/app/src/main/java/com/kassaev/notes/domain/NoteModel.kt new file mode 100644 index 0000000..341857f --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/domain/NoteModel.kt @@ -0,0 +1,27 @@ +package com.kassaev.notes.domain + +import com.kassaev.notes.data.NoteEntity + +data class NoteModel( + val noteId: Int, + var text: String, + var date: String, + var folderId: Int +) { + + constructor(text: String): this( + noteId = 0, + text = text, + date = "", + folderId = 0 + ) + + fun toEntity(): NoteEntity { + return NoteEntity( + noteId = this.noteId, + text = this.text, + date = System.currentTimeMillis(), + folderId = this.folderId + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/navigation/Navigation.kt b/app/src/main/java/com/kassaev/notes/navigation/Navigation.kt new file mode 100644 index 0000000..e1864cc --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/navigation/Navigation.kt @@ -0,0 +1,72 @@ +package com.kassaev.notes.navigation + +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.kassaev.notes.pesentation.MainViewModel +import com.kassaev.notes.pesentation.main_screen.MainScreen +import com.kassaev.notes.pesentation.main_screen.main_screen_detail.MainScreenDetail +import com.kassaev.notes.pesentation.main_screen.main_screen_detail.RecycleBinDetailScreen + +@Composable +fun Navigation() { + val navController: NavHostController = rememberNavController() + NavHost( + navController = navController, + startDestination = Screen.MainScreen.route, + enterTransition = { + EnterTransition.None + }, + exitTransition = { + ExitTransition.None + } + ) { + composable( + route = Screen.MainScreen.route + ) { + val viewModel: MainViewModel = hiltViewModel() + MainScreen( + navController = navController, + viewModel = viewModel + ) + } + composable( + route = Screen.MainScreenDetail.route + "?noteId={noteId}", + arguments = listOf( + navArgument("noteId"){ + nullable = true + defaultValue = null + } + ) + ){ backStackEntry -> + val viewModel: MainViewModel = hiltViewModel() + MainScreenDetail( + navController = navController, + viewModel = viewModel, + noteId = backStackEntry.arguments?.getString("noteId") + ) + } + composable( + route = Screen.RecycleBinDetailScreen.route + "/{noteId}", + arguments = listOf( + navArgument("noteId"){ + type = NavType.StringType + } + ) + ){backStackEntry -> + val viewModel: MainViewModel = hiltViewModel() + RecycleBinDetailScreen( + navController = navController, + viewModel = viewModel, + noteId = backStackEntry.arguments?.getString("noteId") + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/navigation/Screen.kt b/app/src/main/java/com/kassaev/notes/navigation/Screen.kt new file mode 100644 index 0000000..700f677 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/navigation/Screen.kt @@ -0,0 +1,15 @@ +package com.kassaev.notes.navigation + +sealed class Screen(val route: String) { + data object MainScreen: Screen("mainScreen") + data object MainScreenDetail: Screen("mainScreenDetail"){ + fun withArg(arg: String): String{ + return (this.route + "?noteId=$arg") + } + } + data object RecycleBinDetailScreen: Screen("recycleBinDetailScreen"){ + fun withArg(arg: String): String{ + return (this.route + "/$arg") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/pesentation/MainViewModel.kt b/app/src/main/java/com/kassaev/notes/pesentation/MainViewModel.kt new file mode 100644 index 0000000..d42fd29 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/pesentation/MainViewModel.kt @@ -0,0 +1,172 @@ +package com.kassaev.notes.pesentation + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kassaev.notes.data.NoteRepository +import com.kassaev.notes.domain.NoteModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor(private val repository: NoteRepository): ViewModel() { + + private val _noteList = MutableStateFlow(emptyList()) + private val _noteRecycleBinList = MutableStateFlow(emptyList()) + val noteList = _noteList.asStateFlow() + val noteRecycleBinList = _noteRecycleBinList.asStateFlow() + + //refactoring start + var openAlertDialogFAB by mutableStateOf(false) + var openAlertDialogLongClick by mutableStateOf(false) + var exitWithSave by mutableStateOf(true) + var noteIdToDelete: Int? = null + private var currentNote: NoteModel? = null + var textFieldValue by mutableStateOf("") + var selectedDrawerItem by mutableIntStateOf(0) + + suspend fun getNoteById(noteId: Int): NoteModel? = + viewModelScope.async { + currentNote = repository.getNoteById(noteId = noteId)?.toModel() + currentNote + }.await() + + fun createOrUpdateNote(){ + //Is TextField not blank? + if (textFieldValue.isBlank()){ + if (currentNote != null){ + deleteNote() + } + return + } + + //Update existing note + if (currentNote != null){ + //Is text changed for existing note? + if(currentNote!!.text != textFieldValue){ + currentNote?.text = textFieldValue + viewModelScope.launch{ + repository.createOrUpdateNote(currentNote!!.toEntity()) + } + } + }else{ //Or create new + viewModelScope.launch{ + currentNote = repository.createNote( + NoteModel(text = textFieldValue).toEntity() + ) + } + } + } + + fun moveNoteToRecycleBin(){ + textFieldValue.isNotBlank().also { + //Update existing note + currentNote?.let { + viewModelScope.launch{ + repository.createOrUpdateNote(currentNote!!.copy( + folderId = 1 + ).toEntity()) + } + } + } + } + + fun moveNoteToRecycleBinFomDialog(noteId: Int){ + //Update existing note + viewModelScope.launch{ + repository.createOrUpdateNote(getNoteById(noteId)!!.copy( + folderId = 1 + ).toEntity()) + } + } + + //refactoring end + + init { + setNoteList() + setNoteRecycleBinList() + } + fun setNoteList(){ + viewModelScope.launch{ + repository.getAllNotes(0).flowOn(Dispatchers.IO).collect{ noteListFromDB -> + _noteList.update { noteListInVM -> + noteListFromDB.map {noteEntity -> + noteEntity.toModel() + } + } + } + } + } + fun setNoteRecycleBinList(){ + viewModelScope.launch{ + repository.getAllNotes(1).flowOn(Dispatchers.IO).collect{ noteListFromDB -> + _noteRecycleBinList.update { noteListInVM -> + noteListFromDB.map {noteEntity -> + noteEntity.toModel() + } + } + } + } + } + fun restoreNote(){ + currentNote?.let { + viewModelScope.launch{ + repository.createOrUpdateNote(currentNote!!.copy( + folderId = 0 + ).toEntity()) + } + } + } + + fun deleteAllNotesFromRecycleBin(){ + viewModelScope.launch { + repository.deleteAllNotesFromRecycleBin() + } + } + + fun deleteNote(){ + viewModelScope.launch { + currentNote?.noteId?.let {noteId -> + repository.getNoteById(noteId)?.let { noteEntity -> + repository.deleteNote(noteEntity) + } + } + } + } + + fun deleteNote(noteId: Int){ + viewModelScope.launch { + repository.deleteNote(repository.getNoteById(noteId)!!) + } + } + + fun closeDialogLongClick(){ + openAlertDialogLongClick = false + } + + fun openDialogLongClick() { + openAlertDialogLongClick = true + } + + fun closeDialogFAB(){ + openAlertDialogFAB = false + } + + fun openDialogFAB() { + openAlertDialogFAB = true + } + + fun setNoteToDelete(noteId: Int){ + noteIdToDelete = noteId + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/pesentation/main_screen/MainScreen.kt b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/MainScreen.kt new file mode 100644 index 0000000..c3ca642 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/MainScreen.kt @@ -0,0 +1,16 @@ +package com.kassaev.notes.pesentation.main_screen + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import com.kassaev.notes.pesentation.MainViewModel + +@Composable +fun MainScreen( + navController: NavHostController, + viewModel: MainViewModel +){ + MainScreenNavigationDrawer( + navController = navController, + viewModel = viewModel + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/pesentation/main_screen/MainScreenNavigationDrawer.kt b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/MainScreenNavigationDrawer.kt new file mode 100644 index 0000000..aefd653 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/MainScreenNavigationDrawer.kt @@ -0,0 +1,88 @@ +package com.kassaev.notes.pesentation.main_screen + +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.kassaev.notes.R +import com.kassaev.notes.pesentation.MainViewModel +import com.kassaev.notes.pesentation.recycle_bin.RecycleBinScreenScaffold +import kotlinx.coroutines.launch + +@Composable +fun MainScreenNavigationDrawer( + navController: NavController, + viewModel: MainViewModel +) { + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope = rememberCoroutineScope() + + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet( + modifier = Modifier + .requiredWidth(220.dp) + ) { + NavigationDrawerItem( + label = { + Text(text = stringResource(id = R.string.app_name)) + }, + selected = viewModel.selectedDrawerItem == 0, + onClick = { + viewModel.selectedDrawerItem = 0 + scope.launch { + drawerState.close() + } + } + ) + NavigationDrawerItem( + label = { + Text(text = stringResource(id = R.string.recycle_bin)) + }, + selected = viewModel.selectedDrawerItem == 1, + onClick = { + viewModel.selectedDrawerItem = 1 + scope.launch { + drawerState.close() + } + } + ) + } + } + + ) { + when(viewModel.selectedDrawerItem){ + 0 -> MainScreenScaffold( + navController, + viewModel, + drawerState, + scope + ) + 1 -> RecycleBinScreenScaffold( + navController, + viewModel, + drawerState, + scope + ) + } + } +} + +@Composable +fun String.toLocale(): String { + return when(this){ + "main" -> stringResource(R.string.app_name) + "recycle_bin" -> stringResource(R.string.recycle_bin) + else -> {""} + } +} diff --git a/app/src/main/java/com/kassaev/notes/pesentation/main_screen/MainScreenScaffold.kt b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/MainScreenScaffold.kt new file mode 100644 index 0000000..1909fea --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/MainScreenScaffold.kt @@ -0,0 +1,199 @@ +package com.kassaev.notes.pesentation.main_screen + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Menu +import androidx.compose.material3.Card +import androidx.compose.material3.Divider +import androidx.compose.material3.DrawerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.navigation.NavController +import com.kassaev.notes.R +import com.kassaev.notes.navigation.Screen +import com.kassaev.notes.pesentation.MainViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class, + ExperimentalFoundationApi::class +) +@Composable +fun MainScreenScaffold( + navController: NavController, + viewModel: MainViewModel, + drawerState: DrawerState, + scope: CoroutineScope +) { + + val noteList by viewModel.noteList.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = stringResource(id = R.string.app_name)) + }, + navigationIcon = { + IconButton( + onClick = { + scope.launch { + drawerState.open() + } + } + ) { + Icon( + imageVector = Icons.Outlined.Menu, + contentDescription = "Menu" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { + navController.navigate(Screen.MainScreenDetail.route) + } + ) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = "Add Note" + ) + } + } + ) { paddingValues -> + DeleteConfirmDialog(viewModel = viewModel) + LazyColumn( + modifier = Modifier + .padding(paddingValues) + ) { + items(noteList){noteEntity -> + Column( + modifier = Modifier + .combinedClickable( + onClick = { + navController.navigate(Screen.MainScreenDetail.withArg(noteEntity.noteId.toString())) + }, + onLongClick = { + viewModel.openDialogLongClick() + viewModel.setNoteToDelete(noteEntity.noteId) + }, + ) + ) { + Text( + modifier = Modifier + .padding(8.dp), + text = noteEntity.text, + fontSize = MaterialTheme.typography.titleLarge.fontSize, + maxLines = 1, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + end = 8.dp, + bottom = 8.dp + ), + text = noteEntity.date.toString(), + textAlign = TextAlign.End, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + Divider() + } + } + } + } +} + +@Composable +fun DeleteConfirmDialog(viewModel: MainViewModel) { + + if (viewModel.openAlertDialogLongClick){ + Dialog( + onDismissRequest = { + viewModel.closeDialogLongClick() + } + ) { + Card( + modifier = Modifier, + shape = RoundedCornerShape(16.dp), + ) { + Column { + Text( + text = stringResource(id = R.string.delete_note), + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + textAlign = TextAlign.Center, + fontSize = MaterialTheme.typography.headlineLarge.fontSize + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 12.dp, + end = 12.dp, + bottom = 12.dp + ), + horizontalArrangement = Arrangement.SpaceAround + ) { + TextButton( + onClick = { + viewModel.closeDialogLongClick() + } + ) { + Text( + text = stringResource(id = R.string.delete_cancel), + fontSize = MaterialTheme.typography.titleLarge.fontSize, + color = MaterialTheme.colorScheme.secondary + ) + } + TextButton( + onClick = { + viewModel.noteIdToDelete?.let { viewModel.moveNoteToRecycleBinFomDialog(noteId = it) } + viewModel.closeDialogLongClick() + } + ) { + Text( + text = stringResource(id = R.string.delete_yes), + fontSize = MaterialTheme.typography.titleLarge.fontSize, + color = MaterialTheme.colorScheme.error + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/pesentation/main_screen/main_screen_detail/MainScreenDetail.kt b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/main_screen_detail/MainScreenDetail.kt new file mode 100644 index 0000000..573b7b4 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/main_screen_detail/MainScreenDetail.kt @@ -0,0 +1,18 @@ +package com.kassaev.notes.pesentation.main_screen.main_screen_detail + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import com.kassaev.notes.pesentation.MainViewModel + +@Composable +fun MainScreenDetail( + navController: NavHostController, + viewModel: MainViewModel, + noteId: String? +) { + MainScreenDetailScaffold( + navController, + viewModel, + noteId + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/pesentation/main_screen/main_screen_detail/MainScreenDetailScaffold.kt b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/main_screen_detail/MainScreenDetailScaffold.kt new file mode 100644 index 0000000..65d340c --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/pesentation/main_screen/main_screen_detail/MainScreenDetailScaffold.kt @@ -0,0 +1,124 @@ +package com.kassaev.notes.pesentation.main_screen.main_screen_detail + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import androidx.navigation.NavHostController +import com.kassaev.notes.pesentation.MainViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreenDetailScaffold( + navController: NavHostController, + viewModel: MainViewModel, + noteId: String? +) { + val focusRequester = remember { FocusRequester() } + + LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) { + if(viewModel.exitWithSave){ + viewModel.createOrUpdateNote() + } + } + + LaunchedEffect(Unit) { + if(noteId == null){ + focusRequester.requestFocus() + }else{ + viewModel.getNoteById(noteId = noteId.toInt())?.let {noteModel -> + viewModel.textFieldValue = noteModel.text + } + } + } + + BackHandler { + navController.popBackStack() + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = "", + ) + }, + navigationIcon = { + IconButton( + onClick = { + navController.popBackStack() + } + ) { + Icon( + imageVector = Icons.Outlined.ArrowBack, + contentDescription = "Back To Main Screen", + tint = MaterialTheme.colorScheme.primary + ) + } + }, + actions = { + IconButton( + onClick = { + viewModel.exitWithSave = false + viewModel.moveNoteToRecycleBin() + navController.popBackStack() + } + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Delete Note", + tint = MaterialTheme.colorScheme.primary + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ) + ) + } + ) {paddingValues -> + TextField( + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Unspecified, + focusedContainerColor = Color.Unspecified, + + ), + modifier = Modifier + .focusRequester( + focusRequester = focusRequester + ) + .fillMaxSize() + .padding(paddingValues), + value = viewModel.textFieldValue, + onValueChange = { + viewModel.textFieldValue = it + }, + textStyle = TextStyle.Default.copy( + fontSize = MaterialTheme.typography.titleLarge.fontSize + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/pesentation/recycle_bin/RecycleBinScreenScaffold.kt b/app/src/main/java/com/kassaev/notes/pesentation/recycle_bin/RecycleBinScreenScaffold.kt new file mode 100644 index 0000000..ebfa6d1 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/pesentation/recycle_bin/RecycleBinScreenScaffold.kt @@ -0,0 +1,269 @@ +package com.kassaev.notes.pesentation.recycle_bin + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Menu +import androidx.compose.material3.Card +import androidx.compose.material3.Divider +import androidx.compose.material3.DrawerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.navigation.NavController +import com.kassaev.notes.R +import com.kassaev.notes.navigation.Screen +import com.kassaev.notes.pesentation.MainViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class, + ExperimentalFoundationApi::class +) +@Composable +fun RecycleBinScreenScaffold( + navController: NavController, + viewModel: MainViewModel, + drawerState: DrawerState, + scope: CoroutineScope +) { + + val noteList by viewModel.noteRecycleBinList.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = stringResource(id = R.string.recycle_bin)) + }, + navigationIcon = { + IconButton( + onClick = { + scope.launch { + drawerState.open() + } + } + ) { + Icon( + imageVector = Icons.Outlined.Menu, + contentDescription = "Menu" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { + if (noteList.isNotEmpty()){ + viewModel.openDialogFAB() + } + } + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Delete All Notes" + ) + } + } + ) { paddingValues -> + RecycleBinDeleteConfirmDialog(viewModel = viewModel) + RecycleBinDeleteAllConfirmDialog(viewModel = viewModel) + LazyColumn( + modifier = Modifier + .padding(paddingValues) + ) { + items(noteList){noteEntity -> + Column( + modifier = Modifier + .combinedClickable( + onClick = { + navController.navigate(Screen.RecycleBinDetailScreen.withArg(noteEntity.noteId.toString())) + }, + onLongClick = { + viewModel.openDialogLongClick() + viewModel.setNoteToDelete(noteEntity.noteId) + }, + ) + ) { + Text( + modifier = Modifier + .padding(8.dp), + text = noteEntity.text, + fontSize = MaterialTheme.typography.titleLarge.fontSize, + maxLines = 1, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + end = 8.dp, + bottom = 8.dp + ), + text = noteEntity.date.toString(), + textAlign = TextAlign.End, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + Divider() + } + } + } + } +} + +@Composable +fun RecycleBinDeleteConfirmDialog(viewModel: MainViewModel) { + + if (viewModel.openAlertDialogLongClick){ + Dialog( + onDismissRequest = { + viewModel.closeDialogLongClick() + } + ) { + Card( + modifier = Modifier, + shape = RoundedCornerShape(16.dp), + ) { + Column { + Text( + text = stringResource(id = R.string.delete_note), + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + textAlign = TextAlign.Center, + fontSize = MaterialTheme.typography.headlineLarge.fontSize + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 12.dp, + end = 12.dp, + bottom = 12.dp + ), + horizontalArrangement = Arrangement.SpaceAround + ) { + TextButton( + onClick = { + viewModel.closeDialogLongClick() + } + ) { + Text( + text = stringResource(id = R.string.delete_cancel), + fontSize = MaterialTheme.typography.titleLarge.fontSize, + color = MaterialTheme.colorScheme.secondary + ) + } + TextButton( + onClick = { + viewModel.noteIdToDelete?.let { viewModel.deleteNote(noteId = it) } + viewModel.closeDialogLongClick() + } + ) { + Text( + text = stringResource(id = R.string.delete_yes), + fontSize = MaterialTheme.typography.titleLarge.fontSize, + color = MaterialTheme.colorScheme.error + ) + } + } + } + } + } + } +} + +@Composable +fun RecycleBinDeleteAllConfirmDialog(viewModel: MainViewModel) { + + if (viewModel.openAlertDialogFAB){ + Dialog( + onDismissRequest = { + viewModel.closeDialogFAB() + } + ) { + Card( + modifier = Modifier, + shape = RoundedCornerShape(16.dp), + ) { + Column { + Text( + text = stringResource(id = R.string.delete_all_question), + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + textAlign = TextAlign.Center, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + style = LocalTextStyle.current.copy( + lineHeight = 32.sp + ) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 12.dp, + end = 12.dp, + bottom = 12.dp + ), + horizontalArrangement = Arrangement.SpaceAround + ) { + TextButton( + onClick = { + viewModel.closeDialogFAB() + } + ) { + Text( + text = stringResource(id = R.string.delete_cancel), + fontSize = MaterialTheme.typography.titleLarge.fontSize, + color = MaterialTheme.colorScheme.secondary + ) + } + TextButton( + onClick = { + viewModel.deleteAllNotesFromRecycleBin() + viewModel.closeDialogFAB() + } + ) { + Text( + text = stringResource(id = R.string.delete_yes), + fontSize = MaterialTheme.typography.titleLarge.fontSize, + color = MaterialTheme.colorScheme.error + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/pesentation/recycle_bin/recycle_bin_screen_detail/RecycleBinDetailScreen.kt b/app/src/main/java/com/kassaev/notes/pesentation/recycle_bin/recycle_bin_screen_detail/RecycleBinDetailScreen.kt new file mode 100644 index 0000000..194d6b8 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/pesentation/recycle_bin/recycle_bin_screen_detail/RecycleBinDetailScreen.kt @@ -0,0 +1,18 @@ +package com.kassaev.notes.pesentation.main_screen.main_screen_detail + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import com.kassaev.notes.pesentation.MainViewModel + +@Composable +fun RecycleBinDetailScreen( + navController: NavHostController, + viewModel: MainViewModel, + noteId: String? +) { + RecycleBinDetailScreenScaffold( + navController, + viewModel, + noteId + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/pesentation/recycle_bin/recycle_bin_screen_detail/RecycleBinDetailScreenScaffold.kt b/app/src/main/java/com/kassaev/notes/pesentation/recycle_bin/recycle_bin_screen_detail/RecycleBinDetailScreenScaffold.kt new file mode 100644 index 0000000..45f5e8e --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/pesentation/recycle_bin/recycle_bin_screen_detail/RecycleBinDetailScreenScaffold.kt @@ -0,0 +1,140 @@ +package com.kassaev.notes.pesentation.main_screen.main_screen_detail + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import androidx.navigation.NavHostController +import com.kassaev.notes.R +import com.kassaev.notes.navigation.Screen +import com.kassaev.notes.pesentation.MainViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RecycleBinDetailScreenScaffold( + navController: NavHostController, + viewModel: MainViewModel, + noteId: String? +) { + val focusRequester = remember { FocusRequester() } + + LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) { +// if(viewModel.exitWithSave){ +// viewModel.createOrUpdateNote() +// } + } + + LaunchedEffect(Unit) { + if(noteId == null){ + focusRequester.requestFocus() + }else{ + viewModel.getNoteById(noteId = noteId.toInt())?.let {noteModel -> + viewModel.textFieldValue = noteModel.text + } + } + } + + BackHandler { + navController.popBackStack() + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = "", + ) + }, + navigationIcon = { + IconButton( + onClick = { + navController.popBackStack() + } + ) { + Icon( + imageVector = Icons.Outlined.ArrowBack, + contentDescription = "Back To Main Screen", + tint = MaterialTheme.colorScheme.primary + ) + } + }, + actions = { + IconButton( + onClick = { + viewModel.restoreNote() + navController.popBackStack() + } + ) { + Icon( + painter = painterResource(id = R.drawable.restore_from_trash), + contentDescription = "Restore Note", + tint = MaterialTheme.colorScheme.primary + ) + } + IconButton( + onClick = { + viewModel.deleteNote() + navController.popBackStack() + } + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Delete Note", + tint = MaterialTheme.colorScheme.primary + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ) + ) + } + ) {paddingValues -> + TextField( + readOnly = true, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Unspecified, + focusedContainerColor = Color.Unspecified, + + ), + modifier = Modifier + .focusRequester( + focusRequester = focusRequester + ) + .fillMaxSize() + .padding(paddingValues), + value = viewModel.textFieldValue, + onValueChange = { + viewModel.textFieldValue = it + }, + textStyle = TextStyle.Default.copy( + fontSize = MaterialTheme.typography.titleLarge.fontSize + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/ui/theme/Color.kt b/app/src/main/java/com/kassaev/notes/ui/theme/Color.kt new file mode 100644 index 0000000..8601bb2 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/ui/theme/Color.kt @@ -0,0 +1,12 @@ +package com.kassaev.notes.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + diff --git a/app/src/main/java/com/kassaev/notes/ui/theme/Theme.kt b/app/src/main/java/com/kassaev/notes/ui/theme/Theme.kt new file mode 100644 index 0000000..90d39d2 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package com.kassaev.notes.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun NotesTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/kassaev/notes/ui/theme/Type.kt b/app/src/main/java/com/kassaev/notes/ui/theme/Type.kt new file mode 100644 index 0000000..44a0a58 --- /dev/null +++ b/app/src/main/java/com/kassaev/notes/ui/theme/Type.kt @@ -0,0 +1,33 @@ +package com.kassaev.notes.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + // Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/logo_square.xml b/app/src/main/res/drawable/logo_square.xml new file mode 100644 index 0000000..5d03db2 --- /dev/null +++ b/app/src/main/res/drawable/logo_square.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/app/src/main/res/drawable/restore_from_trash.xml b/app/src/main/res/drawable/restore_from_trash.xml new file mode 100644 index 0000000..7492dc0 --- /dev/null +++ b/app/src/main/res/drawable/restore_from_trash.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..24cb48d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..4920742 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..17366a7 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..75778c8 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..210bb31 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..83ed46f Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..4479edf Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..3acd1af Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..52dac81 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..4da7059 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..61f43dd Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..8aeac35 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..e71e69c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..0f6b39a Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..02de061 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..ff256c1 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,9 @@ + + + Блокнот + Удаленные + Удалить? + Удалить + Отмена + Удалить Все Заметки? + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..cb15ddf --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #D0BCFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..37d24c5 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + Notes + Recycle bin + Delete? + Delete + Cancel + Delete All Notes? + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..c57cef2 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +