diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df1197f6..840608d9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -99,6 +99,8 @@ dependencies { implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.material3.adaptive.navigation.suite.android) + implementation(libs.review) + implementation(libs.review.ktx) debugImplementation(libs.androidx.compose.ui.testManifest) diff --git a/app/src/main/java/com/memorati/MainActivity.kt b/app/src/main/java/com/memorati/MainActivity.kt index 977589c2..89f84770 100644 --- a/app/src/main/java/com/memorati/MainActivity.kt +++ b/app/src/main/java/com/memorati/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import com.google.android.play.core.review.ReviewManager import com.memorati.core.ui.theme.MemoratiTheme import com.memorati.feature.assistant.navigation.assistantScreen import com.memorati.feature.cards.navigation.cardsScreen @@ -22,12 +23,23 @@ import com.memorati.feature.settings.navigation.settingsScreen import com.memorati.navigation.TopDestination import com.memorati.ui.navigationSuiteItems import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { + @Inject + lateinit var reviewManager: ReviewManager + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + val request = reviewManager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + reviewManager.launchReviewFlow(this@MainActivity, task.result) + } + } setContent { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() diff --git a/app/src/main/java/com/memorati/di/AppModule.kt b/app/src/main/java/com/memorati/di/AppModule.kt index 438b8272..a5e92117 100644 --- a/app/src/main/java/com/memorati/di/AppModule.kt +++ b/app/src/main/java/com/memorati/di/AppModule.kt @@ -1,10 +1,14 @@ package com.memorati.di +import android.content.Context +import com.google.android.play.core.review.ReviewManager +import com.google.android.play.core.review.ReviewManagerFactory import com.memorati.BuildConfig import com.memorati.core.common.di.AppId import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent @Module @@ -13,4 +17,9 @@ class AppModule { @Provides @AppId fun appId(): String = BuildConfig.APPLICATION_ID + + @Provides + fun reviewManager( + @ApplicationContext context: Context, + ): ReviewManager = ReviewManagerFactory.create(context) } diff --git a/core/algorithm/src/main/java/com/memorati/algorithm/Algorithm.kt b/core/algorithm/src/main/java/com/memorati/algorithm/Algorithm.kt index a03d2877..017bb707 100644 --- a/core/algorithm/src/main/java/com/memorati/algorithm/Algorithm.kt +++ b/core/algorithm/src/main/java/com/memorati/algorithm/Algorithm.kt @@ -56,6 +56,7 @@ internal fun Flashcard.handleReviewResponse( consecutiveCorrectCount = consecutiveCorrectCount, // Apply decay to memory strength over time memoryStrength = additionalInfo.memoryStrength * 0.95, + totalReviews = additionalInfo.totalReviews + 1, ), lastReviewAt = now, ) diff --git a/core/data/src/main/java/com/memorati/core/data/mapper/FlashcardMapper.kt b/core/data/src/main/java/com/memorati/core/data/mapper/FlashcardMapper.kt index 2a2ac606..12799c9d 100644 --- a/core/data/src/main/java/com/memorati/core/data/mapper/FlashcardMapper.kt +++ b/core/data/src/main/java/com/memorati/core/data/mapper/FlashcardMapper.kt @@ -49,10 +49,12 @@ private fun AdditionalInfoEntity.toAdditionalInfo() = AdditionalInfo( difficulty = difficulty, consecutiveCorrectCount = consecutiveCorrectCount, memoryStrength = memoryStrength, + totalReviews = totalReviews, ) private fun AdditionalInfo.toAdditionalInfoEntity() = AdditionalInfoEntity( difficulty = difficulty, consecutiveCorrectCount = consecutiveCorrectCount, memoryStrength = memoryStrength, + totalReviews = totalReviews, ) diff --git a/core/datastore/src/main/java/com/memorati/core/datastore/PreferencesDataSource.kt b/core/datastore/src/main/java/com/memorati/core/datastore/PreferencesDataSource.kt index 7f909be3..6773c6dd 100644 --- a/core/datastore/src/main/java/com/memorati/core/datastore/PreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/memorati/core/datastore/PreferencesDataSource.kt @@ -8,16 +8,26 @@ import com.memorati.core.model.UserData.Companion.END import com.memorati.core.model.UserData.Companion.INTERVAL import com.memorati.core.model.UserData.Companion.START import com.memorati.core.model.UserData.Companion.WEEKS +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.datetime.LocalTime import java.io.IOException -import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds -class PreferencesDataSource @Inject constructor( +interface PreferencesDataSource { + val userData: Flow + suspend fun setStartTime(time: Int) + suspend fun setEndTime(time: Int) + suspend fun setAlarmInterval(interval: Long) + suspend fun setIdiomLanguageTag(tag: String) + suspend fun setWeekCountOfReview(count: Int) + suspend fun setCorrectnessCount(count: Int) +} + +class PreferencesData( private val userPreferences: DataStore, -) { - val userData = userPreferences.data.map { prefs -> +) : PreferencesDataSource { + override val userData = userPreferences.data.map { prefs -> with(prefs) { UserData( idiomLanguageTag = idiomLanguageTag, @@ -30,7 +40,7 @@ class PreferencesDataSource @Inject constructor( } } - suspend fun setStartTime(time: Int) { + override suspend fun setStartTime(time: Int) { try { userPreferences.updateData { it.copy { @@ -42,7 +52,7 @@ class PreferencesDataSource @Inject constructor( } } - suspend fun setEndTime(time: Int) { + override suspend fun setEndTime(time: Int) { try { userPreferences.updateData { it.copy { @@ -54,7 +64,7 @@ class PreferencesDataSource @Inject constructor( } } - suspend fun setAlarmInterval(interval: Long) { + override suspend fun setAlarmInterval(interval: Long) { try { userPreferences.updateData { it.copy { @@ -66,7 +76,7 @@ class PreferencesDataSource @Inject constructor( } } - suspend fun setIdiomLanguageTag(tag: String) { + override suspend fun setIdiomLanguageTag(tag: String) { try { userPreferences.updateData { it.copy { @@ -78,7 +88,7 @@ class PreferencesDataSource @Inject constructor( } } - suspend fun setCorrectnessCount(count: Int) { + override suspend fun setCorrectnessCount(count: Int) { try { userPreferences.updateData { it.copy { @@ -86,11 +96,15 @@ class PreferencesDataSource @Inject constructor( } } } catch (ioException: IOException) { - Log.e("PreferencesDataSource", "Failed to update wordCorrectnessCount preferences", ioException) + Log.e( + "PreferencesDataSource", + "Failed to update wordCorrectnessCount preferences", + ioException, + ) } } - suspend fun setWeekCountOfReview(count: Int) { + override suspend fun setWeekCountOfReview(count: Int) { try { userPreferences.updateData { it.copy { @@ -98,7 +112,11 @@ class PreferencesDataSource @Inject constructor( } } } catch (ioException: IOException) { - Log.e("PreferencesDataSource", "Failed to update weeksOfReview preferences", ioException) + Log.e( + "PreferencesDataSource", + "Failed to update weeksOfReview preferences", + ioException, + ) } } } diff --git a/core/datastore/src/main/java/com/memorati/core/datastore/di/DatastoreModule.kt b/core/datastore/src/main/java/com/memorati/core/datastore/di/DatastoreModule.kt index 8f834922..002daefc 100644 --- a/core/datastore/src/main/java/com/memorati/core/datastore/di/DatastoreModule.kt +++ b/core/datastore/src/main/java/com/memorati/core/datastore/di/DatastoreModule.kt @@ -7,6 +7,8 @@ import androidx.datastore.dataStoreFile import com.memorati.core.common.di.ApplicationScope import com.memorati.core.common.dispatcher.Dispatcher import com.memorati.core.common.dispatcher.MemoratiDispatchers.IO +import com.memorati.core.datastore.PreferencesData +import com.memorati.core.datastore.PreferencesDataSource import com.memorati.core.datastore.UserPreferences import com.memorati.core.datastore.UserPreferencesSerializer import dagger.Module @@ -35,4 +37,10 @@ class DatastoreModule { ) { context.dataStoreFile("user_preferences.pb") } + + @Provides + @Singleton + fun providesUserPreferences( + store: DataStore, + ): PreferencesDataSource = PreferencesData(store) } diff --git a/core/db/src/main/java/com/memorati/core/db/model/AdditionalInfoEntity.kt b/core/db/src/main/java/com/memorati/core/db/model/AdditionalInfoEntity.kt index cce3dd74..18413018 100644 --- a/core/db/src/main/java/com/memorati/core/db/model/AdditionalInfoEntity.kt +++ b/core/db/src/main/java/com/memorati/core/db/model/AdditionalInfoEntity.kt @@ -4,16 +4,23 @@ import kotlinx.serialization.EncodeDefault import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable + @OptIn(ExperimentalSerializationApi::class) @Serializable data class AdditionalInfoEntity( @EncodeDefault @SerialName("difficulty") val difficulty: Double = 1.0, + @EncodeDefault @SerialName("consecutiveCorrectCount") val consecutiveCorrectCount: Int = 0, + @EncodeDefault @SerialName("memoryStrength") val memoryStrength: Double = 1.0, + + @EncodeDefault + @SerialName("totalReviews") + val totalReviews: Int = 0, ) diff --git a/core/db/src/test/java/com/memorati/core/db/model/AdditionalInfoEntityTest.kt b/core/db/src/test/java/com/memorati/core/db/model/AdditionalInfoEntityTest.kt index 55fe33db..164b02d5 100644 --- a/core/db/src/test/java/com/memorati/core/db/model/AdditionalInfoEntityTest.kt +++ b/core/db/src/test/java/com/memorati/core/db/model/AdditionalInfoEntityTest.kt @@ -1,6 +1,5 @@ package com.memorati.core.db.model -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.test.Test @@ -9,9 +8,16 @@ import kotlin.test.assertEquals class AdditionalInfoEntityTest { private val infoString1 = """ - {"difficulty":1.0,"consecutiveCorrectCount":0,"memoryStrength":1.0} + {"difficulty":1.0,"consecutiveCorrectCount":0,"memoryStrength":1.0,"totalReviews":0} """.trimIndent() private val infoString2 = """ + {"difficulty":2.0,"consecutiveCorrectCount":0,"memoryStrength":1.0,"totalReviews":0} + """.trimIndent() + + private val infoString1Legacy = """ + {"difficulty":1.0,"consecutiveCorrectCount":0,"memoryStrength":1.0} + """.trimIndent() + private val infoString2Legacy = """ {"difficulty":2.0,"consecutiveCorrectCount":0,"memoryStrength":1.0} """.trimIndent() @@ -40,4 +46,17 @@ class AdditionalInfoEntityTest { Json.decodeFromString(infoString1), ) } + + @Test + fun `Decode string legacy to AdditionalInfoEntity `() { + assertEquals( + AdditionalInfoEntity(difficulty = 2.0), + Json.decodeFromString(infoString2Legacy), + ) + + assertEquals( + AdditionalInfoEntity(), + Json.decodeFromString(infoString1Legacy), + ) + } } diff --git a/core/design/src/main/java/com/memorati/core/design/icon/Brain.kt b/core/design/src/main/java/com/memorati/core/design/icon/Brain.kt new file mode 100644 index 00000000..657ad306 --- /dev/null +++ b/core/design/src/main/java/com/memorati/core/design/icon/Brain.kt @@ -0,0 +1,147 @@ +package com.memorati.core.design.icon + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +public val Neurology: ImageVector + get() { + if (_Neurology != null) { + return _Neurology!! + } + _Neurology = ImageVector.Builder( + name = "Neurology", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ).apply { + path( + fill = SolidColor(Color.Black), + ) { + moveTo(390f, -120f) + quadToRelative(-51f, 0f, -88f, -35.5f) + reflectiveQuadTo(260f, -241f) + quadToRelative(-60f, -8f, -100f, -53f) + reflectiveQuadToRelative(-40f, -106f) + quadToRelative(0f, -21f, 5.5f, -41.5f) + reflectiveQuadTo(142f, -480f) + quadToRelative(-11f, -18f, -16.5f, -38f) + reflectiveQuadToRelative(-5.5f, -42f) + quadToRelative(0f, -61f, 40f, -105.5f) + reflectiveQuadToRelative(99f, -52.5f) + quadToRelative(3f, -51f, 41f, -86.5f) + reflectiveQuadToRelative(90f, -35.5f) + quadToRelative(26f, 0f, 48.5f, 10f) + reflectiveQuadToRelative(41.5f, 27f) + quadToRelative(18f, -17f, 41f, -27f) + reflectiveQuadToRelative(49f, -10f) + quadToRelative(52f, 0f, 89.5f, 35f) + reflectiveQuadToRelative(40.5f, 86f) + quadToRelative(59f, 8f, 99.5f, 53f) + reflectiveQuadTo(840f, -560f) + quadToRelative(0f, 22f, -5.5f, 42f) + reflectiveQuadTo(818f, -480f) + quadToRelative(11f, 18f, 16.5f, 38.5f) + reflectiveQuadTo(840f, -400f) + quadToRelative(0f, 62f, -40.5f, 106.5f) + reflectiveQuadTo(699f, -241f) + quadToRelative(-5f, 50f, -41.5f, 85.5f) + reflectiveQuadTo(570f, -120f) + quadToRelative(-25f, 0f, -48.5f, -9.5f) + reflectiveQuadTo(480f, -156f) + quadToRelative(-19f, 17f, -42f, 26.5f) + reflectiveQuadToRelative(-48f, 9.5f) + close() + moveToRelative(130f, -590f) + verticalLineToRelative(460f) + quadToRelative(0f, 21f, 14.5f, 35.5f) + reflectiveQuadTo(570f, -200f) + quadToRelative(20f, 0f, 34.5f, -16f) + reflectiveQuadToRelative(15.5f, -36f) + quadToRelative(-21f, -8f, -38.5f, -21.5f) + reflectiveQuadTo(550f, -306f) + quadToRelative(-10f, -14f, -7.5f, -30f) + reflectiveQuadToRelative(16.5f, -26f) + quadToRelative(14f, -10f, 30f, -7.5f) + reflectiveQuadToRelative(26f, 16.5f) + quadToRelative(11f, 16f, 28f, 24.5f) + reflectiveQuadToRelative(37f, 8.5f) + quadToRelative(33f, 0f, 56.5f, -23.5f) + reflectiveQuadTo(760f, -400f) + quadToRelative(0f, -5f, -0.5f, -10f) + reflectiveQuadToRelative(-2.5f, -10f) + quadToRelative(-17f, 10f, -36.5f, 15f) + reflectiveQuadToRelative(-40.5f, 5f) + quadToRelative(-17f, 0f, -28.5f, -11.5f) + reflectiveQuadTo(640f, -440f) + quadToRelative(0f, -17f, 11.5f, -28.5f) + reflectiveQuadTo(680f, -480f) + quadToRelative(33f, 0f, 56.5f, -23.5f) + reflectiveQuadTo(760f, -560f) + quadToRelative(0f, -33f, -23.5f, -56f) + reflectiveQuadTo(680f, -640f) + quadToRelative(-11f, 18f, -28.5f, 31.5f) + reflectiveQuadTo(613f, -587f) + quadToRelative(-16f, 6f, -31f, -1f) + reflectiveQuadToRelative(-20f, -23f) + quadToRelative(-5f, -16f, 1.5f, -31f) + reflectiveQuadToRelative(22.5f, -20f) + quadToRelative(15f, -5f, 24.5f, -18f) + reflectiveQuadToRelative(9.5f, -30f) + quadToRelative(0f, -21f, -14.5f, -35.5f) + reflectiveQuadTo(570f, -760f) + quadToRelative(-21f, 0f, -35.5f, 14.5f) + reflectiveQuadTo(520f, -710f) + close() + moveToRelative(-80f, 460f) + verticalLineToRelative(-460f) + quadToRelative(0f, -21f, -14.5f, -35.5f) + reflectiveQuadTo(390f, -760f) + quadToRelative(-21f, 0f, -35.5f, 14.5f) + reflectiveQuadTo(340f, -710f) + quadToRelative(0f, 16f, 9f, 29.5f) + reflectiveQuadToRelative(24f, 18.5f) + quadToRelative(16f, 5f, 23f, 20f) + reflectiveQuadToRelative(2f, 31f) + quadToRelative(-6f, 16f, -21f, 23f) + reflectiveQuadToRelative(-31f, 1f) + quadToRelative(-21f, -8f, -38.5f, -21.5f) + reflectiveQuadTo(279f, -640f) + quadToRelative(-32f, 1f, -55.5f, 24.5f) + reflectiveQuadTo(200f, -560f) + quadToRelative(0f, 33f, 23.5f, 56.5f) + reflectiveQuadTo(280f, -480f) + quadToRelative(17f, 0f, 28.5f, 11.5f) + reflectiveQuadTo(320f, -440f) + quadToRelative(0f, 17f, -11.5f, 28.5f) + reflectiveQuadTo(280f, -400f) + quadToRelative(-21f, 0f, -40.5f, -5f) + reflectiveQuadTo(203f, -420f) + quadToRelative(-2f, 5f, -2.5f, 10f) + reflectiveQuadToRelative(-0.5f, 10f) + quadToRelative(0f, 33f, 23.5f, 56.5f) + reflectiveQuadTo(280f, -320f) + quadToRelative(20f, 0f, 37f, -8.5f) + reflectiveQuadToRelative(28f, -24.5f) + quadToRelative(10f, -14f, 26f, -16.5f) + reflectiveQuadToRelative(30f, 7.5f) + quadToRelative(14f, 10f, 16.5f, 26f) + reflectiveQuadToRelative(-7.5f, 30f) + quadToRelative(-14f, 19f, -32f, 33f) + reflectiveQuadToRelative(-39f, 22f) + quadToRelative(1f, 20f, 16f, 35.5f) + reflectiveQuadToRelative(35f, 15.5f) + quadToRelative(21f, 0f, 35.5f, -14.5f) + reflectiveQuadTo(440f, -250f) + close() + moveToRelative(40f, -230f) + close() + } + }.build() + return _Neurology!! + } + +private var _Neurology: ImageVector? = null diff --git a/core/design/src/main/java/com/memorati/core/design/icon/MemoratiIcons.kt b/core/design/src/main/java/com/memorati/core/design/icon/MemoratiIcons.kt index ad56308f..935e91dd 100644 --- a/core/design/src/main/java/com/memorati/core/design/icon/MemoratiIcons.kt +++ b/core/design/src/main/java/com/memorati/core/design/icon/MemoratiIcons.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.outlined.Done import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Insights import androidx.compose.material.icons.outlined.KeyboardArrowDown +import androidx.compose.material.icons.outlined.Memory import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.Quiz import androidx.compose.material.icons.outlined.Remove @@ -38,7 +39,6 @@ object MemoratiIcons { val Delete = Icons.Outlined.Delete val Cards = Icons.Outlined.ViewAgenda val Add = Icons.Outlined.Add - val Plus = Icons.Outlined.Add val Minus = Icons.Outlined.Remove val Search = Icons.Outlined.Search val Favourites = Icons.Outlined.Star @@ -62,4 +62,5 @@ object MemoratiIcons { val CompareArrows = Icons.AutoMirrored.Outlined.CompareArrows val Replay = Icons.Outlined.Replay val AvTimer = Icons.Outlined.AvTimer + val Memory = Icons.Outlined.Memory } diff --git a/core/model/src/main/java/com/memorati/core/model/Flashcard.kt b/core/model/src/main/java/com/memorati/core/model/Flashcard.kt index 5c342428..58ffcaa7 100644 --- a/core/model/src/main/java/com/memorati/core/model/Flashcard.kt +++ b/core/model/src/main/java/com/memorati/core/model/Flashcard.kt @@ -19,4 +19,5 @@ data class AdditionalInfo( val difficulty: Double = 1.0, val consecutiveCorrectCount: Int = 0, val memoryStrength: Double = 1.0, + val totalReviews: Int = 0, ) diff --git a/core/ui/src/main/java/com/memorati/core/ui/DevicePreviews.kt b/core/ui/src/main/java/com/memorati/core/ui/DevicePreviews.kt index d7ed73c8..6e0c25fb 100644 --- a/core/ui/src/main/java/com/memorati/core/ui/DevicePreviews.kt +++ b/core/ui/src/main/java/com/memorati/core/ui/DevicePreviews.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.tooling.preview.PreviewDynamicColors import androidx.compose.ui.tooling.preview.PreviewLightDark /** - * Multipreview annotation that represents various device sizes. Add this annotation to a composable + * Multi preview annotation that represents various device sizes. Add this annotation to a composable * to render various devices. */ diff --git a/feature/cards/src/androidTest/java/com/memorati/feature/cards/CardsListTest.kt b/feature/cards/src/androidTest/java/com/memorati/feature/cards/CardsListTest.kt index 95541a2b..b221e32b 100644 --- a/feature/cards/src/androidTest/java/com/memorati/feature/cards/CardsListTest.kt +++ b/feature/cards/src/androidTest/java/com/memorati/feature/cards/CardsListTest.kt @@ -40,6 +40,7 @@ class CardsListTest { ), ), query = "kom", + userData = userData, ), speak = {}, onEdit = {}, diff --git a/feature/cards/src/main/java/com/memorati/feature/cards/CardItem.kt b/feature/cards/src/main/java/com/memorati/feature/cards/CardItem.kt index eaf73d92..7c928147 100644 --- a/feature/cards/src/main/java/com/memorati/feature/cards/CardItem.kt +++ b/feature/cards/src/main/java/com/memorati/feature/cards/CardItem.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider @@ -38,6 +39,7 @@ import com.memorati.core.model.Flashcard import com.memorati.core.ui.DevicePreviews import com.memorati.core.ui.provider.FlashcardProvider import com.memorati.core.ui.theme.MemoratiTheme +import kotlin.math.max @OptIn(ExperimentalLayoutApi::class) @Composable @@ -117,6 +119,29 @@ internal fun CardItem( onEdit = { onEdit(card) }, ) + Box( + modifier = Modifier + .padding(10.dp) + .align(Alignment.BottomStart), + ) { + Icon( + modifier = Modifier + .align(Alignment.Center), + imageVector = MemoratiIcons.Memory, + contentDescription = "Progress", + ) + + CircularProgressIndicator( + progress = { + card.additionalInfo.consecutiveCorrectCount.toFloat() / max( + card.additionalInfo.totalReviews, + state.userData.wordCorrectnessCount, + ) + }, + trackColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f), + ) + } + FlowRow( modifier = Modifier.align(Alignment.BottomStart), ) { diff --git a/feature/cards/src/main/java/com/memorati/feature/cards/CardsList.kt b/feature/cards/src/main/java/com/memorati/feature/cards/CardsList.kt index bf8c9c34..d087c13e 100644 --- a/feature/cards/src/main/java/com/memorati/feature/cards/CardsList.kt +++ b/feature/cards/src/main/java/com/memorati/feature/cards/CardsList.kt @@ -1,6 +1,5 @@ package com.memorati.feature.cards -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -88,7 +87,6 @@ internal fun CardsScreen( } @Composable -@OptIn(ExperimentalFoundationApi::class) private fun Cards( modifier: Modifier = Modifier, lazyListState: LazyGridState, @@ -109,10 +107,10 @@ private fun Cards( ) { state.map.forEach { (date, cards) -> item(span = { GridItemSpan(maxLineSpan) }) { + Modifier.fillMaxWidth() Text( modifier = Modifier - .fillMaxWidth() - .animateItemPlacement() + .animateItem() .padding(all = 8.dp), text = date.toJavaLocalDate() .format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)), @@ -123,7 +121,7 @@ private fun Cards( CardItem( modifier = Modifier .padding(2.dp) - .animateItemPlacement(), + .animateItem(), card = card, state = state, toggleFavoured = toggleFavoured, diff --git a/feature/cards/src/main/java/com/memorati/feature/cards/CardsViewModel.kt b/feature/cards/src/main/java/com/memorati/feature/cards/CardsViewModel.kt index 5789c808..fb539ef1 100644 --- a/feature/cards/src/main/java/com/memorati/feature/cards/CardsViewModel.kt +++ b/feature/cards/src/main/java/com/memorati/feature/cards/CardsViewModel.kt @@ -5,7 +5,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.memorati.core.common.viewmodel.launch import com.memorati.core.data.repository.FlashcardsRepository +import com.memorati.core.datastore.PreferencesDataSource import com.memorati.core.model.Flashcard +import com.memorati.core.model.UserData import com.memorati.feature.cards.speech.Orator import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -21,13 +23,15 @@ import javax.inject.Inject class CardsViewModel @Inject constructor( private val orator: Orator, private val flashcardsRepository: FlashcardsRepository, + private val userPreferences: PreferencesDataSource, ) : ViewModel() { private val queryFlow = MutableStateFlow("") val state = combine( flashcardsRepository.flashcards(), queryFlow, - ) { flashcards, query -> + userPreferences.userData, + ) { flashcards, query, userData -> val map = flashcards.filter { flashcard -> if (query.isEmpty()) true else flashcard.contains(query) }.groupBy { card -> @@ -38,7 +42,7 @@ class CardsViewModel @Inject constructor( entry.key to entry.value.sortedBy { card -> card.idiom } } - CardsState(map = map, query = query) + CardsState(map = map, query = query, userData = userData) }.stateIn( viewModelScope, SharingStarted.Eagerly, @@ -79,4 +83,5 @@ private fun Flashcard.contains(query: String): Boolean { data class CardsState( val map: Map> = emptyMap(), val query: String = "", + val userData: UserData = UserData(), ) diff --git a/feature/cards/src/test/java/com/memorati/feature/cards/CardsViewModelTest.kt b/feature/cards/src/test/java/com/memorati/feature/cards/CardsViewModelTest.kt index e06b8abe..b871fd33 100644 --- a/feature/cards/src/test/java/com/memorati/feature/cards/CardsViewModelTest.kt +++ b/feature/cards/src/test/java/com/memorati/feature/cards/CardsViewModelTest.kt @@ -1,9 +1,13 @@ package com.memorati.feature.cards +import com.memorati.core.datastore.PreferencesDataSource +import com.memorati.core.model.UserData import com.memorati.core.testing.repository.TestFlashcardsRepository import com.memorati.core.testing.rule.MainDispatcherRule import com.memorati.feature.cards.speech.Orator +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -36,6 +40,7 @@ class CardsViewModelTest { cardsViewModel = CardsViewModel( orator = testOrator, flashcardsRepository = TestFlashcardsRepository(), + userPreferences = FakeUserPreferences(), ) } @@ -142,3 +147,26 @@ class CardsViewModelTest { job.cancel() } } + +class FakeUserPreferences() : PreferencesDataSource { + override val userData: Flow + get() = flowOf(UserData()) + + override suspend fun setStartTime(time: Int) { + } + + override suspend fun setEndTime(time: Int) { + } + + override suspend fun setAlarmInterval(interval: Long) { + } + + override suspend fun setIdiomLanguageTag(tag: String) { + } + + override suspend fun setWeekCountOfReview(count: Int) { + } + + override suspend fun setCorrectnessCount(count: Int) { + } +} diff --git a/feature/settings/src/main/java/com/memorati/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/memorati/feature/settings/SettingsScreen.kt index 77dc59a7..f8b45f13 100644 --- a/feature/settings/src/main/java/com/memorati/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/memorati/feature/settings/SettingsScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -21,6 +22,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost @@ -152,6 +154,23 @@ internal fun SettingsScreen( imageVector = MemoratiIcons.Insights, contentPadding = 0.dp, ) { + Text( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResource( + id = R.string.memorization_level, + state.memorizationLevel * 100, + ), + style = MaterialTheme.typography.bodyMedium, + ) + + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp) + .height(5.dp), + progress = { state.memorizationLevel }, + ) + Text( modifier = Modifier.padding(horizontal = 24.dp), text = stringResource( @@ -289,6 +308,7 @@ internal fun SettingsScreenPreview() { state = SettingsState( flashcardsCount = 10, chartEntries = dayEntries(), + memorizationLevel = 50f, ), onBack = {}, onClear = {}, diff --git a/feature/settings/src/main/java/com/memorati/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/memorati/feature/settings/SettingsViewModel.kt index bbb8f079..bbbe78d6 100644 --- a/feature/settings/src/main/java/com/memorati/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/memorati/feature/settings/SettingsViewModel.kt @@ -25,6 +25,7 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import javax.inject.Inject +import kotlin.math.max import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes @@ -50,6 +51,12 @@ class SettingsViewModel @Inject constructor( SettingsState( userData = userData, flashcardsCount = flashcards.size, + memorizationLevel = flashcards.map { card -> + card.additionalInfo.consecutiveCorrectCount.toFloat() / max( + card.additionalInfo.totalReviews, + userData.wordCorrectnessCount, + ) + }.average().toFloat().coerceAtMost(1f), chartEntries = flashcards.groupBy { card -> card.createdAt.toJavaInstant() .atZone(ZoneId.systemDefault()) @@ -67,7 +74,7 @@ class SettingsViewModel @Inject constructor( }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), - initialValue = SettingsState(flashcardsCount = 0), + initialValue = SettingsState(flashcardsCount = 0, memorizationLevel = 0.0f), ) fun exportFlashcards(title: String, context: Context) = launch { diff --git a/feature/settings/src/main/java/com/memorati/feature/settings/model/SettingsState.kt b/feature/settings/src/main/java/com/memorati/feature/settings/model/SettingsState.kt index 46f471d6..710f8fb0 100644 --- a/feature/settings/src/main/java/com/memorati/feature/settings/model/SettingsState.kt +++ b/feature/settings/src/main/java/com/memorati/feature/settings/model/SettingsState.kt @@ -5,6 +5,7 @@ import com.memorati.feature.settings.chart.DayEntry data class SettingsState( val flashcardsCount: Int, + val memorizationLevel: Float, val userData: UserData = UserData(), val error: Exception? = null, val notificationsEnabled: Boolean = true, diff --git a/feature/settings/src/main/res/values-ar/strings.xml b/feature/settings/src/main/res/values-ar/strings.xml index 9c5b1187..69039cdf 100644 --- a/feature/settings/src/main/res/values-ar/strings.xml +++ b/feature/settings/src/main/res/values-ar/strings.xml @@ -14,6 +14,7 @@ موافق إلغاء لديك %d بطاقة فلاش + مستوى تقدمك في الحفظ هو %.2f%% بطاقات Memorati الإشعارات السماح بالإشعارات diff --git a/feature/settings/src/main/res/values-de/strings.xml b/feature/settings/src/main/res/values-de/strings.xml index 1141f484..cf77e56b 100644 --- a/feature/settings/src/main/res/values-de/strings.xml +++ b/feature/settings/src/main/res/values-de/strings.xml @@ -14,6 +14,7 @@ OK Abbrechen Du hast %1$d Karteikarten + Dein Fortschritt beim Auswendiglernen beträgt %.2f%% Memorati-Karten Benachrichtigungen Benachrichtigungen erlauben diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index 094ede11..a8b5db7d 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ OK Cancel You have %d flashcards + Your memorization progress is %.2f%% Memorati Cards Notifications Allow Notifications diff --git a/gradle.properties b/gradle.properties index 30ecb84d..392686b2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,4 +19,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding\=UTF-8 versionMajor=1 versionMinor=0 versionPatch=2 -versionBuild=7 +versionBuild=8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ed3483b..dc97c98d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ androidGradlePlugin = "8.5.2" androidxBrowser = "1.8.0" androidxComposeBom = "2024.06.00" androidxComposeCompiler = "1.5.11" -androidxComposeMaterial3 = "1.2.1" +androidxComposeMaterial3 = "1.3.0-beta05" androidxComposeRuntimeTracing = "1.0.0-beta01" androidxCore = "1.13.1" androidxCoreSplashscreen = "1.0.1" @@ -23,7 +23,7 @@ androidxStartup = "1.1.1" androidxTestCore = "1.6.1" androidxTestExt = "1.2.1" androidxTestRules = "1.6.1" -androidxTestRunner = "1.6.1" +androidxTestRunner = "1.6.2" androidxTracing = "1.2.0" androidxUiAutomator = "2.3.0" androidxWindowManager = "1.3.0" @@ -42,12 +42,13 @@ kotlinxCoroutines = "1.8.1" kotlinxDatetime = "0.6.0" kotlinxSerializationJson = "1.7.1" ksp = "2.0.0-1.0.24" -lint = "31.5.1" +lint = "31.5.2" okhttp = "4.12.0" protobuf = "4.27.3" protobufPlugin = "0.9.4" retrofit = "2.11.0" retrofitKotlinxSerializationJson = "1.0.0" +review = "2.0.1" room = "2.6.1" secrets = "2.0.1" turbine = "1.1.0" @@ -130,6 +131,8 @@ protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin- protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerializationJson" } +review = { module = "com.google.android.play:review", version.ref = "review" } +review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "review" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }