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 4db9b6f..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 { @@ -50,8 +51,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 +97,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") @@ -111,6 +112,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/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() } } + ) + } + ) + } + + 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 9b06dd7..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,8 @@ 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 import com.san.englishbender.core.navigation.Screens.RECORDS_SCREEN @@ -14,6 +16,14 @@ class EBNavigationActions(private val navController: NavHostController) { navController.navigate(STATS_ROUTE) } + fun navigateToBoards() { + navController.navigate(BOARDS_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..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 @@ -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 @@ -34,16 +35,21 @@ data class DrawerNavOptions( ) private val drawerNavOptions = listOf( + DrawerNavOptions( + name = "Records", + route = Destinations.RECORD_ROUTE, + icon = Icons.AutoMirrored.Filled.ViewList + ), + DrawerNavOptions( + name = "Flash-cards", + route = Destinations.BOARDS_ROUTE, + icon = Icons.Default.ViewCarousel + ), DrawerNavOptions( name = "Stats", route = Destinations.STATS_ROUTE, icon = Icons.Default.Analytics ), - DrawerNavOptions( - name = "Records", - route = Destinations.RECORD_ROUTE, - icon = Icons.Default.ViewList - ), ) @Composable @@ -79,6 +85,8 @@ 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/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/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 8e83bc7..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,10 +1,9 @@ -package com.san.englishbender.android.ui.recordDetails +package com.san.englishbender.android.ui.common -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.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 @@ -12,37 +11,36 @@ 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) - 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, @@ -56,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 706c718..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 @@ -15,13 +15,16 @@ 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 import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,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 @@ -40,6 +45,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, @@ -141,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/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/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 new file mode 100644 index 0000000..78b47a6 --- /dev/null +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardScreen.kt @@ -0,0 +1,509 @@ +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.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.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.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.material.icons.outlined.Archive +import androidx.compose.material3.ExperimentalMaterial3Api +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.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.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.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.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 +import com.san.englishbender.core.extensions.ifNotEmpty +import com.san.englishbender.core.extensions.isNotNull +import com.san.englishbender.domain.entities.BoardEntity +import com.san.englishbender.domain.entities.FlashCardEntity +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 org.koin.androidx.compose.getViewModel + + +@Composable +fun BoardScreen( + boardId: String?, + onBackClick: () -> Unit +) { + val viewModel: FlashCardsViewModel = getViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(boardId) { + boardId?.let { + viewModel.observeBoard(it) +// viewModel.getBoard(it) +// viewModel.getFlashCards(it) + } + } + + when { + uiState.isLoading -> LoadingView() + uiState.userMessage.isNotNull -> ErrorView(userMessage = uiState.userMessage) + else -> BoardContent( + uiState, + onCardCreate = { board, flashCard -> viewModel.addCardToBoard(board, flashCard) }, + onCardUpdate = { flashCard -> viewModel.saveCard(flashCard) }, + onCardDelete = { flashCardId -> viewModel.deleteFlashCard(flashCardId) }, + onBackClick + ) + } +} + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalFoundationApi::class, + ExperimentalRichTextApi::class +) +@Composable +fun BoardContent( + uiState: FlashCardsUiState, + onCardCreate: (BoardEntity, FlashCardEntity) -> Unit, + onCardUpdate: (FlashCardEntity) -> Unit, + onCardDelete: (String) -> Unit, + onBackClick: () -> Unit = {}, +) { + val controller = rememberFlipController() + val focusManager = LocalFocusManager.current + +// 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 + + 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( + title = {}, + modifier = Modifier.fillMaxWidth(), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = containerColor + ), + navigationIcon = { + EBIcon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + modifier = Modifier.padding(start = 8.dp), + onClick = { onBackClick() } + ) + }, + actions = { + EBIcon( + imageVector = Icons.Filled.Add, + modifier = Modifier.padding(8.dp), + onClick = { addCardDialog = true } + ) + EBIcon( + imageVector = Icons.Filled.Edit, + modifier = Modifier.padding(8.dp), + onClick = { editCardDialog = true } + ) + EBIcon( + imageVector = Icons.Filled.Delete, + modifier = Modifier.padding(8.dp), + onClick = { cardDeletionDialog = true } + ) + } + ) + } + }, + 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 (uiState.flashCards.isEmpty()) { + Row( + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text("Board is empty") + } + return@Scaffold + } + + 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 = uiState.flashCards.getOrNull(pageIndex) ?: return@HorizontalPager + + LaunchedEffect(card.backText) { + card.backText.ifNotEmpty { richTextState.setHtml(it) } + } + + Flippable( + modifier = Modifier + .fillMaxWidth() + .height(600.dp), + frontSide = { + Box( + modifier = Modifier + .fillMaxSize() + .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) + .background(Color.White, RoundedCornerShape(6.dp)), + contentAlignment = Alignment.Center + ) { + Text( + modifier = Modifier.noRippleClickable { controller.flipToBack() }, + text = card.frontText, + color = Color.Black, + fontSize = 20.sp + ) + } + }, + backSide = { + Box( + modifier = Modifier + .fillMaxSize() + .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()) + .noRippleClickable { controller.flip() }, + state = richTextState, + textStyle = TextStyle(fontSize = 20.sp), + readOnly = true + ) + } + }, + flipController = controller, + 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) +// } +// } +// ) + } + } + + when { + addCardDialog -> AddEditCardDialog( + onSave = { flashCard -> + focusManager.clearFocus() + uiState.board?.let { onCardCreate(it, flashCard) } + }, + dismiss = { addCardDialog = false } + ) + // --- + editCardDialog -> AddEditCardDialog( + flashCard = uiState.flashCards.getOrNull(pagerState.currentPage) ?: return, + onSave = { flashCard -> + focusManager.clearFocus() + onCardUpdate(flashCard) + }, + dismiss = { editCardDialog = false } + ) + // --- + cardDeletionDialog -> CardDeletionDialog( + flashCard = uiState.flashCards.getOrNull(pagerState.currentPage) ?: return, + confirm = { cardId -> onCardDelete(cardId) }, + dismiss = { cardDeletionDialog = false } + ) + } +} + +@Preview +@Composable +fun DialogPreview() { + AddEditCardDialog( + onSave = {}, + dismiss = {}, + ) +} + +@OptIn(ExperimentalRichTextApi::class) +@Composable +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 + ) { + Column( + 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(), + value = word, + placeholder = "Word", + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + onValueChange = { + word = it + card.frontText = it + } + ) + + Spacer(Modifier.height(16.dp)) + + RichTextToolsRow( + state = richTextState, + richTextTools = shortRichTextToolsPanel + ) + BasicRichTextEditor( + modifier = Modifier + .fillMaxWidth() + .border(1.dp, Color.Gray, RoundedCornerShape(6.dp)) + .verticalScroll(state = rememberScrollState()), + state = richTextState, + minLines = 12, + maxLines = 12, + decorationBox = { innerTextField -> + Box(Modifier.padding(12.dp)) { + if (richTextState.annotatedString.text.isEmpty()) { + Text( + text = "Description", + color = Color.LightGray + ) + } + innerTextField() + } + } + ) + } + } +} + +@Composable +fun CardDeletionDialog( + flashCard: FlashCardEntity, + confirm: (String) -> Unit, + dismiss: () -> Unit +) { + BaseDialogContent( + width = 350.dp, + height = 200.dp, + dismiss = dismiss + ) { + Column(Modifier.padding(16.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 = 18.sp + ) + + Spacer(Modifier.height(24.dp)) + + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(Modifier.weight(1f)) + EBTextButton( + text = "Cancel", + onClick = dismiss, + fontSize = 18.sp + ) + EBTextButton( + modifier = Modifier.padding(start = 32.dp), + text = "Delete", + textColor = RedDark, + fontSize = 18.sp, + 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 new file mode 100644 index 0000000..ccb1967 --- /dev/null +++ b/androidApp/src/main/java/com/san/englishbender/android/ui/flashcards/BoardsScreen.kt @@ -0,0 +1,288 @@ +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 +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +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 +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.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.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 +import com.san.englishbender.android.ui.common.widgets.LoadingView +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.BoardsUiState +import com.san.englishbender.ui.flashcards.BoardsViewModel +import io.github.aakira.napier.log +import org.koin.androidx.compose.getViewModel + +@Composable +fun BoardsScreen( + onBoardClick: (String?) -> Unit, + openDrawer: () -> Unit +) { + val viewModel: BoardsViewModel = getViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + when { + uiState.isLoading -> LoadingView() + uiState.userMessage.isNotNull -> ErrorView(userMessage = uiState.userMessage) + else -> BoardsContent( + uiState, + onBoardCreate = { board -> viewModel.saveBoard(board) }, + onBoardClick = onBoardClick, + onJson = { viewModel.loadAndParseJsonFile() }, + openDrawer = openDrawer + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BoardsContent( + uiState: BoardsUiState, + onBoardCreate: (BoardEntity) -> Unit, + onBoardClick: (String?) -> Unit, + onJson: () -> Unit, + openDrawer: () -> Unit +) { + val focusManager = LocalFocusManager.current + var boardCreationDialog by remember { mutableStateOf(false) } + + Scaffold( + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.surfaceVariant, + topBar = { + TopAppBar( + modifier = Modifier.fillMaxWidth(), + title = {}, + navigationIcon = { + EBIcon( + imageVector = Icons.Filled.Menu, + modifier = Modifier.padding(8.dp), + onClick = { openDrawer() } + ) + }, + actions = { + EBIcon( + imageVector = Icons.Filled.MoreVert, + modifier = Modifier.padding(8.dp) + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) + }, + floatingActionButton = { + FloatingActionButton( + contentColor = Color.White, + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + shape = RoundedCornerShape(8.dp), + onClick = { boardCreationDialog = true } + ) { + Icon( + Icons.Filled.Add, + contentDescription = "", + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + ) { 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) + } + } + } + if (boardCreationDialog) { + BoardCreationDialog( + onBoardCreate = onBoardCreate, + dismiss = { + focusManager.clearFocus() + boardCreationDialog = false + } + ) + } +} + +@Composable +fun BoardCreationDialog( + onBoardCreate: (BoardEntity) -> Unit, + dismiss: () -> Unit +) { + BaseDialogContent( + height = 250.dp, + dismiss = dismiss + ) { + val board by remember { mutableStateOf(BoardEntity()) } + var boardName by remember { mutableStateOf("") } + + Column( + Modifier + .fillMaxWidth() + .padding(vertical = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + EBOutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = boardName, + placeholder = "Board name", + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + onValueChange = { + boardName = it + 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 + board.backgroundColor.ifEmpty { + board.backgroundColor = backgroundColors.first().toHex() + } + + onBoardCreate(board) + dismiss() + } + ) + } + } + } +} + +@Composable +fun BoardItem( + board: BoardEntity, + onBoardClick: (String?) -> Unit, +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .clickable { onBoardClick(board.id) }, + elevation = CardDefaults.cardElevation(6.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer) + ) { + Text( + 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 { +// 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/RecordDetailsScreen.kt b/androidApp/src/main/java/com/san/englishbender/android/ui/recordDetails/RecordDetailsScreen.kt index 5938b34..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 @@ -24,8 +24,8 @@ 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 import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField @@ -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 @@ -76,7 +79,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 @@ -93,7 +95,7 @@ fun RecordDetailsScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(recordId) { - if (recordId.isNotNull) viewModel.getRecord(recordId) + recordId?.let { viewModel.getRecord(it) } } RecordDetailsContent( @@ -117,6 +119,7 @@ fun RecordDetailsContent( viewModel: RecordDetailsViewModel, onBackClick: () -> Unit ) { + val context = LocalContext.current val focusManager = LocalFocusManager.current // val coroutineScope = rememberCoroutineScope() @@ -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 @@ -269,7 +277,7 @@ fun RecordDetailsContent( ), ) - Divider( + HorizontalDivider( modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp), color = Color.LightGray ) @@ -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/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/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/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/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 e360f3c..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,16 +30,21 @@ kotlin { } sourceSets { - val coroutineVersion = "1.7.2" +// 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" 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" + getByName("androidMain") { + kotlin.srcDir("build/generated/moko/androidMain/src") + } + val commonMain by getting { dependencies { implementation("io.insert-koin:koin-core:$koinCoreVersion") @@ -74,10 +80,10 @@ 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("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("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") // Add to use coroutines with the SDK + 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 @@ -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/DatabaseModule.kt b/shared/src/commonMain/kotlin/com/san/englishbender/core/di/DatabaseModule.kt index e482fd5..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 @@ -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 { @@ -34,6 +42,8 @@ val databaseModule = module { } single { RecordsRepository(get()) } - single { TagsRepository(get(), get()) } + single { TagsRepository(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..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 @@ -1,5 +1,14 @@ 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.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 import com.san.englishbender.domain.usecases.records.GetRecordsCountUseCase import com.san.englishbender.domain.usecases.records.GetRecordsUseCase @@ -34,4 +43,15 @@ val useCaseModule = module { single { SaveTagUseCase(get()) } single { SaveTagColorUseCase(get()) } single { DeleteTagUseCase(get()) } + + // --- FlashCards + single { GetBoardsFlowUseCase(get()) } + single { GetBoardByIdUseCase(get()) } + single { GetBoardAsFlowUseCase(get()) } + single { GetFlashCardsAsFlowUseCase(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 e9625e5..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 @@ -1,6 +1,8 @@ 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 import com.san.englishbender.ui.stats.StatsViewModel @@ -11,4 +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 { FlashCardsViewModel(get(), get(), get(), 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 23e6241..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,6 +1,8 @@ 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 import com.san.englishbender.core.navigation.Screens.RECORD_DETAIL_SCREEN import com.san.englishbender.core.navigation.Screens.STATS_SCREEN @@ -11,6 +13,8 @@ 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" // --- const val TAG_LIST_SCREEN = "tag_list" @@ -20,6 +24,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 +32,8 @@ 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 const val TAG_CREATE_ROUTE = "$TAG_ADD_EDIT_SCREEN?tagId={tagId}" 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..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,29 +54,36 @@ 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 { - log(tag = "ExceptionHandling") { "getResultFlow s" } emit(Result.Success(action.invoke())) } catch (e: Exception) { - log(tag = "ExceptionHandling") { "getResultFlow f" } emit(Result.Failure(e)) } } -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 +93,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/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..60d738e --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/Board.kt @@ -0,0 +1,50 @@ +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 backgroundColor: String = "" + var isDisabled: Boolean = false + var flashCards: RealmList = realmListOf() + + constructor( + id: String, + name: String, + backgroundColor: String = "", + flashCards: RealmList + ) { + this.id = id + this.name = name + this.backgroundColor = backgroundColor + this.flashCards = flashCards + } + + constructor() {} +} + +fun Board.toEntity() = + BoardEntity( + id = id, + name = name, + backgroundColor = backgroundColor, + flashCards = flashCards.toEntity() + ) + +fun BoardEntity.toLocal() = + Board( + id = id, + name = name, + 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..5d4d46d --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/local/models/FlashCard.kt @@ -0,0 +1,42 @@ +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 + +open class FlashCard : RealmObject { + @PrimaryKey + var id: String = "" + var frontText: String = "" + var backText: String = "" + var isArchived: Boolean = false + + constructor(id: String, frontText: String, backText: String, isArchived: Boolean = false) { + this.id = id + this.frontText = frontText + this.backText = backText + this.isArchived = isArchived + } + + constructor() {} +} + +fun FlashCard.toEntity() = + FlashCardEntity( + id = id, + frontText = frontText, + backText = backText, + isArchived = isArchived + ) + +fun FlashCardEntity.toLocal() = + FlashCard( + id = id, + frontText = frontText, + backText = backText, + isArchived = isArchived + ) + +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..a2ad6dc --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/BoardsRepository.kt @@ -0,0 +1,121 @@ +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.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 + +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 getBoard(id: String): BoardEntity? = doQuery { + realm.query("id == $0", id).first().find()?.toEntity() + } + + override fun getBoardAsFlow(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 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 { + realm.write { copyToRealm(board.toLocal(), 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 { + 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() + 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/data/repositories/FlashCardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/data/repositories/FlashCardsRepository.kt new file mode 100644 index 0000000..fe28feb --- /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(boardId: String): Flow> = flow { + realm.query("").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..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 @@ -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 @@ -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 new file mode 100644 index 0000000..15d2303 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/BoardEntity.kt @@ -0,0 +1,13 @@ +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( + val id: String = randomUUID(), + var name: String = "", + var backgroundColor: String = "", + 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 new file mode 100644 index 0000000..da38ebd --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/entities/FlashCardEntity.kt @@ -0,0 +1,15 @@ +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/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 new file mode 100644 index 0000000..5a9072c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IBoardsRepository.kt @@ -0,0 +1,19 @@ +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 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) + 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/repositories/IFlashCardsRepository.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/repositories/IFlashCardsRepository.kt new file mode 100644 index 0000000..808b529 --- /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(boardId: String) : 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/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/GetBoardByIdUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt new file mode 100644 index 0000000..18bb8e7 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/GetBoardByIdUseCase.kt @@ -0,0 +1,15 @@ +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/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/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/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/domain/usecases/flashCards/SaveFlashCardUseCase.kt b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt new file mode 100644 index 0000000..3bf4147 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/domain/usecases/flashCards/SaveFlashCardUseCase.kt @@ -0,0 +1,8 @@ +package com.san.englishbender.domain.usecases.flashCards + +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 new file mode 100644 index 0000000..60e0cd0 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/BoardsViewModel.kt @@ -0,0 +1,114 @@ +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, + val boards: List = emptyList(), + val userMessage: StringResource? = null +) + +class BoardsViewModel( + private val getBoardsFlowUseCase: GetBoardsFlowUseCase, + private val saveBoardUseCase: SaveBoardUseCase, + private val deleteBoardUseCase: DeleteBoardUseCase +) : ViewModel() { + + val uiState: StateFlow = + getBoardsFlowUseCase() + .map { BoardsUiState(boards = it) } + .catch { BoardsUiState(userMessage = SharedRes.strings.loading_records_error) } + .stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = BoardsUiState(isLoading = true) + ) + + fun saveBoard(board: BoardEntity) = safeLaunch { + getResultFlow { saveBoardUseCase(board) } + .onFailure { showError(SharedRes.strings.remove_record_error) } + .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) } + .onSuccess {} + } + + private fun showError(message: StringResource) = safeLaunch { +// 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 new file mode 100644 index 0000000..013aa3d --- /dev/null +++ b/shared/src/commonMain/kotlin/com/san/englishbender/ui/flashcards/FlashCardsViewModel.kt @@ -0,0 +1,118 @@ +package com.san.englishbender.ui.flashcards + +import androidx.compose.runtime.Immutable +import com.san.englishbender.SharedRes +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.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.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 +) : ViewModel() { + + private val _uiState = MutableStateFlow(FlashCardsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + 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 saveCard(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 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..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 @@ -12,8 +13,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 +63,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 +95,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" } } } 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