From 261b572973d7f94595e46518f55a222a7556c8b7 Mon Sep 17 00:00:00 2001 From: Aleksandr Symbiotov Date: Fri, 9 Feb 2024 21:36:07 +0100 Subject: [PATCH 1/5] Added FlashCardsScreen (intermediate commit) --- androidApp/build.gradle.kts | 6 +- androidApp/src/main/AndroidManifest.xml | 1 + .../android/navigation/EBNavHost.kt | 20 ++ .../android/navigation/NavigationActions.kt | 5 + .../android/ui/common/AppDrawer.kt | 11 +- .../android/ui/flashcards/FlashCardsScreen.kt | 233 ++++++++++++++++++ .../ui/recordDetails/BottomNavigationBar.kt | 4 - .../englishbender/core/di/DatabaseModule.kt | 10 + .../englishbender/core/di/UseCaseModule.kt | 6 + .../englishbender/core/di/ViewModelModule.kt | 2 + .../core/navigation/Destinations.kt | 4 + .../data/local/dataStore/DataStoreRealm.kt | 7 - .../data/local/mappers/RecordMappers.kt | 43 ---- .../data/local/mappers/StatsMappers.kt | 15 -- .../data/local/mappers/TagMappers.kt | 23 -- .../englishbender/data/local/models/Board.kt | 54 ++++ .../data/local/models/FlashCard.kt | 37 +++ .../englishbender/data/local/models/Record.kt | 40 ++- .../englishbender/data/local/models/Stats.kt | 13 +- .../englishbender/data/local/models/Tag.kt | 22 +- .../data/repositories/BoardsRepository.kt | 46 ++++ .../data/repositories/FlashCardsRepository.kt | 46 ++++ .../data/repositories/RecordsRepository.kt | 5 +- .../data/repositories/StatsRepository.kt | 4 +- .../data/repositories/TagsRepository.kt | 4 +- .../domain/entities/BoardEntity.kt | 14 ++ .../domain/entities/FlashCardEntity.kt | 11 + .../domain/repositories/IBoardsRepository.kt | 11 + .../repositories/IFlashCardsRepository.kt | 11 + .../flashCards/GetBoardsFlowUseCase.kt | 11 + .../flashCards/GetFlashCardsFlowUseCase.kt | 11 + .../ui/flashcards/FlashCardsViewModel.kt | 38 +++ 32 files changed, 661 insertions(+), 107 deletions(-) create mode 100644 androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/FlashCardsScreen.kt delete mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/RecordMappers.kt delete mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/StatsMappers.kt delete mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/TagMappers.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/FlashCardsRepository.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IFlashCardsRepository.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardsFlowUseCase.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsFlowUseCase.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 4db9b6f..dd8a994 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -50,8 +50,8 @@ android { // } } -val composeVersion = "1.6.0" -val material3Version = "1.1.2" +val composeVersion = "1.6.1" +val material3Version = "1.2.0" val pagingRuntimeVersion = "3.1.1" val pagingComposeVersion = "1.0.0-alpha18" val koinCoreVersion = "3.4.0" @@ -96,7 +96,7 @@ dependencies { // SplashScreen implementation("androidx.core:core-splashscreen:1.0.1") - implementation("androidx.navigation:navigation-compose:2.7.6") + implementation("androidx.navigation:navigation-compose:2.7.7") // Color picker implementation("com.github.skydoves:colorpicker-compose:1.0.5") diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index b3d7b14..6aaa8f6 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + + val route = Screens.FLASHCARDS_SCREEN.let { route -> + boardId?.let { "$route?boardId=$it" } ?: route + } + navigator.navigateTo(route) + }, + openDrawer = { coroutineScope.launch { drawerState.open() } } + ) + } + ) + } } } \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/navigation/NavigationActions.kt b/androidApp/src/main/java/com/san/englishbender/android/navigation/NavigationActions.kt index 9b06dd7..953fa68 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/navigation/NavigationActions.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/navigation/NavigationActions.kt @@ -1,6 +1,7 @@ package com.san.englishbender.android.navigation import androidx.navigation.NavHostController +import com.san.englishbender.core.navigation.Destinations.FLASHCARDS_ROUTE import com.san.englishbender.core.navigation.Destinations.RECORD_DETAIL_ROUTE import com.san.englishbender.core.navigation.Destinations.STATS_ROUTE import com.san.englishbender.core.navigation.Screens.RECORDS_SCREEN @@ -14,6 +15,10 @@ class EBNavigationActions(private val navController: NavHostController) { navController.navigate(STATS_ROUTE) } + fun navigateToFlashCards() { + navController.navigate(FLASHCARDS_ROUTE) + } + fun navigateToRecords() { navController.navigate(RECORDS_SCREEN) // navController.navigate(RECORDS_SCREEN) { diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/AppDrawer.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/AppDrawer.kt index 80c3471..2f3115b 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/common/AppDrawer.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/AppDrawer.kt @@ -6,8 +6,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material.icons.filled.Analytics -import androidx.compose.material.icons.filled.ViewList +import androidx.compose.material.icons.filled.ViewCarousel import androidx.compose.material3.DrawerState import androidx.compose.material3.Icon import androidx.compose.material3.ModalDrawerSheet @@ -42,7 +43,12 @@ private val drawerNavOptions = listOf( DrawerNavOptions( name = "Records", route = Destinations.RECORD_ROUTE, - icon = Icons.Default.ViewList + icon = Icons.AutoMirrored.Filled.ViewList + ), + DrawerNavOptions( + name = "Flash-cards", + route = Destinations.FLASHCARDS_ROUTE, + icon = Icons.Default.ViewCarousel ), ) @@ -79,6 +85,7 @@ fun AppDrawer( when (item.route) { Destinations.STATS_ROUTE -> navActions.navigateToStats() Destinations.RECORD_ROUTE -> navActions.navigateToRecords() + Destinations.FLASHCARDS_ROUTE -> navActions.navigateToFlashCards() } // navController.navigate(item.route) }, diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/FlashCardsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/FlashCardsScreen.kt new file mode 100644 index 0000000..6ce2196 --- /dev/null +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/FlashCardsScreen.kt @@ -0,0 +1,233 @@ +package com.san.englishbender.android.ui.flashcards + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +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.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +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.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.FilterList +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.san.englishbender.android.ui.common.EBOutlinedButton +import com.san.englishbender.android.ui.common.widgets.ErrorView +import com.san.englishbender.android.ui.common.widgets.LoadingView +import com.san.englishbender.android.ui.theme.BottomSheetContainerColor +import com.san.englishbender.core.extensions.isNotNull +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.ui.flashcards.FlashCardsUiState +import com.san.englishbender.ui.flashcards.FlashCardsViewModel +import org.koin.androidx.compose.getViewModel +import android.view.ViewTreeObserver +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.* +import androidx.compose.ui.focus.focusModifier +import androidx.compose.ui.focus.focusTarget +import androidx.compose.ui.platform.LocalView +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.google.accompanist.insets.ProvideWindowInsets + +@Composable +fun FlashCardsScreen( + onBoardClick: (String?) -> Unit, + openDrawer: () -> Unit +) { + val viewModel: FlashCardsViewModel = getViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + when { + uiState.isLoading -> LoadingView() + uiState.userMessage.isNotNull -> ErrorView(userMessage = uiState.userMessage) + else -> FlashCardsContent( + viewModel, + uiState, + onBoardClick = onBoardClick, + openDrawer = openDrawer + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FlashCardsContent( + viewModel: FlashCardsViewModel, + uiState: FlashCardsUiState, + onBoardClick: (String?) -> Unit, + openDrawer: () -> Unit +) { + val focusManager = LocalFocusManager.current + val bottomSheetState = rememberModalBottomSheetState() + var boardCreationBottomSheet by remember { mutableStateOf(false) } + + Scaffold( + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.surfaceVariant, + topBar = { + TopAppBar( + modifier = Modifier.fillMaxWidth(), + title = {}, + navigationIcon = { + Icon( + rememberVectorPainter(Icons.Filled.Menu), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .padding(8.dp) + .clickable { openDrawer() } + ) + }, + actions = { + Icon( + rememberVectorPainter(Icons.Filled.FilterList), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .padding(8.dp) + .clickable { } + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) + }, + floatingActionButton = { + FloatingActionButton( + contentColor = Color.White, + backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, + shape = RoundedCornerShape(10.dp), + onClick = { boardCreationBottomSheet = true } + ) { + Icon( + Icons.Filled.Edit, + contentDescription = "", + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + ) { paddingValues -> + LazyColumn(modifier = Modifier.padding(paddingValues)) { + items(items = uiState.boards, key = { it.id }) { board -> + BoardCard(board, viewModel, onBoardClick) + } + } + + if (boardCreationBottomSheet) { + ModalBottomSheet( + modifier = Modifier.imePadding(), + containerColor = BottomSheetContainerColor, + onDismissRequest = { boardCreationBottomSheet = false }, + sheetState = bottomSheetState + ) { + focusManager.clearFocus() + BoardCreationBSContent(viewModel) + } + } + } +} + +@Composable +fun BoardCreationBSContent(viewModel: FlashCardsViewModel) { + + var boardName by remember { mutableStateOf("") } + + Column( + Modifier + .fillMaxWidth() + .padding(bottom = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = boardName, + placeholder = { Text("Board name") }, + onValueChange = { + boardName = it + } + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 32.dp, end = 16.dp), + horizontalArrangement = Arrangement.End + ) { + EBOutlinedButton( + text = "Save", + onClick = { + viewModel.saveBoard(boardName) + } + ) + } + } +} + +@Composable +fun BoardCard( + board: BoardEntity, + flashCardsViewModel: FlashCardsViewModel, + onBoardClick: (String?) -> Unit, +) { + Row( + modifier = Modifier + .padding(start = 12.dp) + .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)), + verticalAlignment = Alignment.CenterVertically + ) { + Text(board.name) + } +} + +//@Composable +//fun rememberImeState(): State { +// val imeState = remember { +// mutableStateOf(false) +// } +// +// val view = LocalView.current +// DisposableEffect(view) { +// val listener = ViewTreeObserver.OnGlobalLayoutListener { +// val isKeyboardOpen = ViewCompat.getRootWindowInsets(view) +// ?.isVisible(WindowInsetsCompat.Type.ime()) ?: true +// imeState.value = isKeyboardOpen +// } +// +// view.viewTreeObserver.addOnGlobalLayoutListener(listener) +// onDispose { +// view.viewTreeObserver.removeOnGlobalLayoutListener(listener) +// } +// } +// return imeState +//} \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/BottomNavigationBar.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/BottomNavigationBar.kt index 8e83bc7..fec6d0a 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/BottomNavigationBar.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/BottomNavigationBar.kt @@ -1,7 +1,5 @@ package com.san.englishbender.android.ui.recordDetails -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.icons.Icons @@ -12,10 +10,8 @@ import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.dp sealed class BottomNavItem(var title: String, var icon: ImageVector) { object GrammarCheck : BottomNavItem("GrammarCheck", Icons.Outlined.Spellcheck) diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/DatabaseModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/DatabaseModule.kt index e482fd5..c7f9e0c 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/DatabaseModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/DatabaseModule.kt @@ -3,12 +3,18 @@ package com.san.englishbender.core.di import com.san.englishbender.data.local.dataStore.DataStoreRealm import com.san.englishbender.data.local.dataStore.IDataStore import com.san.englishbender.data.local.models.AppSettings +import com.san.englishbender.data.local.models.Board +import com.san.englishbender.data.local.models.FlashCard import com.san.englishbender.data.local.models.Record import com.san.englishbender.data.local.models.Stats import com.san.englishbender.data.local.models.Tag +import com.san.englishbender.data.repositories.BoardsRepository +import com.san.englishbender.data.repositories.FlashCardsRepository import com.san.englishbender.data.repositories.RecordsRepository import com.san.englishbender.data.repositories.StatsRepository import com.san.englishbender.data.repositories.TagsRepository +import com.san.englishbender.domain.repositories.IBoardsRepository +import com.san.englishbender.domain.repositories.IFlashCardsRepository import com.san.englishbender.domain.repositories.IRecordsRepository import com.san.englishbender.domain.repositories.IStatsRepository import com.san.englishbender.domain.repositories.ITagsRepository @@ -22,6 +28,8 @@ private val dataStoreModels = setOf( Record::class, Tag::class, Stats::class, + Board::class, + FlashCard::class, ) val databaseModule = module { @@ -36,4 +44,6 @@ val databaseModule = module { single { RecordsRepository(get()) } single { TagsRepository(get(), get()) } single { StatsRepository(get()) } + single { BoardsRepository(get()) } + single { FlashCardsRepository(get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt index cde13ec..89b9d87 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt @@ -1,5 +1,7 @@ package com.san.englishbender.core.di +import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase +import com.san.englishbender.domain.usecases.flashCards.GetFlashCardsFlowUseCase import com.san.englishbender.domain.usecases.records.GetRecordFlowUseCase import com.san.englishbender.domain.usecases.records.GetRecordsCountUseCase import com.san.englishbender.domain.usecases.records.GetRecordsUseCase @@ -34,4 +36,8 @@ val useCaseModule = module { single { SaveTagUseCase(get()) } single { SaveTagColorUseCase(get()) } single { DeleteTagUseCase(get()) } + + // --- FlashCards + single { GetBoardsFlowUseCase(get()) } + single { GetFlashCardsFlowUseCase(get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt index e9625e5..43dad58 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt @@ -1,6 +1,7 @@ package com.san.englishbender.core.di import com.san.englishbender.ui.TagsViewModel +import com.san.englishbender.ui.flashcards.FlashCardsViewModel import com.san.englishbender.ui.recordDetails.RecordDetailsViewModel import com.san.englishbender.ui.records.RecordsViewModel import com.san.englishbender.ui.stats.StatsViewModel @@ -11,4 +12,5 @@ val viewModelModule = module { single { RecordDetailsViewModel(get(), get(), get(), get(), get()) } single { StatsViewModel(get(), get()) } single { TagsViewModel(get(), get(), get(), get(), get()) } + single { FlashCardsViewModel(get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/navigation/Destinations.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/navigation/Destinations.kt index 23e6241..807e414 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/navigation/Destinations.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/navigation/Destinations.kt @@ -1,6 +1,7 @@ package com.san.englishbender.core.navigation import com.san.englishbender.core.navigation.Screens.COLOR_PICKER_SCREEN +import com.san.englishbender.core.navigation.Screens.FLASHCARDS_SCREEN import com.san.englishbender.core.navigation.Screens.RECORDS_SCREEN import com.san.englishbender.core.navigation.Screens.RECORD_DETAIL_SCREEN import com.san.englishbender.core.navigation.Screens.STATS_SCREEN @@ -11,6 +12,7 @@ object Screens { const val STATS_SCREEN = "stats" const val RECORDS_SCREEN = "records" const val RECORD_DETAIL_SCREEN = "recordDetail" + const val FLASHCARDS_SCREEN = "flashcards" // --- const val TAG_LIST_SCREEN = "tag_list" @@ -20,6 +22,7 @@ object Screens { object DestinationsArgs { const val RECORD_ID_ARG = "recordId" + const val BOARD_ID_ARG = "boardId" const val TAG_ID_ARG = "tagId" } @@ -27,6 +30,7 @@ object Destinations { const val STATS_ROUTE = STATS_SCREEN const val RECORD_ROUTE = RECORDS_SCREEN const val RECORD_DETAIL_ROUTE = "$RECORD_DETAIL_SCREEN?recordId={recordId}" + const val FLASHCARDS_ROUTE = "$FLASHCARDS_SCREEN?boardId={boardId}" const val TAG_LIST_ROUTE = TAG_LIST_SCREEN const val TAG_CREATE_ROUTE = "$TAG_ADD_EDIT_SCREEN?tagId={tagId}" diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/dataStore/DataStoreRealm.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/dataStore/DataStoreRealm.kt index 88eb9dc..6b6be97 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/dataStore/DataStoreRealm.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/dataStore/DataStoreRealm.kt @@ -1,18 +1,11 @@ package com.san.englishbender.data.local.dataStore -import com.san.englishbender.data.local.mappers.toEntity import com.san.englishbender.data.local.models.AppSettings -import com.san.englishbender.data.local.models.Record -import com.san.englishbender.data.local.models.Stats -import com.san.englishbender.data.local.models.Tag import com.san.englishbender.ioDispatcher import io.realm.kotlin.Realm import io.realm.kotlin.UpdatePolicy -import io.realm.kotlin.ext.asFlow import io.realm.kotlin.ext.query -import io.realm.kotlin.notifications.InitialResults import io.realm.kotlin.notifications.SingleQueryChange -import io.realm.kotlin.notifications.UpdatedResults import io.realm.kotlin.types.RealmObject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/RecordMappers.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/RecordMappers.kt deleted file mode 100644 index ffd29c6..0000000 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/RecordMappers.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.san.englishbender.data.local.mappers - -import com.san.englishbender.data.local.models.Record -import com.san.englishbender.data.local.models.Tag -import com.san.englishbender.domain.entities.RecordEntity -import com.san.englishbender.domain.entities.TagEntity -import io.realm.kotlin.ext.realmListOf -import io.realm.kotlin.ext.toRealmList - -fun Record.toEntity(): RecordEntity = - RecordEntity( - id = id, - title = title, - text = text, - plainText = plainText, - creationDate = creationDate, - isDeleted = isDeleted, - isDraft = isDraft, - backgroundColor = backgroundColor, - tags = tags.map { - TagEntity( - id = it.id, - name = it.name, - color = it.color - ) - } - ) - -fun RecordEntity.toLocal(): Record = - Record( - id = id, - title = title, - text = text, - plainText = plainText, - creationDate = creationDate, - isDeleted = isDeleted, - isDraft = isDraft, - backgroundColor = backgroundColor, - tags = tags?.map { Tag(it.id, it.name, it.color, it.isWhite) }?.toRealmList() ?: realmListOf() - ) - -fun List.toEntity() = this.map { it.toEntity() } -fun List.toLocal() = this.map { it.toLocal() } diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/StatsMappers.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/StatsMappers.kt deleted file mode 100644 index eda57cd..0000000 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/StatsMappers.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.san.englishbender.data.local.mappers - -import com.san.englishbender.data.local.models.Stats -import com.san.englishbender.domain.entities.StatsEntity - - -fun Stats.toEntity() = - StatsEntity( - recordsCount = this.recordsCount, - wordsCount = this.wordsCount, - lettersCount = this.lettersCount - ) - -fun StatsEntity.toLocal() = - Stats(this.recordsCount, this.wordsCount, this.lettersCount) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/TagMappers.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/TagMappers.kt deleted file mode 100644 index 6a54ce7..0000000 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/mappers/TagMappers.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.san.englishbender.data.local.mappers - -import com.san.englishbender.data.local.models.Tag -import com.san.englishbender.domain.entities.TagEntity - -fun Tag.toEntity() = - TagEntity( - id = id, - name = name, - color = color, - isWhite = isWhite - ) - -fun TagEntity.toLocal() = - Tag( - id = id, - name = name, - color = color, - isWhite = isWhite - ) - -fun List.toEntity() = this.map { it.toEntity() } -fun List.toLocal() = this.map { it.toLocal() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt new file mode 100644 index 0000000..f63c31f --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt @@ -0,0 +1,54 @@ +package com.san.englishbender.data.local.models + +import com.san.englishbender.domain.entities.BoardEntity +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.toRealmList +import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class Board : RealmObject { + @PrimaryKey + var id: String = "" + var name: String = "" + var description: String = "" + var backgroundColor: String = "" + var flashCards: RealmList = realmListOf() + + constructor( + id: String, + name: String, + description: String, + backgroundColor: String = "", + flashCards: RealmList + ) { + this.id = id + this.name = name + this.description = description + this.backgroundColor = backgroundColor + this.flashCards = flashCards + } + + constructor() {} +} + +fun Board.toEntity() = + BoardEntity( + id = id, + name = name, + description = description, + backgroundColor = backgroundColor, + flashCards = flashCards.toEntity() + ) + +fun BoardEntity.toLocal() = + Board( + id = id, + name = name, + description = description, + backgroundColor = backgroundColor, + flashCards = flashCards.toLocal().toRealmList() + ) + +fun List.toEntity() = this.map { it.toEntity() } +fun List.toLocal() = this.map { it.toLocal() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt new file mode 100644 index 0000000..9ede25c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt @@ -0,0 +1,37 @@ +package com.san.englishbender.data.local.models + +import com.san.englishbender.domain.entities.FlashCardEntity +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +open class FlashCard : RealmObject { + @PrimaryKey + var id: String = "" + var word: String = "" + var description: String = "" + + constructor(id: String, word: String, description: String) { + this.id = id + this.word = word + this.description = description + } + + constructor() {} +} + +fun FlashCard.toEntity() = + FlashCardEntity( + id = id, + word = word, + description = description + ) + +fun FlashCardEntity.toLocal() = + FlashCard( + id = id, + word = word, + description = description + ) + +fun List.toEntity() = this.map { it.toEntity() } +fun List.toLocal() = this.map { it.toLocal() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Record.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Record.kt index daff1f6..2fe10a2 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Record.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Record.kt @@ -1,6 +1,9 @@ package com.san.englishbender.data.local.models +import com.san.englishbender.domain.entities.RecordEntity +import com.san.englishbender.domain.entities.TagEntity import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.toRealmList import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.PrimaryKey @@ -40,4 +43,39 @@ class Record : RealmObject { } constructor() {} -} \ No newline at end of file +} + +fun Record.toEntity(): RecordEntity = + RecordEntity( + id = id, + title = title, + text = text, + plainText = plainText, + creationDate = creationDate, + isDeleted = isDeleted, + isDraft = isDraft, + backgroundColor = backgroundColor, + tags = tags.map { + TagEntity( + id = it.id, + name = it.name, + color = it.color + ) + } + ) + +fun RecordEntity.toLocal(): Record = + Record( + id = id, + title = title, + text = text, + plainText = plainText, + creationDate = creationDate, + isDeleted = isDeleted, + isDraft = isDraft, + backgroundColor = backgroundColor, + tags = tags?.map { Tag(it.id, it.name, it.color, it.isWhite) }?.toRealmList() ?: realmListOf() + ) + +fun List.toEntity() = this.map { it.toEntity() } +fun List.toLocal() = this.map { it.toLocal() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Stats.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Stats.kt index 5d0cd17..ba841f4 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Stats.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Stats.kt @@ -1,5 +1,6 @@ package com.san.englishbender.data.local.models +import com.san.englishbender.domain.entities.StatsEntity import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.PrimaryKey @@ -17,4 +18,14 @@ class Stats : RealmObject { } constructor() {} -} \ No newline at end of file +} + +fun Stats.toEntity() = + StatsEntity( + recordsCount = this.recordsCount, + wordsCount = this.wordsCount, + lettersCount = this.lettersCount + ) + +fun StatsEntity.toLocal() = + Stats(this.recordsCount, this.wordsCount, this.lettersCount) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Tag.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Tag.kt index 3b8a95c..333a45d 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Tag.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Tag.kt @@ -1,5 +1,6 @@ package com.san.englishbender.data.local.models +import com.san.englishbender.domain.entities.TagEntity import io.realm.kotlin.ext.backlinks import io.realm.kotlin.query.RealmResults import io.realm.kotlin.types.RealmObject @@ -21,4 +22,23 @@ open class Tag : RealmObject { } constructor() {} -} \ No newline at end of file +} + +fun Tag.toEntity() = + TagEntity( + id = id, + name = name, + color = color, + isWhite = isWhite + ) + +fun TagEntity.toLocal() = + Tag( + id = id, + name = name, + color = color, + isWhite = isWhite + ) + +fun List.toEntity() = this.map { it.toEntity() } +fun List.toLocal() = this.map { it.toLocal() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt new file mode 100644 index 0000000..32c509c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt @@ -0,0 +1,46 @@ +package com.san.englishbender.data.repositories + +import com.san.englishbender.core.extensions.doQuery +import com.san.englishbender.data.local.models.Board +import com.san.englishbender.data.local.models.toEntity +import com.san.englishbender.data.local.models.toLocal +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.repositories.IBoardsRepository +import com.san.englishbender.ioDispatcher +import io.realm.kotlin.Realm +import io.realm.kotlin.UpdatePolicy +import io.realm.kotlin.ext.query +import io.realm.kotlin.notifications.InitialResults +import io.realm.kotlin.notifications.UpdatedResults +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +class BoardsRepository( + private val realm: Realm +): IBoardsRepository { + override fun getBoardsFlow(): Flow> = flow { + realm.query(Board::class).asFlow().collect { changes -> + when (changes) { + is InitialResults, + is UpdatedResults -> emit(changes.list.toList().toEntity()) + else -> {} + } + } + }.flowOn(ioDispatcher) + + override suspend fun getBoards(): List = doQuery { + realm.query(Board::class).find().map { it.toEntity() } + } + + override suspend fun saveBoard(board: BoardEntity): Unit = doQuery { + realm.write { copyToRealm(board.toLocal(), UpdatePolicy.ALL) } + } + + override suspend fun deleteBoard(boardId: String): Unit = doQuery { + realm.write { + val board = query("id == $0", boardId).find() + delete(board) + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/FlashCardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/FlashCardsRepository.kt new file mode 100644 index 0000000..7c7a31f --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/FlashCardsRepository.kt @@ -0,0 +1,46 @@ +package com.san.englishbender.data.repositories + +import com.san.englishbender.core.extensions.doQuery +import com.san.englishbender.data.local.models.FlashCard +import com.san.englishbender.data.local.models.toEntity +import com.san.englishbender.data.local.models.toLocal +import com.san.englishbender.domain.entities.FlashCardEntity +import com.san.englishbender.domain.repositories.IFlashCardsRepository +import com.san.englishbender.ioDispatcher +import io.realm.kotlin.Realm +import io.realm.kotlin.UpdatePolicy +import io.realm.kotlin.ext.query +import io.realm.kotlin.notifications.InitialResults +import io.realm.kotlin.notifications.UpdatedResults +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +class FlashCardsRepository( + private val realm: Realm +): IFlashCardsRepository { + override fun getFlashCardsFlow(): Flow> = flow { + realm.query(FlashCard::class).asFlow().collect { changes -> + when (changes) { + is InitialResults, + is UpdatedResults -> emit(changes.list.toList().toEntity()) + else -> {} + } + } + }.flowOn(ioDispatcher) + + override suspend fun getFlashCards(): List = doQuery { + realm.query(FlashCard::class).find().map { it.toEntity() } + } + + override suspend fun saveFlashCard(card: FlashCardEntity): Unit = doQuery { + realm.write { copyToRealm(card.toLocal(), UpdatePolicy.ALL) } + } + + override suspend fun deleteFlashCard(cardId: String): Unit = doQuery { + realm.write { + val card = query("id == $0", cardId).find() + delete(card) + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/RecordsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/RecordsRepository.kt index fdba55a..a2247bf 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/RecordsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/RecordsRepository.kt @@ -1,10 +1,9 @@ package com.san.englishbender.data.repositories import com.san.englishbender.core.extensions.doQuery -import com.san.englishbender.data.local.mappers.toEntity -import com.san.englishbender.data.local.mappers.toLocal import com.san.englishbender.data.local.models.Record -import com.san.englishbender.data.local.models.Tag +import com.san.englishbender.data.local.models.toEntity +import com.san.englishbender.data.local.models.toLocal import com.san.englishbender.domain.entities.RecordEntity import com.san.englishbender.domain.repositories.IRecordsRepository import com.san.englishbender.ioDispatcher diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/StatsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/StatsRepository.kt index 34b876f..447d008 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/StatsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/StatsRepository.kt @@ -1,9 +1,9 @@ package com.san.englishbender.data.repositories import com.san.englishbender.core.extensions.doQuery -import com.san.englishbender.data.local.mappers.toEntity -import com.san.englishbender.data.local.mappers.toLocal import com.san.englishbender.data.local.models.Stats +import com.san.englishbender.data.local.models.toEntity +import com.san.englishbender.data.local.models.toLocal import com.san.englishbender.domain.entities.StatsEntity import com.san.englishbender.domain.repositories.IStatsRepository import com.san.englishbender.ioDispatcher diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/TagsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/TagsRepository.kt index 9cb30a9..7a39b20 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/TagsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/TagsRepository.kt @@ -2,10 +2,10 @@ package com.san.englishbender.data.repositories import com.san.englishbender.core.extensions.doQuery import com.san.englishbender.data.local.dataStore.IDataStore -import com.san.englishbender.data.local.mappers.toEntity -import com.san.englishbender.data.local.mappers.toLocal import com.san.englishbender.data.local.models.AppSettings import com.san.englishbender.data.local.models.Tag +import com.san.englishbender.data.local.models.toEntity +import com.san.englishbender.data.local.models.toLocal import com.san.englishbender.domain.entities.TagEntity import com.san.englishbender.domain.repositories.ITagsRepository import com.san.englishbender.ioDispatcher diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt new file mode 100644 index 0000000..7270a93 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt @@ -0,0 +1,14 @@ +package com.san.englishbender.domain.entities + +import com.san.englishbender.CommonParcelable +import com.san.englishbender.CommonParcelize + +@CommonParcelize +data class BoardEntity( + var id: String = "", + var name: String = "", + var description: String = "", + var creationDate: Long = 0, + var backgroundColor: String = "", + var flashCards: List +) : CommonParcelable \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt new file mode 100644 index 0000000..82ca8fd --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt @@ -0,0 +1,11 @@ +package com.san.englishbender.domain.entities + +import com.san.englishbender.CommonParcelable +import com.san.englishbender.CommonParcelize + +@CommonParcelize +data class FlashCardEntity( + var id: String = "", + var word: String = "", + var description: String = "" +) : CommonParcelable \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt new file mode 100644 index 0000000..106de6f --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt @@ -0,0 +1,11 @@ +package com.san.englishbender.domain.repositories + +import com.san.englishbender.domain.entities.BoardEntity +import kotlinx.coroutines.flow.Flow + +interface IBoardsRepository { + fun getBoardsFlow() : Flow> + suspend fun getBoards() : List + suspend fun saveBoard(board: BoardEntity) + suspend fun deleteBoard(boardId: String) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IFlashCardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IFlashCardsRepository.kt new file mode 100644 index 0000000..535bb86 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IFlashCardsRepository.kt @@ -0,0 +1,11 @@ +package com.san.englishbender.domain.repositories + +import com.san.englishbender.domain.entities.FlashCardEntity +import kotlinx.coroutines.flow.Flow + +interface IFlashCardsRepository { + fun getFlashCardsFlow() : Flow> + suspend fun getFlashCards() : List + suspend fun saveFlashCard(card: FlashCardEntity) + suspend fun deleteFlashCard(cardId: String) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardsFlowUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardsFlowUseCase.kt new file mode 100644 index 0000000..a05d745 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardsFlowUseCase.kt @@ -0,0 +1,11 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.repositories.IBoardsRepository +import kotlinx.coroutines.flow.Flow + +class GetBoardsFlowUseCase( + private val boardsRepository: IBoardsRepository +) { + operator fun invoke(): Flow> = boardsRepository.getBoardsFlow() +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsFlowUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsFlowUseCase.kt new file mode 100644 index 0000000..1aa2c0a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsFlowUseCase.kt @@ -0,0 +1,11 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.data.repositories.FlashCardsRepository +import com.san.englishbender.domain.entities.FlashCardEntity +import kotlinx.coroutines.flow.Flow + +class GetFlashCardsFlowUseCase( + private val flashCardsRepository: FlashCardsRepository +) { + operator fun invoke(): Flow> = flashCardsRepository.getFlashCardsFlow() +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt new file mode 100644 index 0000000..1c29bce --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt @@ -0,0 +1,38 @@ +package com.san.englishbender.ui.flashcards + +import com.san.englishbender.SharedRes +import com.san.englishbender.core.extensions.WhileUiSubscribed +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase +import com.san.englishbender.ui.ViewModel +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + + +data class FlashCardsUiState( + val isLoading: Boolean = false, + val boards: List = emptyList(), + val userMessage: StringResource? = null +) + +class FlashCardsViewModel( + getBoardsFlowUseCase: GetBoardsFlowUseCase, +) : ViewModel() { + + val uiState: StateFlow = + getBoardsFlowUseCase() + .map { FlashCardsUiState(boards = it) } + .catch { FlashCardsUiState(userMessage = SharedRes.strings.loading_records_error) } + .stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = FlashCardsUiState(isLoading = true) + ) + + fun saveBoard(boardName: String) = safeLaunch { + + } +} \ No newline at end of file From c38b0fb19c3b9db6fea5a5b08530f0a65eae0577 Mon Sep 17 00:00:00 2001 From: Aleksandr Symbiotov Date: Sun, 11 Feb 2024 19:19:45 +0100 Subject: [PATCH 2/5] Implementation of adding boards and flashcards, +refactoring --- androidApp/build.gradle.kts | 2 + .../android/navigation/EBNavHost.kt | 27 +- .../android/navigation/NavigationActions.kt | 5 + .../android/ui/common/AppDrawer.kt | 13 +- .../ui/common/BackgroundColorPicker.kt | 79 ++++ .../android/ui/common/DialogHeader.kt | 28 +- .../android/ui/common/TextComposables.kt | 33 ++ .../android/ui/flashcards/BoardScreen.kt | 375 ++++++++++++++++++ .../{FlashCardsScreen.kt => BoardsScreen.kt} | 173 ++++---- .../ui/recordDetails/RecordDetailsScreen.kt | 5 +- .../BackgroundColorPickerBSContent.kt | 1 - .../android/ui/tags/AddEditTagScreen.kt | 4 +- .../android/ui/tags/ColorPickerScreen.kt | 4 +- .../android/ui/tags/TagsScreen.kt | 21 +- .../englishbender/android/ui/theme/Color.kt | 13 + .../englishbender/core/di/DatabaseModule.kt | 2 +- .../englishbender/core/di/UseCaseModule.kt | 6 +- .../englishbender/core/di/ViewModelModule.kt | 4 +- .../core/navigation/Destinations.kt | 3 + .../com/san/englishbender/data/Result.kt | 6 +- .../englishbender/data/local/models/Board.kt | 5 - .../data/local/models/FlashCard.kt | 19 +- .../data/repositories/BoardsRepository.kt | 11 +- .../data/repositories/FlashCardsRepository.kt | 10 +- .../data/repositories/TagsRepository.kt | 12 +- .../domain/entities/BoardEntity.kt | 7 +- .../domain/entities/FlashCardEntity.kt | 6 +- .../domain/entities/RecordEntity.kt | 2 + .../domain/repositories/IBoardsRepository.kt | 1 + .../repositories/IFlashCardsRepository.kt | 4 +- .../flashCards/GetBoardByIdUseCase.kt | 11 + .../flashCards/GetFlashCardsFlowUseCase.kt | 11 - .../usecases/flashCards/SaveBoardUseCase.kt | 8 + .../ui/flashcards/BoardsViewModel.kt | 99 +++++ .../ui/flashcards/FlashCardsViewModel.kt | 79 +++- .../recordDetails/RecordDetailsViewModel.kt | 14 +- .../ui/records/RecordsViewModel.kt | 8 +- 37 files changed, 931 insertions(+), 180 deletions(-) create mode 100644 androidApp/src/main/java/com/san/englishbender/android/ui/common/BackgroundColorPicker.kt create mode 100644 androidApp/src/main/java/com/san/englishbender/android/ui/common/TextComposables.kt create mode 100644 androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt rename androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/{FlashCardsScreen.kt => BoardsScreen.kt} (60%) create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt delete mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsFlowUseCase.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveBoardUseCase.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index dd8a994..d263ba8 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -111,6 +111,8 @@ dependencies { implementation("androidx.work:work-runtime-ktx:2.9.0") + implementation("com.wajahatkarim:flippable:1.5.4") + implementation("androidx.compose.ui:ui:1.5.0") implementation("androidx.compose.ui:ui-tooling:1.5.0") implementation("androidx.compose.ui:ui-tooling-preview:1.5.0") diff --git a/androidApp/src/main/java/com/san/englishbender/android/navigation/EBNavHost.kt b/androidApp/src/main/java/com/san/englishbender/android/navigation/EBNavHost.kt index e3b41f8..703947f 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/navigation/EBNavHost.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/navigation/EBNavHost.kt @@ -18,15 +18,18 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.san.englishbender.android.ui.EBAppState import com.san.englishbender.android.ui.common.AppDrawer -import com.san.englishbender.android.ui.flashcards.FlashCardsScreen +import com.san.englishbender.android.ui.flashcards.BoardScreen +import com.san.englishbender.android.ui.flashcards.BoardsScreen import com.san.englishbender.android.ui.recordDetails.RecordDetailsScreen import com.san.englishbender.android.ui.records.RecordsScreen import com.san.englishbender.android.ui.stats.StatsScreen import com.san.englishbender.core.navigation.Destinations +import com.san.englishbender.core.navigation.DestinationsArgs.BOARD_ID_ARG import com.san.englishbender.core.navigation.DestinationsArgs.RECORD_ID_ARG import com.san.englishbender.core.navigation.NavigationCommand import com.san.englishbender.core.navigation.Navigator import com.san.englishbender.core.navigation.Screens +import io.github.aakira.napier.log import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -120,13 +123,13 @@ fun EBNavHost( ) } - composable(route = Destinations.FLASHCARDS_ROUTE) { + composable(route = Destinations.BOARDS_ROUTE) { AppDrawer( drawerState, currentRoute, navActions, content = { - FlashCardsScreen( + BoardsScreen( onBoardClick = { boardId -> val route = Screens.FLASHCARDS_SCREEN.let { route -> boardId?.let { "$route?boardId=$it" } ?: route @@ -138,5 +141,23 @@ fun EBNavHost( } ) } + + composable( + route = Destinations.FLASHCARDS_ROUTE, + arguments = listOf( + navArgument(BOARD_ID_ARG) { + nullable = true + defaultValue = null + type = NavType.StringType + }, + ), + ) { entry -> + val boardId = entry.arguments?.getString(BOARD_ID_ARG) + + BoardScreen( + boardId, + onBackClick = { navigator.popBackStack() } + ) + } } } \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/navigation/NavigationActions.kt b/androidApp/src/main/java/com/san/englishbender/android/navigation/NavigationActions.kt index 953fa68..e5568d0 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/navigation/NavigationActions.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/navigation/NavigationActions.kt @@ -1,6 +1,7 @@ package com.san.englishbender.android.navigation import androidx.navigation.NavHostController +import com.san.englishbender.core.navigation.Destinations.BOARDS_ROUTE import com.san.englishbender.core.navigation.Destinations.FLASHCARDS_ROUTE import com.san.englishbender.core.navigation.Destinations.RECORD_DETAIL_ROUTE import com.san.englishbender.core.navigation.Destinations.STATS_ROUTE @@ -15,6 +16,10 @@ class EBNavigationActions(private val navController: NavHostController) { navController.navigate(STATS_ROUTE) } + fun navigateToBoards() { + navController.navigate(BOARDS_ROUTE) + } + fun navigateToFlashCards() { navController.navigate(FLASHCARDS_ROUTE) } diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/AppDrawer.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/AppDrawer.kt index 2f3115b..768571a 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/common/AppDrawer.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/AppDrawer.kt @@ -35,11 +35,6 @@ data class DrawerNavOptions( ) private val drawerNavOptions = listOf( - DrawerNavOptions( - name = "Stats", - route = Destinations.STATS_ROUTE, - icon = Icons.Default.Analytics - ), DrawerNavOptions( name = "Records", route = Destinations.RECORD_ROUTE, @@ -47,9 +42,14 @@ private val drawerNavOptions = listOf( ), DrawerNavOptions( name = "Flash-cards", - route = Destinations.FLASHCARDS_ROUTE, + route = Destinations.BOARDS_ROUTE, icon = Icons.Default.ViewCarousel ), + DrawerNavOptions( + name = "Stats", + route = Destinations.STATS_ROUTE, + icon = Icons.Default.Analytics + ), ) @Composable @@ -85,6 +85,7 @@ fun AppDrawer( when (item.route) { Destinations.STATS_ROUTE -> navActions.navigateToStats() Destinations.RECORD_ROUTE -> navActions.navigateToRecords() + Destinations.BOARDS_ROUTE -> navActions.navigateToBoards() Destinations.FLASHCARDS_ROUTE -> navActions.navigateToFlashCards() } // navController.navigate(item.route) diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/BackgroundColorPicker.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/BackgroundColorPicker.kt new file mode 100644 index 0000000..23cb95e --- /dev/null +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/BackgroundColorPicker.kt @@ -0,0 +1,79 @@ +package com.san.englishbender.android.ui.common + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.san.englishbender.android.core.extensions.noRippleClickable +import com.san.englishbender.android.ui.theme.backgroundColors +import com.san.englishbender.android.ui.theme.selectedLabelColor +import com.san.englishbender.core.extensions.ifNotEmpty +import io.github.aakira.napier.log + +@Composable +fun BackgroundColorPicker( + modifier: Modifier = Modifier, + label: String = "Background Color", + listState: LazyListState = rememberLazyListState(), + onClick: (color: Color) -> Unit +) { + var selectedColor by remember { mutableStateOf(backgroundColors.first()) } + + Column(modifier = modifier) { + label.ifNotEmpty { + Text( + modifier = Modifier.padding(8.dp), + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + text = label + ) + } + + LazyRow( + state = listState, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(backgroundColors.size) { index -> + val color = backgroundColors[index] + val border = when (selectedColor == color) { + true -> Modifier.border(2.dp, selectedLabelColor, RoundedCornerShape(4.dp)) + false -> Modifier.border(1.dp, Color.LightGray, RoundedCornerShape(4.dp)) + } + Card( + modifier = Modifier + .size(60.dp) + .then(border) + .noRippleClickable { + selectedColor = color + log(tag = "containerColor") { "onClick: $color" } + onClick(color) + }, + colors = CardDefaults.cardColors(containerColor = color) + ) {} + } + } + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/DialogHeader.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/DialogHeader.kt index a241742..c11f8f6 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/common/DialogHeader.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/DialogHeader.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Icon import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -16,15 +16,33 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun DialogHeader( +fun DialogHeader(title: String) { + Row(modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = title, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } +} + +@Composable +fun DialogNavHeader( title: String, - onClick: () -> Unit + onClick: () -> Unit = {} ) { - Row(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) { + Row(modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp)) { Row(Modifier.weight(1f)) { Icon( modifier = Modifier.clickable { onClick() }, - imageVector = Icons.Filled.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null ) } diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/TextComposables.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/TextComposables.kt new file mode 100644 index 0000000..9505b21 --- /dev/null +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/TextComposables.kt @@ -0,0 +1,33 @@ +package com.san.englishbender.android.ui.common + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType + +@Composable +fun EBOutlinedTextField( + modifier: Modifier = Modifier, + value: String, + placeholder: String = "", + singleLine: Boolean = false, + keyboardOptions: KeyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + onValueChange: (String) -> Unit = {} +) { + OutlinedTextField( + modifier = modifier, + value = value, + singleLine = singleLine, + placeholder = { Text(placeholder) }, + keyboardOptions = keyboardOptions, + onValueChange = onValueChange + ) +} \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt new file mode 100644 index 0000000..b4b7ef5 --- /dev/null +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt @@ -0,0 +1,375 @@ +package com.san.englishbender.android.ui.flashcards + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi +import com.mohamedrejeb.richeditor.model.rememberRichTextState +import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor +import com.san.englishbender.android.core.extensions.toColor +import com.san.englishbender.android.core.extensions.toHex +import com.san.englishbender.android.ui.common.BackgroundColorPicker +import com.san.englishbender.android.ui.common.BaseDialogContent +import com.san.englishbender.android.ui.common.EBOutlinedButton +import com.san.englishbender.android.ui.common.EBOutlinedTextField +import com.san.englishbender.android.ui.common.richText.RichTextStyleRow +import com.san.englishbender.android.ui.common.widgets.ErrorView +import com.san.englishbender.android.ui.common.widgets.LoadingView +import com.san.englishbender.android.ui.theme.backgroundColors +import com.san.englishbender.core.extensions.ifNotEmpty +import com.san.englishbender.core.extensions.isNotNull +import com.san.englishbender.domain.entities.FlashCardEntity +import com.san.englishbender.ui.flashcards.BoardUiState +import com.san.englishbender.ui.flashcards.BoardsViewModel +import com.san.englishbender.ui.flashcards.FlashCardsUiState +import com.san.englishbender.ui.flashcards.FlashCardsViewModel +import com.wajahatkarim.flippable.FlipAnimationType +import com.wajahatkarim.flippable.Flippable +import com.wajahatkarim.flippable.rememberFlipController +import io.github.aakira.napier.log +import org.koin.androidx.compose.getViewModel + + +@Composable +fun BoardScreen( + boardId: String?, + onBackClick: () -> Unit +) { + val viewModel: BoardsViewModel = getViewModel() + val uiState by viewModel.boardUiState.collectAsStateWithLifecycle() + + LaunchedEffect(boardId) { + boardId?.let { viewModel.getBoard(it) } + } + + when { + uiState.isLoading -> LoadingView() + uiState.userMessage.isNotNull -> ErrorView(userMessage = uiState.userMessage) + else -> BoardContent( + viewModel, + uiState, + onBackClick + ) + } +} + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalFoundationApi::class, ExperimentalRichTextApi::class +) +@Composable +fun BoardContent( + viewModel: BoardsViewModel, + uiState: BoardUiState, + onBackClick: () -> Unit +) { + val focusManager = LocalFocusManager.current + val controller = rememberFlipController() + val cards = uiState.board?.flashCards ?: emptyList() + val pagerState = rememberPagerState(pageCount = { cards.size }) +// val pagerState = rememberPagerState(pageCount = { 1 }) + var boardCreationDialog by remember { mutableStateOf(false) } + + val containerColor = uiState.board?.backgroundColor?.toColor + ?: MaterialTheme.colorScheme.surfaceVariant + + val richTextState = rememberRichTextState() + richTextState.setConfig( + linkColor = Color.Blue, + linkTextDecoration = TextDecoration.Underline, + codeColor = Color.DarkGray, + codeBackgroundColor = Color.Transparent, + codeStrokeColor = Color.Transparent, + ) + + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = containerColor, + topBar = { + key(containerColor) { + TopAppBar( + modifier = Modifier.fillMaxWidth(), + title = {}, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = containerColor + ), + navigationIcon = { + Icon( + rememberVectorPainter(Icons.AutoMirrored.Filled.ArrowBack), + contentDescription = null, + modifier = Modifier + .padding(start = 8.dp) + .clickable { onBackClick() } + ) + }, + actions = { + Icon( + rememberVectorPainter(Icons.Filled.Add), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .padding(8.dp) + .clickable { boardCreationDialog = true } + ) + Icon( + rememberVectorPainter(Icons.Filled.Edit), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .padding(8.dp) + .clickable { } + ) + Icon( + rememberVectorPainter(Icons.Filled.Delete), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .padding(8.dp) + .clickable { } + ) + } + ) + } + } + ) { paddingValues -> + + Column(Modifier.fillMaxSize()) { + + if (cards.isEmpty()) { + Row( + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text("Board is empty") + } + return@Scaffold + } + + HorizontalPager( + state = pagerState + ) { pageIndex -> + + val card = cards[pageIndex] + + LaunchedEffect(Unit) { + card.back.ifNotEmpty { richTextState.setHtml(it) } + } + +// val card = FlashCardEntity( +// front = "squander", +// back = "waste (something, especially money or time) in a reckless and foolish manner", +// ) + + Spacer(Modifier.height(32.dp)) + + Flippable( + modifier = Modifier + .fillMaxWidth() + .height(600.dp) + .padding(paddingValues), + frontSide = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) + .background(Color.White, RoundedCornerShape(6.dp)), + contentAlignment = Alignment.Center + ) { + Text( + text = card.front, + color = Color.Black, + fontSize = 20.sp + ) + } + }, + backSide = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) + .background(Color.White, RoundedCornerShape(6.dp)), + contentAlignment = Alignment.Center + ) { + BasicRichTextEditor( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .verticalScroll(state = rememberScrollState()), + state = richTextState, + textStyle = TextStyle( + fontSize = 20.sp + ), + readOnly = true + ) + } + }, + flipController = controller, + flipAnimationType = FlipAnimationType.HORIZONTAL_CLOCKWISE + ) + } + } + } + + if (boardCreationDialog) { + CardCreationDialog( + onCardCreate = { flashCard -> + uiState.board?.let { + it.flashCards = it.flashCards.plus(listOf(flashCard)) + viewModel.saveBoard(it) + } + }, + dismiss = { boardCreationDialog = false } + ) + } +} + +@OptIn(ExperimentalRichTextApi::class) +@Composable +fun CardCreationDialog( + onCardCreate: (FlashCardEntity) -> Unit, + dismiss: () -> Unit +) { + BaseDialogContent( + height = 450.dp, + dismiss = dismiss + ) { + var word by remember { mutableStateOf("") } + val card by remember { mutableStateOf(FlashCardEntity()) } + + val richTextState = rememberRichTextState() + richTextState.setConfig( + linkColor = Color.Blue, + linkTextDecoration = TextDecoration.Underline, + codeColor = Color.DarkGray, + codeBackgroundColor = Color.Transparent, + codeStrokeColor = Color.Transparent, + ) + + Column( + Modifier + .fillMaxWidth() + .padding(vertical = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + EBOutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = word, + placeholder = "Word", + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + onValueChange = { + word = it + card.front = it + } + ) + + Spacer(Modifier.height(16.dp)) + + RichTextStyleRow( + modifier = Modifier.padding(horizontal = 16.dp), + state = richTextState + ) + BasicRichTextEditor( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) + .verticalScroll(state = rememberScrollState()), + state = richTextState, + minLines = 3, + decorationBox = { innerTextField -> + Box(Modifier.padding(8.dp)) { + if (richTextState.annotatedString.text.isEmpty()) { + Text( + text = "Description", + color = Color.LightGray + ) + } + innerTextField() + } + } + ) + + Spacer(Modifier.weight(1f)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + horizontalArrangement = Arrangement.End + ) { + EBOutlinedButton( + text = "Save", + onClick = { + if (card.front.isEmpty()) return@EBOutlinedButton + + // TODO: delete it when RichTextEditor has onValueChanged callback + card.back = richTextState.toHtml() + + onCardCreate(card) + dismiss() + } + ) + } + } + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/FlashCardsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt similarity index 60% rename from androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/FlashCardsScreen.kt rename to androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt index 6ce2196..9d3d9f1 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/FlashCardsScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt @@ -5,28 +5,26 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.FloatingActionButton -import androidx.compose.material.Icon +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -37,39 +35,36 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.san.englishbender.android.core.extensions.toHex +import com.san.englishbender.android.ui.common.BackgroundColorPicker +import com.san.englishbender.android.ui.common.BaseDialogContent import com.san.englishbender.android.ui.common.EBOutlinedButton +import com.san.englishbender.android.ui.common.EBOutlinedTextField import com.san.englishbender.android.ui.common.widgets.ErrorView import com.san.englishbender.android.ui.common.widgets.LoadingView -import com.san.englishbender.android.ui.theme.BottomSheetContainerColor +import com.san.englishbender.android.ui.theme.backgroundColors import com.san.englishbender.core.extensions.isNotNull import com.san.englishbender.domain.entities.BoardEntity -import com.san.englishbender.ui.flashcards.FlashCardsUiState -import com.san.englishbender.ui.flashcards.FlashCardsViewModel +import com.san.englishbender.ui.flashcards.BoardsUiState +import com.san.englishbender.ui.flashcards.BoardsViewModel import org.koin.androidx.compose.getViewModel -import android.view.ViewTreeObserver -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.* -import androidx.compose.ui.focus.focusModifier -import androidx.compose.ui.focus.focusTarget -import androidx.compose.ui.platform.LocalView -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import com.google.accompanist.insets.ProvideWindowInsets @Composable -fun FlashCardsScreen( +fun BoardsScreen( onBoardClick: (String?) -> Unit, openDrawer: () -> Unit ) { - val viewModel: FlashCardsViewModel = getViewModel() - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val viewModel: BoardsViewModel = getViewModel() + val uiState by viewModel.boardsUiState.collectAsStateWithLifecycle() when { uiState.isLoading -> LoadingView() uiState.userMessage.isNotNull -> ErrorView(userMessage = uiState.userMessage) - else -> FlashCardsContent( + else -> BoardsContent( viewModel, uiState, onBoardClick = onBoardClick, @@ -80,15 +75,14 @@ fun FlashCardsScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun FlashCardsContent( - viewModel: FlashCardsViewModel, - uiState: FlashCardsUiState, +fun BoardsContent( + viewModel: BoardsViewModel, + uiState: BoardsUiState, onBoardClick: (String?) -> Unit, openDrawer: () -> Unit ) { val focusManager = LocalFocusManager.current - val bottomSheetState = rememberModalBottomSheetState() - var boardCreationBottomSheet by remember { mutableStateOf(false) } + var boardCreationDialog by remember { mutableStateOf(false) } Scaffold( modifier = Modifier.fillMaxWidth(), @@ -125,9 +119,9 @@ fun FlashCardsContent( floatingActionButton = { FloatingActionButton( contentColor = Color.White, - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, + containerColor = MaterialTheme.colorScheme.tertiaryContainer, shape = RoundedCornerShape(10.dp), - onClick = { boardCreationBottomSheet = true } + onClick = { boardCreationDialog = true } ) { Icon( Icons.Filled.Edit, @@ -139,74 +133,107 @@ fun FlashCardsContent( ) { paddingValues -> LazyColumn(modifier = Modifier.padding(paddingValues)) { items(items = uiState.boards, key = { it.id }) { board -> - BoardCard(board, viewModel, onBoardClick) + BoardItem(board, onBoardClick) } } - - if (boardCreationBottomSheet) { - ModalBottomSheet( - modifier = Modifier.imePadding(), - containerColor = BottomSheetContainerColor, - onDismissRequest = { boardCreationBottomSheet = false }, - sheetState = bottomSheetState - ) { + } + if (boardCreationDialog) { + BoardCreationDialog( + viewModel, + dismiss = { focusManager.clearFocus() - BoardCreationBSContent(viewModel) + boardCreationDialog = false } - } + ) } } @Composable -fun BoardCreationBSContent(viewModel: FlashCardsViewModel) { - - var boardName by remember { mutableStateOf("") } - - Column( - Modifier - .fillMaxWidth() - .padding(bottom = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally +fun BoardCreationDialog( + viewModel: BoardsViewModel, + dismiss: () -> Unit +) { + BaseDialogContent( + height = 250.dp, + dismiss = dismiss ) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - value = boardName, - placeholder = { Text("Board name") }, - onValueChange = { - boardName = it - } - ) - Row( - modifier = Modifier + var board by remember { mutableStateOf(BoardEntity()) } + var boardName by remember { mutableStateOf("") } + var boardColor by remember { mutableStateOf(backgroundColors.first()) } + + Column( + Modifier .fillMaxWidth() - .padding(top = 32.dp, end = 16.dp), - horizontalArrangement = Arrangement.End + .padding(vertical = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - EBOutlinedButton( - text = "Save", - onClick = { - viewModel.saveBoard(boardName) - } + EBOutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = board.name, + placeholder = "Board name", + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + onValueChange = { board.name = it } + ) + + BackgroundColorPicker( + modifier = Modifier.padding( + vertical = 12.dp, + horizontal = 8.dp + ), + label = "", + listState = rememberLazyListState(), + onClick = { color: Color -> board.backgroundColor = color.toHex() } ) + + Spacer(Modifier.weight(1f)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + horizontalArrangement = Arrangement.End + ) { + EBOutlinedButton( + text = "Save", + onClick = { + if (board.name.isEmpty()) return@EBOutlinedButton + + viewModel.saveBoard(board) + dismiss() + } + ) + } } } } @Composable -fun BoardCard( +fun BoardItem( board: BoardEntity, - flashCardsViewModel: FlashCardsViewModel, onBoardClick: (String?) -> Unit, ) { Row( modifier = Modifier - .padding(start = 12.dp) + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 8.dp + ) .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)), verticalAlignment = Alignment.CenterVertically ) { - Text(board.name) + Text( + modifier = Modifier + .padding(12.dp) + .clickable { onBoardClick(board.id) }, + text = board.name + ) } } diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt index 5938b34..9fca441 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField @@ -93,7 +94,7 @@ fun RecordDetailsScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(recordId) { - if (recordId.isNotNull) viewModel.getRecord(recordId) + recordId?.let { viewModel.getRecord(it) } } RecordDetailsContent( @@ -269,7 +270,7 @@ fun RecordDetailsContent( ), ) - Divider( + HorizontalDivider( modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp), color = Color.LightGray ) diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/bottomSheets/BackgroundColorPickerBSContent.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/bottomSheets/BackgroundColorPickerBSContent.kt index cc8e5f5..27c52fc 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/bottomSheets/BackgroundColorPickerBSContent.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/bottomSheets/BackgroundColorPickerBSContent.kt @@ -72,7 +72,6 @@ fun BackgroundColorPickerBSContent( .clickable(onClick = { onClick(color) }), colors = CardDefaults.cardColors(containerColor = color), border = BorderStroke(1.dp, Color.LightGray) -// border = if (index == 0) BorderStroke(1.dp, Color.LightGray) else null ) {} } } diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/tags/AddEditTagScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/tags/AddEditTagScreen.kt index 9fa0516..5a42be3 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/tags/AddEditTagScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/tags/AddEditTagScreen.kt @@ -39,7 +39,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.san.englishbender.android.core.extensions.noRippleClickable import com.san.englishbender.android.core.extensions.toColor import com.san.englishbender.android.core.extensions.toHex -import com.san.englishbender.android.ui.common.DialogHeader +import com.san.englishbender.android.ui.common.DialogNavHeader import com.san.englishbender.android.ui.common.EBOutlinedButton import com.san.englishbender.android.ui.common.EBOutlinedIconButton import com.san.englishbender.android.ui.common.FontColorChangeButton @@ -82,7 +82,7 @@ fun AddEditTagScreen( .fillMaxWidth() .padding(16.dp) ) { - DialogHeader( + DialogNavHeader( title = title, onClick = onBack ) diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/tags/ColorPickerScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/tags/ColorPickerScreen.kt index 33b43ee..3f29f86 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/tags/ColorPickerScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/tags/ColorPickerScreen.kt @@ -28,7 +28,7 @@ import com.github.skydoves.colorpicker.compose.BrightnessSlider import com.github.skydoves.colorpicker.compose.ColorEnvelope import com.github.skydoves.colorpicker.compose.HsvColorPicker import com.github.skydoves.colorpicker.compose.rememberColorPickerController -import com.san.englishbender.android.ui.common.DialogHeader +import com.san.englishbender.android.ui.common.DialogNavHeader import com.san.englishbender.android.ui.common.EBOutlinedButton @@ -47,7 +47,7 @@ fun ColorPickerScreen( .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - DialogHeader( + DialogNavHeader( title = "Color Picker", onClick = onBack ) diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/tags/TagsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/tags/TagsScreen.kt index 03c3edf..4df6262 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/tags/TagsScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/tags/TagsScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.material.icons.outlined.Edit import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember @@ -30,12 +29,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.san.englishbender.android.core.extensions.toColor +import com.san.englishbender.android.ui.common.DialogHeader import com.san.englishbender.android.ui.common.EBOutlinedButton import com.san.englishbender.android.ui.theme.ColorsPreset import com.san.englishbender.android.ui.theme.selectedLabelColor import com.san.englishbender.domain.entities.TagEntity import com.san.englishbender.ui.TagsViewModel -import io.github.aakira.napier.log @Composable @@ -59,14 +58,16 @@ fun TagsScreen( Column(modifier = Modifier.padding(16.dp)) { - Text( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(bottom = 16.dp), - text = "Tags", - fontWeight = FontWeight.Bold, - fontSize = 16.sp - ) +// Text( +// modifier = Modifier +// .align(Alignment.CenterHorizontally) +// .padding(bottom = 16.dp), +// text = "Tags", +// fontWeight = FontWeight.Bold, +// fontSize = 16.sp +// ) + + DialogHeader(title = "Tags") Column(modifier = Modifier .weight(1f) diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/theme/Color.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/theme/Color.kt index 93948e1..4137831 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/theme/Color.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/theme/Color.kt @@ -59,6 +59,19 @@ val darkReplyBlueColors = darkColorScheme( surface = Color.White ) +val backgroundColors = listOf( + Color(0xFFFFFFFF), + Color(0xFFFFFFCC), + Color(0xFFFFCC99), + Color(0xFFFFCCCC), + Color(0xFFFFCCFF), + Color(0xFFCCCCFF), + Color(0xFF99CCFF), + Color(0xFFCCFFFF), + Color(0xFF99FFCC), + Color(0xFFCCFF99), +) + object ColorsPreset { val coral: Color = Color(0xFFF29131) diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/DatabaseModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/DatabaseModule.kt index c7f9e0c..11e2da0 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/DatabaseModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/DatabaseModule.kt @@ -42,7 +42,7 @@ val databaseModule = module { } single { RecordsRepository(get()) } - single { TagsRepository(get(), get()) } + single { TagsRepository(get()) } single { StatsRepository(get()) } single { BoardsRepository(get()) } single { FlashCardsRepository(get()) } diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt index 89b9d87..435f293 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt @@ -1,7 +1,8 @@ package com.san.englishbender.core.di import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase -import com.san.englishbender.domain.usecases.flashCards.GetFlashCardsFlowUseCase +import com.san.englishbender.domain.usecases.flashCards.GetBoardByIdUseCase +import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase import com.san.englishbender.domain.usecases.records.GetRecordFlowUseCase import com.san.englishbender.domain.usecases.records.GetRecordsCountUseCase import com.san.englishbender.domain.usecases.records.GetRecordsUseCase @@ -39,5 +40,6 @@ val useCaseModule = module { // --- FlashCards single { GetBoardsFlowUseCase(get()) } - single { GetFlashCardsFlowUseCase(get()) } + single { GetBoardByIdUseCase(get()) } + single { SaveBoardUseCase(get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt index 43dad58..74be3ee 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt @@ -1,6 +1,7 @@ package com.san.englishbender.core.di import com.san.englishbender.ui.TagsViewModel +import com.san.englishbender.ui.flashcards.BoardsViewModel import com.san.englishbender.ui.flashcards.FlashCardsViewModel import com.san.englishbender.ui.recordDetails.RecordDetailsViewModel import com.san.englishbender.ui.records.RecordsViewModel @@ -12,5 +13,6 @@ val viewModelModule = module { single { RecordDetailsViewModel(get(), get(), get(), get(), get()) } single { StatsViewModel(get(), get()) } single { TagsViewModel(get(), get(), get(), get(), get()) } - single { FlashCardsViewModel(get()) } + single { BoardsViewModel(get(), get(), get()) } + single { FlashCardsViewModel(get(), get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/navigation/Destinations.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/navigation/Destinations.kt index 807e414..e796974 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/navigation/Destinations.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/navigation/Destinations.kt @@ -1,5 +1,6 @@ package com.san.englishbender.core.navigation +import com.san.englishbender.core.navigation.Screens.BOARDS_SCREEN import com.san.englishbender.core.navigation.Screens.COLOR_PICKER_SCREEN import com.san.englishbender.core.navigation.Screens.FLASHCARDS_SCREEN import com.san.englishbender.core.navigation.Screens.RECORDS_SCREEN @@ -12,6 +13,7 @@ object Screens { const val STATS_SCREEN = "stats" const val RECORDS_SCREEN = "records" const val RECORD_DETAIL_SCREEN = "recordDetail" + const val BOARDS_SCREEN = "boards" const val FLASHCARDS_SCREEN = "flashcards" // --- @@ -30,6 +32,7 @@ object Destinations { const val STATS_ROUTE = STATS_SCREEN const val RECORD_ROUTE = RECORDS_SCREEN const val RECORD_DETAIL_ROUTE = "$RECORD_DETAIL_SCREEN?recordId={recordId}" + const val BOARDS_ROUTE = "$BOARDS_SCREEN?boardId={boardId}" const val FLASHCARDS_ROUTE = "$FLASHCARDS_SCREEN?boardId={boardId}" const val TAG_LIST_ROUTE = TAG_LIST_SCREEN diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt index 66263bf..44350eb 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt @@ -70,13 +70,13 @@ suspend fun getResultFlow(action: suspend () -> T): Flow> = flow { } } -suspend fun Flow>.ifSuccess(block: suspend (T) -> Unit) { +suspend fun Flow>.onSuccess(block: suspend (T) -> Unit) { this.collect { if (it is Result.Success) block(it.data) } } -fun Flow>.ifFailure(block: (Throwable) -> Unit): Flow> { +fun Flow>.onFailure(block: (Throwable) -> Unit): Flow> { return this.map { if (it is Result.Failure) { log(tag = "ifFailureException") { "ifFailure exception: ${it.exception}" } @@ -86,7 +86,7 @@ fun Flow>.ifFailure(block: (Throwable) -> Unit): Flow> { } } -suspend fun ifFailure(action: suspend () -> Unit, block: (Throwable) -> Unit) { +suspend fun onFailure(action: suspend () -> Unit, block: (Throwable) -> Unit) { try { action() } catch (e: Exception) { diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt index f63c31f..97b65bc 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt @@ -11,20 +11,17 @@ class Board : RealmObject { @PrimaryKey var id: String = "" var name: String = "" - var description: String = "" var backgroundColor: String = "" var flashCards: RealmList = realmListOf() constructor( id: String, name: String, - description: String, backgroundColor: String = "", flashCards: RealmList ) { this.id = id this.name = name - this.description = description this.backgroundColor = backgroundColor this.flashCards = flashCards } @@ -36,7 +33,6 @@ fun Board.toEntity() = BoardEntity( id = id, name = name, - description = description, backgroundColor = backgroundColor, flashCards = flashCards.toEntity() ) @@ -45,7 +41,6 @@ fun BoardEntity.toLocal() = Board( id = id, name = name, - description = description, backgroundColor = backgroundColor, flashCards = flashCards.toLocal().toRealmList() ) diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt index 9ede25c..e0004d4 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt @@ -1,18 +1,19 @@ package com.san.englishbender.data.local.models import com.san.englishbender.domain.entities.FlashCardEntity -import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.annotations.PrimaryKey -open class FlashCard : RealmObject { - @PrimaryKey +open class FlashCard : EmbeddedRealmObject { var id: String = "" - var word: String = "" + var front: String = "" + var back: String = "" var description: String = "" - constructor(id: String, word: String, description: String) { + constructor(id: String, front: String, back: String, description: String) { this.id = id - this.word = word + this.front = front + this.back = back this.description = description } @@ -22,14 +23,16 @@ open class FlashCard : RealmObject { fun FlashCard.toEntity() = FlashCardEntity( id = id, - word = word, + front = front, + back = back, description = description ) fun FlashCardEntity.toLocal() = FlashCard( id = id, - word = word, + front = front, + back = back, description = description ) diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt index 32c509c..5740b37 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt @@ -1,12 +1,14 @@ package com.san.englishbender.data.repositories import com.san.englishbender.core.extensions.doQuery +import com.san.englishbender.core.extensions.toFlow import com.san.englishbender.data.local.models.Board import com.san.englishbender.data.local.models.toEntity import com.san.englishbender.data.local.models.toLocal import com.san.englishbender.domain.entities.BoardEntity import com.san.englishbender.domain.repositories.IBoardsRepository import com.san.englishbender.ioDispatcher +import io.github.aakira.napier.log import io.realm.kotlin.Realm import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.ext.query @@ -15,6 +17,7 @@ import io.realm.kotlin.notifications.UpdatedResults import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map class BoardsRepository( private val realm: Realm @@ -33,8 +36,14 @@ class BoardsRepository( realm.query(Board::class).find().map { it.toEntity() } } + override suspend fun getBoard(id: String): BoardEntity? = doQuery { + realm.query("id == $0", id).first().find()?.toEntity() + } + override suspend fun saveBoard(board: BoardEntity): Unit = doQuery { - realm.write { copyToRealm(board.toLocal(), UpdatePolicy.ALL) } + val local = board.toLocal() + log(tag = "containerColor") { "local: $local" } + realm.write { copyToRealm(local, UpdatePolicy.ALL) } } override suspend fun deleteBoard(boardId: String): Unit = doQuery { diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/FlashCardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/FlashCardsRepository.kt index 7c7a31f..fe28feb 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/FlashCardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/FlashCardsRepository.kt @@ -19,8 +19,8 @@ import kotlinx.coroutines.flow.flowOn class FlashCardsRepository( private val realm: Realm ): IFlashCardsRepository { - override fun getFlashCardsFlow(): Flow> = flow { - realm.query(FlashCard::class).asFlow().collect { changes -> + override fun getFlashCardsFlow(boardId: String): Flow> = flow { + realm.query("").asFlow().collect { changes -> when (changes) { is InitialResults, is UpdatedResults -> emit(changes.list.toList().toEntity()) @@ -33,9 +33,9 @@ class FlashCardsRepository( realm.query(FlashCard::class).find().map { it.toEntity() } } - override suspend fun saveFlashCard(card: FlashCardEntity): Unit = doQuery { - realm.write { copyToRealm(card.toLocal(), UpdatePolicy.ALL) } - } +// override suspend fun saveFlashCard(card: FlashCardEntity): Unit = doQuery { +// realm.write { copyToRealm(card.toLocal(), UpdatePolicy.ALL) } +// } override suspend fun deleteFlashCard(cardId: String): Unit = doQuery { realm.write { diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/TagsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/TagsRepository.kt index 7a39b20..18e3b55 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/TagsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/TagsRepository.kt @@ -16,14 +16,24 @@ import io.realm.kotlin.ext.query import io.realm.kotlin.notifications.InitialResults import io.realm.kotlin.notifications.UpdatedResults import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn class TagsRepository( private val realm: Realm, - private val dataStore: IDataStore +// private val dataStore: IDataStore ) : ITagsRepository { +// val tags = realm +// .query() +// .asFlow() +// .map { result -> result.list.toList().toEntity() } +// .flowOn(ioDispatcher) + override fun getAllTagsFlow(): Flow> = flow { realm.query(Tag::class).asFlow().collect { changes -> when (changes) { diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt index 7270a93..689c2e3 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt @@ -2,13 +2,12 @@ package com.san.englishbender.domain.entities import com.san.englishbender.CommonParcelable import com.san.englishbender.CommonParcelize +import com.san.englishbender.randomUUID @CommonParcelize data class BoardEntity( - var id: String = "", + var id: String = randomUUID(), var name: String = "", - var description: String = "", - var creationDate: Long = 0, var backgroundColor: String = "", - var flashCards: List + var flashCards: List = emptyList() ) : CommonParcelable \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt index 82ca8fd..45800ed 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt @@ -2,10 +2,12 @@ package com.san.englishbender.domain.entities import com.san.englishbender.CommonParcelable import com.san.englishbender.CommonParcelize +import com.san.englishbender.randomUUID @CommonParcelize data class FlashCardEntity( - var id: String = "", - var word: String = "", + var id: String = randomUUID(), + var front: String = "", + var back: String = "", var description: String = "" ) : CommonParcelable \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/RecordEntity.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/RecordEntity.kt index 9376845..b9e5375 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/RecordEntity.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/RecordEntity.kt @@ -1,8 +1,10 @@ package com.san.englishbender.domain.entities +import androidx.compose.runtime.Immutable import com.san.englishbender.CommonParcelable import com.san.englishbender.CommonParcelize +@Immutable @CommonParcelize data class RecordEntity( var title: String = "", diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt index 106de6f..f689bb9 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.Flow interface IBoardsRepository { fun getBoardsFlow() : Flow> suspend fun getBoards() : List + suspend fun getBoard(id: String) : BoardEntity? suspend fun saveBoard(board: BoardEntity) suspend fun deleteBoard(boardId: String) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IFlashCardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IFlashCardsRepository.kt index 535bb86..808b529 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IFlashCardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IFlashCardsRepository.kt @@ -4,8 +4,8 @@ import com.san.englishbender.domain.entities.FlashCardEntity import kotlinx.coroutines.flow.Flow interface IFlashCardsRepository { - fun getFlashCardsFlow() : Flow> + fun getFlashCardsFlow(boardId: String) : Flow> suspend fun getFlashCards() : List - suspend fun saveFlashCard(card: FlashCardEntity) +// suspend fun saveFlashCard(card: FlashCardEntity) suspend fun deleteFlashCard(cardId: String) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt new file mode 100644 index 0000000..0f3981a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt @@ -0,0 +1,11 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.repositories.IBoardsRepository + +class GetBoardByIdUseCase( + private val boardRepository: IBoardsRepository +) { + suspend operator fun invoke(boardId: String): BoardEntity? = + boardRepository.getBoard(boardId) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsFlowUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsFlowUseCase.kt deleted file mode 100644 index 1aa2c0a..0000000 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsFlowUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.san.englishbender.domain.usecases.flashCards - -import com.san.englishbender.data.repositories.FlashCardsRepository -import com.san.englishbender.domain.entities.FlashCardEntity -import kotlinx.coroutines.flow.Flow - -class GetFlashCardsFlowUseCase( - private val flashCardsRepository: FlashCardsRepository -) { - operator fun invoke(): Flow> = flashCardsRepository.getFlashCardsFlow() -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveBoardUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveBoardUseCase.kt new file mode 100644 index 0000000..083a240 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveBoardUseCase.kt @@ -0,0 +1,8 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.repositories.IBoardsRepository + +class SaveBoardUseCase(private val boardRepository: IBoardsRepository) { + suspend operator fun invoke(board: BoardEntity) = boardRepository.saveBoard(board) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt new file mode 100644 index 0000000..b6e24ea --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt @@ -0,0 +1,99 @@ +package com.san.englishbender.ui.flashcards + +import androidx.compose.runtime.Immutable +import com.san.englishbender.SharedRes +import com.san.englishbender.core.extensions.WhileUiSubscribed +import com.san.englishbender.core.extensions.isNull +import com.san.englishbender.data.getResultFlow +import com.san.englishbender.data.onFailure +import com.san.englishbender.data.onSuccess +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.usecases.flashCards.GetBoardByIdUseCase +import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase +import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase +import com.san.englishbender.randomUUID +import com.san.englishbender.ui.ViewModel +import com.san.englishbender.ui.records.RecordsUiState +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update + +@Immutable +data class BoardsUiState( + val isLoading: Boolean = false, + val boards: List = emptyList(), + val userMessage: StringResource? = null +) + +@Immutable +data class BoardUiState( + val isLoading: Boolean = false, + val board: BoardEntity? = null, + val userMessage: StringResource? = null +) + +class BoardsViewModel( + private val getBoardsFlowUseCase: GetBoardsFlowUseCase, + private val getBoardByIdUseCase: GetBoardByIdUseCase, + private val saveBoardUseCase: SaveBoardUseCase +) : ViewModel() { + + private val _boardUiState = MutableStateFlow(BoardUiState()) + val boardUiState: StateFlow = _boardUiState.asStateFlow() + + val boardsUiState: StateFlow = + getBoardsFlowUseCase() + .map { BoardsUiState(boards = it) } + .catch { BoardsUiState(userMessage = SharedRes.strings.loading_records_error) } + .stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = BoardsUiState(isLoading = true) + ) + + fun getBoard(boardId: String) = safeLaunch { + getResultFlow { getBoardByIdUseCase(boardId) } + .onFailure { + _boardUiState.update { + it.copy( + isLoading = false, + userMessage = SharedRes.strings.remove_record_error + ) + } + } + .onSuccess { boardEntity -> + if (boardEntity.isNull) { + _boardUiState.update { + it.copy( + isLoading = false, + userMessage = SharedRes.strings.remove_record_error + ) + } + return@onSuccess + } + + _boardUiState.update { state -> + state.copy( + isLoading = false, + board = boardEntity + ) + } + } + } + + fun saveBoard(board: BoardEntity) = safeLaunch { + getResultFlow { saveBoardUseCase(board) } + .onFailure { RecordsUiState(userMessage = SharedRes.strings.remove_record_error) } + .onSuccess { } + } + + fun deleteBoard(boardId: String) = safeLaunch { + + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt index 1c29bce..34f6333 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt @@ -1,38 +1,81 @@ package com.san.englishbender.ui.flashcards import com.san.englishbender.SharedRes -import com.san.englishbender.core.extensions.WhileUiSubscribed +import com.san.englishbender.core.extensions.isNull +import com.san.englishbender.data.getResultFlow +import com.san.englishbender.data.onFailure +import com.san.englishbender.data.onSuccess import com.san.englishbender.domain.entities.BoardEntity -import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase +import com.san.englishbender.domain.usecases.flashCards.GetBoardByIdUseCase +import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase +import com.san.englishbender.randomUUID import com.san.englishbender.ui.ViewModel +import com.san.englishbender.ui.records.RecordsUiState import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update data class FlashCardsUiState( val isLoading: Boolean = false, - val boards: List = emptyList(), + val board: BoardEntity? = null, val userMessage: StringResource? = null ) class FlashCardsViewModel( - getBoardsFlowUseCase: GetBoardsFlowUseCase, + private val getBoardByIdUseCase: GetBoardByIdUseCase, + private val saveBoardUseCase: SaveBoardUseCase ) : ViewModel() { - val uiState: StateFlow = - getBoardsFlowUseCase() - .map { FlashCardsUiState(boards = it) } - .catch { FlashCardsUiState(userMessage = SharedRes.strings.loading_records_error) } - .stateIn( - scope = viewModelScope, - started = WhileUiSubscribed, - initialValue = FlashCardsUiState(isLoading = true) - ) - - fun saveBoard(boardName: String) = safeLaunch { + private val _uiState = MutableStateFlow(FlashCardsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun getBoard(boardId: String) = safeLaunch { + getResultFlow { getBoardByIdUseCase(boardId) } + .onFailure { + _uiState.update { + it.copy( + isLoading = false, + userMessage = SharedRes.strings.remove_record_error + ) + } + } + .onSuccess { boardEntity -> + if (boardEntity.isNull) { + _uiState.update { + it.copy( + isLoading = false, + userMessage = SharedRes.strings.remove_record_error + ) + } + return@onSuccess + } + + _uiState.update { state -> + state.copy( + isLoading = false, + board = boardEntity + ) + } + } + } + + fun saveFlashCard(boardName: String, boardColor: String) = safeLaunch { + + val board = BoardEntity().apply { + id = randomUUID() + name = boardName + backgroundColor = boardColor + } + + getResultFlow { saveBoardUseCase(board) } + .onFailure { RecordsUiState(userMessage = SharedRes.strings.remove_record_error) } + .onSuccess { } + } + + fun deleteFlashCard(cardId: String) = safeLaunch { } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/recordDetails/RecordDetailsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/recordDetails/RecordDetailsViewModel.kt index f76b737..077c09e 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/recordDetails/RecordDetailsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/recordDetails/RecordDetailsViewModel.kt @@ -12,8 +12,8 @@ import com.san.englishbender.SharedRes import com.san.englishbender.core.Event import com.san.englishbender.core.navigation.Navigator import com.san.englishbender.data.getResultFlow -import com.san.englishbender.data.ifFailure -import com.san.englishbender.data.ifSuccess +import com.san.englishbender.data.onFailure +import com.san.englishbender.data.onSuccess import com.san.englishbender.domain.entities.RecordEntity import com.san.englishbender.domain.entities.TagEntity import com.san.englishbender.domain.entities.isNotEqual @@ -62,11 +62,9 @@ class RecordDetailsViewModel( private var prevText: String = "" private val results = mutableListOf() - fun getRecord(recordId: String?) { - val recId = recordId ?: return - + fun getRecord(recordId: String) { combine( - getRecordFlowUseCase(recId), + getRecordFlowUseCase(recordId), getTagsFlowUseCase() ) { recordEntity, tags -> _uiState.update { state -> @@ -96,11 +94,11 @@ class RecordDetailsViewModel( currRecordState.tags = selectedTags getResultFlow { saveRecordUseCase(currRecordState) } - .ifFailure { + .onFailure { saveInProgress = false showUserMessage(SharedRes.strings.save_record_error) } - .ifSuccess { + .onSuccess { updateStatsUseCase( prevRecordState = prevRecordState, currRecordState = currRecordState diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/records/RecordsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/records/RecordsViewModel.kt index 98cfd60..eb12f4c 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/records/RecordsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/records/RecordsViewModel.kt @@ -3,8 +3,8 @@ package com.san.englishbender.ui.records import com.san.englishbender.SharedRes import com.san.englishbender.core.extensions.WhileUiSubscribed import com.san.englishbender.data.getResultFlow -import com.san.englishbender.data.ifFailure -import com.san.englishbender.data.ifSuccess +import com.san.englishbender.data.onFailure +import com.san.englishbender.data.onSuccess import com.san.englishbender.domain.entities.RecordEntity import com.san.englishbender.domain.entities.TagEntity import com.san.englishbender.domain.usecases.records.GetRecordsUseCase @@ -42,8 +42,8 @@ class RecordsViewModel( fun removeRecord(record: RecordEntity) = safeLaunch { getResultFlow { removeRecordUseCase(record) } - .ifFailure { RecordsUiState(userMessage = SharedRes.strings.remove_record_error) } - .ifSuccess { + .onFailure { RecordsUiState(userMessage = SharedRes.strings.remove_record_error) } + .onSuccess { log(tag = "ExceptionHandling") { "getResultFlow success" } } } From 7075b8fb0551d3c6ded771ac94af25fb6cd3419a Mon Sep 17 00:00:00 2001 From: Aleksandr Symbiotov Date: Tue, 13 Feb 2024 22:45:09 +0100 Subject: [PATCH 3/5] Refactoring functionality for boards and flashcards --- .../android/navigation/EBNavHost.kt | 4 +- .../android/ui/common/ButtonComposables.kt | 25 ++ .../android/ui/flashcards/BoardScreen.kt | 233 +++++++++++++----- .../android/ui/flashcards/BoardsScreen.kt | 59 +++-- .../ui/recordDetails/RecordDetailsScreen.kt | 3 +- shared/build.gradle.kts | 6 +- .../englishbender/core/di/UseCaseModule.kt | 2 + .../englishbender/core/di/ViewModelModule.kt | 2 +- .../com/san/englishbender/data/Result.kt | 2 - .../data/local/models/FlashCard.kt | 27 +- .../data/repositories/BoardsRepository.kt | 33 ++- .../domain/entities/BoardEntity.kt | 2 +- .../domain/entities/FlashCardEntity.kt | 5 +- .../domain/repositories/IBoardsRepository.kt | 3 + .../flashCards/GetBoardByIdUseCase.kt | 4 + .../flashCards/SaveFlashCardUseCase.kt | 9 + .../ui/flashcards/BoardsViewModel.kt | 76 ++++-- .../ui/flashcards/FlashCardsViewModel.kt | 1 - .../recordDetails/RecordDetailsViewModel.kt | 1 + 19 files changed, 372 insertions(+), 125 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt diff --git a/androidApp/src/main/java/com/san/englishbender/android/navigation/EBNavHost.kt b/androidApp/src/main/java/com/san/englishbender/android/navigation/EBNavHost.kt index 703947f..5aef851 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/navigation/EBNavHost.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/navigation/EBNavHost.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -29,10 +30,11 @@ import com.san.englishbender.core.navigation.DestinationsArgs.RECORD_ID_ARG import com.san.englishbender.core.navigation.NavigationCommand import com.san.englishbender.core.navigation.Navigator import com.san.englishbender.core.navigation.Screens -import io.github.aakira.napier.log +import com.san.englishbender.ui.flashcards.BoardsViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.koin.androidx.compose.getViewModel @Composable diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt index 706c718..6130f26 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -40,6 +41,30 @@ import androidx.compose.ui.unit.sp import com.san.englishbender.android.core.extensions.noRippleClickable +@Composable +fun EBTextButton( + modifier: Modifier = Modifier, + text: String, + fontSize: TextUnit = 14.sp, + textColor: Color = Color.DarkGray, + contentPadding: PaddingValues = PaddingValues(0.dp), + colors: ButtonColors = ButtonDefaults.outlinedButtonColors(contentColor = Color.DarkGray), + onClick: () -> Unit, +) { + TextButton( + modifier = modifier, + colors = colors, + contentPadding = contentPadding, + onClick = onClick + ) { + Text( + text = text, + fontSize = fontSize, + color = textColor + ) + } +} + @Composable fun EBOutlinedButton( modifier: Modifier = Modifier, diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt index b4b7ef5..3651c84 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt @@ -13,20 +13,18 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -50,30 +48,30 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi +import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.model.rememberRichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor import com.san.englishbender.android.core.extensions.toColor -import com.san.englishbender.android.core.extensions.toHex -import com.san.englishbender.android.ui.common.BackgroundColorPicker import com.san.englishbender.android.ui.common.BaseDialogContent import com.san.englishbender.android.ui.common.EBOutlinedButton import com.san.englishbender.android.ui.common.EBOutlinedTextField +import com.san.englishbender.android.ui.common.EBTextButton import com.san.englishbender.android.ui.common.richText.RichTextStyleRow import com.san.englishbender.android.ui.common.widgets.ErrorView import com.san.englishbender.android.ui.common.widgets.LoadingView -import com.san.englishbender.android.ui.theme.backgroundColors +import com.san.englishbender.android.ui.theme.RedDark import com.san.englishbender.core.extensions.ifNotEmpty import com.san.englishbender.core.extensions.isNotNull +import com.san.englishbender.core.extensions.isNull +import com.san.englishbender.domain.entities.BoardEntity import com.san.englishbender.domain.entities.FlashCardEntity import com.san.englishbender.ui.flashcards.BoardUiState import com.san.englishbender.ui.flashcards.BoardsViewModel -import com.san.englishbender.ui.flashcards.FlashCardsUiState -import com.san.englishbender.ui.flashcards.FlashCardsViewModel import com.wajahatkarim.flippable.FlipAnimationType import com.wajahatkarim.flippable.Flippable import com.wajahatkarim.flippable.rememberFlipController @@ -89,16 +87,31 @@ fun BoardScreen( val viewModel: BoardsViewModel = getViewModel() val uiState by viewModel.boardUiState.collectAsStateWithLifecycle() +// log(tag = "ExceptionHandling") { "BoardScreen.uiState.board: ${uiState.board}" } + LaunchedEffect(boardId) { - boardId?.let { viewModel.getBoard(it) } + boardId?.let { + log(tag = "FuckThisShit") { "viewModel.getBoard(it)" } + viewModel.getBoard(it) } } +// LaunchedEffect(viewModel.boardIsSaved.value) { +// log(tag = "FuckThisShit") { "viewModel.boardIsSaved" } +// viewModel.boardIsSaved.value.getContentIfNotHandled()?.let { +// boardId?.let { +// log(tag = "FuckThisShit") { "viewModel.getBoard(it)" } +// viewModel.getBoard(it) } +// } +// } + when { uiState.isLoading -> LoadingView() uiState.userMessage.isNotNull -> ErrorView(userMessage = uiState.userMessage) else -> BoardContent( - viewModel, uiState, + onCardCreate = { board, flashCard -> viewModel.addCardToBoard(board, flashCard) }, + onCardUpdate = { flashCard -> viewModel.updateCard(flashCard) }, + onCardDelete = { flashCardId -> viewModel.deleteCard(flashCardId) }, onBackClick ) } @@ -106,20 +119,32 @@ fun BoardScreen( @OptIn( ExperimentalMaterial3Api::class, - ExperimentalFoundationApi::class, ExperimentalRichTextApi::class + ExperimentalFoundationApi::class, + ExperimentalRichTextApi::class ) @Composable fun BoardContent( - viewModel: BoardsViewModel, uiState: BoardUiState, - onBackClick: () -> Unit + onCardCreate: (BoardEntity, FlashCardEntity) -> Unit, + onCardUpdate: (FlashCardEntity) -> Unit, + onCardDelete: (String) -> Unit, + onBackClick: () -> Unit = {}, ) { - val focusManager = LocalFocusManager.current val controller = rememberFlipController() + val focusManager = LocalFocusManager.current val cards = uiState.board?.flashCards ?: emptyList() + + log(tag = "FuckThisShit") { "cards.size: ${cards.size}" } + + cards.find { it.id == "0786d6a4-f235-4016-a58b-5040324af033" }?.let { + log(tag = "ExceptionHandling") { "front: ${it.frontText}" } + log(tag = "ExceptionHandling") { "back: ${it.backText}" } + } + val pagerState = rememberPagerState(pageCount = { cards.size }) -// val pagerState = rememberPagerState(pageCount = { 1 }) - var boardCreationDialog by remember { mutableStateOf(false) } + var addCardDialog by remember { mutableStateOf(false) } + var editCardDialog by remember { mutableStateOf(false) } + var cardDeletionDialog by remember { mutableStateOf(false) } val containerColor = uiState.board?.backgroundColor?.toColor ?: MaterialTheme.colorScheme.surfaceVariant @@ -160,7 +185,7 @@ fun BoardContent( tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier .padding(8.dp) - .clickable { boardCreationDialog = true } + .clickable { addCardDialog = true } ) Icon( rememberVectorPainter(Icons.Filled.Edit), @@ -168,7 +193,7 @@ fun BoardContent( tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier .padding(8.dp) - .clickable { } + .clickable { editCardDialog = true } ) Icon( rememberVectorPainter(Icons.Filled.Delete), @@ -176,7 +201,7 @@ fun BoardContent( tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier .padding(8.dp) - .clickable { } + .clickable { cardDeletionDialog = true } ) } ) @@ -197,23 +222,19 @@ fun BoardContent( return@Scaffold } + Spacer(Modifier.height(32.dp)) + HorizontalPager( state = pagerState ) { pageIndex -> + val card = cards.getOrNull(pageIndex) ?: return@HorizontalPager - val card = cards[pageIndex] +// log(tag = "ExceptionHandling") { "Pager card: $card" } - LaunchedEffect(Unit) { - card.back.ifNotEmpty { richTextState.setHtml(it) } + LaunchedEffect(card) { + card.backText.ifNotEmpty { richTextState.setHtml(it) } } -// val card = FlashCardEntity( -// front = "squander", -// back = "waste (something, especially money or time) in a reckless and foolish manner", -// ) - - Spacer(Modifier.height(32.dp)) - Flippable( modifier = Modifier .fillMaxWidth() @@ -229,7 +250,7 @@ fun BoardContent( contentAlignment = Alignment.Center ) { Text( - text = card.front, + text = card.frontText, color = Color.Black, fontSize = 20.sp ) @@ -250,9 +271,7 @@ fun BoardContent( .padding(horizontal = 16.dp) .verticalScroll(state = rememberScrollState()), state = richTextState, - textStyle = TextStyle( - fontSize = 20.sp - ), + textStyle = TextStyle(fontSize = 20.sp), readOnly = true ) } @@ -264,23 +283,64 @@ fun BoardContent( } } - if (boardCreationDialog) { - CardCreationDialog( - onCardCreate = { flashCard -> - uiState.board?.let { - it.flashCards = it.flashCards.plus(listOf(flashCard)) - viewModel.saveBoard(it) - } + when { + addCardDialog -> AddCardDialog( + onCreate = { flashCard -> + focusManager.clearFocus() + uiState.board?.let { onCardCreate(it, flashCard) } }, - dismiss = { boardCreationDialog = false } - ) + dismiss = { addCardDialog = false }) + // --- + editCardDialog -> EditCardDialog( + currentFlashCard = cards.getOrNull(pagerState.currentPage), + onUpdate = { flashCard -> + focusManager.clearFocus() + onCardUpdate(flashCard) + }, + dismiss = { editCardDialog = false }) + // --- + cardDeletionDialog -> { + val card = cards.getOrNull(pagerState.currentPage) ?: return + + CardDeletionDialog( + flashCard = card, + confirm = { cardId -> onCardDelete(cardId) }, + dismiss = { cardDeletionDialog = false } + ) + } } } +@Composable +fun AddCardDialog( + onCreate: (FlashCardEntity) -> Unit, + dismiss: () -> Unit +) { + CardContent( + onSave = onCreate, + dismiss = dismiss + ) +} + +@Composable +fun EditCardDialog( + currentFlashCard: FlashCardEntity?, + onUpdate: (FlashCardEntity) -> Unit, + dismiss: () -> Unit +) { + CardContent( + currentFlashCard, + onSave = onUpdate, + dismiss = dismiss + ) +} + @OptIn(ExperimentalRichTextApi::class) @Composable -fun CardCreationDialog( - onCardCreate: (FlashCardEntity) -> Unit, +fun CardContent( + flashCard: FlashCardEntity? = null, + richTextState: RichTextState = rememberRichTextState(), + onSave: (FlashCardEntity) -> Unit, dismiss: () -> Unit ) { BaseDialogContent( @@ -288,9 +348,15 @@ fun CardCreationDialog( dismiss = dismiss ) { var word by remember { mutableStateOf("") } - val card by remember { mutableStateOf(FlashCardEntity()) } + val card by remember { mutableStateOf(flashCard ?: FlashCardEntity()) } + + flashCard?.let { + LaunchedEffect(it) { + word = it.frontText + richTextState.setHtml(it.backText) + } + } - val richTextState = rememberRichTextState() richTextState.setConfig( linkColor = Color.Blue, linkTextDecoration = TextDecoration.Underline, @@ -318,14 +384,14 @@ fun CardCreationDialog( ), onValueChange = { word = it - card.front = it + card.frontText = it } ) Spacer(Modifier.height(16.dp)) RichTextStyleRow( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = 8.dp), state = richTextState ) BasicRichTextEditor( @@ -335,7 +401,8 @@ fun CardCreationDialog( .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) .verticalScroll(state = rememberScrollState()), state = richTextState, - minLines = 3, + minLines = 12, + maxLines = 12, decorationBox = { innerTextField -> Box(Modifier.padding(8.dp)) { if (richTextState.annotatedString.text.isEmpty()) { @@ -360,16 +427,72 @@ fun CardCreationDialog( EBOutlinedButton( text = "Save", onClick = { - if (card.front.isEmpty()) return@EBOutlinedButton + if (word.isEmpty()) return@EBOutlinedButton // TODO: delete it when RichTextEditor has onValueChanged callback - card.back = richTextState.toHtml() + card.backText = richTextState.toHtml() - onCardCreate(card) - dismiss() + onSave(card) +// dismiss() } ) } } } -} \ No newline at end of file +} + +@Composable +fun CardDeletionDialog( + flashCard: FlashCardEntity, + confirm: (String) -> Unit, + dismiss: () -> Unit +) { + Dialog(onDismissRequest = dismiss) { + Column { + Spacer(Modifier.height(8.dp)) + + Text( + text = "Warning", + fontSize = 20.sp + ) + + Spacer(Modifier.height(16.dp)) + + Text( + modifier = Modifier.padding(top = 16.dp, bottom = 24.dp), + text = "Are you sure to delete the card \"${flashCard.frontText}\"?", + fontSize = 16.sp + ) + + Spacer(Modifier.height(24.dp)) + + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(Modifier.weight(1f)) + EBTextButton( + text = "Cancel", + onClick = dismiss + ) + EBTextButton( + modifier = Modifier.padding(start = 16.dp), + text = "Delete", + colors = ButtonDefaults.buttonColors(contentColor = RedDark), + onClick = { confirm(flashCard.id) } + ) + } + } + } +} + +//@Preview +//@Composable +//fun BoardContentPreview() { +// BoardContent( +// uiState = BoardUiState(), +// onCardCreate = { board, card -> }, +// onCardUpdate = {}, +// onCardDelete = {} +// ) +//} \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt index 9d3d9f1..65f8ecf 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt @@ -14,9 +14,10 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -55,6 +56,7 @@ import org.koin.androidx.compose.getViewModel @Composable fun BoardsScreen( + onBoardClick: (String?) -> Unit, openDrawer: () -> Unit ) { @@ -65,9 +67,10 @@ fun BoardsScreen( uiState.isLoading -> LoadingView() uiState.userMessage.isNotNull -> ErrorView(userMessage = uiState.userMessage) else -> BoardsContent( - viewModel, uiState, + onBoardCreate = { board -> viewModel.saveBoard(board) }, onBoardClick = onBoardClick, + onGetCards = { viewModel.getCards() }, openDrawer = openDrawer ) } @@ -76,9 +79,10 @@ fun BoardsScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun BoardsContent( - viewModel: BoardsViewModel, uiState: BoardsUiState, + onBoardCreate: (BoardEntity) -> Unit, onBoardClick: (String?) -> Unit, + onGetCards: () -> Unit, openDrawer: () -> Unit ) { val focusManager = LocalFocusManager.current @@ -124,14 +128,20 @@ fun BoardsContent( onClick = { boardCreationDialog = true } ) { Icon( - Icons.Filled.Edit, + Icons.Filled.Add, contentDescription = "", tint = MaterialTheme.colorScheme.onPrimaryContainer ) } } ) { paddingValues -> + LazyColumn(modifier = Modifier.padding(paddingValues)) { + item { + Button(onClick = onGetCards) { + Text("Get cards") + } + } items(items = uiState.boards, key = { it.id }) { board -> BoardItem(board, onBoardClick) } @@ -139,7 +149,7 @@ fun BoardsContent( } if (boardCreationDialog) { BoardCreationDialog( - viewModel, + onBoardCreate = onBoardCreate, dismiss = { focusManager.clearFocus() boardCreationDialog = false @@ -150,16 +160,15 @@ fun BoardsContent( @Composable fun BoardCreationDialog( - viewModel: BoardsViewModel, + onBoardCreate: (BoardEntity) -> Unit, dismiss: () -> Unit ) { BaseDialogContent( height = 250.dp, dismiss = dismiss ) { - var board by remember { mutableStateOf(BoardEntity()) } + val board by remember { mutableStateOf(BoardEntity()) } var boardName by remember { mutableStateOf("") } - var boardColor by remember { mutableStateOf(backgroundColors.first()) } Column( Modifier @@ -171,14 +180,17 @@ fun BoardCreationDialog( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - value = board.name, + value = boardName, placeholder = "Board name", singleLine = true, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Done ), - onValueChange = { board.name = it } + onValueChange = { + boardName = it + board.name = it + } ) BackgroundColorPicker( @@ -203,8 +215,12 @@ fun BoardCreationDialog( text = "Save", onClick = { if (board.name.isEmpty()) return@EBOutlinedButton + board.backgroundColor.ifEmpty { + board.backgroundColor = backgroundColors.first().toHex() + } - viewModel.saveBoard(board) + onBoardCreate(board) +// viewModel.saveBoard(board) dismiss() } ) @@ -225,18 +241,31 @@ fun BoardItem( horizontal = 16.dp, vertical = 8.dp ) - .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)), + .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) + .clickable { onBoardClick(board.id) }, verticalAlignment = Alignment.CenterVertically ) { Text( - modifier = Modifier - .padding(12.dp) - .clickable { onBoardClick(board.id) }, + modifier = Modifier.padding(12.dp), text = board.name ) } } +//@PreviewLightDark +//@Preview +//@Composable +//fun BoardsScreenPreview() { +// EnglishBenderTheme { +// BoardsScreen( +// uiState = BoardsUiState(), +// onBoardCreate = {}, +// onBoardClick = {}, +// openDrawer = {} +// ) +// } +//} + //@Composable //fun rememberImeState(): State { // val imeState = remember { diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt index 9fca441..f72bd1c 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -77,7 +76,6 @@ import com.san.englishbender.android.ui.theme.BottomSheetContainerColor import com.san.englishbender.android.ui.theme.RedDark import com.san.englishbender.core.AppConstants import com.san.englishbender.core.extensions.ifNotEmpty -import com.san.englishbender.core.extensions.isNotNull import com.san.englishbender.ui.recordDetails.DetailUiState import com.san.englishbender.ui.recordDetails.RecordDetailsViewModel import org.koin.androidx.compose.getViewModel @@ -118,6 +116,7 @@ fun RecordDetailsContent( viewModel: RecordDetailsViewModel, onBackClick: () -> Unit ) { + val context = LocalContext.current val focusManager = LocalFocusManager.current // val coroutineScope = rememberCoroutineScope() diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index e360f3c..b3f47bd 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -29,12 +29,12 @@ kotlin { } sourceSets { - val coroutineVersion = "1.7.2" + val coroutineVersion = "1.7.3" val retrofitCoroutineAdapterVersion = "0.9.2" val retrofitVersion = "2.9.0" val okHttpVersion = "4.11.0" val moshiVersion = "1.13.0" - val lifecycleViewModelVersion = "2.6.2" + val lifecycleViewModelVersion = "2.7.0" val koinCoreVersion = "3.4.2" val koinAndroidVersion = "3.4.2" val koinComposeVersion = "3.4.5" @@ -74,7 +74,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") // Realm - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2") // Add to use coroutines with the SDK + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") // Add to use coroutines with the SDK api("io.realm.kotlin:library-base:1.11.0") // Add to only use the local database api("io.realm.kotlin:library-sync:1.11.0") // Add to use Device Sync compileOnly("io.realm.kotlin:library-base:1.11.0") diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt index 435f293..e3ae6c6 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt @@ -3,6 +3,7 @@ package com.san.englishbender.core.di import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardByIdUseCase import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase +import com.san.englishbender.domain.usecases.flashCards.SaveFlashCardUseCase import com.san.englishbender.domain.usecases.records.GetRecordFlowUseCase import com.san.englishbender.domain.usecases.records.GetRecordsCountUseCase import com.san.englishbender.domain.usecases.records.GetRecordsUseCase @@ -42,4 +43,5 @@ val useCaseModule = module { single { GetBoardsFlowUseCase(get()) } single { GetBoardByIdUseCase(get()) } single { SaveBoardUseCase(get()) } + single { SaveFlashCardUseCase(get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt index 74be3ee..e573515 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt @@ -13,6 +13,6 @@ val viewModelModule = module { single { RecordDetailsViewModel(get(), get(), get(), get(), get()) } single { StatsViewModel(get(), get()) } single { TagsViewModel(get(), get(), get(), get(), get()) } - single { BoardsViewModel(get(), get(), get()) } + single { BoardsViewModel(get(), get(), get(), get()) } single { FlashCardsViewModel(get(), get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt index 44350eb..1e149d1 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt @@ -62,10 +62,8 @@ suspend fun getResult(action: suspend () -> T) = try { suspend fun getResultFlow(action: suspend () -> T): Flow> = flow { return@flow try { - log(tag = "ExceptionHandling") { "getResultFlow s" } emit(Result.Success(action.invoke())) } catch (e: Exception) { - log(tag = "ExceptionHandling") { "getResultFlow f" } emit(Result.Failure(e)) } } diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt index e0004d4..fd7b317 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt @@ -1,20 +1,17 @@ package com.san.englishbender.data.local.models import com.san.englishbender.domain.entities.FlashCardEntity -import io.realm.kotlin.types.EmbeddedRealmObject -import io.realm.kotlin.types.annotations.PrimaryKey +import io.realm.kotlin.types.RealmObject -open class FlashCard : EmbeddedRealmObject { +open class FlashCard : RealmObject { var id: String = "" - var front: String = "" - var back: String = "" - var description: String = "" + var frontText: String = "" + var backText: String = "" - constructor(id: String, front: String, back: String, description: String) { + constructor(id: String, frontText: String, backText: String) { this.id = id - this.front = front - this.back = back - this.description = description + this.frontText = frontText + this.backText = backText } constructor() {} @@ -23,17 +20,15 @@ open class FlashCard : EmbeddedRealmObject { fun FlashCard.toEntity() = FlashCardEntity( id = id, - front = front, - back = back, - description = description + frontText = frontText, + backText = backText ) fun FlashCardEntity.toLocal() = FlashCard( id = id, - front = front, - back = back, - description = description + frontText = frontText, + backText = backText ) fun List.toEntity() = this.map { it.toEntity() } diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt index 5740b37..faf9752 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt @@ -1,32 +1,35 @@ package com.san.englishbender.data.repositories import com.san.englishbender.core.extensions.doQuery -import com.san.englishbender.core.extensions.toFlow import com.san.englishbender.data.local.models.Board import com.san.englishbender.data.local.models.toEntity import com.san.englishbender.data.local.models.toLocal import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.entities.FlashCardEntity import com.san.englishbender.domain.repositories.IBoardsRepository import com.san.englishbender.ioDispatcher import io.github.aakira.napier.log import io.realm.kotlin.Realm import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.ext.query +import io.realm.kotlin.notifications.InitialObject import io.realm.kotlin.notifications.InitialResults +import io.realm.kotlin.notifications.SingleQueryChange +import io.realm.kotlin.notifications.UpdatedObject import io.realm.kotlin.notifications.UpdatedResults import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map class BoardsRepository( private val realm: Realm -): IBoardsRepository { +) : IBoardsRepository { override fun getBoardsFlow(): Flow> = flow { realm.query(Board::class).asFlow().collect { changes -> when (changes) { is InitialResults, is UpdatedResults -> emit(changes.list.toList().toEntity()) + else -> {} } } @@ -40,9 +43,31 @@ class BoardsRepository( realm.query("id == $0", id).first().find()?.toEntity() } + override fun getBoardFlow(id: String): Flow = flow { + realm.query("id == $0", id) + .first() + .asFlow() + .collect { changes: SingleQueryChange -> + when (changes) { + is InitialObject<*>, + is UpdatedObject<*> -> changes.obj?.toEntity()?.let { emit(it) } + else -> {} + } + } + }.flowOn(ioDispatcher) + override suspend fun saveBoard(board: BoardEntity): Unit = doQuery { + log(tag = "BoardsViewModel") { "saveBoard: $board cards.size: ${board.flashCards.size}" } val local = board.toLocal() - log(tag = "containerColor") { "local: $local" } + realm.write { copyToRealm(local, UpdatePolicy.ALL) } + } + + override suspend fun saveFlashCard(card: FlashCardEntity): Unit = doQuery { + log(tag = "getCards") { "card: $card" } + val local = card.toLocal() + log(tag = "getCards") { "local: $local" } + log(tag = "getCards") { "front: ${local.frontText}" } + log(tag = "getCards") { "back: ${local.backText}" } realm.write { copyToRealm(local, UpdatePolicy.ALL) } } diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt index 689c2e3..15d2303 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt @@ -6,7 +6,7 @@ import com.san.englishbender.randomUUID @CommonParcelize data class BoardEntity( - var id: String = randomUUID(), + val id: String = randomUUID(), var name: String = "", var backgroundColor: String = "", var flashCards: List = emptyList() diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt index 45800ed..d2376d8 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt @@ -7,7 +7,6 @@ import com.san.englishbender.randomUUID @CommonParcelize data class FlashCardEntity( var id: String = randomUUID(), - var front: String = "", - var back: String = "", - var description: String = "" + var frontText: String = "", + var backText: String = "", ) : CommonParcelable \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt index f689bb9..68443e9 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt @@ -1,12 +1,15 @@ package com.san.englishbender.domain.repositories import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.entities.FlashCardEntity import kotlinx.coroutines.flow.Flow interface IBoardsRepository { fun getBoardsFlow() : Flow> suspend fun getBoards() : List suspend fun getBoard(id: String) : BoardEntity? + fun getBoardFlow(id: String) : Flow suspend fun saveBoard(board: BoardEntity) + suspend fun saveFlashCard(card: FlashCardEntity) suspend fun deleteBoard(boardId: String) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt index 0f3981a..18bb8e7 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt @@ -2,10 +2,14 @@ package com.san.englishbender.domain.usecases.flashCards import com.san.englishbender.domain.entities.BoardEntity import com.san.englishbender.domain.repositories.IBoardsRepository +import kotlinx.coroutines.flow.Flow class GetBoardByIdUseCase( private val boardRepository: IBoardsRepository ) { suspend operator fun invoke(boardId: String): BoardEntity? = boardRepository.getBoard(boardId) + +// operator fun invoke(boardId: String): Flow = +// boardRepository.getBoardFlow(boardId) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt new file mode 100644 index 0000000..87c4124 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt @@ -0,0 +1,9 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.entities.FlashCardEntity +import com.san.englishbender.domain.repositories.IBoardsRepository + +class SaveFlashCardUseCase(private val boardRepository: IBoardsRepository) { + suspend operator fun invoke(card: FlashCardEntity) = boardRepository.saveFlashCard(card) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt index b6e24ea..45e790e 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt @@ -8,18 +8,18 @@ import com.san.englishbender.data.getResultFlow import com.san.englishbender.data.onFailure import com.san.englishbender.data.onSuccess import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.entities.FlashCardEntity import com.san.englishbender.domain.usecases.flashCards.GetBoardByIdUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase -import com.san.englishbender.randomUUID +import com.san.englishbender.domain.usecases.flashCards.SaveFlashCardUseCase import com.san.englishbender.ui.ViewModel -import com.san.englishbender.ui.records.RecordsUiState import dev.icerock.moko.resources.StringResource +import io.github.aakira.napier.log import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -41,7 +41,8 @@ data class BoardUiState( class BoardsViewModel( private val getBoardsFlowUseCase: GetBoardsFlowUseCase, private val getBoardByIdUseCase: GetBoardByIdUseCase, - private val saveBoardUseCase: SaveBoardUseCase + private val saveBoardUseCase: SaveBoardUseCase, + private val saveFlashCardUseCase: SaveFlashCardUseCase ) : ViewModel() { private val _boardUiState = MutableStateFlow(BoardUiState()) @@ -59,25 +60,14 @@ class BoardsViewModel( fun getBoard(boardId: String) = safeLaunch { getResultFlow { getBoardByIdUseCase(boardId) } - .onFailure { - _boardUiState.update { - it.copy( - isLoading = false, - userMessage = SharedRes.strings.remove_record_error - ) - } - } + .onFailure { showError(SharedRes.strings.remove_record_error) } .onSuccess { boardEntity -> + log(tag = "BoardsViewModel") { "onSuccess" } + log(tag = "BoardsViewModel") { "flashCards.size: ${boardEntity?.flashCards?.size}" } if (boardEntity.isNull) { - _boardUiState.update { - it.copy( - isLoading = false, - userMessage = SharedRes.strings.remove_record_error - ) - } + showError(SharedRes.strings.remove_record_error) return@onSuccess } - _boardUiState.update { state -> state.copy( isLoading = false, @@ -87,13 +77,57 @@ class BoardsViewModel( } } + fun getCards() = safeLaunch { + + val board = boardsUiState.value.boards.first() + + log(tag = "getCards") { "cards.size: ${board.flashCards.size}" } + + board.flashCards.forEach { card -> + log(tag = "getCards") { "card.front: ${card.frontText}" } + log(tag = "getCards") { "card.back: ${card.backText}" } + } + } + fun saveBoard(board: BoardEntity) = safeLaunch { getResultFlow { saveBoardUseCase(board) } - .onFailure { RecordsUiState(userMessage = SharedRes.strings.remove_record_error) } - .onSuccess { } + .onFailure { showError(SharedRes.strings.remove_record_error) } + .onSuccess {} + } + + fun addCardToBoard(board: BoardEntity, flashCard: FlashCardEntity) = safeLaunch { + + // TODO: + board.flashCards = board.flashCards.plus(listOf(flashCard)) + + getResultFlow { saveBoardUseCase(board) } + .onFailure { showError(SharedRes.strings.remove_record_error) } + .onSuccess { + log(tag = "BoardsViewModel") { "onSuccess addCardToBoard" } + getBoard(board.id) + } + } + + fun updateCard(card: FlashCardEntity) = safeLaunch { + getResultFlow { saveFlashCardUseCase(card) } + .onFailure { } + .onSuccess { } } fun deleteBoard(boardId: String) = safeLaunch { } + + fun deleteCard(cardId: String) = safeLaunch { + + } + + private fun showError(message: StringResource) = safeLaunch { + _boardUiState.update { + it.copy( + isLoading = false, + userMessage = message + ) + } + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt index 34f6333..7d10e5c 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt @@ -65,7 +65,6 @@ class FlashCardsViewModel( fun saveFlashCard(boardName: String, boardColor: String) = safeLaunch { val board = BoardEntity().apply { - id = randomUUID() name = boardName backgroundColor = boardColor } diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/recordDetails/RecordDetailsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/recordDetails/RecordDetailsViewModel.kt index 077c09e..2f0a5c5 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/recordDetails/RecordDetailsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/recordDetails/RecordDetailsViewModel.kt @@ -1,5 +1,6 @@ package com.san.englishbender.ui.recordDetails +import androidx.compose.runtime.mutableStateOf import com.aallam.openai.api.BetaOpenAI import com.aallam.openai.api.chat.ChatCompletionRequest import com.aallam.openai.api.chat.ChatMessage From fbeabf9b1191b367eb064438ade70a7a889c7ceb Mon Sep 17 00:00:00 2001 From: Aleksandr Symbiotov Date: Wed, 14 Feb 2024 22:39:49 +0100 Subject: [PATCH 4/5] Refactoring of adding and saving boards/flashcards --- .../android/ui/common/BaseDialogContent.kt | 9 +- .../android/ui/common/ButtonComposables.kt | 21 ++ .../android/ui/flashcards/BoardScreen.kt | 278 +++++++----------- .../android/ui/flashcards/BoardsScreen.kt | 12 +- shared/build.gradle.kts | 6 +- .../englishbender/core/di/UseCaseModule.kt | 8 + .../englishbender/core/di/ViewModelModule.kt | 4 +- .../com/san/englishbender/data/Result.kt | 19 +- .../data/local/models/FlashCard.kt | 2 + .../data/repositories/BoardsRepository.kt | 27 +- .../domain/repositories/IBoardsRepository.kt | 4 +- .../flashCards/AddFlashCardToBoardUseCase.kt | 9 + .../usecases/flashCards/DeleteBoardUseCase.kt | 10 + .../flashCards/DeleteFlashCardUseCase.kt | 10 + .../flashCards/GetBoardAsFlowUseCase.kt | 12 + .../flashCards/SaveFlashCardUseCase.kt | 1 - .../ui/flashcards/BoardsViewModel.kt | 93 +----- .../ui/flashcards/FlashCardsViewModel.kt | 71 ++--- 18 files changed, 282 insertions(+), 314 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/AddFlashCardToBoardUseCase.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/DeleteBoardUseCase.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/DeleteFlashCardUseCase.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardAsFlowUseCase.kt diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/BaseDialogContent.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/BaseDialogContent.kt index cfb5ac4..38c7839 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/common/BaseDialogContent.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/BaseDialogContent.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -18,6 +19,7 @@ import com.san.englishbender.android.core.extensions.noRippleClickable @Composable fun BaseDialogContent( + modifier: Modifier = Modifier, width: Dp = 300.dp, height: Dp = 450.dp, shape: Shape = RoundedCornerShape(12.dp), @@ -25,6 +27,8 @@ fun BaseDialogContent( dismiss: () -> Unit = {}, content: @Composable () -> Unit, ) { +// val heightDp = height?.let { Modifier.height(height) } + Box( modifier = Modifier .fillMaxSize() @@ -32,13 +36,14 @@ fun BaseDialogContent( .noRippleClickable { dismiss() } ) { Box( - modifier = Modifier + modifier = modifier .align(Alignment.Center) +// .then(heightDp) .width(width) .height(height) .clip(shape) .background(containerColor) - .noRippleClickable { } + .noRippleClickable {} ) { content() } diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt index 6130f26..1aa8b8e 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt @@ -15,12 +15,14 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.FontDownload import androidx.compose.material.icons.outlined.Palette import androidx.compose.material3.Button import androidx.compose.material3.OutlinedButton import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -31,7 +33,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -166,4 +170,21 @@ fun FontColorChangeButton( // tint = if (state) Color.White else Color.Black // ) // } +} + +@Composable +fun EBIcon( + modifier: Modifier = Modifier, + imageVector: ImageVector, + contentDescription: String? = null, + onClick: () -> Unit +) { + Icon( + painter = rememberVectorPainter(imageVector), + contentDescription = contentDescription, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = modifier + .padding(8.dp) + .clickable { onClick() } + ) } \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt index 3651c84..d0ec071 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt @@ -3,7 +3,6 @@ package com.san.englishbender.android.ui.flashcards import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -24,9 +23,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -42,22 +39,23 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.model.rememberRichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor +import com.san.englishbender.android.core.extensions.noRippleClickable import com.san.englishbender.android.core.extensions.toColor import com.san.englishbender.android.ui.common.BaseDialogContent +import com.san.englishbender.android.ui.common.EBIcon import com.san.englishbender.android.ui.common.EBOutlinedButton import com.san.englishbender.android.ui.common.EBOutlinedTextField import com.san.englishbender.android.ui.common.EBTextButton @@ -67,15 +65,13 @@ import com.san.englishbender.android.ui.common.widgets.LoadingView import com.san.englishbender.android.ui.theme.RedDark import com.san.englishbender.core.extensions.ifNotEmpty import com.san.englishbender.core.extensions.isNotNull -import com.san.englishbender.core.extensions.isNull import com.san.englishbender.domain.entities.BoardEntity import com.san.englishbender.domain.entities.FlashCardEntity -import com.san.englishbender.ui.flashcards.BoardUiState -import com.san.englishbender.ui.flashcards.BoardsViewModel +import com.san.englishbender.ui.flashcards.FlashCardsUiState +import com.san.englishbender.ui.flashcards.FlashCardsViewModel import com.wajahatkarim.flippable.FlipAnimationType import com.wajahatkarim.flippable.Flippable import com.wajahatkarim.flippable.rememberFlipController -import io.github.aakira.napier.log import org.koin.androidx.compose.getViewModel @@ -84,26 +80,13 @@ fun BoardScreen( boardId: String?, onBackClick: () -> Unit ) { - val viewModel: BoardsViewModel = getViewModel() - val uiState by viewModel.boardUiState.collectAsStateWithLifecycle() - -// log(tag = "ExceptionHandling") { "BoardScreen.uiState.board: ${uiState.board}" } + val viewModel: FlashCardsViewModel = getViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(boardId) { - boardId?.let { - log(tag = "FuckThisShit") { "viewModel.getBoard(it)" } - viewModel.getBoard(it) } + boardId?.let { viewModel.getBoard(it) } } -// LaunchedEffect(viewModel.boardIsSaved.value) { -// log(tag = "FuckThisShit") { "viewModel.boardIsSaved" } -// viewModel.boardIsSaved.value.getContentIfNotHandled()?.let { -// boardId?.let { -// log(tag = "FuckThisShit") { "viewModel.getBoard(it)" } -// viewModel.getBoard(it) } -// } -// } - when { uiState.isLoading -> LoadingView() uiState.userMessage.isNotNull -> ErrorView(userMessage = uiState.userMessage) @@ -111,7 +94,7 @@ fun BoardScreen( uiState, onCardCreate = { board, flashCard -> viewModel.addCardToBoard(board, flashCard) }, onCardUpdate = { flashCard -> viewModel.updateCard(flashCard) }, - onCardDelete = { flashCardId -> viewModel.deleteCard(flashCardId) }, + onCardDelete = { flashCardId -> viewModel.deleteFlashCard(flashCardId) }, onBackClick ) } @@ -124,7 +107,7 @@ fun BoardScreen( ) @Composable fun BoardContent( - uiState: BoardUiState, + uiState: FlashCardsUiState, onCardCreate: (BoardEntity, FlashCardEntity) -> Unit, onCardUpdate: (FlashCardEntity) -> Unit, onCardDelete: (String) -> Unit, @@ -132,16 +115,10 @@ fun BoardContent( ) { val controller = rememberFlipController() val focusManager = LocalFocusManager.current - val cards = uiState.board?.flashCards ?: emptyList() - - log(tag = "FuckThisShit") { "cards.size: ${cards.size}" } - - cards.find { it.id == "0786d6a4-f235-4016-a58b-5040324af033" }?.let { - log(tag = "ExceptionHandling") { "front: ${it.frontText}" } - log(tag = "ExceptionHandling") { "back: ${it.backText}" } - } + val cards = uiState.board?.flashCards ?: emptyList() val pagerState = rememberPagerState(pageCount = { cards.size }) + var addCardDialog by remember { mutableStateOf(false) } var editCardDialog by remember { mutableStateOf(false) } var cardDeletionDialog by remember { mutableStateOf(false) } @@ -164,44 +141,33 @@ fun BoardContent( topBar = { key(containerColor) { TopAppBar( - modifier = Modifier.fillMaxWidth(), title = {}, + modifier = Modifier.fillMaxWidth(), colors = TopAppBarDefaults.topAppBarColors( containerColor = containerColor ), navigationIcon = { - Icon( - rememberVectorPainter(Icons.AutoMirrored.Filled.ArrowBack), - contentDescription = null, - modifier = Modifier - .padding(start = 8.dp) - .clickable { onBackClick() } + EBIcon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + modifier = Modifier.padding(start = 8.dp), + onClick = { onBackClick() } ) }, actions = { - Icon( - rememberVectorPainter(Icons.Filled.Add), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier - .padding(8.dp) - .clickable { addCardDialog = true } + EBIcon( + imageVector = Icons.Filled.Add, + modifier = Modifier.padding(8.dp), + onClick = { addCardDialog = true } ) - Icon( - rememberVectorPainter(Icons.Filled.Edit), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier - .padding(8.dp) - .clickable { editCardDialog = true } + EBIcon( + imageVector = Icons.Filled.Edit, + modifier = Modifier.padding(8.dp), + onClick = { editCardDialog = true } ) - Icon( - rememberVectorPainter(Icons.Filled.Delete), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier - .padding(8.dp) - .clickable { cardDeletionDialog = true } + EBIcon( + imageVector = Icons.Filled.Delete, + modifier = Modifier.padding(8.dp), + onClick = { cardDeletionDialog = true } ) } ) @@ -209,8 +175,11 @@ fun BoardContent( } ) { paddingValues -> - Column(Modifier.fillMaxSize()) { - + Column( + Modifier + .fillMaxSize() + .padding(paddingValues) + ) { if (cards.isEmpty()) { Row( modifier = Modifier.fillMaxSize(), @@ -229,17 +198,14 @@ fun BoardContent( ) { pageIndex -> val card = cards.getOrNull(pageIndex) ?: return@HorizontalPager -// log(tag = "ExceptionHandling") { "Pager card: $card" } - - LaunchedEffect(card) { + LaunchedEffect(card.backText) { card.backText.ifNotEmpty { richTextState.setHtml(it) } } Flippable( modifier = Modifier .fillMaxWidth() - .height(600.dp) - .padding(paddingValues), + .height(600.dp), frontSide = { Box( modifier = Modifier @@ -250,6 +216,7 @@ fun BoardContent( contentAlignment = Alignment.Center ) { Text( + modifier = Modifier.noRippleClickable { controller.flipToBack() }, text = card.frontText, color = Color.Black, fontSize = 20.sp @@ -269,7 +236,8 @@ fun BoardContent( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) - .verticalScroll(state = rememberScrollState()), + .verticalScroll(state = rememberScrollState()) + .noRippleClickable { controller.flip() }, state = richTextState, textStyle = TextStyle(fontSize = 20.sp), readOnly = true @@ -284,97 +252,97 @@ fun BoardContent( } when { - addCardDialog -> AddCardDialog( - onCreate = { flashCard -> + addCardDialog -> AddEditCardDialog( + onSave = { flashCard -> focusManager.clearFocus() uiState.board?.let { onCardCreate(it, flashCard) } }, - dismiss = { addCardDialog = false }) + dismiss = { addCardDialog = false } + ) // --- - editCardDialog -> EditCardDialog( - currentFlashCard = cards.getOrNull(pagerState.currentPage), - onUpdate = { flashCard -> + editCardDialog -> AddEditCardDialog( + flashCard = cards.getOrNull(pagerState.currentPage) ?: return, + onSave = { flashCard -> focusManager.clearFocus() onCardUpdate(flashCard) }, - dismiss = { editCardDialog = false }) + dismiss = { editCardDialog = false } + ) // --- - cardDeletionDialog -> { - val card = cards.getOrNull(pagerState.currentPage) ?: return - - CardDeletionDialog( - flashCard = card, - confirm = { cardId -> onCardDelete(cardId) }, - dismiss = { cardDeletionDialog = false } - ) - } + cardDeletionDialog -> CardDeletionDialog( + flashCard = cards.getOrNull(pagerState.currentPage) ?: return, + confirm = { cardId -> onCardDelete(cardId) }, + dismiss = { cardDeletionDialog = false } + ) } } +@Preview @Composable -fun AddCardDialog( - onCreate: (FlashCardEntity) -> Unit, - dismiss: () -> Unit -) { - CardContent( - onSave = onCreate, - dismiss = dismiss - ) -} - -@Composable -fun EditCardDialog( - currentFlashCard: FlashCardEntity?, - onUpdate: (FlashCardEntity) -> Unit, - dismiss: () -> Unit -) { - CardContent( - currentFlashCard, - onSave = onUpdate, - dismiss = dismiss +fun DialogPreview() { + AddEditCardDialog( + onSave = {}, + dismiss = {}, ) } @OptIn(ExperimentalRichTextApi::class) @Composable -fun CardContent( +fun AddEditCardDialog( flashCard: FlashCardEntity? = null, richTextState: RichTextState = rememberRichTextState(), onSave: (FlashCardEntity) -> Unit, dismiss: () -> Unit ) { + var word by remember { mutableStateOf("") } + val card by remember { mutableStateOf(flashCard ?: FlashCardEntity()) } + richTextState.setConfig( + linkColor = Color.Blue, + linkTextDecoration = TextDecoration.Underline, + codeColor = Color.DarkGray, + codeBackgroundColor = Color.Transparent, + codeStrokeColor = Color.Transparent, + ) + flashCard?.let { + LaunchedEffect(it) { + word = it.frontText + richTextState.setHtml(it.backText) + } + } + BaseDialogContent( height = 450.dp, dismiss = dismiss ) { - var word by remember { mutableStateOf("") } - val card by remember { mutableStateOf(flashCard ?: FlashCardEntity()) } - - flashCard?.let { - LaunchedEffect(it) { - word = it.frontText - richTextState.setHtml(it.backText) - } - } - - richTextState.setConfig( - linkColor = Color.Blue, - linkTextDecoration = TextDecoration.Underline, - codeColor = Color.DarkGray, - codeBackgroundColor = Color.Transparent, - codeStrokeColor = Color.Transparent, - ) - Column( - Modifier - .fillMaxWidth() - .padding(vertical = 24.dp), + Modifier.fillMaxWidth().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "New flashcard", + fontSize = 16.sp + ) + EBOutlinedButton( + text = "Save", + onClick = { + if (word.isEmpty()) return@EBOutlinedButton + + // TODO: delete it when RichTextEditor has onValueChanged callback + card.backText = richTextState.toHtml() + + onSave(card) + dismiss() + } + ) + } + EBOutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = Modifier.fillMaxWidth(), value = word, placeholder = "Word", singleLine = true, @@ -391,20 +359,18 @@ fun CardContent( Spacer(Modifier.height(16.dp)) RichTextStyleRow( - modifier = Modifier.padding(horizontal = 8.dp), state = richTextState ) BasicRichTextEditor( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) .verticalScroll(state = rememberScrollState()), state = richTextState, minLines = 12, maxLines = 12, decorationBox = { innerTextField -> - Box(Modifier.padding(8.dp)) { + Box(Modifier.padding(12.dp)) { if (richTextState.annotatedString.text.isEmpty()) { Text( text = "Description", @@ -415,28 +381,6 @@ fun CardContent( } } ) - - Spacer(Modifier.weight(1f)) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(end = 16.dp), - horizontalArrangement = Arrangement.End - ) { - EBOutlinedButton( - text = "Save", - onClick = { - if (word.isEmpty()) return@EBOutlinedButton - - // TODO: delete it when RichTextEditor has onValueChanged callback - card.backText = richTextState.toHtml() - - onSave(card) -// dismiss() - } - ) - } } } } @@ -447,10 +391,12 @@ fun CardDeletionDialog( confirm: (String) -> Unit, dismiss: () -> Unit ) { - Dialog(onDismissRequest = dismiss) { - Column { - Spacer(Modifier.height(8.dp)) - + BaseDialogContent( + width = 350.dp, + height = 200.dp, + dismiss = dismiss + ) { + Column(Modifier.padding(16.dp)) { Text( text = "Warning", fontSize = 20.sp @@ -461,7 +407,7 @@ fun CardDeletionDialog( Text( modifier = Modifier.padding(top = 16.dp, bottom = 24.dp), text = "Are you sure to delete the card \"${flashCard.frontText}\"?", - fontSize = 16.sp + fontSize = 18.sp ) Spacer(Modifier.height(24.dp)) @@ -473,12 +419,14 @@ fun CardDeletionDialog( Spacer(Modifier.weight(1f)) EBTextButton( text = "Cancel", - onClick = dismiss + onClick = dismiss, + fontSize = 18.sp ) EBTextButton( - modifier = Modifier.padding(start = 16.dp), + modifier = Modifier.padding(start = 32.dp), text = "Delete", - colors = ButtonDefaults.buttonColors(contentColor = RedDark), + textColor = RedDark, + fontSize = 18.sp, onClick = { confirm(flashCard.id) } ) } diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt index 65f8ecf..255ccbf 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -56,12 +55,11 @@ import org.koin.androidx.compose.getViewModel @Composable fun BoardsScreen( - onBoardClick: (String?) -> Unit, openDrawer: () -> Unit ) { val viewModel: BoardsViewModel = getViewModel() - val uiState by viewModel.boardsUiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() when { uiState.isLoading -> LoadingView() @@ -70,7 +68,6 @@ fun BoardsScreen( uiState, onBoardCreate = { board -> viewModel.saveBoard(board) }, onBoardClick = onBoardClick, - onGetCards = { viewModel.getCards() }, openDrawer = openDrawer ) } @@ -82,7 +79,6 @@ fun BoardsContent( uiState: BoardsUiState, onBoardCreate: (BoardEntity) -> Unit, onBoardClick: (String?) -> Unit, - onGetCards: () -> Unit, openDrawer: () -> Unit ) { val focusManager = LocalFocusManager.current @@ -135,13 +131,7 @@ fun BoardsContent( } } ) { paddingValues -> - LazyColumn(modifier = Modifier.padding(paddingValues)) { - item { - Button(onClick = onGetCards) { - Text("Get cards") - } - } items(items = uiState.boards, key = { it.id }) { board -> BoardItem(board, onBoardClick) } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index b3f47bd..e1ac815 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -75,9 +75,9 @@ kotlin { // Realm api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") // Add to use coroutines with the SDK - api("io.realm.kotlin:library-base:1.11.0") // Add to only use the local database - api("io.realm.kotlin:library-sync:1.11.0") // Add to use Device Sync - compileOnly("io.realm.kotlin:library-base:1.11.0") + api("io.realm.kotlin:library-base:1.12.0") // Add to only use the local database + api("io.realm.kotlin:library-sync:1.12.0") // Add to use Device Sync + compileOnly("io.realm.kotlin:library-base:1.12.0") // implementation("com.github.vicpinm:krealmextensions:2.5.0") // Moco diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt index e3ae6c6..f7c6b8e 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt @@ -1,5 +1,9 @@ package com.san.englishbender.core.di +import com.san.englishbender.domain.usecases.flashCards.AddFlashCardToBoardUseCase +import com.san.englishbender.domain.usecases.flashCards.DeleteBoardUseCase +import com.san.englishbender.domain.usecases.flashCards.DeleteFlashCardUseCase +import com.san.englishbender.domain.usecases.flashCards.GetBoardAsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardByIdUseCase import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase @@ -42,6 +46,10 @@ val useCaseModule = module { // --- FlashCards single { GetBoardsFlowUseCase(get()) } single { GetBoardByIdUseCase(get()) } + single { GetBoardAsFlowUseCase(get()) } single { SaveBoardUseCase(get()) } + single { AddFlashCardToBoardUseCase(get()) } single { SaveFlashCardUseCase(get()) } + single { DeleteBoardUseCase(get()) } + single { DeleteFlashCardUseCase(get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt index e573515..f3d3e03 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt @@ -13,6 +13,6 @@ val viewModelModule = module { single { RecordDetailsViewModel(get(), get(), get(), get(), get()) } single { StatsViewModel(get(), get()) } single { TagsViewModel(get(), get(), get(), get(), get()) } - single { BoardsViewModel(get(), get(), get(), get()) } - single { FlashCardsViewModel(get(), get()) } + single { BoardsViewModel(get(), get(), get()) } + single { FlashCardsViewModel(get(), get(), get(), get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt index 1e149d1..2860b8f 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/Result.kt @@ -54,11 +54,20 @@ fun Flow.asResult(): Flow> { .catch { emit(Result.Failure(it)) } } -suspend fun getResult(action: suspend () -> T) = try { - Result.Success(action.invoke()) -} catch (e: Exception) { - Result.Failure(e) -} +//suspend fun getResult(action: suspend () -> T) = try { +// Result.Success(action.invoke()) +//} catch (e: Exception) { +// Result.Failure(e) +//} + +//suspend fun getResult(action: suspend () -> Flow) : T = try { +// action.invoke().collect { +// return@collect Result.Success(it) +// } +//// Result.Success(action.invoke()) +//} catch (e: Exception) { +// Result.Failure(e) +//} suspend fun getResultFlow(action: suspend () -> T): Flow> = flow { return@flow try { diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt index fd7b317..b4284cf 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt @@ -2,8 +2,10 @@ package com.san.englishbender.data.local.models import com.san.englishbender.domain.entities.FlashCardEntity import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey open class FlashCard : RealmObject { + @PrimaryKey var id: String = "" var frontText: String = "" var backText: String = "" diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt index faf9752..87aca63 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt @@ -2,13 +2,13 @@ package com.san.englishbender.data.repositories import com.san.englishbender.core.extensions.doQuery import com.san.englishbender.data.local.models.Board +import com.san.englishbender.data.local.models.FlashCard import com.san.englishbender.data.local.models.toEntity import com.san.englishbender.data.local.models.toLocal import com.san.englishbender.domain.entities.BoardEntity import com.san.englishbender.domain.entities.FlashCardEntity import com.san.englishbender.domain.repositories.IBoardsRepository import com.san.englishbender.ioDispatcher -import io.github.aakira.napier.log import io.realm.kotlin.Realm import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.ext.query @@ -43,7 +43,7 @@ class BoardsRepository( realm.query("id == $0", id).first().find()?.toEntity() } - override fun getBoardFlow(id: String): Flow = flow { + override fun getBoardAsFlow(id: String): Flow = flow { realm.query("id == $0", id) .first() .asFlow() @@ -57,18 +57,20 @@ class BoardsRepository( }.flowOn(ioDispatcher) override suspend fun saveBoard(board: BoardEntity): Unit = doQuery { - log(tag = "BoardsViewModel") { "saveBoard: $board cards.size: ${board.flashCards.size}" } val local = board.toLocal() realm.write { copyToRealm(local, UpdatePolicy.ALL) } } + override suspend fun addFlashCardToBoard(boardId: String, flashCard: FlashCardEntity): Unit = + doQuery { + realm.write { + val board = this.query("id == $0", boardId).first().find() + board?.flashCards?.add(flashCard.toLocal()) + } + } + override suspend fun saveFlashCard(card: FlashCardEntity): Unit = doQuery { - log(tag = "getCards") { "card: $card" } - val local = card.toLocal() - log(tag = "getCards") { "local: $local" } - log(tag = "getCards") { "front: ${local.frontText}" } - log(tag = "getCards") { "back: ${local.backText}" } - realm.write { copyToRealm(local, UpdatePolicy.ALL) } + realm.write { copyToRealm(card.toLocal(), UpdatePolicy.ALL) } } override suspend fun deleteBoard(boardId: String): Unit = doQuery { @@ -77,4 +79,11 @@ class BoardsRepository( delete(board) } } + + override suspend fun deleteFlashCard(cardId: String): Unit = doQuery { + realm.write { + val card = query("id == $0", cardId).find() + delete(card) + } + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt index 68443e9..c239b35 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt @@ -8,8 +8,10 @@ interface IBoardsRepository { fun getBoardsFlow() : Flow> suspend fun getBoards() : List suspend fun getBoard(id: String) : BoardEntity? - fun getBoardFlow(id: String) : Flow + fun getBoardAsFlow(id: String) : Flow suspend fun saveBoard(board: BoardEntity) + suspend fun addFlashCardToBoard(boardId: String, flashCard: FlashCardEntity) suspend fun saveFlashCard(card: FlashCardEntity) suspend fun deleteBoard(boardId: String) + suspend fun deleteFlashCard(cardId: String) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/AddFlashCardToBoardUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/AddFlashCardToBoardUseCase.kt new file mode 100644 index 0000000..fb1ae0b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/AddFlashCardToBoardUseCase.kt @@ -0,0 +1,9 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.domain.entities.FlashCardEntity +import com.san.englishbender.domain.repositories.IBoardsRepository + +class AddFlashCardToBoardUseCase(private val boardRepository: IBoardsRepository) { + suspend operator fun invoke(boardId: String, flashCardEntity: FlashCardEntity) = + boardRepository.addFlashCardToBoard(boardId, flashCardEntity) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/DeleteBoardUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/DeleteBoardUseCase.kt new file mode 100644 index 0000000..bd33131 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/DeleteBoardUseCase.kt @@ -0,0 +1,10 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.domain.repositories.IBoardsRepository + +class DeleteBoardUseCase( + private val boardRepository: IBoardsRepository +) { + suspend operator fun invoke(boardId: String): Unit = + boardRepository.deleteBoard(boardId) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/DeleteFlashCardUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/DeleteFlashCardUseCase.kt new file mode 100644 index 0000000..1da3055 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/DeleteFlashCardUseCase.kt @@ -0,0 +1,10 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.domain.repositories.IBoardsRepository + +class DeleteFlashCardUseCase( + private val boardRepository: IBoardsRepository +) { + suspend operator fun invoke(cardId: String): Unit = + boardRepository.deleteFlashCard(cardId) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardAsFlowUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardAsFlowUseCase.kt new file mode 100644 index 0000000..1ebb1dd --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardAsFlowUseCase.kt @@ -0,0 +1,12 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.repositories.IBoardsRepository +import kotlinx.coroutines.flow.Flow + +class GetBoardAsFlowUseCase( + private val boardRepository: IBoardsRepository +) { + operator fun invoke(boardId: String): Flow = + boardRepository.getBoardAsFlow(boardId) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt index 87c4124..3bf4147 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt @@ -1,6 +1,5 @@ package com.san.englishbender.domain.usecases.flashCards -import com.san.englishbender.domain.entities.BoardEntity import com.san.englishbender.domain.entities.FlashCardEntity import com.san.englishbender.domain.repositories.IBoardsRepository diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt index 45e790e..0deed2f 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt @@ -3,22 +3,16 @@ package com.san.englishbender.ui.flashcards import androidx.compose.runtime.Immutable import com.san.englishbender.SharedRes import com.san.englishbender.core.extensions.WhileUiSubscribed -import com.san.englishbender.core.extensions.isNull import com.san.englishbender.data.getResultFlow import com.san.englishbender.data.onFailure import com.san.englishbender.data.onSuccess import com.san.englishbender.domain.entities.BoardEntity -import com.san.englishbender.domain.entities.FlashCardEntity -import com.san.englishbender.domain.usecases.flashCards.GetBoardByIdUseCase +import com.san.englishbender.domain.usecases.flashCards.DeleteBoardUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase -import com.san.englishbender.domain.usecases.flashCards.SaveFlashCardUseCase import com.san.englishbender.ui.ViewModel import dev.icerock.moko.resources.StringResource -import io.github.aakira.napier.log -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -31,24 +25,13 @@ data class BoardsUiState( val userMessage: StringResource? = null ) -@Immutable -data class BoardUiState( - val isLoading: Boolean = false, - val board: BoardEntity? = null, - val userMessage: StringResource? = null -) - class BoardsViewModel( private val getBoardsFlowUseCase: GetBoardsFlowUseCase, - private val getBoardByIdUseCase: GetBoardByIdUseCase, private val saveBoardUseCase: SaveBoardUseCase, - private val saveFlashCardUseCase: SaveFlashCardUseCase + private val deleteBoardUseCase: DeleteBoardUseCase ) : ViewModel() { - private val _boardUiState = MutableStateFlow(BoardUiState()) - val boardUiState: StateFlow = _boardUiState.asStateFlow() - - val boardsUiState: StateFlow = + val uiState: StateFlow = getBoardsFlowUseCase() .map { BoardsUiState(boards = it) } .catch { BoardsUiState(userMessage = SharedRes.strings.loading_records_error) } @@ -58,76 +41,24 @@ class BoardsViewModel( initialValue = BoardsUiState(isLoading = true) ) - fun getBoard(boardId: String) = safeLaunch { - getResultFlow { getBoardByIdUseCase(boardId) } - .onFailure { showError(SharedRes.strings.remove_record_error) } - .onSuccess { boardEntity -> - log(tag = "BoardsViewModel") { "onSuccess" } - log(tag = "BoardsViewModel") { "flashCards.size: ${boardEntity?.flashCards?.size}" } - if (boardEntity.isNull) { - showError(SharedRes.strings.remove_record_error) - return@onSuccess - } - _boardUiState.update { state -> - state.copy( - isLoading = false, - board = boardEntity - ) - } - } - } - - fun getCards() = safeLaunch { - - val board = boardsUiState.value.boards.first() - - log(tag = "getCards") { "cards.size: ${board.flashCards.size}" } - - board.flashCards.forEach { card -> - log(tag = "getCards") { "card.front: ${card.frontText}" } - log(tag = "getCards") { "card.back: ${card.backText}" } - } - } - fun saveBoard(board: BoardEntity) = safeLaunch { getResultFlow { saveBoardUseCase(board) } .onFailure { showError(SharedRes.strings.remove_record_error) } .onSuccess {} } - fun addCardToBoard(board: BoardEntity, flashCard: FlashCardEntity) = safeLaunch { - - // TODO: - board.flashCards = board.flashCards.plus(listOf(flashCard)) - - getResultFlow { saveBoardUseCase(board) } - .onFailure { showError(SharedRes.strings.remove_record_error) } - .onSuccess { - log(tag = "BoardsViewModel") { "onSuccess addCardToBoard" } - getBoard(board.id) - } - } - - fun updateCard(card: FlashCardEntity) = safeLaunch { - getResultFlow { saveFlashCardUseCase(card) } - .onFailure { } - .onSuccess { } - } - fun deleteBoard(boardId: String) = safeLaunch { - - } - - fun deleteCard(cardId: String) = safeLaunch { - + getResultFlow { deleteBoardUseCase(boardId) } + .onFailure { showError(SharedRes.strings.remove_record_error) } + .onSuccess {} } private fun showError(message: StringResource) = safeLaunch { - _boardUiState.update { - it.copy( - isLoading = false, - userMessage = message - ) - } +// uiState.update { +// it.copy( +// isLoading = false, +// userMessage = message +// ) +// } } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt index 7d10e5c..a215d9d 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt @@ -1,23 +1,26 @@ package com.san.englishbender.ui.flashcards +import androidx.compose.runtime.Immutable import com.san.englishbender.SharedRes import com.san.englishbender.core.extensions.isNull import com.san.englishbender.data.getResultFlow import com.san.englishbender.data.onFailure import com.san.englishbender.data.onSuccess import com.san.englishbender.domain.entities.BoardEntity -import com.san.englishbender.domain.usecases.flashCards.GetBoardByIdUseCase -import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase -import com.san.englishbender.randomUUID +import com.san.englishbender.domain.entities.FlashCardEntity +import com.san.englishbender.domain.usecases.flashCards.AddFlashCardToBoardUseCase +import com.san.englishbender.domain.usecases.flashCards.DeleteFlashCardUseCase +import com.san.englishbender.domain.usecases.flashCards.GetBoardAsFlowUseCase +import com.san.englishbender.domain.usecases.flashCards.SaveFlashCardUseCase import com.san.englishbender.ui.ViewModel -import com.san.englishbender.ui.records.RecordsUiState import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.update - +@Immutable data class FlashCardsUiState( val isLoading: Boolean = false, val board: BoardEntity? = null, @@ -25,34 +28,23 @@ data class FlashCardsUiState( ) class FlashCardsViewModel( - private val getBoardByIdUseCase: GetBoardByIdUseCase, - private val saveBoardUseCase: SaveBoardUseCase + private val getBoardAsFlowUseCase: GetBoardAsFlowUseCase, + private val addFlashCardToBoardUseCase: AddFlashCardToBoardUseCase, + private val saveFlashCardUseCase: SaveFlashCardUseCase, + private val deleteFlashCardUseCase: DeleteFlashCardUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(FlashCardsUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun getBoard(boardId: String) = safeLaunch { - getResultFlow { getBoardByIdUseCase(boardId) } - .onFailure { - _uiState.update { - it.copy( - isLoading = false, - userMessage = SharedRes.strings.remove_record_error - ) - } - } - .onSuccess { boardEntity -> + getBoardAsFlowUseCase(boardId) + .catch { showError(SharedRes.strings.remove_record_error) } + .collect { boardEntity -> if (boardEntity.isNull) { - _uiState.update { - it.copy( - isLoading = false, - userMessage = SharedRes.strings.remove_record_error - ) - } - return@onSuccess + showError(SharedRes.strings.remove_record_error) + return@collect } - _uiState.update { state -> state.copy( isLoading = false, @@ -62,19 +54,30 @@ class FlashCardsViewModel( } } - fun saveFlashCard(boardName: String, boardColor: String) = safeLaunch { - - val board = BoardEntity().apply { - name = boardName - backgroundColor = boardColor - } + fun addCardToBoard(board: BoardEntity, flashCard: FlashCardEntity) = safeLaunch { + getResultFlow { addFlashCardToBoardUseCase(board.id, flashCard) } + .onFailure { showError(SharedRes.strings.remove_record_error) } + .onSuccess {} + } - getResultFlow { saveBoardUseCase(board) } - .onFailure { RecordsUiState(userMessage = SharedRes.strings.remove_record_error) } - .onSuccess { } + fun updateCard(card: FlashCardEntity) = safeLaunch { + getResultFlow { saveFlashCardUseCase(card) } + .onFailure {} + .onSuccess {} } fun deleteFlashCard(cardId: String) = safeLaunch { + getResultFlow { deleteFlashCardUseCase(cardId) } + .onFailure { showError(SharedRes.strings.remove_record_error) } + .onSuccess {} + } + private fun showError(message: StringResource) = safeLaunch { + _uiState.update { + it.copy( + isLoading = false, + userMessage = message + ) + } } } \ No newline at end of file From ce2c97b36344f53253687bdb2f80f62493ec7e1a Mon Sep 17 00:00:00 2001 From: Aleksandr Symbiotov Date: Thu, 15 Feb 2024 23:36:55 +0100 Subject: [PATCH 5/5] Minor refactoring --- .ideaBackup/.gitignore | 3 + .ideaBackup/.name | 1 + .ideaBackup/appInsightsSettings.xml | 26 +++ .ideaBackup/compiler.xml | 6 + .ideaBackup/deploymentTargetDropDown.xml | 10 + .ideaBackup/gradle.xml | 20 ++ .../inspectionProfiles/Project_Default.xml | 41 ++++ .ideaBackup/kotlinc.xml | 6 + .ideaBackup/migrations.xml | 10 + .ideaBackup/misc.xml | 9 + .ideaBackup/vcs.xml | 6 + androidApp/build.gradle.kts | 3 +- .../BottomNavBar.kt} | 36 ++-- .../android/ui/common/ButtonComposables.kt | 2 +- .../ui/common/richText/RichTextStyleRow.kt | 72 ++----- .../ui/common/richText/RichTextToolsRow.kt | 192 ++++++++++++++++++ .../android/ui/flashcards/BoardScreen.kt | 97 +++++++-- .../android/ui/flashcards/BoardsScreen.kt | 57 +++--- .../ui/recordDetails/RecordDetailsScreen.kt | 21 +- .../android/ui/records/RecordsScreen.kt | 43 ++-- build.gradle.kts | 1 + shared/build.gradle.kts | 17 +- .../kotlin/com/san/englishbender/Platform.kt | 11 + .../kotlin/com/san/englishbender/Platform.kt | 4 + .../englishbender/core/di/UseCaseModule.kt | 2 + .../englishbender/core/di/ViewModelModule.kt | 2 +- .../englishbender/data/local/models/Board.kt | 1 + .../data/local/models/FlashCard.kt | 11 +- .../data/repositories/BoardsRepository.kt | 36 +++- .../domain/entities/FlashCardEntity.kt | 3 + .../domain/repositories/IBoardsRepository.kt | 2 + .../flashCards/GetFlashCardsAsFlowUseCase.kt | 12 ++ .../ui/flashcards/BoardsViewModel.kt | 50 +++++ .../ui/flashcards/FlashCardsViewModel.kt | 69 +++++-- .../src/commonMain/resources/commonWords.json | 16 ++ .../kotlin/com/san/englishbender/Platform.kt | 31 +++ 36 files changed, 757 insertions(+), 172 deletions(-) create mode 100644 .ideaBackup/.gitignore create mode 100644 .ideaBackup/.name create mode 100644 .ideaBackup/appInsightsSettings.xml create mode 100644 .ideaBackup/compiler.xml create mode 100644 .ideaBackup/deploymentTargetDropDown.xml create mode 100644 .ideaBackup/gradle.xml create mode 100644 .ideaBackup/inspectionProfiles/Project_Default.xml create mode 100644 .ideaBackup/kotlinc.xml create mode 100644 .ideaBackup/migrations.xml create mode 100644 .ideaBackup/misc.xml create mode 100644 .ideaBackup/vcs.xml rename androidApp/src/main/java/com/san/englishbender/android/ui/{recordDetails/BottomNavigationBar.kt => common/BottomNavBar.kt} (59%) create mode 100644 androidApp/src/main/java/com/san/englishbender/android/ui/common/richText/RichTextToolsRow.kt create mode 100644 shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsAsFlowUseCase.kt create mode 100644 shared/src/commonMain/resources/commonWords.json diff --git a/.ideaBackup/.gitignore b/.ideaBackup/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.ideaBackup/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.ideaBackup/.name b/.ideaBackup/.name new file mode 100644 index 0000000..1e12bee --- /dev/null +++ b/.ideaBackup/.name @@ -0,0 +1 @@ +EnglishBender \ No newline at end of file diff --git a/.ideaBackup/appInsightsSettings.xml b/.ideaBackup/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.ideaBackup/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.ideaBackup/compiler.xml b/.ideaBackup/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.ideaBackup/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.ideaBackup/deploymentTargetDropDown.xml b/.ideaBackup/deploymentTargetDropDown.xml new file mode 100644 index 0000000..b1d56a7 --- /dev/null +++ b/.ideaBackup/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.ideaBackup/gradle.xml b/.ideaBackup/gradle.xml new file mode 100644 index 0000000..ca5fba8 --- /dev/null +++ b/.ideaBackup/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.ideaBackup/inspectionProfiles/Project_Default.xml b/.ideaBackup/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..44ca2d9 --- /dev/null +++ b/.ideaBackup/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/.ideaBackup/kotlinc.xml b/.ideaBackup/kotlinc.xml new file mode 100644 index 0000000..ae3f30a --- /dev/null +++ b/.ideaBackup/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.ideaBackup/migrations.xml b/.ideaBackup/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.ideaBackup/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.ideaBackup/misc.xml b/.ideaBackup/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.ideaBackup/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.ideaBackup/vcs.xml b/.ideaBackup/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.ideaBackup/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index d263ba8..b2c89a6 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -18,7 +18,8 @@ android { } composeOptions { // kotlinCompilerExtensionVersion = "1.4.0" - kotlinCompilerExtensionVersion = "1.4.8" +// kotlinCompilerExtensionVersion = "1.4.8" + kotlinCompilerExtensionVersion = "1.5.9" } packagingOptions { resources { diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/BottomNavigationBar.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/BottomNavBar.kt similarity index 59% rename from androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/BottomNavigationBar.kt rename to androidApp/src/main/java/com/san/englishbender/android/ui/common/BottomNavBar.kt index fec6d0a..900e150 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/BottomNavigationBar.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/BottomNavBar.kt @@ -1,8 +1,9 @@ -package com.san.englishbender.android.ui.recordDetails +package com.san.englishbender.android.ui.common -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Archive import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Spellcheck import androidx.compose.material.icons.outlined.Translate @@ -13,32 +14,33 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -sealed class BottomNavItem(var title: String, var icon: ImageVector) { - object GrammarCheck : BottomNavItem("GrammarCheck", Icons.Outlined.Spellcheck) - object Translate : BottomNavItem("Translate", Icons.Outlined.Translate) - object Settings : BottomNavItem("Settings", Icons.Outlined.Settings) +sealed class BottomNavItem(var label: String, var icon: ImageVector) + +sealed class DeckNavItem(label: String, icon: ImageVector) : BottomNavItem(label, icon) { + data object SendToArchive : DeckNavItem("Archive", Icons.Outlined.Archive) +} + +sealed class RecordDetailsNavItem(label: String, icon: ImageVector) : BottomNavItem(label, icon) { + data object GrammarCheck : RecordDetailsNavItem("GrammarCheck", Icons.Outlined.Spellcheck) + data object Translate : RecordDetailsNavItem("Translate", Icons.Outlined.Translate) + data object Settings : RecordDetailsNavItem("Settings", Icons.Outlined.Settings) } @Composable -fun NavigationBar( +fun BottomNavBar( hasLabel: Boolean = false, containerColor: Color = Color.White, + navItems: List, navItemClicked: (navItem: BottomNavItem) -> Unit ) { - val navItems = listOf( - BottomNavItem.GrammarCheck, - BottomNavItem.Translate, - BottomNavItem.Settings - ) - NavigationBar( contentColor = Color.Black, containerColor = containerColor ) { - navItems.forEachIndexed { index, navItem -> + navItems.forEach{ navItem -> NavigationBarItem( icon = { Icon(navItem.icon, contentDescription = null) }, - label = { if (hasLabel) Text(navItem.title) }, + label = { if (hasLabel) Text(navItem.label) }, selected = false, colors = NavigationBarItemDefaults.colors( selectedIconColor = Color.Black, @@ -52,4 +54,4 @@ fun NavigationBar( ) } } -} +} \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt index 1aa8b8e..1ec4059 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/ButtonComposables.kt @@ -177,7 +177,7 @@ fun EBIcon( modifier: Modifier = Modifier, imageVector: ImageVector, contentDescription: String? = null, - onClick: () -> Unit + onClick: () -> Unit = {} ) { Icon( painter = rememberVectorPainter(imageVector), diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/richText/RichTextStyleRow.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/richText/RichTextStyleRow.kt index 3bfceab..c1bc52d 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/common/richText/RichTextStyleRow.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/richText/RichTextStyleRow.kt @@ -6,30 +6,33 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.FormatAlignLeft -import androidx.compose.material.icons.automirrored.outlined.FormatAlignRight import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted import androidx.compose.material.icons.filled.Circle -import androidx.compose.material.icons.outlined.* +import androidx.compose.material.icons.outlined.Circle +import androidx.compose.material.icons.outlined.Code +import androidx.compose.material.icons.outlined.FormatBold +import androidx.compose.material.icons.outlined.FormatItalic +import androidx.compose.material.icons.outlined.FormatListNumbered +import androidx.compose.material.icons.outlined.FormatSize +import androidx.compose.material.icons.outlined.FormatStrikethrough +import androidx.compose.material.icons.outlined.FormatUnderlined import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.mohamedrejeb.richeditor.model.RichTextState -import io.github.aakira.napier.log + @Composable fun RichTextStyleRow( modifier: Modifier = Modifier, - state: RichTextState, + state: RichTextState ) { // val currentParagraphStyle = state.currentParagraphStyle // val isCentered = currentParagraphStyle.textAlign == TextAlign.Center @@ -83,7 +86,6 @@ fun RichTextStyleRow( icon = Icons.Outlined.FormatBold ) } - item { RichTextStyleButton( onClick = { @@ -93,79 +95,53 @@ fun RichTextStyleRow( icon = Icons.Outlined.FormatItalic ) } - item { RichTextStyleButton( onClick = { - state.toggleSpanStyle( - SpanStyle( - textDecoration = TextDecoration.Underline - ) - ) + state.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline)) }, isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true, icon = Icons.Outlined.FormatUnderlined ) } - item { RichTextStyleButton( onClick = { - state.toggleSpanStyle( - SpanStyle( - textDecoration = TextDecoration.LineThrough - ) - ) + state.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) }, isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true, icon = Icons.Outlined.FormatStrikethrough ) } - item { RichTextStyleButton( onClick = { - state.toggleSpanStyle( - SpanStyle( - fontSize = 28.sp - ) - ) + state.toggleSpanStyle(SpanStyle(fontSize = 28.sp)) }, isSelected = state.currentSpanStyle.fontSize == 28.sp, icon = Icons.Outlined.FormatSize ) } - item { RichTextStyleButton( onClick = { - state.toggleSpanStyle( - SpanStyle( - color = Color.Red - ) - ) + state.toggleSpanStyle(SpanStyle(color = Color.Red)) }, isSelected = state.currentSpanStyle.color == Color.Red, icon = Icons.Filled.Circle, tint = Color.Red ) } - item { RichTextStyleButton( onClick = { - state.toggleSpanStyle( - SpanStyle( - background = Color.Yellow - ) - ) + state.toggleSpanStyle(SpanStyle(background = Color.Yellow)) }, isSelected = state.currentSpanStyle.background == Color.Yellow, icon = Icons.Outlined.Circle, tint = Color.Yellow ) } - item { Box( Modifier @@ -174,29 +150,20 @@ fun RichTextStyleRow( .background(Color(0xFF393B3D)) ) } - item { RichTextStyleButton( - onClick = { - state.toggleUnorderedList() - }, + onClick = { state.toggleUnorderedList() }, isSelected = state.isUnorderedList, icon = Icons.AutoMirrored.Outlined.FormatListBulleted, ) } - item { RichTextStyleButton( - onClick = { - state.toggleOrderedList() - }, + onClick = { state.toggleOrderedList() }, isSelected = state.isOrderedList, icon = Icons.Outlined.FormatListNumbered, ) } - - - item { Box( Modifier @@ -205,12 +172,9 @@ fun RichTextStyleRow( .background(Color(0xFF393B3D)) ) } - item { RichTextStyleButton( - onClick = { - state.toggleCodeSpan() - }, + onClick = { state.toggleCodeSpan() }, isSelected = state.isCodeSpan, icon = Icons.Outlined.Code, ) diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/common/richText/RichTextToolsRow.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/common/richText/RichTextToolsRow.kt new file mode 100644 index 0000000..c991ee3 --- /dev/null +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/common/richText/RichTextToolsRow.kt @@ -0,0 +1,192 @@ +package com.san.englishbender.android.ui.common.richText + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.FormatAlignLeft +import androidx.compose.material.icons.automirrored.outlined.FormatAlignRight +import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted +import androidx.compose.material.icons.filled.Circle +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mohamedrejeb.richeditor.model.RichTextState + +enum class RichTextTools { + FormatAlignLeft, + FormatAlignCenter, + FormatAlignRight, + Bold, + Italic, + Underline, + FormatStrikethrough, + FormatSize, + FontColor, + BackgroundColor, + FormatListBulleted, + FormatListNumbered, + Code, + Divider +} + +val fullRichTextToolsPanel = RichTextTools.values().toList() +val shortRichTextToolsPanel = listOf( + RichTextTools.Bold, + RichTextTools.Italic, + RichTextTools.Underline, + RichTextTools.Divider, + RichTextTools.FormatListBulleted, + RichTextTools.FormatListNumbered, +) + +@Composable +fun RichTextToolsRow( + modifier: Modifier = Modifier, + state: RichTextState, + richTextTools: List = fullRichTextToolsPanel +) { +// val currentParagraphStyle = state.currentParagraphStyle + + LazyRow( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + ) { + items(richTextTools) { item -> + when (item) { + RichTextTools.FormatAlignLeft -> RichTextStyleButton( + onClick = { + state.toggleParagraphStyle(ParagraphStyle(textAlign = TextAlign.Left)) + }, + isSelected = state.currentParagraphStyle.textAlign == TextAlign.Left, + icon = Icons.AutoMirrored.Outlined.FormatAlignLeft + ) + + RichTextTools.FormatAlignCenter -> RichTextStyleButton( + onClick = { + state.toggleParagraphStyle(ParagraphStyle(textAlign = TextAlign.Center)) + }, + isSelected = state.currentParagraphStyle.textAlign == TextAlign.Center, + icon = Icons.Outlined.FormatAlignCenter + ) + + RichTextTools.FormatAlignRight -> RichTextStyleButton( + onClick = { + state.toggleParagraphStyle(ParagraphStyle(textAlign = TextAlign.Right)) + }, + isSelected = state.currentParagraphStyle.textAlign == TextAlign.Right, + icon = Icons.AutoMirrored.Outlined.FormatAlignRight + ) + + RichTextTools.Bold -> RichTextStyleButton( + onClick = { + state.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold)) + }, + isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold, + icon = Icons.Outlined.FormatBold + ) + + RichTextTools.Italic -> RichTextStyleButton( + onClick = { + state.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic)) + }, + isSelected = state.currentSpanStyle.fontStyle == FontStyle.Italic, + icon = Icons.Outlined.FormatItalic + ) + + RichTextTools.Underline -> RichTextStyleButton( + onClick = { + state.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline)) + }, + isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true, + icon = Icons.Outlined.FormatUnderlined + ) + + RichTextTools.FormatStrikethrough -> RichTextStyleButton( + onClick = { + state.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) + }, + isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true, + icon = Icons.Outlined.FormatStrikethrough + ) + + RichTextTools.FormatSize -> RichTextStyleButton( + onClick = { + state.toggleSpanStyle(SpanStyle(fontSize = 28.sp)) + }, + isSelected = state.currentSpanStyle.fontSize == 28.sp, + icon = Icons.Outlined.FormatSize + ) + + RichTextTools.FontColor -> RichTextStyleButton( + onClick = { + state.toggleSpanStyle(SpanStyle(color = Color.Red)) + }, + isSelected = state.currentSpanStyle.color == Color.Red, + icon = Icons.Filled.Circle, + tint = Color.Red + ) + + RichTextTools.BackgroundColor -> RichTextStyleButton( + onClick = { + state.toggleSpanStyle(SpanStyle(background = Color.Yellow)) + }, + isSelected = state.currentSpanStyle.background == Color.Yellow, + icon = Icons.Outlined.Circle, + tint = Color.Yellow + ) + + RichTextTools.FormatListBulleted -> RichTextStyleButton( + onClick = { state.toggleUnorderedList() }, + isSelected = state.isUnorderedList, + icon = Icons.AutoMirrored.Outlined.FormatListBulleted, + ) + + RichTextTools.FormatListNumbered -> RichTextStyleButton( + onClick = { state.toggleOrderedList() }, + isSelected = state.isOrderedList, + icon = Icons.Outlined.FormatListNumbered, + ) + + RichTextTools.Code -> RichTextStyleButton( + onClick = { state.toggleCodeSpan() }, + isSelected = state.isCodeSpan, + icon = Icons.Outlined.Code, + ) + + RichTextTools.Divider -> VerticalDivider( + Modifier + .height(24.dp) + .background(Color(0xFF393B3D)) + ) + } + } + +// item { +// VerticalDivider( +// Modifier +// .height(24.dp) +// .background(Color(0xFF393B3D)) +// ) +// Box( +// Modifier +// .height(24.dp) +// .width(1.dp) +// .background(Color(0xFF393B3D)) +// ) + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt index d0ec071..78b47a6 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.outlined.Archive import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -55,11 +56,17 @@ import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor import com.san.englishbender.android.core.extensions.noRippleClickable import com.san.englishbender.android.core.extensions.toColor import com.san.englishbender.android.ui.common.BaseDialogContent +import com.san.englishbender.android.ui.common.BottomNavBar +import com.san.englishbender.android.ui.common.BottomNavItem +import com.san.englishbender.android.ui.common.DeckNavItem import com.san.englishbender.android.ui.common.EBIcon import com.san.englishbender.android.ui.common.EBOutlinedButton +import com.san.englishbender.android.ui.common.EBOutlinedIconButton import com.san.englishbender.android.ui.common.EBOutlinedTextField import com.san.englishbender.android.ui.common.EBTextButton -import com.san.englishbender.android.ui.common.richText.RichTextStyleRow +import com.san.englishbender.android.ui.common.RecordDetailsNavItem +import com.san.englishbender.android.ui.common.richText.RichTextToolsRow +import com.san.englishbender.android.ui.common.richText.shortRichTextToolsPanel import com.san.englishbender.android.ui.common.widgets.ErrorView import com.san.englishbender.android.ui.common.widgets.LoadingView import com.san.englishbender.android.ui.theme.RedDark @@ -84,7 +91,11 @@ fun BoardScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(boardId) { - boardId?.let { viewModel.getBoard(it) } + boardId?.let { + viewModel.observeBoard(it) +// viewModel.getBoard(it) +// viewModel.getFlashCards(it) + } } when { @@ -93,7 +104,7 @@ fun BoardScreen( else -> BoardContent( uiState, onCardCreate = { board, flashCard -> viewModel.addCardToBoard(board, flashCard) }, - onCardUpdate = { flashCard -> viewModel.updateCard(flashCard) }, + onCardUpdate = { flashCard -> viewModel.saveCard(flashCard) }, onCardDelete = { flashCardId -> viewModel.deleteFlashCard(flashCardId) }, onBackClick ) @@ -116,13 +127,16 @@ fun BoardContent( val controller = rememberFlipController() val focusManager = LocalFocusManager.current - val cards = uiState.board?.flashCards ?: emptyList() - val pagerState = rememberPagerState(pageCount = { cards.size }) +// val cards = uiState.board?.flashCards ?: emptyList() +// val cards = uiState.flashCards ?: emptyList() + val pagerState = rememberPagerState(pageCount = { uiState.flashCards.size }) var addCardDialog by remember { mutableStateOf(false) } var editCardDialog by remember { mutableStateOf(false) } var cardDeletionDialog by remember { mutableStateOf(false) } +// var bottomNavItem by remember { mutableStateOf(DeckNavItem.SendToArchive) } + val containerColor = uiState.board?.backgroundColor?.toColor ?: MaterialTheme.colorScheme.surfaceVariant @@ -172,15 +186,41 @@ fun BoardContent( } ) } - } + }, + bottomBar = { + BottomNavBar( + navItems = listOf( + DeckNavItem.SendToArchive, + ), + containerColor = containerColor, + navItemClicked = { navItem -> +// bottomNavItem = navItem + + when (navItem) { + DeckNavItem.SendToArchive -> { + uiState.flashCards.getOrNull(pagerState.currentPage)?.let { + it.isArchived = true + onCardUpdate(it) + } + } + else -> {} + } + } + ) + }, ) { paddingValues -> Column( Modifier .fillMaxSize() .padding(paddingValues) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp + ) ) { - if (cards.isEmpty()) { + if (uiState.flashCards.isEmpty()) { Row( modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically, @@ -191,12 +231,21 @@ fun BoardContent( return@Scaffold } - Spacer(Modifier.height(32.dp)) + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "${(pagerState.currentPage + 1)}/${uiState.flashCards.size}", + fontSize = 18.sp + ) + } HorizontalPager( state = pagerState ) { pageIndex -> - val card = cards.getOrNull(pageIndex) ?: return@HorizontalPager + val card = uiState.flashCards.getOrNull(pageIndex) ?: return@HorizontalPager LaunchedEffect(card.backText) { card.backText.ifNotEmpty { richTextState.setHtml(it) } @@ -210,7 +259,6 @@ fun BoardContent( Box( modifier = Modifier .fillMaxSize() - .padding(16.dp) .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) .background(Color.White, RoundedCornerShape(6.dp)), contentAlignment = Alignment.Center @@ -227,7 +275,6 @@ fun BoardContent( Box( modifier = Modifier .fillMaxSize() - .padding(16.dp) .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) .background(Color.White, RoundedCornerShape(6.dp)), contentAlignment = Alignment.Center @@ -248,6 +295,17 @@ fun BoardContent( flipAnimationType = FlipAnimationType.HORIZONTAL_CLOCKWISE ) } + +// EBOutlinedIconButton( +// imageVector = Icons.Outlined.Archive, +// modifier = Modifier.padding(vertical = 16.dp), +// onClick = { +// cards.getOrNull(pagerState.currentPage)?.let { +// it.isArchived = true +// onCardUpdate(it) +// } +// } +// ) } } @@ -261,7 +319,7 @@ fun BoardContent( ) // --- editCardDialog -> AddEditCardDialog( - flashCard = cards.getOrNull(pagerState.currentPage) ?: return, + flashCard = uiState.flashCards.getOrNull(pagerState.currentPage) ?: return, onSave = { flashCard -> focusManager.clearFocus() onCardUpdate(flashCard) @@ -270,7 +328,7 @@ fun BoardContent( ) // --- cardDeletionDialog -> CardDeletionDialog( - flashCard = cards.getOrNull(pagerState.currentPage) ?: return, + flashCard = uiState.flashCards.getOrNull(pagerState.currentPage) ?: return, confirm = { cardId -> onCardDelete(cardId) }, dismiss = { cardDeletionDialog = false } ) @@ -315,11 +373,15 @@ fun AddEditCardDialog( dismiss = dismiss ) { Column( - Modifier.fillMaxWidth().padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Row( - modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { @@ -358,8 +420,9 @@ fun AddEditCardDialog( Spacer(Modifier.height(16.dp)) - RichTextStyleRow( - state = richTextState + RichTextToolsRow( + state = richTextState, + richTextTools = shortRichTextToolsPanel ) BasicRichTextEditor( modifier = Modifier diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt index 255ccbf..ccb1967 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt @@ -1,13 +1,16 @@ package com.san.englishbender.android.ui.flashcards +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -17,6 +20,12 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CardElevation import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -37,11 +46,14 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.san.englishbender.android.core.extensions.noRippleClickable import com.san.englishbender.android.core.extensions.toHex import com.san.englishbender.android.ui.common.BackgroundColorPicker import com.san.englishbender.android.ui.common.BaseDialogContent +import com.san.englishbender.android.ui.common.EBIcon import com.san.englishbender.android.ui.common.EBOutlinedButton import com.san.englishbender.android.ui.common.EBOutlinedTextField import com.san.englishbender.android.ui.common.widgets.ErrorView @@ -51,6 +63,7 @@ import com.san.englishbender.core.extensions.isNotNull import com.san.englishbender.domain.entities.BoardEntity import com.san.englishbender.ui.flashcards.BoardsUiState import com.san.englishbender.ui.flashcards.BoardsViewModel +import io.github.aakira.napier.log import org.koin.androidx.compose.getViewModel @Composable @@ -68,6 +81,7 @@ fun BoardsScreen( uiState, onBoardCreate = { board -> viewModel.saveBoard(board) }, onBoardClick = onBoardClick, + onJson = { viewModel.loadAndParseJsonFile() }, openDrawer = openDrawer ) } @@ -79,6 +93,7 @@ fun BoardsContent( uiState: BoardsUiState, onBoardCreate: (BoardEntity) -> Unit, onBoardClick: (String?) -> Unit, + onJson: () -> Unit, openDrawer: () -> Unit ) { val focusManager = LocalFocusManager.current @@ -92,23 +107,16 @@ fun BoardsContent( modifier = Modifier.fillMaxWidth(), title = {}, navigationIcon = { - Icon( - rememberVectorPainter(Icons.Filled.Menu), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier - .padding(8.dp) - .clickable { openDrawer() } + EBIcon( + imageVector = Icons.Filled.Menu, + modifier = Modifier.padding(8.dp), + onClick = { openDrawer() } ) }, actions = { - Icon( - rememberVectorPainter(Icons.Filled.FilterList), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier - .padding(8.dp) - .clickable { } + EBIcon( + imageVector = Icons.Filled.MoreVert, + modifier = Modifier.padding(8.dp) ) }, colors = TopAppBarDefaults.topAppBarColors( @@ -120,7 +128,7 @@ fun BoardsContent( FloatingActionButton( contentColor = Color.White, containerColor = MaterialTheme.colorScheme.tertiaryContainer, - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(8.dp), onClick = { boardCreationDialog = true } ) { Icon( @@ -131,7 +139,12 @@ fun BoardsContent( } } ) { paddingValues -> - LazyColumn(modifier = Modifier.padding(paddingValues)) { + LazyColumn(modifier = Modifier.padding(paddingValues).padding(16.dp)) { + item { + Button(onClick = { onJson() }) { + Text("Test") + } + } items(items = uiState.boards, key = { it.id }) { board -> BoardItem(board, onBoardClick) } @@ -210,7 +223,6 @@ fun BoardCreationDialog( } onBoardCreate(board) -// viewModel.saveBoard(board) dismiss() } ) @@ -224,16 +236,13 @@ fun BoardItem( board: BoardEntity, onBoardClick: (String?) -> Unit, ) { - Row( + Card( modifier = Modifier .fillMaxWidth() - .padding( - horizontal = 16.dp, - vertical = 8.dp - ) - .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) + .padding(vertical = 8.dp) .clickable { onBoardClick(board.id) }, - verticalAlignment = Alignment.CenterVertically + elevation = CardDefaults.cardElevation(6.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer) ) { Text( modifier = Modifier.padding(12.dp), diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt index f72bd1c..ccab96b 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt @@ -66,7 +66,10 @@ import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor import com.san.englishbender.Strings import com.san.englishbender.android.core.extensions.toColor import com.san.englishbender.android.core.extensions.toHex +import com.san.englishbender.android.ui.common.BottomNavBar +import com.san.englishbender.android.ui.common.BottomNavItem import com.san.englishbender.android.ui.common.EBOutlinedButton +import com.san.englishbender.android.ui.common.RecordDetailsNavItem import com.san.englishbender.android.ui.common.richText.RichTextStyleRow import com.san.englishbender.android.ui.recordDetails.bottomSheets.BackgroundColorPickerBSContent import com.san.englishbender.android.ui.recordDetails.bottomSheets.GrammarCheckBSContent @@ -162,7 +165,7 @@ fun RecordDetailsContent( else record.backgroundColor.toColor ) } - var bottomNavItem by remember { mutableStateOf(BottomNavItem.Translate) } + var bottomNavItem by remember { mutableStateOf(RecordDetailsNavItem.Translate) } var tagsDialog by remember { mutableStateOf(false) } val selectedTags = remember(record) { record.tags?.toMutableStateList() ?: mutableStateListOf() @@ -212,8 +215,13 @@ fun RecordDetailsContent( } }, bottomBar = { - NavigationBar( + BottomNavBar( containerColor = containerColor, + navItems = listOf( + RecordDetailsNavItem.GrammarCheck, + RecordDetailsNavItem.Translate, + RecordDetailsNavItem.Settings + ), navItemClicked = { navItem -> bottomNavItem = navItem openBottomSheet = true @@ -334,21 +342,20 @@ fun RecordDetailsContent( ) { focusManager.clearFocus() when (bottomNavItem) { - BottomNavItem.GrammarCheck -> GrammarCheckBSContent( + RecordDetailsNavItem.GrammarCheck -> GrammarCheckBSContent( viewModel, richTextState.annotatedString.text ) - - BottomNavItem.Translate -> TranslatedTextBSContent( + RecordDetailsNavItem.Translate -> TranslatedTextBSContent( text = "Some translated text" ) - - BottomNavItem.Settings -> BackgroundColorPickerBSContent( + RecordDetailsNavItem.Settings -> BackgroundColorPickerBSContent( onClick = { color -> containerColor = color record.backgroundColor = color.toHex() } ) + else -> {} } } } diff --git a/androidApp/src/main/java/com/san/englishbender/android/ui/records/RecordsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/records/RecordsScreen.kt index 860e097..5630c6e 100644 --- a/androidApp/src/main/java/com/san/englishbender/android/ui/records/RecordsScreen.kt +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/records/RecordsScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material.FloatingActionButton import androidx.compose.material.Icon import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.Menu @@ -42,6 +43,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.san.englishbender.android.core.extensions.truncateText +import com.san.englishbender.android.ui.common.EBIcon import com.san.englishbender.android.ui.common.widgets.ErrorView import com.san.englishbender.android.ui.common.widgets.LoadingView import com.san.englishbender.core.AppConstants.RECORD_MAX_LENGTH_DESCRIPTION @@ -93,31 +95,22 @@ fun RecordsContent( modifier = Modifier.fillMaxWidth(), title = {}, navigationIcon = { - Icon( - rememberVectorPainter(Icons.Filled.Menu), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier - .padding(8.dp) - .clickable { openDrawer() } + EBIcon( + imageVector = Icons.Filled.Menu, + modifier = Modifier.padding(8.dp), + onClick = { openDrawer() } ) }, actions = { - Icon( - rememberVectorPainter(Icons.Filled.FilterList), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier - .padding(8.dp) - .clickable { } + EBIcon( + imageVector = Icons.Filled.FilterList, + modifier = Modifier.padding(8.dp), + onClick = {} ) - Icon( - rememberVectorPainter(Icons.Filled.Settings), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier - .padding(8.dp) - .clickable { } + EBIcon( + imageVector = Icons.Filled.Settings, + modifier = Modifier.padding(8.dp), + onClick = {} ) }, colors = TopAppBarDefaults.topAppBarColors( @@ -129,14 +122,12 @@ fun RecordsContent( FloatingActionButton( contentColor = Color.White, backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, - shape = RoundedCornerShape(10.dp), - onClick = { - onRecordClick(null) - } + shape = RoundedCornerShape(8.dp), + onClick = { onRecordClick(null) } ) { Icon( Icons.Filled.Edit, - contentDescription = "", + contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer ) } diff --git a/build.gradle.kts b/build.gradle.kts index 265fe3c..9970dbc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ plugins { id("com.android.library").version("7.4.2").apply(false) kotlin("android").version("1.8.0").apply(false) kotlin("multiplatform").version("1.8.22").apply(false) + kotlin("plugin.serialization") version "1.9.22" } tasks.register("clean", Delete::class) { diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index e1ac815..f611a4c 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("kotlin-kapt") id("io.realm.kotlin") version "1.11.0" id("dev.icerock.mobile.multiplatform-resources") + kotlin("plugin.serialization") } kotlin { @@ -29,7 +30,8 @@ kotlin { } sourceSets { - val coroutineVersion = "1.7.3" +// val coroutineVersion = "1.7.3" + val coroutineVersion = "1.8.0" val retrofitCoroutineAdapterVersion = "0.9.2" val retrofitVersion = "2.9.0" val okHttpVersion = "4.11.0" @@ -39,6 +41,10 @@ kotlin { val koinAndroidVersion = "3.4.2" val koinComposeVersion = "3.4.5" + getByName("androidMain") { + kotlin.srcDir("build/generated/moko/androidMain/src") + } + val commonMain by getting { dependencies { implementation("io.insert-koin:koin-core:$koinCoreVersion") @@ -90,7 +96,7 @@ kotlin { // Napier api("io.github.aakira:napier:2.6.1") -// implementation("org.gradle:gradle-tooling-api:7.4.2") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") } } val commonTest by getting { @@ -209,6 +215,13 @@ android { } } } + + sourceSets["main"].resources.setSrcDirs( + listOf( + "src/androidMain/resources", + "src/commonMain/resources" + ) + ) } // Don't cache SNAPSHOT (changing) dependencies. diff --git a/shared/src/androidMain/kotlin/com/san/englishbender/Platform.kt b/shared/src/androidMain/kotlin/com/san/englishbender/Platform.kt index 483483c..dd658ac 100644 --- a/shared/src/androidMain/kotlin/com/san/englishbender/Platform.kt +++ b/shared/src/androidMain/kotlin/com/san/englishbender/Platform.kt @@ -16,6 +16,7 @@ import kotlinx.parcelize.Parceler import kotlinx.parcelize.Parcelize import kotlinx.parcelize.TypeParceler import org.koin.dsl.module +import java.io.InputStreamReader import java.util.UUID actual typealias CommonParcelize = Parcelize @@ -67,4 +68,14 @@ actual class Strings( id.format(*args.toTypedArray()).toString(context) } } +} + +internal actual class SharedFileReader{ + actual fun loadJsonFile(fileName: String): String? { + return javaClass.classLoader?.getResourceAsStream(fileName).use { stream -> + InputStreamReader(stream).use { reader -> + reader.readText() + } + } + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/Platform.kt b/shared/src/commonMain/kotlin/com/san/englishbender/Platform.kt index 28a1156..7741db3 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/Platform.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/Platform.kt @@ -54,4 +54,8 @@ expect class Platform() { expect class Strings { fun get(id: StringResource, args: List = emptyList()): String +} + +internal expect class SharedFileReader() { + fun loadJsonFile(fileName: String): String? } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt index f7c6b8e..0b78e2b 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/UseCaseModule.kt @@ -6,6 +6,7 @@ import com.san.englishbender.domain.usecases.flashCards.DeleteFlashCardUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardAsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardByIdUseCase +import com.san.englishbender.domain.usecases.flashCards.GetFlashCardsAsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase import com.san.englishbender.domain.usecases.flashCards.SaveFlashCardUseCase import com.san.englishbender.domain.usecases.records.GetRecordFlowUseCase @@ -47,6 +48,7 @@ val useCaseModule = module { single { GetBoardsFlowUseCase(get()) } single { GetBoardByIdUseCase(get()) } single { GetBoardAsFlowUseCase(get()) } + single { GetFlashCardsAsFlowUseCase(get()) } single { SaveBoardUseCase(get()) } single { AddFlashCardToBoardUseCase(get()) } single { SaveFlashCardUseCase(get()) } diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt index f3d3e03..79c1e5e 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/ViewModelModule.kt @@ -14,5 +14,5 @@ val viewModelModule = module { single { StatsViewModel(get(), get()) } single { TagsViewModel(get(), get(), get(), get(), get()) } single { BoardsViewModel(get(), get(), get()) } - single { FlashCardsViewModel(get(), get(), get(), get()) } + single { FlashCardsViewModel(get(), get(), get(), get(), get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt index 97b65bc..60d738e 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt @@ -12,6 +12,7 @@ class Board : RealmObject { var id: String = "" var name: String = "" var backgroundColor: String = "" + var isDisabled: Boolean = false var flashCards: RealmList = realmListOf() constructor( diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt index b4284cf..5d4d46d 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt @@ -1,6 +1,7 @@ package com.san.englishbender.data.local.models import com.san.englishbender.domain.entities.FlashCardEntity +import io.realm.kotlin.query.RealmResults import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.PrimaryKey @@ -9,11 +10,13 @@ open class FlashCard : RealmObject { var id: String = "" var frontText: String = "" var backText: String = "" + var isArchived: Boolean = false - constructor(id: String, frontText: String, backText: String) { + constructor(id: String, frontText: String, backText: String, isArchived: Boolean = false) { this.id = id this.frontText = frontText this.backText = backText + this.isArchived = isArchived } constructor() {} @@ -23,14 +26,16 @@ fun FlashCard.toEntity() = FlashCardEntity( id = id, frontText = frontText, - backText = backText + backText = backText, + isArchived = isArchived ) fun FlashCardEntity.toLocal() = FlashCard( id = id, frontText = frontText, - backText = backText + backText = backText, + isArchived = isArchived ) fun List.toEntity() = this.map { it.toEntity() } diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt index 87aca63..a2ad6dc 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt @@ -12,12 +12,18 @@ import com.san.englishbender.ioDispatcher import io.realm.kotlin.Realm import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.ext.query +import io.realm.kotlin.notifications.DeletedList +import io.realm.kotlin.notifications.InitialList import io.realm.kotlin.notifications.InitialObject import io.realm.kotlin.notifications.InitialResults +import io.realm.kotlin.notifications.ListChange import io.realm.kotlin.notifications.SingleQueryChange +import io.realm.kotlin.notifications.UpdatedList import io.realm.kotlin.notifications.UpdatedObject import io.realm.kotlin.notifications.UpdatedResults +import io.realm.kotlin.query.find import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -56,9 +62,27 @@ class BoardsRepository( } }.flowOn(ioDispatcher) + override fun getFlashCardsAsFlow(id: String): Flow> = flow { + realm.query("id == $0", id) + .first() + .find() + ?.also { board -> + board.flashCards + .asFlow() + .collect { listChange: ListChange -> + when (listChange) { + is InitialList, + is UpdatedList, + is DeletedList -> emit( + listChange.list.filter { !it.isArchived }.toEntity() + ) + } + } + } + }.flowOn(ioDispatcher) + override suspend fun saveBoard(board: BoardEntity): Unit = doQuery { - val local = board.toLocal() - realm.write { copyToRealm(local, UpdatePolicy.ALL) } + realm.write { copyToRealm(board.toLocal(), UpdatePolicy.ALL) } } override suspend fun addFlashCardToBoard(boardId: String, flashCard: FlashCardEntity): Unit = @@ -73,6 +97,14 @@ class BoardsRepository( realm.write { copyToRealm(card.toLocal(), UpdatePolicy.ALL) } } +// override suspend fun sendCardToArchive(boardId: String, cardId: String): Unit = +// doQuery { +// realm.write { +// val board = this.query("id == $0", boardId).first().find() +// board?.flashCards?.find { it.id == cardId }?.isArchived = true +// } +// } + override suspend fun deleteBoard(boardId: String): Unit = doQuery { realm.write { val board = query("id == $0", boardId).find() diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt index d2376d8..da38ebd 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt @@ -3,10 +3,13 @@ package com.san.englishbender.domain.entities import com.san.englishbender.CommonParcelable import com.san.englishbender.CommonParcelize import com.san.englishbender.randomUUID +import kotlinx.serialization.Serializable +@Serializable @CommonParcelize data class FlashCardEntity( var id: String = randomUUID(), var frontText: String = "", var backText: String = "", + var isArchived: Boolean = false ) : CommonParcelable \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt index c239b35..5a9072c 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt @@ -9,6 +9,8 @@ interface IBoardsRepository { suspend fun getBoards() : List suspend fun getBoard(id: String) : BoardEntity? fun getBoardAsFlow(id: String) : Flow + fun getFlashCardsAsFlow(id: String): Flow> +// suspend fun sendCardToArchive(boardId: String, cardId: String) suspend fun saveBoard(board: BoardEntity) suspend fun addFlashCardToBoard(boardId: String, flashCard: FlashCardEntity) suspend fun saveFlashCard(card: FlashCardEntity) diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsAsFlowUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsAsFlowUseCase.kt new file mode 100644 index 0000000..a2f0bc1 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetFlashCardsAsFlowUseCase.kt @@ -0,0 +1,12 @@ +package com.san.englishbender.domain.usecases.flashCards + +import com.san.englishbender.domain.entities.FlashCardEntity +import com.san.englishbender.domain.repositories.IBoardsRepository +import kotlinx.coroutines.flow.Flow + +class GetFlashCardsAsFlowUseCase( + private val boardRepository: IBoardsRepository +) { + operator fun invoke(boardId: String): Flow> = + boardRepository.getFlashCardsAsFlow(boardId) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt index 0deed2f..60e0cd0 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt @@ -1,23 +1,29 @@ package com.san.englishbender.ui.flashcards import androidx.compose.runtime.Immutable +import com.san.englishbender.SharedFileReader import com.san.englishbender.SharedRes import com.san.englishbender.core.extensions.WhileUiSubscribed import com.san.englishbender.data.getResultFlow import com.san.englishbender.data.onFailure import com.san.englishbender.data.onSuccess import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.entities.FlashCardEntity import com.san.englishbender.domain.usecases.flashCards.DeleteBoardUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.SaveBoardUseCase import com.san.englishbender.ui.ViewModel import dev.icerock.moko.resources.StringResource +import io.github.aakira.napier.log import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.serialization.* +import kotlinx.serialization.json.* + @Immutable data class BoardsUiState( val isLoading: Boolean = false, @@ -47,6 +53,50 @@ class BoardsViewModel( .onSuccess {} } + @Serializable + data class CardsContainer( + val cards: List = emptyList() + ) + + fun json() = safeLaunch { + val listOfCards = CardsContainer( + cards = listOf( + FlashCardEntity( + frontText = "Hustle", + backText = "Full of activity" + ), + FlashCardEntity( + frontText = "serene", + backText = "calm, peaceful, and untroubled; tranquil" + ) + ) + ) + log(tag = "decodeFromString") { "encodeToString" } + + val cardJson = "{\"frontText\":\"Hustle\",\"backText\":\"Full of activity\" }" + + try { +// val jsonString = Json.encodeToString(listOfCards) +// log(tag = "decodeFromString") { "jsonString: $jsonString" } + + val card = Json.decodeFromString(cardJson) +// val cards = Json.decodeFromString(jsonString) + log(tag = "decodeFromString") { "card: $card" } +// log(tag = "decodeFromString") { "cards: $cards" } + } catch (e: Exception) { + log(tag = "decodeFromString") { "e: $e" } + } + + } + + private val sharedFileReader: SharedFileReader = SharedFileReader() + fun loadAndParseJsonFile() { + val jsonString = sharedFileReader.loadJsonFile("commonWords.json") +// val commonWords = sharedFileReader.loadJsonFile("commonWords.json") + + log(tag = "decodeFromString") { "jsonString: $jsonString" } + } + fun deleteBoard(boardId: String) = safeLaunch { getResultFlow { deleteBoardUseCase(boardId) } .onFailure { showError(SharedRes.strings.remove_record_error) } diff --git a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt index a215d9d..013aa3d 100644 --- a/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt @@ -2,7 +2,6 @@ package com.san.englishbender.ui.flashcards import androidx.compose.runtime.Immutable import com.san.englishbender.SharedRes -import com.san.englishbender.core.extensions.isNull import com.san.englishbender.data.getResultFlow import com.san.englishbender.data.onFailure import com.san.englishbender.data.onSuccess @@ -11,24 +10,28 @@ import com.san.englishbender.domain.entities.FlashCardEntity import com.san.englishbender.domain.usecases.flashCards.AddFlashCardToBoardUseCase import com.san.englishbender.domain.usecases.flashCards.DeleteFlashCardUseCase import com.san.englishbender.domain.usecases.flashCards.GetBoardAsFlowUseCase +import com.san.englishbender.domain.usecases.flashCards.GetFlashCardsAsFlowUseCase import com.san.englishbender.domain.usecases.flashCards.SaveFlashCardUseCase import com.san.englishbender.ui.ViewModel import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.update @Immutable data class FlashCardsUiState( val isLoading: Boolean = false, val board: BoardEntity? = null, + val flashCards: List = emptyList(), val userMessage: StringResource? = null ) class FlashCardsViewModel( private val getBoardAsFlowUseCase: GetBoardAsFlowUseCase, + private val getFlashCardsAsFlowUseCase: GetFlashCardsAsFlowUseCase, private val addFlashCardToBoardUseCase: AddFlashCardToBoardUseCase, private val saveFlashCardUseCase: SaveFlashCardUseCase, private val deleteFlashCardUseCase: DeleteFlashCardUseCase @@ -37,30 +40,62 @@ class FlashCardsViewModel( private val _uiState = MutableStateFlow(FlashCardsUiState()) val uiState: StateFlow = _uiState.asStateFlow() - fun getBoard(boardId: String) = safeLaunch { - getBoardAsFlowUseCase(boardId) - .catch { showError(SharedRes.strings.remove_record_error) } - .collect { boardEntity -> - if (boardEntity.isNull) { - showError(SharedRes.strings.remove_record_error) - return@collect - } - _uiState.update { state -> - state.copy( - isLoading = false, - board = boardEntity - ) - } + fun observeBoard(boardId: String) = safeLaunch { + combine( + getBoardAsFlowUseCase(boardId), + getFlashCardsAsFlowUseCase(boardId) + ) { boardEntity, flashCards -> + _uiState.update { state -> + state.copy( + isLoading = false, + board = boardEntity, + flashCards = flashCards + ) } + }.launchIn(viewModelScope) } +// fun getBoard(boardId: String) = safeLaunch { +// getBoardAsFlowUseCase(boardId) +// .catch { showError(SharedRes.strings.remove_record_error) } +// .collect { boardEntity -> +// if (boardEntity.isNull) { +// showError(SharedRes.strings.remove_record_error) +// return@collect +// } +// _uiState.update { state -> +// state.copy( +// isLoading = false, +// board = boardEntity +// ) +// } +// } +// } +// +// fun getFlashCards(boardId: String) = safeLaunch { +// getFlashCardsAsFlowUseCase(boardId) +// .catch { showError(SharedRes.strings.remove_record_error) } +// .collect { flashCards -> +// if (flashCards.isNull) { +// showError(SharedRes.strings.remove_record_error) +// return@collect +// } +// _uiState.update { state -> +// state.copy( +// isLoading = false, +// flashCards = flashCards +// ) +// } +// } +// } + fun addCardToBoard(board: BoardEntity, flashCard: FlashCardEntity) = safeLaunch { getResultFlow { addFlashCardToBoardUseCase(board.id, flashCard) } .onFailure { showError(SharedRes.strings.remove_record_error) } .onSuccess {} } - fun updateCard(card: FlashCardEntity) = safeLaunch { + fun saveCard(card: FlashCardEntity) = safeLaunch { getResultFlow { saveFlashCardUseCase(card) } .onFailure {} .onSuccess {} diff --git a/shared/src/commonMain/resources/commonWords.json b/shared/src/commonMain/resources/commonWords.json new file mode 100644 index 0000000..3afd66f --- /dev/null +++ b/shared/src/commonMain/resources/commonWords.json @@ -0,0 +1,16 @@ +{ + "cards": [ + { + "frontText":"Word1", + "backText":"Full of activity" + }, + { + "frontText":"Word2", + "backText":"Full of activity" + }, + { + "frontText":"Word3", + "backText":"Full of activity" + } + ] +} \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/com/san/englishbender/Platform.kt b/shared/src/iosMain/kotlin/com/san/englishbender/Platform.kt index dde8222..370507f 100644 --- a/shared/src/iosMain/kotlin/com/san/englishbender/Platform.kt +++ b/shared/src/iosMain/kotlin/com/san/englishbender/Platform.kt @@ -65,4 +65,35 @@ actual class Strings { id.format(*args.toTypedArray()).localized() } } +} + +internal actual class SharedFileReader{ + private val bundle: NSBundle = NSBundle.bundleForClass(BundleMarker) + + actual fun loadJsonFile(fileName: String): String? { + val (filename, type) = when (val lastPeriodIndex = fileName.lastIndexOf('.')) { + 0 -> { + null to fileName.drop(1) + } + in 1..Int.MAX_VALUE -> { + fileName.take(lastPeriodIndex) to fileName.drop(lastPeriodIndex + 1) + } + else -> { + fileName to null + } + } + val path = bundle.pathForResource(filename, type) ?: error("Couldn't get path of $fileName (parsed as: ${listOfNotNull(filename, type).joinToString(".")})") + + return memScoped { + val errorPtr = alloc>() + + NSString.stringWithContentsOfFile(path, encoding = NSUTF8StringEncoding, error = errorPtr.ptr) ?: run { + error("Couldn't load resource: $fileName. Error: ${errorPtr.value?.localizedDescription}") + } + } + } + + private class BundleMarker : NSObject() { + companion object : NSObjectMeta() + } } \ No newline at end of file