diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 00000000..165d9c32 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 00000000..b268ef36 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 4e3844e8..ecd50f02 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,18 +1,19 @@ + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..28422375 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 860da66a..d5d35ec4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 00000000..16660f1d --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/4/9/49714c180cf12e6bb0fb0f86b8a2e6b52e75ed1d b/.idea/sonarlint/issuestore/4/9/49714c180cf12e6bb0fb0f86b8a2e6b52e75ed1d new file mode 100644 index 00000000..6f1e916c --- /dev/null +++ b/.idea/sonarlint/issuestore/4/9/49714c180cf12e6bb0fb0f86b8a2e6b52e75ed1d @@ -0,0 +1,2 @@ + +O kotlin:S1172"2Remove this unused function parameter "darkTheme".(�������� \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/5/d/5dd0036e6eaabc1cbb2545b80a0bfffc2708a45b b/.idea/sonarlint/issuestore/5/d/5dd0036e6eaabc1cbb2545b80a0bfffc2708a45b new file mode 100644 index 00000000..e69de29b diff --git a/.idea/sonarlint/issuestore/9/e/9ea6805d7eed655f05d5c8957775a6e66211bf8e b/.idea/sonarlint/issuestore/9/e/9ea6805d7eed655f05d5c8957775a6e66211bf8e new file mode 100644 index 00000000..e69de29b diff --git a/.idea/sonarlint/issuestore/f/0/f07866736216be0ee2aba49e392191aeae700a35 b/.idea/sonarlint/issuestore/f/0/f07866736216be0ee2aba49e392191aeae700a35 new file mode 100644 index 00000000..e69de29b diff --git a/.idea/sonarlint/issuestore/f/4/f4a01d6a4fcb971362ec00a83903fd3902f52164 b/.idea/sonarlint/issuestore/f/4/f4a01d6a4fcb971362ec00a83903fd3902f52164 new file mode 100644 index 00000000..e69de29b diff --git a/.idea/sonarlint/issuestore/index.pb b/.idea/sonarlint/issuestore/index.pb index e69de29b..003d1499 100644 --- a/.idea/sonarlint/issuestore/index.pb +++ b/.idea/sonarlint/issuestore/index.pb @@ -0,0 +1,11 @@ + +y +Iapp/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Shape.kt,9\e\9ea6805d7eed655f05d5c8957775a6e66211bf8e +@ +app/build.gradle,f\4\f4a01d6a4fcb971362ec00a83903fd3902f52164 +< + build.gradle,f\0\f07866736216be0ee2aba49e392191aeae700a35 +y +Iapp/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Theme.kt,4\9\49714c180cf12e6bb0fb0f86b8a2e6b52e75ed1d +y +Iapp/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Color.kt,5\d\5dd0036e6eaabc1cbb2545b80a0bfffc2708a45b \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 5ec04ab2..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,90 +0,0 @@ -plugins { - id 'com.android.application' - id 'kotlin-android' - id 'kotlin-kapt' - id 'dagger.hilt.android.plugin' -} - -android { - compileSdk 31 - - defaultConfig { - applicationId "com.plcoding.cleanarchitecturenoteapp" - minSdk 21 - targetSdk 31 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary true - } - } - - buildTypes { - release { - minifyEnabled 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' - useIR = true - } - buildFeatures { - compose true - } - composeOptions { - kotlinCompilerExtensionVersion compose_version - kotlinCompilerVersion '1.5.21' - } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } -} - -dependencies { - - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' - implementation "androidx.compose.ui:ui:$compose_version" - implementation "androidx.compose.material:material:$compose_version" - implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.activity:activity-compose:1.3.1' - testImplementation 'junit:junit:4.+' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" - debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" - - // Compose dependencies - implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0-beta01" - implementation "androidx.navigation:navigation-compose:2.4.0-alpha09" - implementation "androidx.compose.material:material-icons-extended:$compose_version" - implementation "androidx.hilt:hilt-navigation-compose:1.0.0-alpha03" - - // Coroutines - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1' - - //Dagger - Hilt - implementation "com.google.dagger:hilt-android:2.38.1" - kapt "com.google.dagger:hilt-android-compiler:2.37" - implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03" - kapt "androidx.hilt:hilt-compiler:1.0.0" - - // Room - implementation "androidx.room:room-runtime:2.3.0" - kapt "androidx.room:room-compiler:2.3.0" - - // Kotlin Extensions and Coroutines support for Room - implementation "androidx.room:room-ktx:2.3.0" -} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..c121e9f1 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,87 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + id("kotlin-kapt") + id("com.google.dagger.hilt.android") +} + +android { + namespace = "com.plcoding.cleanarchitecturenoteapp" + compileSdk = 35 + + defaultConfig { + applicationId = "com.plcoding.cleanarchitecturenoteapp" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = 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) + 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) + + // Compose dependencies + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0-beta01") + implementation("androidx.navigation:navigation-compose:2.8.3") + implementation("androidx.compose.material:material-icons-extended:1.7.5") + implementation("androidx.hilt:hilt-navigation-compose:1.0.0-alpha03") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1") + + //Dagger - Hilt + implementation("com.google.dagger:hilt-android:2.49") + kapt("com.google.dagger:hilt-android-compiler:2.49") + kapt("androidx.hilt:hilt-compiler:1.0.0") + + // Room + implementation("androidx.room:room-runtime:2.6.1") + kapt("androidx.room:room-compiler:2.6.1") + + // Kotlin Extensions and Coroutines support for Room + implementation("androidx.room:room-ktx:2.6.1") +} + +kapt { + correctErrorTypes = true +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb434..ff59496d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 052e8301..88340904 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.plcoding.cleanarchitecturenoteapp"> diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/MainActivity.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/MainActivity.kt deleted file mode 100644 index 8282fe24..00000000 --- a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/MainActivity.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.plcoding.cleanarchitecturenoteapp - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview -import com.plcoding.cleanarchitecturenoteapp.ui.theme.CleanArchitectureNoteAppTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - CleanArchitectureNoteAppTheme { - - } - } - } -} diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/NoteApp.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/NoteApp.kt new file mode 100644 index 00000000..ec915d87 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/NoteApp.kt @@ -0,0 +1,7 @@ +package com.plcoding.cleanarchitecturenoteapp + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class NoteApp : Application() \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/di/AppModule.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/di/AppModule.kt new file mode 100644 index 00000000..92660b5f --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/di/AppModule.kt @@ -0,0 +1,45 @@ +package com.plcoding.cleanarchitecturenoteapp.di + +import android.app.Application +import androidx.room.Room +import com.plcoding.cleanarchitecturenoteapp.feature_note.data.data_source.NoteDatabase +import com.plcoding.cleanarchitecturenoteapp.feature_note.data.repository.NoteRepositoryImpl +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.repository.NoteRepository +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.use_case.* +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun provideNoteDatabase(app: Application): NoteDatabase { + return Room.databaseBuilder( + app, + NoteDatabase::class.java, + NoteDatabase.DATABASE_NAME + ).build() + } + + @Provides + @Singleton + fun provideNoteRepository(db: NoteDatabase): NoteRepository { + return NoteRepositoryImpl(db.noteDao) + } + + @Provides + @Singleton + fun provideNoteUseCases(repository: NoteRepository): NoteUseCases { + return NoteUseCases( + getNotes = GetNotes(repository), + deleteNote = DeleteNote(repository), + addNote = AddNote(repository), + getNote = GetNote(repository) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/data/data_source/NoteDao.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/data/data_source/NoteDao.kt new file mode 100644 index 00000000..fdcb3405 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/data/data_source/NoteDao.kt @@ -0,0 +1,21 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.data.data_source + +import androidx.room.* +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import kotlinx.coroutines.flow.Flow + +@Dao +interface NoteDao { + + @Query("SELECT * FROM note") + fun getNotes(): Flow> + + @Query("SELECT * FROM note WHERE id = :id") + suspend fun getNoteById(id: Int): Note? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertNote(note: Note) + + @Delete + suspend fun deleteNote(note: Note) +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/data/data_source/NoteDatabase.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/data/data_source/NoteDatabase.kt new file mode 100644 index 00000000..4dd871af --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/data/data_source/NoteDatabase.kt @@ -0,0 +1,18 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.data.data_source + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note + +@Database( + entities = [Note::class], + version = 1 +) +abstract class NoteDatabase: RoomDatabase() { + + abstract val noteDao: NoteDao + + companion object { + const val DATABASE_NAME = "notes_db" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/data/repository/NoteRepositoryImpl.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/data/repository/NoteRepositoryImpl.kt new file mode 100644 index 00000000..39d93125 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/data/repository/NoteRepositoryImpl.kt @@ -0,0 +1,27 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.data.repository + +import com.plcoding.cleanarchitecturenoteapp.feature_note.data.data_source.NoteDao +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.repository.NoteRepository +import kotlinx.coroutines.flow.Flow + +class NoteRepositoryImpl( + private val dao: NoteDao +) : NoteRepository { + + override fun getNotes(): Flow> { + return dao.getNotes() + } + + override suspend fun getNoteById(id: Int): Note? { + return dao.getNoteById(id) + } + + override suspend fun insertNote(note: Note) { + dao.insertNote(note) + } + + override suspend fun deleteNote(note: Note) { + dao.deleteNote(note) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/model/Note.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/model/Note.kt new file mode 100644 index 00000000..b7473f7e --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/model/Note.kt @@ -0,0 +1,20 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.plcoding.cleanarchitecturenoteapp.ui.theme.* + +@Entity(tableName = "note") +data class Note( + val title: String, + val content: String, + val timestamp: Long, + val color: Int, + @PrimaryKey val id: Int? = null +) { + companion object { + val noteColors = listOf(RedOrange, LightGreen, Violet, BabyBlue, RedPink) + } +} + +class InvalidNoteException(message: String): Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/repository/NoteRepository.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/repository/NoteRepository.kt new file mode 100644 index 00000000..471b748a --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/repository/NoteRepository.kt @@ -0,0 +1,15 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.domain.repository + +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import kotlinx.coroutines.flow.Flow + +interface NoteRepository { + + fun getNotes(): Flow> + + suspend fun getNoteById(id: Int): Note? + + suspend fun insertNote(note: Note) + + suspend fun deleteNote(note: Note) +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/AddNote.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/AddNote.kt new file mode 100644 index 00000000..5e243dff --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/AddNote.kt @@ -0,0 +1,21 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.domain.use_case + +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.InvalidNoteException +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.repository.NoteRepository + +class AddNote( + private val repository: NoteRepository +) { + + @Throws(InvalidNoteException::class) + suspend operator fun invoke(note: Note) { + if(note.title.isBlank()) { + throw InvalidNoteException("The title of the note can't be empty.") + } + if(note.content.isBlank()) { + throw InvalidNoteException("The content of the note can't be empty.") + } + repository.insertNote(note) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/DeleteNote.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/DeleteNote.kt new file mode 100644 index 00000000..e57acdfe --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/DeleteNote.kt @@ -0,0 +1,13 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.domain.use_case + +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.repository.NoteRepository + +class DeleteNote( + private val repository: NoteRepository +) { + + suspend operator fun invoke(note: Note) { + repository.deleteNote(note) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/GetNote.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/GetNote.kt new file mode 100644 index 00000000..84e8978c --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/GetNote.kt @@ -0,0 +1,13 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.domain.use_case + +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.repository.NoteRepository + +class GetNote( + private val repository: NoteRepository +) { + + suspend operator fun invoke(id: Int): Note? { + return repository.getNoteById(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/GetNotes.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/GetNotes.kt new file mode 100644 index 00000000..ac94e18f --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/GetNotes.kt @@ -0,0 +1,36 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.domain.use_case + +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.repository.NoteRepository +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util.NoteOrder +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util.OrderType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetNotes( + private val repository: NoteRepository +) { + + operator fun invoke( + noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending) + ): Flow> { + return repository.getNotes().map { notes -> + when(noteOrder.orderType) { + is OrderType.Ascending -> { + when(noteOrder) { + is NoteOrder.Title -> notes.sortedBy { it.title.lowercase() } + is NoteOrder.Date -> notes.sortedBy { it.timestamp } + is NoteOrder.Color -> notes.sortedBy { it.color } + } + } + is OrderType.Descending -> { + when(noteOrder) { + is NoteOrder.Title -> notes.sortedByDescending { it.title.lowercase() } + is NoteOrder.Date -> notes.sortedByDescending { it.timestamp } + is NoteOrder.Color -> notes.sortedByDescending { it.color } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/NoteUseCases.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/NoteUseCases.kt new file mode 100644 index 00000000..a89b00c1 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/use_case/NoteUseCases.kt @@ -0,0 +1,8 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.domain.use_case + +data class NoteUseCases( + val getNotes: GetNotes, + val deleteNote: DeleteNote, + val addNote: AddNote, + val getNote: GetNote +) diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/util/NoteOrder.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/util/NoteOrder.kt new file mode 100644 index 00000000..eb7fa913 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/util/NoteOrder.kt @@ -0,0 +1,15 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util + +sealed class NoteOrder(val orderType: OrderType) { + class Title(orderType: OrderType): NoteOrder(orderType) + class Date(orderType: OrderType): NoteOrder(orderType) + class Color(orderType: OrderType): NoteOrder(orderType) + + fun copy(orderType: OrderType): NoteOrder { + return when(this) { + is Title -> Title(orderType) + is Date -> Date(orderType) + is Color -> Color(orderType) + } + } +} diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/util/OrderType.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/util/OrderType.kt new file mode 100644 index 00000000..de897a19 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/domain/util/OrderType.kt @@ -0,0 +1,6 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util + +sealed class OrderType { + object Ascending: OrderType() + object Descending: OrderType() +} diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/MainActivity.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/MainActivity.kt new file mode 100644 index 00000000..dd194689 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/MainActivity.kt @@ -0,0 +1,67 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.plcoding.cleanarchitecturenoteapp.feature_note.presentation.add_edit_note.AddEditNoteScreen +import com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes.NotesScreen +import com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.util.Screen +import com.plcoding.cleanarchitecturenoteapp.ui.theme.CleanArchitectureNoteAppTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + @ExperimentalAnimationApi + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + CleanArchitectureNoteAppTheme { + Surface( + color = MaterialTheme.colorScheme.background + ) { + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = Screen.NotesScreen.route + ) { + composable(route = Screen.NotesScreen.route) { + NotesScreen(navController = navController) + } + composable( + route = Screen.AddEditNoteScreen.route + + "?noteId={noteId}¬eColor={noteColor}", + arguments = listOf( + navArgument( + name = "noteId" + ) { + type = NavType.IntType + defaultValue = -1 + }, + navArgument( + name = "noteColor" + ) { + type = NavType.IntType + defaultValue = -1 + }, + ) + ) { + val color = it.arguments?.getInt("noteColor") ?: -1 + AddEditNoteScreen( + navController = navController, + noteColor = color + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/AddEditNoteEvent.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/AddEditNoteEvent.kt new file mode 100644 index 00000000..84c80676 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/AddEditNoteEvent.kt @@ -0,0 +1,13 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.add_edit_note + +import androidx.compose.ui.focus.FocusState + +sealed class AddEditNoteEvent{ + data class EnteredTitle(val value: String): AddEditNoteEvent() + data class ChangeTitleFocus(val focusState: FocusState): AddEditNoteEvent() + data class EnteredContent(val value: String): AddEditNoteEvent() + data class ChangeContentFocus(val focusState: FocusState): AddEditNoteEvent() + data class ChangeColor(val color: Int): AddEditNoteEvent() + object SaveNote: AddEditNoteEvent() +} + diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/AddEditNoteScreen.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/AddEditNoteScreen.kt new file mode 100644 index 00000000..a8542ce0 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/AddEditNoteScreen.kt @@ -0,0 +1,147 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.add_edit_note + +import androidx.compose.animation.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Save +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.add_edit_note.components.TransparentHintTextField +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@Composable +fun AddEditNoteScreen( + navController: NavController, + noteColor: Int, + viewModel: AddEditNoteViewModel = hiltViewModel() +) { + val titleState = viewModel.noteTitle.value + val contentState = viewModel.noteContent.value + + val snackbarHostState = remember { SnackbarHostState() } + + val noteBackgroundAnimatable = remember { + Animatable( + Color(if (noteColor != -1) noteColor else viewModel.noteColor.value) + ) + } + val scope = rememberCoroutineScope() + + LaunchedEffect(key1 = true) { + viewModel.eventFlow.collectLatest { event -> + when(event) { + is AddEditNoteViewModel.UiEvent.ShowSnackbar -> { + snackbarHostState.showSnackbar( + message = event.message + ) + } + is AddEditNoteViewModel.UiEvent.SaveNote -> { + navController.navigateUp() + } + } + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + floatingActionButton = { + FloatingActionButton( + onClick = { + viewModel.onEvent(AddEditNoteEvent.SaveNote) + }, + containerColor = MaterialTheme.colorScheme.primary + ) { + Icon(imageVector = Icons.Default.Save, contentDescription = "Save note") + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(noteBackgroundAnimatable.value) + .padding(paddingValues) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Note.noteColors.forEach { color -> + val colorInt = color.toArgb() + Box( + modifier = Modifier + .size(50.dp) + .shadow(15.dp, CircleShape) + .clip(CircleShape) + .background(color) + .border( + width = 3.dp, + color = if (viewModel.noteColor.value == colorInt) { + Color.Black + } else Color.Transparent, + shape = CircleShape + ) + .clickable { + scope.launch { + noteBackgroundAnimatable.animateTo( + targetValue = Color(colorInt), + animationSpec = tween( + durationMillis = 500 + ) + ) + } + viewModel.onEvent(AddEditNoteEvent.ChangeColor(colorInt)) + } + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + TransparentHintTextField( + text = titleState.text, + hint = titleState.hint, + onValueChange = { + viewModel.onEvent(AddEditNoteEvent.EnteredTitle(it)) + }, + onFocusChange = { + viewModel.onEvent(AddEditNoteEvent.ChangeTitleFocus(it)) + }, + isHintVisible = titleState.isHintVisible, + singleLine = true, + textStyle = MaterialTheme.typography.headlineSmall + ) + Spacer(modifier = Modifier.height(16.dp)) + TransparentHintTextField( + text = contentState.text, + hint = contentState.hint, + onValueChange = { + viewModel.onEvent(AddEditNoteEvent.EnteredContent(it)) + }, + onFocusChange = { + viewModel.onEvent(AddEditNoteEvent.ChangeContentFocus(it)) + }, + isHintVisible = contentState.isHintVisible, + textStyle = MaterialTheme.typography.bodyLarge, + modifier = Modifier.fillMaxHeight() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/AddEditNoteViewModel.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/AddEditNoteViewModel.kt new file mode 100644 index 00000000..147aecf4 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/AddEditNoteViewModel.kt @@ -0,0 +1,119 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.add_edit_note + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.toArgb +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.InvalidNoteException +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.use_case.NoteUseCases +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AddEditNoteViewModel @Inject constructor( + private val noteUseCases: NoteUseCases, + savedStateHandle: SavedStateHandle +) : ViewModel() { + + private val _noteTitle = mutableStateOf(NoteTextFieldState( + hint = "Enter title..." + )) + val noteTitle: State = _noteTitle + + private val _noteContent = mutableStateOf(NoteTextFieldState( + hint = "Enter some content" + )) + val noteContent: State = _noteContent + + private val _noteColor = mutableStateOf(Note.noteColors.random().toArgb()) + val noteColor: State = _noteColor + + private val _eventFlow = MutableSharedFlow() + val eventFlow = _eventFlow.asSharedFlow() + + private var currentNoteId: Int? = null + + init { + savedStateHandle.get("noteId")?.let { noteId -> + if(noteId != -1) { + viewModelScope.launch { + noteUseCases.getNote(noteId)?.also { note -> + currentNoteId = note.id + _noteTitle.value = noteTitle.value.copy( + text = note.title, + isHintVisible = false + ) + _noteContent.value = _noteContent.value.copy( + text = note.content, + isHintVisible = false + ) + _noteColor.value = note.color + } + } + } + } + } + + fun onEvent(event: AddEditNoteEvent) { + when(event) { + is AddEditNoteEvent.EnteredTitle -> { + _noteTitle.value = noteTitle.value.copy( + text = event.value + ) + } + is AddEditNoteEvent.ChangeTitleFocus -> { + _noteTitle.value = noteTitle.value.copy( + isHintVisible = !event.focusState.isFocused && + noteTitle.value.text.isBlank() + ) + } + is AddEditNoteEvent.EnteredContent -> { + _noteContent.value = _noteContent.value.copy( + text = event.value + ) + } + is AddEditNoteEvent.ChangeContentFocus -> { + _noteContent.value = _noteContent.value.copy( + isHintVisible = !event.focusState.isFocused && + _noteContent.value.text.isBlank() + ) + } + is AddEditNoteEvent.ChangeColor -> { + _noteColor.value = event.color + } + is AddEditNoteEvent.SaveNote -> { + viewModelScope.launch { + try { + noteUseCases.addNote( + Note( + title = noteTitle.value.text, + content = noteContent.value.text, + timestamp = System.currentTimeMillis(), + color = noteColor.value, + id = currentNoteId + ) + ) + _eventFlow.emit(UiEvent.SaveNote) + } catch(e: InvalidNoteException) { + _eventFlow.emit( + UiEvent.ShowSnackbar( + message = e.message ?: "Couldn't save note" + ) + ) + } + } + } + } + } + + sealed class UiEvent { + data class ShowSnackbar(val message: String): UiEvent() + object SaveNote: UiEvent() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/NoteTextFieldState.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/NoteTextFieldState.kt new file mode 100644 index 00000000..6a488bca --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/NoteTextFieldState.kt @@ -0,0 +1,7 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.add_edit_note + +data class NoteTextFieldState( + val text: String = "", + val hint: String = "", + val isHintVisible: Boolean = true +) diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/components/TransparentHintTextField.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/components/TransparentHintTextField.kt new file mode 100644 index 00000000..77f262e1 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/add_edit_note/components/TransparentHintTextField.kt @@ -0,0 +1,43 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.add_edit_note.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusState +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle + +@Composable +fun TransparentHintTextField( + text: String, + hint: String, + modifier: Modifier = Modifier, + isHintVisible: Boolean = true, + onValueChange: (String) -> Unit, + textStyle: TextStyle = TextStyle(), + singleLine: Boolean = false, + onFocusChange: (FocusState) -> Unit +) { + Box( + modifier = modifier + ) { + BasicTextField( + value = text, + onValueChange = onValueChange, + singleLine = singleLine, + textStyle = textStyle, + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { + onFocusChange(it) + } + ) + if(isHintVisible) { + Text(text = hint, style = textStyle, color = Color.DarkGray) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesEvent.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesEvent.kt new file mode 100644 index 00000000..624c0634 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesEvent.kt @@ -0,0 +1,11 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes + +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util.NoteOrder + +sealed class NotesEvent { + data class Order(val noteOrder: NoteOrder): NotesEvent() + data class DeleteNote(val note: Note): NotesEvent() + object RestoreNote: NotesEvent() + object ToggleOrderSection: NotesEvent() +} diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesScreen.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesScreen.kt new file mode 100644 index 00000000..08a547f5 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesScreen.kt @@ -0,0 +1,119 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes + +import androidx.compose.animation.* +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Sort +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes.components.NoteItem +import com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes.components.OrderSection +import com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.util.Screen +import kotlinx.coroutines.launch + +@ExperimentalAnimationApi +@Composable +fun NotesScreen( + navController: NavController, + viewModel: NotesViewModel = hiltViewModel() +) { + val state = viewModel.state.value + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + floatingActionButton = { + FloatingActionButton( + onClick = { + navController.navigate(Screen.AddEditNoteScreen.route) + }, + containerColor = MaterialTheme.colorScheme.primary + ) { + Icon(imageVector = Icons.Default.Add, contentDescription = "Add note") + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Your note", + style = MaterialTheme.typography.headlineMedium + ) + IconButton( + onClick = { + viewModel.onEvent(NotesEvent.ToggleOrderSection) + }, + ) { + Icon( + imageVector = Icons.Default.Sort, + contentDescription = "Sort" + ) + } + } + AnimatedVisibility( + visible = state.isOrderSectionVisible, + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically() + ) { + OrderSection( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + noteOrder = state.noteOrder, + onOrderChange = { + viewModel.onEvent(NotesEvent.Order(it)) + } + ) + } + Spacer(modifier = Modifier.height(16.dp)) + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(state.notes) { note -> + NoteItem( + note = note, + modifier = Modifier + .fillMaxWidth() + .clickable { + navController.navigate( + Screen.AddEditNoteScreen.route + + "?noteId=${note.id}¬eColor=${note.color}" + ) + }, + onDeleteClick = { + viewModel.onEvent(NotesEvent.DeleteNote(note)) + scope.launch { + val result = snackbarHostState.showSnackbar( + message = "Note deleted", + actionLabel = "Undo" + ) + if(result == SnackbarResult.ActionPerformed) { + viewModel.onEvent(NotesEvent.RestoreNote) + } + } + } + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesState.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesState.kt new file mode 100644 index 00000000..d85bfdc2 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesState.kt @@ -0,0 +1,11 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes + +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util.NoteOrder +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util.OrderType + +data class NotesState( + val notes: List = emptyList(), + val noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending), + val isOrderSectionVisible: Boolean = false +) diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesViewModel.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesViewModel.kt new file mode 100644 index 00000000..bd11c8d6 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/NotesViewModel.kt @@ -0,0 +1,75 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.use_case.NoteUseCases +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util.NoteOrder +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util.OrderType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class NotesViewModel @Inject constructor( + private val noteUseCases: NoteUseCases +) : ViewModel() { + + private val _state = mutableStateOf(NotesState()) + val state: State = _state + + private var recentlyDeletedNote: Note? = null + + private var getNotesJob: Job? = null + + init { + getNotes(NoteOrder.Date(OrderType.Descending)) + } + + fun onEvent(event: NotesEvent) { + when (event) { + is NotesEvent.Order -> { + if (state.value.noteOrder::class == event.noteOrder::class && + state.value.noteOrder.orderType == event.noteOrder.orderType + ) { + return + } + getNotes(event.noteOrder) + } + is NotesEvent.DeleteNote -> { + viewModelScope.launch { + noteUseCases.deleteNote(event.note) + recentlyDeletedNote = event.note + } + } + is NotesEvent.RestoreNote -> { + viewModelScope.launch { + noteUseCases.addNote(recentlyDeletedNote ?: return@launch) + recentlyDeletedNote = null + } + } + is NotesEvent.ToggleOrderSection -> { + _state.value = state.value.copy( + isOrderSectionVisible = !state.value.isOrderSectionVisible + ) + } + } + } + + private fun getNotes(noteOrder: NoteOrder) { + getNotesJob?.cancel() + getNotesJob = noteUseCases.getNotes(noteOrder) + .onEach { notes -> + _state.value = state.value.copy( + notes = notes, + noteOrder = noteOrder + ) + } + .launchIn(viewModelScope) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/components/DefaultRadioButton.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/components/DefaultRadioButton.kt new file mode 100644 index 00000000..feb257e8 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/components/DefaultRadioButton.kt @@ -0,0 +1,37 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun DefaultRadioButton( + text: String, + selected: Boolean, + onSelect: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selected, + onClick = onSelect, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colorScheme.primary, + unselectedColor = MaterialTheme.colorScheme.onBackground + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = text, style = MaterialTheme.typography.bodyLarge) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/components/NoteItem.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/components/NoteItem.kt new file mode 100644 index 00000000..790d0af4 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/components/NoteItem.kt @@ -0,0 +1,95 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.core.graphics.ColorUtils +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.model.Note + +@Composable +fun NoteItem( + note: Note, + modifier: Modifier = Modifier, + cornerRadius: Dp = 10.dp, + cutCornerSize: Dp = 30.dp, + onDeleteClick: () -> Unit +) { + Box( + modifier = modifier + ) { + Canvas(modifier = Modifier.matchParentSize()) { + val clipPath = Path().apply { + lineTo(size.width - cutCornerSize.toPx(), 0f) + lineTo(size.width, cutCornerSize.toPx()) + lineTo(size.width, size.height) + lineTo(0f, size.height) + close() + } + + clipPath(clipPath) { + drawRoundRect( + color = Color(note.color), + size = size, + cornerRadius = CornerRadius(cornerRadius.toPx()) + ) + drawRoundRect( + color = Color( + ColorUtils.blendARGB(note.color, 0x000000, 0.2f) + ), + topLeft = Offset(size.width - cutCornerSize.toPx(), -100f), + size = Size(cutCornerSize.toPx() + 100f, cutCornerSize.toPx() + 100f), + cornerRadius = CornerRadius(cornerRadius.toPx()) + ) + } + } + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .padding(end = 32.dp) + ) { + Text( + text = note.title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = note.content, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 10, + overflow = TextOverflow.Ellipsis + ) + } + IconButton( + onClick = onDeleteClick, + modifier = Modifier.align(Alignment.BottomEnd) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete note", + tint = MaterialTheme.colorScheme.onSurface + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/components/OrderSection.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/components/OrderSection.kt new file mode 100644 index 00000000..0fdc3634 --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/notes/components/OrderSection.kt @@ -0,0 +1,61 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.notes.components + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util.NoteOrder +import com.plcoding.cleanarchitecturenoteapp.feature_note.domain.util.OrderType + +@Composable +fun OrderSection( + modifier: Modifier = Modifier, + noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending), + onOrderChange: (NoteOrder) -> Unit +) { + Column( + modifier = modifier + ) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + DefaultRadioButton( + text = "Title", + selected = noteOrder is NoteOrder.Title, + onSelect = { onOrderChange(NoteOrder.Title(noteOrder.orderType)) } + ) + Spacer(modifier = Modifier.width(8.dp)) + DefaultRadioButton( + text = "Date", + selected = noteOrder is NoteOrder.Date, + onSelect = { onOrderChange(NoteOrder.Date(noteOrder.orderType)) } + ) + Spacer(modifier = Modifier.width(8.dp)) + DefaultRadioButton( + text = "Color", + selected = noteOrder is NoteOrder.Color, + onSelect = { onOrderChange(NoteOrder.Color(noteOrder.orderType)) } + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth() + ) { + DefaultRadioButton( + text = "Ascending", + selected = noteOrder.orderType is OrderType.Ascending, + onSelect = { + onOrderChange(noteOrder.copy(OrderType.Ascending)) + } + ) + Spacer(modifier = Modifier.width(8.dp)) + DefaultRadioButton( + text = "Descending", + selected = noteOrder.orderType is OrderType.Descending, + onSelect = { + onOrderChange(noteOrder.copy(OrderType.Descending)) + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/util/Screen.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/util/Screen.kt new file mode 100644 index 00000000..9a0e1d1e --- /dev/null +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/feature_note/presentation/util/Screen.kt @@ -0,0 +1,6 @@ +package com.plcoding.cleanarchitecturenoteapp.feature_note.presentation.util + +sealed class Screen(val route: String) { + object NotesScreen: Screen("notes_screen") + object AddEditNoteScreen: Screen("add_edit_note_screen") +} diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Shape.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Shape.kt index 3a70048a..1f872ff6 100644 --- a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Shape.kt +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Shape.kt @@ -1,7 +1,7 @@ package com.plcoding.cleanarchitecturenoteapp.ui.theme import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Shapes +import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp val Shapes = Shapes( diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Theme.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Theme.kt index 24a1508f..2c6e1c67 100644 --- a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Theme.kt +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Theme.kt @@ -1,11 +1,11 @@ package com.plcoding.cleanarchitecturenoteapp.ui.theme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -private val DarkColorPalette = darkColors( +private val DarkColorPalette = darkColorScheme( primary = Color.White, background = DarkGray, onBackground = Color.White, @@ -16,7 +16,7 @@ private val DarkColorPalette = darkColors( @Composable fun CleanArchitectureNoteAppTheme(darkTheme: Boolean = true, content: @Composable() () -> Unit) { MaterialTheme( - colors = DarkColorPalette, + colorScheme = DarkColorPalette, typography = Typography, shapes = Shapes, content = content diff --git a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Type.kt b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Type.kt index 4db73a88..e4befb58 100644 --- a/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Type.kt +++ b/app/src/main/java/com/plcoding/cleanarchitecturenoteapp/ui/theme/Type.kt @@ -1,6 +1,6 @@ package com.plcoding.cleanarchitecturenoteapp.ui.theme -import androidx.compose.material.Typography +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 @@ -8,7 +8,7 @@ import androidx.compose.ui.unit.sp // Set of Material typography styles to start with val Typography = Typography( - body1 = TextStyle( + bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml deleted file mode 100644 index 423caff1..00000000 --- a/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index c2739918..3d0dccbe 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,26 +1,3 @@ - - - - - - -