From 11298766449b795cbb85cb9abb36b3106cd49ae5 Mon Sep 17 00:00:00 2001 From: IntFxZen Date: Sat, 11 Apr 2026 14:30:55 +0300 Subject: [PATCH 1/4] chore: setup Koin DI and application bootstrap --- app/build.gradle.kts | 24 +--- app/src/main/AndroidManifest.xml | 1 + .../java/com/itlab/notes/NotesApplication.kt | 16 +++ .../main/java/com/itlab/notes/di/AppModule.kt | 120 ++++++++++++++++++ gradle/libs.versions.toml | 10 +- settings-gradle.lockfile | 4 + 6 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/itlab/notes/NotesApplication.kt create mode 100644 app/src/main/java/com/itlab/notes/di/AppModule.kt create mode 100644 settings-gradle.lockfile diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1bbae64..e1ccdb8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,26 +37,6 @@ android { buildFeatures { compose = true } - testOptions { - managedDevices { - localDevices { - create("pixel7api34X86") { - device = "Pixel 7" - apiLevel = 34 - systemImageSource = "aosp-atd" - require64Bit = true - testedAbi = "x86_64" - } - create("pixel7api34Arm64") { - device = "Pixel 7" - apiLevel = 34 - systemImageSource = "aosp-atd" - require64Bit = true - testedAbi = "arm64-v8a" - } - } - } - } } kotlin { @@ -77,6 +57,10 @@ dependencies { implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.materialicons.extended) + implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3f41043..fd51c78 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> { InMemoryNotesRepository() } + single { InMemoryFolderRepository() } + + factory { CreateNoteUseCase(get()) } + factory { CreateFolderUseCase(get()) } + factory { DeleteFolderUseCase(get()) } + factory { DeleteNoteUseCase(get()) } + factory { UpdateNoteUseCase(get()) } + factory { ObserveNotesByFolderUseCase(get()) } + factory { ObserveFoldersUseCase(get()) } +// factory { +// NotesUseCases( +// createFolderUseCase = get(), +// deleteFolderUseCase = get(), +// createNoteUseCase = get(), +// deleteNoteUseCase = get(), +// updateNoteUseCase = get(), +// observeNotesByFolderUseCase = get(), +// observeFoldersUseCase = get(), +// ) +// } +// +// viewModel { +// NotesViewModel( +// useCases = get(), +// ) +// } + } + +private class InMemoryNotesRepository : NotesRepository { + private val notesFlow = MutableStateFlow>(emptyList()) + + override fun observeNotes(): Flow> = notesFlow + + override fun observeNotesByFolder(folderId: String): Flow> = + notesFlow.map { notes -> notes.filter { it.folderId == folderId } } + + override suspend fun getNoteById(id: String): Note? = notesFlow.value.firstOrNull { it.id == id } + + override suspend fun createNote(note: Note): String { + notesFlow.value = notesFlow.value + note + return note.id + } + + override suspend fun updateNote(note: Note) { + notesFlow.value = + notesFlow.value.map { existing -> + if (existing.id == note.id) note else existing + } + } + + override suspend fun deleteNote(id: String) { + notesFlow.value = notesFlow.value.filterNot { it.id == id } + } +} + +private class InMemoryFolderRepository : NoteFolderRepository { + private val foldersFlow = + MutableStateFlow( + listOf( + NoteFolder(id = "all", name = "All Notes"), + NoteFolder(id = "study", name = "My Study"), + NoteFolder(id = "cook", name = "How to Cook"), + NoteFolder(id = "poems", name = "My poems"), + ), + ) + + override fun observeFolders(): Flow> = foldersFlow + + override suspend fun createFolder(folder: NoteFolder): String { + foldersFlow.value = foldersFlow.value + folder + return folder.id + } + + override suspend fun renameFolder( + id: String, + name: String, + ) { + foldersFlow.value = + foldersFlow.value.map { folder -> + if (folder.id == id) folder.copy(name = name) else folder + } + } + + override suspend fun deleteFolder(id: String) { + foldersFlow.value = foldersFlow.value.filterNot { it.id == id } + } + + override suspend fun getFolderById(id: String): NoteFolder? = foldersFlow.value.firstOrNull { it.id == id } + + override suspend fun updateFolder(folder: NoteFolder) { + foldersFlow.value = + foldersFlow.value.map { existing -> + if (existing.id == folder.id) folder else existing + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index edd42bd..7af37ce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,8 @@ kotlinxSerializationJson = "1.6.3" robolectric = "4.16.1" coroutinesTest = "1.7.3" coreTesting = "2.2.0" +lifecycleViewmodelKtx = "2.10.0" +koin = "3.5.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -37,6 +39,7 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-materialicons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } @@ -48,6 +51,11 @@ timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "tim androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutinesTest" } +androidx-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "coreTesting" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } +koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } +koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } mockk = { group = "io.mockk", name = "mockk", version = "1.13.10" } robolectric = { group = "org.robolectric", name = "robolectric", version = "4.12.1" } androidx-test-core = { group = "androidx.test", name = "core", version = "1.5.0" } @@ -61,4 +69,4 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlintGradle" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/settings-gradle.lockfile b/settings-gradle.lockfile new file mode 100644 index 0000000..709a43f --- /dev/null +++ b/settings-gradle.lockfile @@ -0,0 +1,4 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +empty=incomingCatalogForLibs0 From 1d9978c14d5ec66222e9ae592aff128b4b9f7896 Mon Sep 17 00:00:00 2001 From: IntFxZen Date: Sat, 11 Apr 2026 15:21:58 +0300 Subject: [PATCH 2/4] chore: add NotesApp entry point and final DI setup --- .../main/java/com/itlab/notes/di/AppModule.kt | 37 ++++++++++--------- .../main/java/com/itlab/notes/ui/NotesApp.kt | 25 +++++++++---- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/itlab/notes/di/AppModule.kt b/app/src/main/java/com/itlab/notes/di/AppModule.kt index 1c85520..a9d86db 100644 --- a/app/src/main/java/com/itlab/notes/di/AppModule.kt +++ b/app/src/main/java/com/itlab/notes/di/AppModule.kt @@ -11,9 +11,12 @@ import com.itlab.domain.usecase.DeleteNoteUseCase import com.itlab.domain.usecase.ObserveFoldersUseCase import com.itlab.domain.usecase.ObserveNotesByFolderUseCase import com.itlab.domain.usecase.UpdateNoteUseCase +import com.itlab.notes.ui.NotesUseCases +import com.itlab.notes.ui.NotesViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map +import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module // import com.itlab.notes.ui.NotesUseCases @@ -31,23 +34,23 @@ val appModule = factory { UpdateNoteUseCase(get()) } factory { ObserveNotesByFolderUseCase(get()) } factory { ObserveFoldersUseCase(get()) } -// factory { -// NotesUseCases( -// createFolderUseCase = get(), -// deleteFolderUseCase = get(), -// createNoteUseCase = get(), -// deleteNoteUseCase = get(), -// updateNoteUseCase = get(), -// observeNotesByFolderUseCase = get(), -// observeFoldersUseCase = get(), -// ) -// } -// -// viewModel { -// NotesViewModel( -// useCases = get(), -// ) -// } + factory { + NotesUseCases( + createFolderUseCase = get(), + deleteFolderUseCase = get(), + createNoteUseCase = get(), + deleteNoteUseCase = get(), + updateNoteUseCase = get(), + observeNotesByFolderUseCase = get(), + observeFoldersUseCase = get(), + ) + } + + viewModel { + NotesViewModel( + useCases = get(), + ) + } } private class InMemoryNotesRepository : NotesRepository { diff --git a/app/src/main/java/com/itlab/notes/ui/NotesApp.kt b/app/src/main/java/com/itlab/notes/ui/NotesApp.kt index 4a9e4c3..7dc6f89 100644 --- a/app/src/main/java/com/itlab/notes/ui/NotesApp.kt +++ b/app/src/main/java/com/itlab/notes/ui/NotesApp.kt @@ -1,22 +1,29 @@ package com.itlab.notes.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import com.itlab.notes.ui.editor.editorScreen import com.itlab.notes.ui.notes.DirectoryItemUi import com.itlab.notes.ui.notes.NoteItemUi +import com.itlab.notes.ui.notes.NotesListActions import com.itlab.notes.ui.notes.directoriesScreen import com.itlab.notes.ui.notes.notesListScreen +import org.koin.androidx.compose.koinViewModel @Composable fun notesApp() { - val viewModel = remember { NotesViewModel() } + val viewModel: NotesViewModel = koinViewModel() val state = viewModel.uiState when (val screen = state.screen) { NotesUiScreen.Directories -> { directoriesScreen( directories = state.directories, + onCreateDirectory = { name -> + viewModel.onEvent(NotesUiEvent.CreateDirectory(name)) + }, + onDeleteDirectory = { directory -> + viewModel.onEvent(NotesUiEvent.DeleteDirectory(directory.id)) + }, onDirectoryClick = { directory -> viewModel.onEvent(NotesUiEvent.OpenDirectory(directory)) }, @@ -28,11 +35,15 @@ fun notesApp() { notesListScreen( directoryName = directory.name, notes = state.notes, - onBack = { viewModel.onEvent(NotesUiEvent.BackToDirectories) }, - onAddNoteClick = { viewModel.onEvent(NotesUiEvent.CreateNote) }, - onNoteClick = { note: NoteItemUi -> - viewModel.onEvent(NotesUiEvent.OpenNote(note)) - }, + actions = + NotesListActions( + onBack = { viewModel.onEvent(NotesUiEvent.BackToDirectories) }, + onAddNoteClick = { viewModel.onEvent(NotesUiEvent.CreateNote) }, + onNoteDelete = { note -> viewModel.onEvent(NotesUiEvent.DeleteNote(note.id)) }, + onNoteClick = { note: NoteItemUi -> + viewModel.onEvent(NotesUiEvent.OpenNote(note)) + }, + ), ) } From b4f4bbb564a8f3e388056bd9005a18cde392c551 Mon Sep 17 00:00:00 2001 From: IntFxZen Date: Sat, 11 Apr 2026 15:31:33 +0300 Subject: [PATCH 3/4] feat: implement logic layer and data contracts --- .../main/java/com/itlab/notes/ui/NotesApp.kt | 94 ++++---- .../com/itlab/notes/ui/NotesUiContract.kt | 12 + .../java/com/itlab/notes/ui/NotesUseCases.kt | 19 ++ .../java/com/itlab/notes/ui/NotesViewModel.kt | 207 +++++++++++------- .../itlab/notes/ui/notes/DirectoriesScreen.kt | 8 +- .../itlab/notes/ui/notes/DirectoryItemUi.kt | 1 + .../com/itlab/notes/ui/notes/NoteItemUi.kt | 1 + 7 files changed, 204 insertions(+), 138 deletions(-) create mode 100644 app/src/main/java/com/itlab/notes/ui/NotesUseCases.kt diff --git a/app/src/main/java/com/itlab/notes/ui/NotesApp.kt b/app/src/main/java/com/itlab/notes/ui/NotesApp.kt index 7dc6f89..e788e5c 100644 --- a/app/src/main/java/com/itlab/notes/ui/NotesApp.kt +++ b/app/src/main/java/com/itlab/notes/ui/NotesApp.kt @@ -1,59 +1,53 @@ package com.itlab.notes.ui import androidx.compose.runtime.Composable -import com.itlab.notes.ui.editor.editorScreen -import com.itlab.notes.ui.notes.DirectoryItemUi -import com.itlab.notes.ui.notes.NoteItemUi -import com.itlab.notes.ui.notes.NotesListActions -import com.itlab.notes.ui.notes.directoriesScreen -import com.itlab.notes.ui.notes.notesListScreen import org.koin.androidx.compose.koinViewModel @Composable fun notesApp() { val viewModel: NotesViewModel = koinViewModel() - val state = viewModel.uiState - - when (val screen = state.screen) { - NotesUiScreen.Directories -> { - directoriesScreen( - directories = state.directories, - onCreateDirectory = { name -> - viewModel.onEvent(NotesUiEvent.CreateDirectory(name)) - }, - onDeleteDirectory = { directory -> - viewModel.onEvent(NotesUiEvent.DeleteDirectory(directory.id)) - }, - onDirectoryClick = { directory -> - viewModel.onEvent(NotesUiEvent.OpenDirectory(directory)) - }, - ) - } - - is NotesUiScreen.DirectoryNotes -> { - val directory: DirectoryItemUi = screen.directory - notesListScreen( - directoryName = directory.name, - notes = state.notes, - actions = - NotesListActions( - onBack = { viewModel.onEvent(NotesUiEvent.BackToDirectories) }, - onAddNoteClick = { viewModel.onEvent(NotesUiEvent.CreateNote) }, - onNoteDelete = { note -> viewModel.onEvent(NotesUiEvent.DeleteNote(note.id)) }, - onNoteClick = { note: NoteItemUi -> - viewModel.onEvent(NotesUiEvent.OpenNote(note)) - }, - ), - ) - } - - is NotesUiScreen.NoteEditor -> { - editorScreen( - directoryName = screen.directory.name, - note = screen.note, - onBack = { viewModel.onEvent(NotesUiEvent.BackToDirectoryNotes) }, - onSave = { updated -> viewModel.onEvent(NotesUiEvent.SaveNote(updated)) }, - ) - } - } +// val state = viewModel.uiState +// +// when (val screen = state.screen) { +// NotesUiScreen.Directories -> { +// directoriesScreen( +// directories = state.directories, +// onCreateDirectory = { name -> +// viewModel.onEvent(NotesUiEvent.CreateDirectory(name)) +// }, +// onDeleteDirectory = { directory -> +// viewModel.onEvent(NotesUiEvent.DeleteDirectory(directory.id)) +// }, +// onDirectoryClick = { directory -> +// viewModel.onEvent(NotesUiEvent.OpenDirectory(directory)) +// }, +// ) +// } +// +// is NotesUiScreen.DirectoryNotes -> { +// val directory: DirectoryItemUi = screen.directory +// notesListScreen( +// directoryName = directory.name, +// notes = state.notes, +// actions = +// NotesListActions( +// onBack = { viewModel.onEvent(NotesUiEvent.BackToDirectories) }, +// onAddNoteClick = { viewModel.onEvent(NotesUiEvent.CreateNote) }, +// onNoteDelete = { note -> viewModel.onEvent(NotesUiEvent.DeleteNote(note.id)) }, +// onNoteClick = { note: NoteItemUi -> +// viewModel.onEvent(NotesUiEvent.OpenNote(note)) +// }, +// ), +// ) +// } +// +// is NotesUiScreen.NoteEditor -> { +// editorScreen( +// directoryName = screen.directory.name, +// note = screen.note, +// onBack = { viewModel.onEvent(NotesUiEvent.BackToDirectoryNotes) }, +// onSave = { updated -> viewModel.onEvent(NotesUiEvent.SaveNote(updated)) }, +// ) +// } +// } } diff --git a/app/src/main/java/com/itlab/notes/ui/NotesUiContract.kt b/app/src/main/java/com/itlab/notes/ui/NotesUiContract.kt index a83b2e3..0aba6bc 100644 --- a/app/src/main/java/com/itlab/notes/ui/NotesUiContract.kt +++ b/app/src/main/java/com/itlab/notes/ui/NotesUiContract.kt @@ -39,11 +39,23 @@ sealed interface NotesUiEvent { data object CreateNote : NotesUiEvent + data class CreateDirectory( + val name: String, + ) : NotesUiEvent + + data class DeleteDirectory( + val directoryId: String, + ) : NotesUiEvent + data object BackToDirectoryNotes : NotesUiEvent data class SaveNote( val note: NoteItemUi, ) : NotesUiEvent + + data class DeleteNote( + val noteId: String, + ) : NotesUiEvent } interface NotesViewModelContract { diff --git a/app/src/main/java/com/itlab/notes/ui/NotesUseCases.kt b/app/src/main/java/com/itlab/notes/ui/NotesUseCases.kt new file mode 100644 index 0000000..bbc9ee1 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/ui/NotesUseCases.kt @@ -0,0 +1,19 @@ +package com.itlab.notes.ui + +import com.itlab.domain.usecase.CreateFolderUseCase +import com.itlab.domain.usecase.CreateNoteUseCase +import com.itlab.domain.usecase.DeleteFolderUseCase +import com.itlab.domain.usecase.DeleteNoteUseCase +import com.itlab.domain.usecase.ObserveFoldersUseCase +import com.itlab.domain.usecase.ObserveNotesByFolderUseCase +import com.itlab.domain.usecase.UpdateNoteUseCase + +data class NotesUseCases( + val createFolderUseCase: CreateFolderUseCase, + val deleteFolderUseCase: DeleteFolderUseCase, + val createNoteUseCase: CreateNoteUseCase, + val deleteNoteUseCase: DeleteNoteUseCase, + val updateNoteUseCase: UpdateNoteUseCase, + val observeNotesByFolderUseCase: ObserveNotesByFolderUseCase, + val observeFoldersUseCase: ObserveFoldersUseCase, +) diff --git a/app/src/main/java/com/itlab/notes/ui/NotesViewModel.kt b/app/src/main/java/com/itlab/notes/ui/NotesViewModel.kt index 39d34be..08b39ad 100644 --- a/app/src/main/java/com/itlab/notes/ui/NotesViewModel.kt +++ b/app/src/main/java/com/itlab/notes/ui/NotesViewModel.kt @@ -3,18 +3,42 @@ package com.itlab.notes.ui import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.itlab.domain.model.ContentItem +import com.itlab.domain.model.Note +import com.itlab.domain.model.NoteFolder import com.itlab.notes.ui.notes.DirectoryItemUi import com.itlab.notes.ui.notes.NoteItemUi +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch import java.util.UUID -class NotesViewModel : NotesViewModelContract { +class NotesViewModel( + private val useCases: NotesUseCases, +) : ViewModel(), + NotesViewModelContract { override var uiState: NotesUiState by mutableStateOf( - NotesUiState( - screen = NotesUiScreen.Directories, - directories = previewDirectoriesFallback(), - ), + NotesUiState(screen = NotesUiScreen.Directories), ) private set + private var notesJob: Job? = null + + init { + viewModelScope.launch { + useCases.observeFoldersUseCase().collect { folders -> + val existingCounts = uiState.directories.associate { it.id to it.noteCount } + uiState = + uiState.copy( + directories = + folders.map { folder -> + folder.toUi(noteCount = existingCounts[folder.id] ?: 0) + }, + ) + } + } + } override fun onEvent(event: NotesUiEvent) { when (event) { @@ -22,8 +46,41 @@ class NotesViewModel : NotesViewModelContract { NotesUiEvent.BackToDirectories -> backToDirectories() is NotesUiEvent.OpenNote -> openNote(event.note) NotesUiEvent.CreateNote -> createNote() + is NotesUiEvent.CreateDirectory -> { + val normalized = event.name.trim() + if (normalized.isNotBlank()) { + viewModelScope.launch { + useCases.createFolderUseCase(NoteFolder(name = normalized)) + } + } + } + + is NotesUiEvent.DeleteDirectory -> { + if (event.directoryId != "all") { + viewModelScope.launch { + val notesInDirectory = + useCases + .observeNotesByFolderUseCase(event.directoryId) + .firstOrNull() + .orEmpty() + notesInDirectory.forEach { note -> + useCases.deleteNoteUseCase(note.id) + } + useCases.deleteFolderUseCase(event.directoryId) + if ((uiState.screen as? NotesUiScreen.DirectoryNotes)?.directory?.id == event.directoryId) { + backToDirectories() + } + } + } + } + NotesUiEvent.BackToDirectoryNotes -> backToDirectoryNotes() is NotesUiEvent.SaveNote -> saveNote(event.note) + is NotesUiEvent.DeleteNote -> { + viewModelScope.launch { + useCases.deleteNoteUseCase(event.noteId) + } + } } } @@ -31,11 +88,27 @@ class NotesViewModel : NotesViewModelContract { uiState = uiState.copy( screen = NotesUiScreen.DirectoryNotes(directory = directory), - notes = notesFallbackForDirectory(directory), + notes = emptyList(), ) + notesJob?.cancel() + val folderId = directory.id.asDomainFolderId() + notesJob = + viewModelScope.launch { + useCases.observeNotesByFolderUseCase(folderId).collect { notes -> + uiState = + uiState.copy( + notes = notes.map { it.toUi() }, + screen = + NotesUiScreen.DirectoryNotes( + directory = directory.copy(noteCount = notes.size), + ), + ) + } + } } private fun backToDirectories() { + notesJob?.cancel() uiState = uiState.copy( screen = NotesUiScreen.Directories, @@ -48,11 +121,7 @@ class NotesViewModel : NotesViewModelContract { if (dir != null) { uiState = uiState.copy( - screen = - NotesUiScreen.NoteEditor( - directory = dir, - note = note, - ), + screen = NotesUiScreen.NoteEditor(directory = dir, note = note), ) } } @@ -65,14 +134,11 @@ class NotesViewModel : NotesViewModelContract { id = UUID.randomUUID().toString(), title = "", content = "", + folderId = dir.id.asDomainFolderId(), ) uiState = uiState.copy( - screen = - NotesUiScreen.NoteEditor( - directory = dir, - note = newNote, - ), + screen = NotesUiScreen.NoteEditor(directory = dir, note = newNote), ) } } @@ -80,83 +146,56 @@ class NotesViewModel : NotesViewModelContract { private fun backToDirectoryNotes() { val editor = uiState.screen as? NotesUiScreen.NoteEditor if (editor != null) { - uiState = - uiState.copy( - screen = NotesUiScreen.DirectoryNotes(directory = editor.directory), - ) + uiState = uiState.copy(screen = NotesUiScreen.DirectoryNotes(directory = editor.directory)) } } private fun saveNote(note: NoteItemUi) { - val editor = uiState.screen as? NotesUiScreen.NoteEditor - if (editor != null) { - val updatedNotes = uiState.notes.toMutableList() - val index = updatedNotes.indexOfFirst { it.id == note.id } - if (index >= 0) { - updatedNotes[index] = note + val editor = uiState.screen as? NotesUiScreen.NoteEditor ?: return + viewModelScope.launch { + val existing = + useCases + .observeNotesByFolderUseCase(null) + .firstOrNull() + .orEmpty() + .any { it.id == note.id } + val targetFolderId = note.folderId ?: editor.directory.id.asDomainFolderId() + val domainNote = note.toDomain(folderId = targetFolderId) + if (existing) { + useCases.updateNoteUseCase(domainNote) } else { - updatedNotes.add(note) + useCases.createNoteUseCase(domainNote) } - - val updatedDirectory = editor.directory.copy(noteCount = updatedNotes.size) - uiState = - uiState.copy( - screen = NotesUiScreen.DirectoryNotes(directory = updatedDirectory), - notes = updatedNotes, - ) + uiState = uiState.copy(screen = NotesUiScreen.DirectoryNotes(directory = editor.directory)) } } - private fun notesFallbackForDirectory(directory: DirectoryItemUi): List = - when (directory.name) { - "My Study" -> - listOf( - NoteItemUi( - id = "my-study-1", - title = "Lecture notes", - content = "Topic: coroutines\n- suspend\n- scope\n- dispatcher", - ), - NoteItemUi( - id = "my-study-2", - title = "Homework", - content = "Due Friday.\nChecklist:\n1) ...\n2) ...", - ), - ) + override fun onCleared() { + notesJob?.cancel() + super.onCleared() + } +} - "How to Cook" -> - listOf( - NoteItemUi( - id = "cook-1", - title = "Cherry pie", - content = "Ingredients:\n- Flour 300g\n- Cherries 200g\n- Sugar 120g", - ), - NoteItemUi( - id = "cook-2", - title = "Pasta", - content = "Sauce: tomatoes + garlic + basil.\nTime: 20 minutes.", - ), - ) +internal fun NoteFolder.toUi(noteCount: Int): DirectoryItemUi = + DirectoryItemUi(id = id, name = name, noteCount = noteCount) - else -> - listOf( - NoteItemUi( - id = "other-1", - title = "First note", - content = "Temporary placeholder while notes load from the data layer.", - ), - NoteItemUi( - id = "other-2", - title = "Second note", - content = "Connect the data layer and pass the list into UI.", - ), - ) - } +internal fun Note.toUi(): NoteItemUi = + NoteItemUi( + id = id, + title = title, + content = + contentItems + .filterIsInstance() + .joinToString("\n") { it.text }, + folderId = folderId, + ) - private fun previewDirectoriesFallback(): List = - listOf( - DirectoryItemUi(name = "All Notes", noteCount = 0), - DirectoryItemUi(name = "My Study", noteCount = 0), - DirectoryItemUi(name = "How to Cook", noteCount = 0), - DirectoryItemUi(name = "My poems", noteCount = 0), - ) -} +internal fun NoteItemUi.toDomain(folderId: String?): Note = + Note( + id = id, + title = title, + folderId = folderId, + contentItems = listOf(ContentItem.Text(content)), + ) + +internal fun String.asDomainFolderId(): String? = if (this == "all") null else this diff --git a/app/src/main/java/com/itlab/notes/ui/notes/DirectoriesScreen.kt b/app/src/main/java/com/itlab/notes/ui/notes/DirectoriesScreen.kt index 451bb57..4ab28ad 100644 --- a/app/src/main/java/com/itlab/notes/ui/notes/DirectoriesScreen.kt +++ b/app/src/main/java/com/itlab/notes/ui/notes/DirectoriesScreen.kt @@ -141,8 +141,8 @@ private fun directoryRow( private fun previewDirectoriesFallback(): List = listOf( - DirectoryItemUi(name = "All Notes", noteCount = 0), - DirectoryItemUi(name = "My Study", noteCount = 0), - DirectoryItemUi(name = "How to Cook", noteCount = 0), - DirectoryItemUi(name = "My poems", noteCount = 0), + DirectoryItemUi(name = "All Notes", noteCount = 0, id = "1"), + DirectoryItemUi(name = "My Study", noteCount = 0, id = "0"), + DirectoryItemUi(name = "How to Cook", noteCount = 0, id = "2"), + DirectoryItemUi(name = "My poems", noteCount = 0, id = "3"), ) diff --git a/app/src/main/java/com/itlab/notes/ui/notes/DirectoryItemUi.kt b/app/src/main/java/com/itlab/notes/ui/notes/DirectoryItemUi.kt index 000674c..b6fc4ed 100644 --- a/app/src/main/java/com/itlab/notes/ui/notes/DirectoryItemUi.kt +++ b/app/src/main/java/com/itlab/notes/ui/notes/DirectoryItemUi.kt @@ -1,6 +1,7 @@ package com.itlab.notes.ui.notes data class DirectoryItemUi( + val id: String, val name: String, val noteCount: Int, ) diff --git a/app/src/main/java/com/itlab/notes/ui/notes/NoteItemUi.kt b/app/src/main/java/com/itlab/notes/ui/notes/NoteItemUi.kt index fc9aa79..618a112 100644 --- a/app/src/main/java/com/itlab/notes/ui/notes/NoteItemUi.kt +++ b/app/src/main/java/com/itlab/notes/ui/notes/NoteItemUi.kt @@ -4,4 +4,5 @@ data class NoteItemUi( val id: String, val title: String, val content: String, + val folderId: String? = null, ) From 04eedbe11487a6337d5d53377245256e716588b3 Mon Sep 17 00:00:00 2001 From: IntFxZen Date: Sat, 11 Apr 2026 15:41:30 +0300 Subject: [PATCH 4/4] fix: remove unused viewModel --- app/src/main/java/com/itlab/notes/ui/NotesApp.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/itlab/notes/ui/NotesApp.kt b/app/src/main/java/com/itlab/notes/ui/NotesApp.kt index e788e5c..27f58f8 100644 --- a/app/src/main/java/com/itlab/notes/ui/NotesApp.kt +++ b/app/src/main/java/com/itlab/notes/ui/NotesApp.kt @@ -1,11 +1,11 @@ package com.itlab.notes.ui import androidx.compose.runtime.Composable -import org.koin.androidx.compose.koinViewModel +// import org.koin.androidx.compose.koinViewModel @Composable fun notesApp() { - val viewModel: NotesViewModel = koinViewModel() +// val viewModel: NotesViewModel = koinViewModel() // val state = viewModel.uiState // // when (val screen = state.screen) {