diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aab43c41..8a880084 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,24 +1,26 @@ - + + + + android:theme="@style/Theme.NewQuiz.Starting"> + android:hardwareAccelerated="true" + android:theme="@style/Theme.NewQuiz.Starting"> @@ -28,19 +30,18 @@ - + android:value="ca-app-pub-1923025671607389~2529814126" /> - + android:value="true" /> + android:value="true" /> + tools:node="remove" /> + \ No newline at end of file diff --git a/app/src/main/java/com/infinitepower/newquiz/NewQuizApp.kt b/app/src/main/java/com/infinitepower/newquiz/NewQuizApp.kt index 8d91d9fe..b1bb6747 100644 --- a/app/src/main/java/com/infinitepower/newquiz/NewQuizApp.kt +++ b/app/src/main/java/com/infinitepower/newquiz/NewQuizApp.kt @@ -33,7 +33,7 @@ class NewQuizApp : Application(), Configuration.Provider { initializeMobileAds() initializeRemoteConfig() createNotificationChannels() - //createDailyWordleWork() + createDailyWordleWork() } private fun initializeMobileAds() { diff --git a/app/src/main/java/com/infinitepower/newquiz/core/navigation/AppNavGraphs.kt b/app/src/main/java/com/infinitepower/newquiz/core/navigation/AppNavGraphs.kt index 8432b910..87bac730 100644 --- a/app/src/main/java/com/infinitepower/newquiz/core/navigation/AppNavGraphs.kt +++ b/app/src/main/java/com/infinitepower/newquiz/core/navigation/AppNavGraphs.kt @@ -15,6 +15,7 @@ import com.infinitepower.newquiz.quiz_presentation.destinations.QuizScreenDestin import com.infinitepower.newquiz.home_presentation.destinations.HomeScreenDestination import com.infinitepower.newquiz.home_presentation.destinations.LoginScreenDestination import com.infinitepower.newquiz.quiz_presentation.destinations.QuizListScreenDestination +import com.infinitepower.newquiz.quiz_presentation.destinations.ResultsScreenDestination import com.infinitepower.newquiz.quiz_presentation.destinations.SavedQuestionsScreenDestination import com.infinitepower.newquiz.settings_presentation.destinations.SettingsScreenDestination import com.infinitepower.newquiz.wordle.destinations.DailyWordSelectorScreenDestination @@ -56,7 +57,8 @@ internal object AppNavGraphs { WordleListScreenDestination, DailyWordSelectorScreenDestination, QuizListScreenDestination, - LoginScreenDestination + LoginScreenDestination, + ResultsScreenDestination ) ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 903a5d29..51c9214a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ NewQuiz + DynamicFeatureTest \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e0d1da8b..933d9638 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,7 @@ plugins { id("org.jetbrains.kotlin.plugin.serialization") apply false id("com.google.dagger.hilt.android") apply false id("com.google.gms.google-services") apply false + id("com.android.dynamic-feature") version "7.4.0-alpha09" apply false } tasks.register("clean", Delete::class) { diff --git a/core/src/main/java/com/infinitepower/newquiz/core/common/dataStore/SettingsCommon.kt b/core/src/main/java/com/infinitepower/newquiz/core/common/dataStore/SettingsCommon.kt index 5b2fdf42..f37379ac 100644 --- a/core/src/main/java/com/infinitepower/newquiz/core/common/dataStore/SettingsCommon.kt +++ b/core/src/main/java/com/infinitepower/newquiz/core/common/dataStore/SettingsCommon.kt @@ -1,17 +1,65 @@ package com.infinitepower.newquiz.core.common.dataStore import android.content.Context +import androidx.annotation.Keep +import androidx.annotation.RawRes +import androidx.annotation.StringRes import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import com.infinitepower.newquiz.core.R import com.infinitepower.newquiz.core.dataStore.manager.PreferenceRequest +import java.util.Locale val Context.settingsDataStore: DataStore by preferencesDataStore(name = "settings") object SettingsCommon { object ShowLoginCard : PreferenceRequest(booleanPreferencesKey("showLoginCard"), true) - object QuickQuizQuestionsSize : PreferenceRequest(intPreferencesKey("quickQuizQuestionsSize"), 5) + object QuickQuizQuestionsSize : + PreferenceRequest(intPreferencesKey("quickQuizQuestionsSize"), 5) + + object InfiniteWordleQuizLanguage : PreferenceRequest( + stringPreferencesKey("infiniteWordleQuizLanguage"), + getInfiniteWordleDefaultLang() + ) + + object WordleInfiniteRowsLimited : PreferenceRequest(booleanPreferencesKey("wordleInfiniteRowsLimited"), false) + + object WordleInfiniteRowsLimit : PreferenceRequest(intPreferencesKey("wordleInfiniteRowsLimit"), 6) + + object WordleHardMode : PreferenceRequest(booleanPreferencesKey("wordleHardMode"), false) + + object WordleColorBlindMode : PreferenceRequest(booleanPreferencesKey("wordleColorBlindMode"), false) + + object WordleLetterHints : PreferenceRequest(booleanPreferencesKey("wordleLetterHints"), false) +} + +@Keep +data class SettingsWordleLang( + val key: String, + @StringRes val languageId: Int, + @RawRes val rawListId: Int +) + +val infiniteWordleSupportedLang = listOf( + SettingsWordleLang( + key = "en", + languageId = R.string.english, + rawListId = R.raw.wordle_list + ), + SettingsWordleLang( + key = "pt", + languageId = R.string.portuguese, + rawListId = R.raw.wordle_list_pt + ), +) + +private fun getInfiniteWordleDefaultLang(): String { + val localeLanguage = Locale.getDefault().language + val langKeys = infiniteWordleSupportedLang.map { it.key } + return if (localeLanguage in langKeys) localeLanguage else "en" } \ No newline at end of file diff --git a/core/src/main/java/com/infinitepower/newquiz/core/common/viewmodel/NavEvent.kt b/core/src/main/java/com/infinitepower/newquiz/core/common/viewmodel/NavEvent.kt index 8d3c06d2..1df1f449 100644 --- a/core/src/main/java/com/infinitepower/newquiz/core/common/viewmodel/NavEvent.kt +++ b/core/src/main/java/com/infinitepower/newquiz/core/common/viewmodel/NavEvent.kt @@ -5,7 +5,9 @@ import com.ramcosta.composedestinations.spec.Direction sealed class NavEvent { object PopBackStack : NavEvent() - data class Navigate(val direction: Direction) : NavEvent() + data class Navigate( + val direction: Direction + ) : NavEvent() data class ShowSnackBar(val message: String) : NavEvent() } diff --git a/core/src/main/java/com/infinitepower/newquiz/core/common/viewmodel/NavEventViewModel.kt b/core/src/main/java/com/infinitepower/newquiz/core/common/viewmodel/NavEventViewModel.kt index 91b43a96..b2f170a0 100644 --- a/core/src/main/java/com/infinitepower/newquiz/core/common/viewmodel/NavEventViewModel.kt +++ b/core/src/main/java/com/infinitepower/newquiz/core/common/viewmodel/NavEventViewModel.kt @@ -11,9 +11,13 @@ abstract class NavEventViewModel : ViewModel() { private val _navEvent = MutableSharedFlow() val navEvent = _navEvent.asSharedFlow() - fun sendUiEvent(uiEvent: NavEvent) { + suspend fun sendNavEvent(event: NavEvent) { + _navEvent.emit(event) + } + + fun sendNavEventAsync(event: NavEvent) { viewModelScope.launch(Dispatchers.IO) { - _navEvent.emit(uiEvent) + _navEvent.emit(event) } } } \ No newline at end of file diff --git a/core/src/main/java/com/infinitepower/newquiz/core/theme/ExtendedColor.kt b/core/src/main/java/com/infinitepower/newquiz/core/theme/ExtendedColor.kt index 423ce52e..b761341d 100644 --- a/core/src/main/java/com/infinitepower/newquiz/core/theme/ExtendedColor.kt +++ b/core/src/main/java/com/infinitepower/newquiz/core/theme/ExtendedColor.kt @@ -104,6 +104,12 @@ private val initializeExtend = ExtendedColors( harmonized = true, roles = initializeColorRoles() ), + CustomColor( + key = CustomColor.Keys.Blue, + color = Color.Blue, + harmonized = true, + roles = initializeColorRoles() + ), ) ) diff --git a/core/src/main/java/com/infinitepower/newquiz/core/ui/home_card/components/HomeMediumCard.kt b/core/src/main/java/com/infinitepower/newquiz/core/ui/home_card/components/HomeMediumCard.kt index 3ad10552..e41c6a2c 100644 --- a/core/src/main/java/com/infinitepower/newquiz/core/ui/home_card/components/HomeMediumCard.kt +++ b/core/src/main/java/com/infinitepower/newquiz/core/ui/home_card/components/HomeMediumCard.kt @@ -45,7 +45,9 @@ fun HomeMediumCard( HomeCardIcon( icon = data.icon, contentDescription = title, - modifier = Modifier.size(75.dp) + modifier = Modifier + .size(75.dp) + .padding(MaterialTheme.spacing.small), ) } Spacer(modifier = Modifier.width(spaceMedium)) diff --git a/data/src/main/res/raw/wordle_list.txt b/core/src/main/res/raw/wordle_list.txt similarity index 100% rename from data/src/main/res/raw/wordle_list.txt rename to core/src/main/res/raw/wordle_list.txt diff --git a/data/src/main/res/raw/wordle_list_pt.txt b/core/src/main/res/raw/wordle_list_pt.txt similarity index 100% rename from data/src/main/res/raw/wordle_list_pt.txt rename to core/src/main/res/raw/wordle_list_pt.txt diff --git a/core/src/main/res/values-pt/strings.xml b/core/src/main/res/values-pt/strings.xml new file mode 100644 index 00000000..4be967fb --- /dev/null +++ b/core/src/main/res/values-pt/strings.xml @@ -0,0 +1,47 @@ + + + Quiz rápido + Voltar + Perguntas guardadas + Wordle + Wordle infinito + Wordle diário + Verificar + Jogar novamente + Fechar + Ver anúncio + A carregar o anúncio + Fim de jogo + Você perdeu o jogo.\\n Quer assistir um anúncio para poder adicionar mais uma linha? + Item vazio + Item %1$s nada + Item %1$s present + Item %1$s correto + 4 Letras + 5 Letras + 6 Letras + Voltar o mês + Próximo mês + Opções + Geral + Quiz + Idioma do quiz + Inglês + Português + Quiz normal + Modo daltónico + Cores com alto contraste + Informações + Ajudas da letra + Ajuda sobre a letra presente que aparece duas ou mais vezes. + Modo difícil + Todas as dicas usadas têm de ser usadas na próxima linha + Linhas limitadas no wordle infinito + Limite das linhas + Limite das linhas do wordle infinito. + Limpar os dados do calendário + Limpar todas vitórias/derrotas do calendário. + Linhas limitadas + Apagar Definições + Remova todas as definições guardadas + \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 72b288d1..6422554f 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -1,9 +1,47 @@ + Normal Quiz Quick Quiz Back Saved questions Wordle Wordle infinite Daily wordle + Verify + Play again + Close + Watch ad + Loading rewarded ad + Game Over + You lost the game.\nDo you want to watch one ad to try one more row? + Item empty + Item %1$s none + Item %1$s present + Item %1$s correct + 4 Letters + 5 Letters + 6 Letters + Back month + Next month + Settings + General + Quiz + Quiz language + English + Portuguese + Color blind mode + High contrast colors + Info + Letter hints + Hint above the letter that it appears twice or more in the hidden word + Hard mode + Any revealed hints must be used in subsequent guesses + Rows limited + Wordle infinite row limited. + Row limit + Wordle infinite row limit value. + Clean calendar data + Cleans all saved calendar wins/losses. + Clear Preferences + Remove all saved settings \ No newline at end of file diff --git a/data/src/main/java/com/infinitepower/newquiz/data/repository/wordle/WordleRepositoryImpl.kt b/data/src/main/java/com/infinitepower/newquiz/data/repository/wordle/WordleRepositoryImpl.kt index 59392e4c..50691d1c 100644 --- a/data/src/main/java/com/infinitepower/newquiz/data/repository/wordle/WordleRepositoryImpl.kt +++ b/data/src/main/java/com/infinitepower/newquiz/data/repository/wordle/WordleRepositoryImpl.kt @@ -1,9 +1,13 @@ package com.infinitepower.newquiz.data.repository.wordle import android.content.Context +import android.util.Log import com.infinitepower.newquiz.core.common.FlowResource import com.infinitepower.newquiz.core.common.Resource -import com.infinitepower.newquiz.data.R +import com.infinitepower.newquiz.core.common.dataStore.SettingsCommon +import com.infinitepower.newquiz.core.common.dataStore.infiniteWordleSupportedLang +import com.infinitepower.newquiz.core.dataStore.manager.DataStoreManager +import com.infinitepower.newquiz.core.di.SettingsDataStoreManager import com.infinitepower.newquiz.domain.repository.wordle.WordleRepository import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -14,10 +18,17 @@ import javax.inject.Singleton @Singleton class WordleRepositoryImpl @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + @SettingsDataStoreManager private val settingsDataStoreManager: DataStoreManager ) : WordleRepository { override suspend fun getAllWords(): Set = withContext(Dispatchers.IO) { - val wordleListInputStream = context.resources.openRawResource(R.raw.wordle_list_pt) + val quizLanguage = settingsDataStoreManager.getPreference(SettingsCommon.InfiniteWordleQuizLanguage) + + val listRawId = infiniteWordleSupportedLang.find { lang -> + lang.key == quizLanguage + }?.rawListId ?: throw NullPointerException("Wordle language not found") + + val wordleListInputStream = context.resources.openRawResource(listRawId) try { wordleListInputStream @@ -45,7 +56,51 @@ class WordleRepositoryImpl @Inject constructor( emit(Resource.Success(randomWord)) } catch (e: Exception) { e.printStackTrace() - emit(Resource.Error(e.localizedMessage ?: "A error occurred while getting word")) + emit(Resource.Error(e.localizedMessage ?: "A error occurred while getting word.")) } } + + override fun isColorBlindEnabled(): FlowResource = flow { + try { + emit(Resource.Loading()) + + val isColorBlindEnabled = settingsDataStoreManager.getPreference(SettingsCommon.WordleColorBlindMode) + emit(Resource.Success(isColorBlindEnabled)) + } catch (e: Exception) { + e.printStackTrace() + emit(Resource.Error(e.localizedMessage ?: "A error occurred while checking if color blind is enabled.")) + } + } + + override fun isLetterHintEnabled(): FlowResource = flow { + try { + emit(Resource.Loading()) + + val isLetterHintEnabled = settingsDataStoreManager.getPreference(SettingsCommon.WordleLetterHints) + emit(Resource.Success(isLetterHintEnabled)) + } catch (e: Exception) { + e.printStackTrace() + emit(Resource.Error(e.localizedMessage ?: "A error occurred while checking if letter hint is enabled.")) + } + } + + override fun isHardModeEnabled(): FlowResource = flow { + try { + emit(Resource.Loading()) + + val isHardModeEnabled = settingsDataStoreManager.getPreference(SettingsCommon.WordleHardMode) + emit(Resource.Success(isHardModeEnabled)) + } catch (e: Exception) { + e.printStackTrace() + emit(Resource.Error(e.localizedMessage ?: "A error occurred while checking if hard mode is enabled.")) + } + } + + override suspend fun getWordleMaxRows(defaultMaxRow: Int): Int { + // If is row limited return row limit value else return int max value + val isRowLimited = settingsDataStoreManager.getPreference(SettingsCommon.WordleInfiniteRowsLimited) + if (isRowLimited) return settingsDataStoreManager.getPreference(SettingsCommon.WordleInfiniteRowsLimit) + + return defaultMaxRow + } } \ No newline at end of file diff --git a/data/src/main/java/com/infinitepower/newquiz/data/repository/wordle/daily/DailyWordleRepositoryImpl.kt b/data/src/main/java/com/infinitepower/newquiz/data/repository/wordle/daily/DailyWordleRepositoryImpl.kt index d648f388..ad3fc06d 100644 --- a/data/src/main/java/com/infinitepower/newquiz/data/repository/wordle/daily/DailyWordleRepositoryImpl.kt +++ b/data/src/main/java/com/infinitepower/newquiz/data/repository/wordle/daily/DailyWordleRepositoryImpl.kt @@ -55,8 +55,8 @@ class DailyWordleRepositoryImpl @Inject constructor( dailyWordleDao.insertCalendarItem(item) } - override suspend fun clearAll() { - dailyWordleDao.clearAll() + override suspend fun clearAllCalendarItems() { + dailyWordleDao.clearAllCalendarItems() } override fun getAllDailyWords( diff --git a/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/WordleRepository.kt b/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/WordleRepository.kt index 02b13cab..f45d322d 100644 --- a/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/WordleRepository.kt +++ b/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/WordleRepository.kt @@ -6,4 +6,14 @@ interface WordleRepository { suspend fun getAllWords(): Set fun generateRandomWord(): FlowResource + + fun isColorBlindEnabled(): FlowResource + + fun isLetterHintEnabled(): FlowResource + + fun isHardModeEnabled(): FlowResource + + suspend fun getWordleMaxRows( + defaultMaxRow: Int = Int.MAX_VALUE + ): Int } \ No newline at end of file diff --git a/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/daily/DailyWordleDao.kt b/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/daily/DailyWordleDao.kt index 0f74f1ce..86c2e941 100644 --- a/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/daily/DailyWordleDao.kt +++ b/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/daily/DailyWordleDao.kt @@ -21,5 +21,5 @@ interface DailyWordleDao { suspend fun insertCalendarItem(item: WordleDailyCalendarItem) @Query("DELETE FROM wordle_daily_calendar") - suspend fun clearAll() + suspend fun clearAllCalendarItems() } \ No newline at end of file diff --git a/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/daily/DailyWordleRepository.kt b/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/daily/DailyWordleRepository.kt index bfdb35b5..4285d8f8 100644 --- a/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/daily/DailyWordleRepository.kt +++ b/domain/src/main/java/com/infinitepower/newquiz/domain/repository/wordle/daily/DailyWordleRepository.kt @@ -17,7 +17,7 @@ interface DailyWordleRepository { suspend fun insertCalendarItem(item: WordleDailyCalendarItem) - suspend fun clearAll() + suspend fun clearAllCalendarItems() fun getAllDailyWords( wordSize: Int, diff --git a/home-presentation/src/main/java/com/infinitepower/newquiz/home_presentation/data/HomeCardItemData.kt b/home-presentation/src/main/java/com/infinitepower/newquiz/home_presentation/data/HomeCardItemData.kt index 8805c798..f77f842b 100644 --- a/home-presentation/src/main/java/com/infinitepower/newquiz/home_presentation/data/HomeCardItemData.kt +++ b/home-presentation/src/main/java/com/infinitepower/newquiz/home_presentation/data/HomeCardItemData.kt @@ -15,7 +15,7 @@ internal class HomeCardItemData( ) : CardItemDataCore { override val items = listOf( HomeCardItem.GroupTitle( - title = CoreR.string.quick_quiz, + title = CoreR.string.normal_quiz, ), HomeCardItem.LargeCard( title = CoreR.string.quick_quiz, @@ -26,7 +26,7 @@ internal class HomeCardItemData( title = CoreR.string.wordle, ), HomeCardItem.LargeCard( - title = CoreR.string.wordle, + title = CoreR.string.wordle_infinite, icon = CardIcon.Icon(Icons.Rounded.QuestionMark), onClick = homeNavigator::navigateToWordle ), diff --git a/model/src/main/java/com/infinitepower/newquiz/model/question/QuestionStep.kt b/model/src/main/java/com/infinitepower/newquiz/model/question/QuestionStep.kt index 51ec7b40..f1db9a15 100644 --- a/model/src/main/java/com/infinitepower/newquiz/model/question/QuestionStep.kt +++ b/model/src/main/java/com/infinitepower/newquiz/model/question/QuestionStep.kt @@ -21,15 +21,23 @@ sealed class QuestionStep { data class Current( override val question: Question ) : QuestionStep() { - fun changeToCompleted(correct: Boolean) = Completed(question, correct) + fun changeToCompleted( + correct: Boolean, + selectedAnswer: SelectedAnswer + ) = Completed(question, correct, selectedAnswer) } @Keep @Serializable data class Completed( override val question: Question, - val correct: Boolean + val correct: Boolean, + val selectedAnswer: SelectedAnswer = SelectedAnswer.NONE ) : QuestionStep() fun asCurrent() = Current(question) -} \ No newline at end of file +} + +fun List.isAllCompleted(): Boolean = all { it is QuestionStep.Completed } + +fun List.countCorrectQuestions(): Int = count { it.correct } \ No newline at end of file diff --git a/model/src/main/java/com/infinitepower/newquiz/model/question/SelectedAnswer.kt b/model/src/main/java/com/infinitepower/newquiz/model/question/SelectedAnswer.kt new file mode 100644 index 00000000..754367cd --- /dev/null +++ b/model/src/main/java/com/infinitepower/newquiz/model/question/SelectedAnswer.kt @@ -0,0 +1,26 @@ +package com.infinitepower.newquiz.model.question + +import kotlinx.serialization.Serializable + +@JvmInline +@Serializable +value class SelectedAnswer private constructor(val index: Int) { + companion object { + val NONE = SelectedAnswer(-1) + + fun fromIndex(index: Int): SelectedAnswer = SelectedAnswer(index) + } + + private val isNone: Boolean + get() = index == -1 + + val isSelected: Boolean + get() = !isNone + + fun isCorrect(question: Question): Boolean = + !isNone && question.correctAns == index + + init { + require(index >= -1) { "SelectedAnswer index must be greater than -1" } + } +} \ No newline at end of file diff --git a/model/src/main/java/com/infinitepower/newquiz/model/wordle/WordleItem.kt b/model/src/main/java/com/infinitepower/newquiz/model/wordle/WordleItem.kt index cd91c955..54212adb 100644 --- a/model/src/main/java/com/infinitepower/newquiz/model/wordle/WordleItem.kt +++ b/model/src/main/java/com/infinitepower/newquiz/model/wordle/WordleItem.kt @@ -43,6 +43,7 @@ sealed class WordleItem { } companion object { + /** Creates [None] wordle item from char with false verified */ fun fromChar(char: Char) = None(WordleChar(char), false) } } diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreen.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreen.kt index 0e59ae23..fdc8354c 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreen.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreen.kt @@ -5,31 +5,32 @@ import androidx.annotation.Keep import androidx.compose.animation.* import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.* import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController import com.infinitepower.newquiz.core.common.annotation.compose.PreviewNightLight +import com.infinitepower.newquiz.core.common.viewmodel.NavEvent import com.infinitepower.newquiz.core.theme.NewQuizTheme import com.infinitepower.newquiz.core.theme.spacing import com.infinitepower.newquiz.model.question.Question import com.infinitepower.newquiz.model.question.QuestionStep +import com.infinitepower.newquiz.model.question.SelectedAnswer import com.infinitepower.newquiz.model.question.getBasicQuestion import com.infinitepower.newquiz.quiz_presentation.components.CardQuestionAnswers -import com.infinitepower.newquiz.quiz_presentation.components.QuizStepView +import com.infinitepower.newquiz.quiz_presentation.components.QuizStepViewRow import com.infinitepower.newquiz.quiz_presentation.components.QuizTopBar +import com.infinitepower.newquiz.quiz_presentation.destinations.QuizScreenDestination +import com.infinitepower.newquiz.quiz_presentation.destinations.ResultsScreenDestination import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.navigation.navigate +import kotlinx.coroutines.launch @Keep data class QuizScreenNavArg( @@ -40,13 +41,33 @@ data class QuizScreenNavArg( @Composable @Destination(navArgsDelegate = QuizScreenNavArg::class) fun QuizScreen( - navigator: DestinationsNavigator, + navigator: NavController, windowWidthSizeClass: WindowWidthSizeClass, windowHeightSizeClass: WindowHeightSizeClass, viewModel: QuizScreenViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() + LaunchedEffect(key1 = true) { + viewModel + .navEvent + .collect { event -> + when (event) { + is NavEvent.Navigate -> { + navigator.navigate(event.direction) { + navigator.currentDestination?.route?.let { route -> + launchSingleTop = true + popUpTo(route) { + inclusive = true + } + } + } + } + else -> {} + } + } + } + QuizScreenImpl( onBackClick = navigator::popBackStack, windowWidthSizeClass = windowWidthSizeClass, @@ -98,26 +119,10 @@ private fun ColumnScope.QuizContentWidthCompact( val spaceLarge = MaterialTheme.spacing.large Spacer(modifier = Modifier.height(spaceMedium)) - LazyRow( + QuizStepViewRow( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), - contentPadding = PaddingValues(horizontal = spaceMedium) - ) { - itemsIndexed( - items = uiState.questionSteps, - key = { _, step -> step.question.id } - ) { index, step -> - val position = index + 1 - - QuizStepView( - questionStep = step, - position = position, - enabled = false - ) - } - } - + questionSteps = uiState.questionSteps + ) AnimatedVisibility( visible = uiState.currentQuestionStep != null ) { @@ -179,24 +184,10 @@ private fun ColumnScope.QuizContentWidthMedium( .weight(1f) .padding(horizontal = spaceMedium), ) { - LazyRow( + QuizStepViewRow( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), - ) { - itemsIndexed( - items = uiState.questionSteps, - key = { _, step -> step.question.id } - ) { index, step -> - val position = index + 1 - - QuizStepView( - questionStep = step, - position = position, - enabled = false - ) - } - } + questionSteps = uiState.questionSteps + ) Spacer(modifier = Modifier.height(spaceMedium)) if (currentQuestion!= null) { Text( diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenUiEvent.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenUiEvent.kt index 4e4648cc..27a1d896 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenUiEvent.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenUiEvent.kt @@ -1,5 +1,7 @@ package com.infinitepower.newquiz.quiz_presentation +import com.infinitepower.newquiz.model.question.SelectedAnswer + sealed class QuizScreenUiEvent { data class SelectAnswer(val answer: SelectedAnswer) : QuizScreenUiEvent() diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenUiState.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenUiState.kt index 73a2ee2e..d5df4bb6 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenUiState.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenUiState.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import com.infinitepower.newquiz.model.question.Question import com.infinitepower.newquiz.model.question.QuestionStep +import com.infinitepower.newquiz.model.question.SelectedAnswer +import com.infinitepower.newquiz.model.question.isAllCompleted @Keep data class QuizScreenUiState( @@ -18,29 +20,15 @@ data class QuizScreenUiState( fun getQuestionPositionFormatted(): String = "Question ${currentQuestionIndex + 1}/${questionSteps.size}" + /** + * Gets new question index. + * If question is the last question retuns -1. + * @return new question index + */ fun getNextIndex(): Int = if (currentQuestionIndex == questionSteps.lastIndex) -1 else currentQuestionIndex + 1 -} - -@JvmInline -value class SelectedAnswer private constructor(val index: Int) { - companion object { - val NONE = SelectedAnswer(-1) - - fun fromIndex(index: Int): SelectedAnswer = SelectedAnswer(index) - } - private val isNone: Boolean - get() = index == -1 - - val isSelected: Boolean - get() = !isNone - - fun isCorrect(question: Question): Boolean = - !isNone && question.correctAns == index - - init { - require(index >= -1) { "SelectedAnswer index must be greater than -1" } - } + val isGameEnded: Boolean + get() = questionSteps.isAllCompleted() } @JvmInline diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenViewModel.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenViewModel.kt index 2aee1145..1a08908e 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenViewModel.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/QuizScreenViewModel.kt @@ -2,20 +2,26 @@ package com.infinitepower.newquiz.quiz_presentation import android.os.CountDownTimer import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.infinitepower.newquiz.core.common.Resource import com.infinitepower.newquiz.core.common.dataStore.SettingsCommon +import com.infinitepower.newquiz.core.common.viewmodel.NavEvent +import com.infinitepower.newquiz.core.common.viewmodel.NavEventViewModel import com.infinitepower.newquiz.core.dataStore.manager.DataStoreManager import com.infinitepower.newquiz.core.di.SettingsDataStoreManager import com.infinitepower.newquiz.domain.repository.question.saved_questions.SavedQuestionsRepository import com.infinitepower.newquiz.domain.use_case.question.GetRandomQuestionUseCase import com.infinitepower.newquiz.model.question.Question -import com.infinitepower.newquiz.model.question.getBasicQuestion +import com.infinitepower.newquiz.model.question.QuestionStep +import com.infinitepower.newquiz.model.question.SelectedAnswer +import com.infinitepower.newquiz.quiz_presentation.destinations.ResultsScreenDestination import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import javax.inject.Inject const val QUIZ_COUNTDOWN_IN_MILLIS = 30000L @@ -26,7 +32,7 @@ class QuizScreenViewModel @Inject constructor( @SettingsDataStoreManager private val dataStoreManager: DataStoreManager, private val savedQuestionsRepository: SavedQuestionsRepository, savedStateHandle: SavedStateHandle -) : ViewModel() { +) : NavEventViewModel() { private val _uiState = MutableStateFlow(QuizScreenUiState()) val uiState = _uiState.asStateFlow() @@ -84,9 +90,7 @@ class QuizScreenViewModel @Inject constructor( val questionSteps = questions.map { question -> question.toQuestionStep() } _uiState.update { currentState -> - currentState.copy( - questionSteps = questionSteps - ) + currentState.copy(questionSteps = questionSteps) } nextQuestion() @@ -96,24 +100,30 @@ class QuizScreenViewModel @Inject constructor( _uiState.update { currentState -> val nextIndex = currentState.getNextIndex() - val steps = if (nextIndex == -1) { - currentState.questionSteps - } else { - timer.start() - - currentState - .questionSteps - .toMutableList() - .apply { - val step = currentState.questionSteps[nextIndex].asCurrent() - set(nextIndex, step) - } - } + when { + currentState.isGameEnded -> { + endGame(currentState.questionSteps.filterIsInstance()) - currentState.copy( - questionSteps = steps, - currentQuestionIndex = nextIndex - ) + currentState.copy(currentQuestionIndex = -1) + } + nextIndex == -1 -> currentState.copy(currentQuestionIndex = nextIndex) + else -> { + timer.start() + + val newSteps = currentState + .questionSteps + .toMutableList() + .apply { + val step = currentState.questionSteps[nextIndex].asCurrent() + set(nextIndex, step) + } + + currentState.copy( + questionSteps = newSteps, + currentQuestionIndex = nextIndex + ) + } + } } } @@ -125,6 +135,7 @@ class QuizScreenViewModel @Inject constructor( private fun verifyQuestion() { timer.cancel() + _uiState.update { currentState -> val steps = currentState .questionSteps @@ -135,7 +146,9 @@ class QuizScreenViewModel @Inject constructor( if (currentQuestionStep != null) { val questionCorrect = currentState.selectedAnswer.isCorrect(currentQuestionStep.question) - set(currentQuestionIndex, currentQuestionStep.changeToCompleted(questionCorrect)) + + val completedQuestionStep = currentQuestionStep.changeToCompleted(questionCorrect, currentState.selectedAnswer) + set(currentQuestionIndex, completedQuestionStep) } } @@ -154,4 +167,12 @@ class QuizScreenViewModel @Inject constructor( savedQuestionsRepository.insertQuestions(currentQuestion) } + + private fun endGame(questionSteps: List) { + viewModelScope.launch(Dispatchers.IO) { + val questionStepsStr = Json.encodeToString(questionSteps) + delay(1000) + sendNavEventAsync(NavEvent.Navigate(ResultsScreenDestination(questionStepsStr))) + } + } } \ No newline at end of file diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/CardQuestionAnswer.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/CardQuestionAnswer.kt index 1f8d0a11..75160674 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/CardQuestionAnswer.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/CardQuestionAnswer.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -23,7 +24,7 @@ import com.infinitepower.newquiz.core.theme.CustomColor import com.infinitepower.newquiz.core.theme.NewQuizTheme import com.infinitepower.newquiz.core.theme.extendedColors import com.infinitepower.newquiz.core.theme.spacing -import com.infinitepower.newquiz.quiz_presentation.SelectedAnswer +import com.infinitepower.newquiz.model.question.SelectedAnswer @Composable @ExperimentalMaterial3Api @@ -31,7 +32,9 @@ internal fun CardQuestionAnswers( modifier: Modifier = Modifier, answers: List, selectedAnswer: SelectedAnswer, - onOptionClick: (selectedAnswer: SelectedAnswer) -> Unit + isResultsScreen: Boolean = false, + resultsSelectedAnswer: SelectedAnswer = SelectedAnswer.NONE, + onOptionClick: (selectedAnswer: SelectedAnswer) -> Unit = {} ) { val spaceSmall = MaterialTheme.spacing.small @@ -42,12 +45,13 @@ internal fun CardQuestionAnswers( verticalArrangement = Arrangement.spacedBy(spaceSmall), ) { answers.forEachIndexed { index, answer -> - val selected = selectedAnswer.index == index - CardQuestionAnswer( modifier = Modifier.fillMaxWidth(), description = answer, - selected = selected, + selected = selectedAnswer.index == index, + isResults = isResultsScreen, + resultAnswerCorrect = resultsSelectedAnswer.index == index, + answerCorrect = selectedAnswer == resultsSelectedAnswer, onClick = { onOptionClick(SelectedAnswer.fromIndex(index)) } ) } @@ -63,28 +67,41 @@ internal fun CardQuestionAnswer( selected: Boolean, isResults: Boolean = false, resultAnswerCorrect: Boolean = false, + answerCorrect: Boolean = false, onClick: () -> Unit ) { - val color = animateColorAsState( + val color by animateColorAsState( targetValue = when { - selected -> MaterialTheme.colorScheme.primary + isResults && selected && !answerCorrect -> MaterialTheme.extendedColors.getColorAccentByKey(key = CustomColor.Keys.Red) isResults && resultAnswerCorrect -> MaterialTheme.extendedColors.getColorAccentByKey(key = CustomColor.Keys.Green) + selected -> MaterialTheme.colorScheme.primary else -> MaterialTheme.colorScheme.surface } ) + val textColor by animateColorAsState( + targetValue = when { + isResults && selected && !answerCorrect ->MaterialTheme.extendedColors.getColorOnAccentByKey(key = CustomColor.Keys.Red) + isResults && resultAnswerCorrect -> MaterialTheme.extendedColors.getColorOnAccentByKey(key = CustomColor.Keys.Green) + selected -> MaterialTheme.colorScheme.onPrimary + else -> MaterialTheme.colorScheme.onSurface + } + ) + Surface( modifier = modifier.fillMaxWidth(), shape = CircleShape, tonalElevation = 8.dp, - color = color.value, + color = color, onClick = onClick, - selected = selected + selected = selected, + enabled = !isResults ) { Text( text = description, modifier = Modifier.padding(MaterialTheme.spacing.medium), - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, + color = textColor ) } } diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/QuizStepView.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/QuizStepView.kt index 13a5fd1d..ad8027dd 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/QuizStepView.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/QuizStepView.kt @@ -18,9 +18,39 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.infinitepower.newquiz.core.common.annotation.compose.PreviewNightLight import com.infinitepower.newquiz.core.theme.NewQuizTheme +import com.infinitepower.newquiz.core.theme.spacing import com.infinitepower.newquiz.model.question.QuestionStep import com.infinitepower.newquiz.model.question.getBasicQuestion +@Composable +internal fun QuizStepViewRow( + modifier: Modifier = Modifier, + questionSteps: List, + isResultsScreen: Boolean = false, + onClick: (index: Int, questionStep: QuestionStep) -> Unit = { _, _ -> } +) { + LazyRow( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), + contentPadding = PaddingValues(horizontal = MaterialTheme.spacing.medium) + ) { + itemsIndexed( + items = questionSteps, + key = { _, step -> step.question.id } + ) { index, step -> + val position = index + 1 + + QuizStepView( + questionStep = step, + position = position, + enabled = isResultsScreen, + onClick = { onClick(index, step) } + ) + } + } +} + @Composable @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) internal fun QuizStepView( diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/QuizTopBar.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/QuizTopBar.kt index 47428e14..289ff376 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/QuizTopBar.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/components/QuizTopBar.kt @@ -1,22 +1,24 @@ package com.infinitepower.newquiz.quiz_presentation.components import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.SkipNext +import androidx.compose.material3.* import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout -import com.infinitepower.newquiz.core.R import com.infinitepower.newquiz.core.theme.spacing import com.infinitepower.newquiz.core.ui.components.icon.button.BackIconButton import com.infinitepower.newquiz.compose.ui.RoundCircularProgressIndicator +import com.infinitepower.newquiz.core.common.annotation.compose.PreviewNightLight +import com.infinitepower.newquiz.core.theme.NewQuizTheme @Composable internal fun QuizTopBar( @@ -29,7 +31,7 @@ internal fun QuizTopBar( val spaceMedium = MaterialTheme.spacing.medium ConstraintLayout(modifier = modifier) { - val (btnBackRef, progressRef) = createRefs() + val (btnBackRef, progressRef, btnSkipRef) = createRefs() BackIconButton( onClick = onBackClick, @@ -60,5 +62,37 @@ internal fun QuizTopBar( style = MaterialTheme.typography.titleMedium ) } + + FilledTonalIconButton( + onClick = {}, + modifier = Modifier.constrainAs(btnSkipRef) { + top.linkTo(progressRef.top) + bottom.linkTo(progressRef.bottom) + end.linkTo(parent.end, spaceMedium) + } + ) { + Icon( + imageVector = Icons.Rounded.SkipNext, + contentDescription = "Skip question", + ) + } + } +} + +@Composable +@PreviewNightLight +private fun QuizTopBarPreview() { + NewQuizTheme { + Surface { + QuizTopBar( + windowHeightSizeClass = WindowHeightSizeClass.Medium, + progressText = "0:00", + progressIndicatorValue = 0f, + onBackClick = {}, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) + } } } \ No newline at end of file diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreen.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreen.kt index 023f37f8..42928075 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreen.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreen.kt @@ -7,9 +7,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.hilt.navigation.compose.hiltViewModel import com.infinitepower.newquiz.core.theme.spacing import com.infinitepower.newquiz.core.ui.home_card.components.HomeCardItemContent import com.infinitepower.newquiz.core.ui.home_card.model.HomeCardItem @@ -20,10 +23,13 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator @Composable @Destination fun QuizListScreen( - navigator: DestinationsNavigator + navigator: DestinationsNavigator, + viewModel: QuizListScreenViewModel = hiltViewModel() ) { - val cardItemData = remember { - QuizListCardItemData(navigator) + val uiState by viewModel.uiState.collectAsState() + + val cardItemData = remember(uiState.savedQuestionsSize) { + QuizListCardItemData(navigator, uiState.savedQuestionsSize) } QuizListScreenImpl(cardItemData = cardItemData.items) diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreenUiState.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreenUiState.kt new file mode 100644 index 00000000..320227e0 --- /dev/null +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreenUiState.kt @@ -0,0 +1,8 @@ +package com.infinitepower.newquiz.quiz_presentation.list + +import androidx.annotation.Keep + +@Keep +data class QuizListScreenUiState( + val savedQuestionsSize: Int = 0 +) \ No newline at end of file diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreenViewModel.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreenViewModel.kt new file mode 100644 index 00000000..18180a55 --- /dev/null +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/QuizListScreenViewModel.kt @@ -0,0 +1,26 @@ +package com.infinitepower.newquiz.quiz_presentation.list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.infinitepower.newquiz.domain.repository.question.saved_questions.SavedQuestionsRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@HiltViewModel +class QuizListScreenViewModel @Inject constructor( + private val savedQuestionsRepository: SavedQuestionsRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(QuizListScreenUiState()) + val uiState = _uiState.asStateFlow() + + init { + savedQuestionsRepository + .getFlowQuestions() + .onEach { res -> + _uiState.update { currentState -> + currentState.copy(savedQuestionsSize = res.size) + } + }.launchIn(viewModelScope) + } +} \ No newline at end of file diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/data/QuizListCardItemData.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/data/QuizListCardItemData.kt index f0fda6b1..65d271a9 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/data/QuizListCardItemData.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/list/data/QuizListCardItemData.kt @@ -12,7 +12,8 @@ import com.infinitepower.newquiz.quiz_presentation.destinations.SavedQuestionsSc import com.ramcosta.composedestinations.navigation.DestinationsNavigator internal class QuizListCardItemData( - navigator: DestinationsNavigator + navigator: DestinationsNavigator, + savedQuestionsSize: Int ) : CardItemDataCore { override val items = listOf( HomeCardItem.GroupTitle( @@ -26,10 +27,11 @@ internal class QuizListCardItemData( HomeCardItem.GroupTitle( title = R.string.saved_questions, ), - HomeCardItem.LargeCard( + HomeCardItem.MediumCard( title = R.string.saved_questions, icon = CardIcon.Icon(Icons.Rounded.Save), - onClick = { navigator.navigate(SavedQuestionsScreenDestination) } + onClick = { navigator.navigate(SavedQuestionsScreenDestination) }, + description = "$savedQuestionsSize questions available" ), ) } \ No newline at end of file diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/results/ResultsScreen.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/results/ResultsScreen.kt new file mode 100644 index 00000000..8efc846f --- /dev/null +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/results/ResultsScreen.kt @@ -0,0 +1,178 @@ +package com.infinitepower.newquiz.quiz_presentation.results + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import com.infinitepower.newquiz.core.R +import com.infinitepower.newquiz.core.common.annotation.compose.PreviewNightLight +import com.infinitepower.newquiz.core.theme.NewQuizTheme +import com.infinitepower.newquiz.core.theme.spacing +import com.infinitepower.newquiz.model.question.* +import com.infinitepower.newquiz.quiz_presentation.components.CardQuestionAnswers +import com.infinitepower.newquiz.quiz_presentation.components.QuizStepViewRow +import com.infinitepower.newquiz.quiz_presentation.destinations.QuizScreenDestination +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json + +@Composable +@Destination +fun ResultsScreen( + questionStepsStr: String, + navigator: DestinationsNavigator +) { + val questionSteps: List = Json.decodeFromString(questionStepsStr) + + ResultsScreenImpl( + questionSteps = questionSteps, + onBackClick = navigator::popBackStack, + onPlayAgainClick = { navigator.navigate(QuizScreenDestination()) } + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun ResultsScreenImpl( + questionSteps: List, + onBackClick: () -> Unit, + onPlayAgainClick: () -> Unit +) { + val winnerSpec = LottieCompositionSpec.RawRes(R.raw.trophy_winner) + val winnerLottieComposition by rememberLottieComposition(spec = winnerSpec) + + val spaceMedium = MaterialTheme.spacing.medium + val spaceLarge = MaterialTheme.spacing.large + + val (questionDialog, setQuestionDialog) = remember { + mutableStateOf(null) + } + + Scaffold( + topBar = { + SmallTopAppBar( + title = { + Text(text = "Results screen") + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .padding(spaceMedium) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Surface( + modifier = Modifier.size(300.dp), + tonalElevation = 8.dp, + shape = CircleShape + ) { + LottieAnimation( + composition = winnerLottieComposition, + modifier = Modifier + .fillMaxSize() + .padding(spaceMedium), + iterations = LottieConstants.IterateForever, + ) + } + Spacer(modifier = Modifier.height(spaceLarge)) + Text( + text = "${questionSteps.countCorrectQuestions()}/${questionSteps.size} correct questions", + style = MaterialTheme.typography.headlineMedium + ) + Spacer(modifier = Modifier.height(spaceLarge)) + QuizStepViewRow( + modifier = Modifier.fillMaxWidth(), + questionSteps = questionSteps, + isResultsScreen = true, + onClick = { index, _ -> + setQuestionDialog(index) + } + ) + Spacer(modifier = Modifier.weight(1f)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(spaceMedium), + modifier = Modifier.padding(bottom = spaceLarge) + ) { + OutlinedButton( + onClick = onPlayAgainClick, + modifier = Modifier.weight(1f) + ) { + Text(text = "Play again") + } + Button( + onClick = onBackClick, + modifier = Modifier.weight(1f) + ) { + Text(text = "Back") + } + } + } + } + + if (questionDialog != null) { + val questionStep = questionSteps[questionDialog] + val question = questionStep.question + + AlertDialog( + onDismissRequest = { setQuestionDialog(null) }, + title = { Text(text = question.description) }, + text = { + CardQuestionAnswers( + answers = question.answers, + selectedAnswer = questionStep.selectedAnswer, + resultsSelectedAnswer = SelectedAnswer.fromIndex(question.correctAns), + isResultsScreen = true + ) + }, + confirmButton = { + TextButton(onClick = { setQuestionDialog(null) }) { + Text(text = "Close") + } + } + ) + } +} + +@Composable +@PreviewNightLight +private fun ResultsScreenPreview() { + val questionSteps = listOf( + QuestionStep.Completed( + question = getBasicQuestion(), + correct = true + ), + QuestionStep.Completed( + question = getBasicQuestion(), + correct = false + ), + QuestionStep.Completed( + question = getBasicQuestion(), + correct = true + ), + ) + + NewQuizTheme { + Surface { + ResultsScreenImpl( + questionSteps = questionSteps, + onBackClick = {}, + onPlayAgainClick = {} + ) + } + } +} \ No newline at end of file diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/saved_questions/SavedQuestionsUiState.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/saved_questions/SavedQuestionsUiState.kt index e8e1dd1e..e14ba54a 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/saved_questions/SavedQuestionsUiState.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/saved_questions/SavedQuestionsUiState.kt @@ -11,7 +11,7 @@ data class SavedQuestionsUiState( val arrayListSelectedQuestions: ArrayList get() = ArrayList(selectedQuestions.takeLast(100)) - fun randomQuestions(limit: Int = 10): ArrayList { + fun randomQuestions(limit: Int = 5): ArrayList { val randomQuestions = questions.shuffled().take(limit) return ArrayList(randomQuestions) } diff --git a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/saved_questions/SavedQuestionsViewModel.kt b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/saved_questions/SavedQuestionsViewModel.kt index 89463e23..f2732f41 100644 --- a/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/saved_questions/SavedQuestionsViewModel.kt +++ b/quiz-presentation/src/main/java/com/infinitepower/newquiz/quiz_presentation/saved_questions/SavedQuestionsViewModel.kt @@ -36,15 +36,13 @@ class SavedQuestionsViewModel @Inject constructor( */ init { - viewModelScope.launch(Dispatchers.IO) { - savedQuestionsRepository - .getFlowQuestions() - .collect { questions -> - _uiState.update { currentState -> - currentState.copy(questions = questions) - } + savedQuestionsRepository + .getFlowQuestions() + .onEach { questions -> + _uiState.update { currentState -> + currentState.copy(questions = questions) } - } + }.launchIn(viewModelScope) } fun onEvent(event: SavedQuestionsUiEvent) { diff --git a/settings-presentation/build.gradle.kts b/settings-presentation/build.gradle.kts index a5af5066..8a494517 100644 --- a/settings-presentation/build.gradle.kts +++ b/settings-presentation/build.gradle.kts @@ -80,6 +80,8 @@ dependencies { ksp("io.github.raamcosta.compose-destinations:ksp:_") implementation(project(Modules.core)) + implementation(project(Modules.data)) + implementation(project(Modules.domain)) } tasks.withType { diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/SettingsScreen.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/SettingsScreen.kt index 8044595e..8a2fcdf9 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/SettingsScreen.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/SettingsScreen.kt @@ -29,10 +29,12 @@ import com.infinitepower.newquiz.core.theme.CustomColor import com.infinitepower.newquiz.core.theme.extendedColors import com.infinitepower.newquiz.core.theme.spacing import com.infinitepower.newquiz.core.ui.components.icon.button.BackIconButton +import com.infinitepower.newquiz.domain.repository.wordle.daily.DailyWordleRepository import com.infinitepower.newquiz.settings_presentation.components.PreferencesScreen import com.infinitepower.newquiz.settings_presentation.data.SettingsScreenPageData import com.infinitepower.newquiz.settings_presentation.destinations.SettingsScreenDestination import com.infinitepower.newquiz.settings_presentation.model.ScreenKey +import com.infinitepower.newquiz.core.R as CoreR import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.spec.Direction @@ -52,6 +54,7 @@ fun SettingsScreen( SettingsScreenImpl( uiState = uiState, + dailyWordleRepository = settingsViewModel.dailyWordleRepository, onBackClick = navigator::popBackStack, onNavigateClickClick = navigator::navigate ) @@ -60,6 +63,7 @@ fun SettingsScreen( @Composable private fun SettingsScreenImpl( uiState: SettingsUiState, + dailyWordleRepository: DailyWordleRepository, onBackClick: () -> Unit, onNavigateClickClick: (direction: Direction) -> Unit ) { @@ -70,7 +74,8 @@ private fun SettingsScreenImpl( ) else -> PreferencesScreen( page = SettingsScreenPageData.getPage(uiState.screenKey), - onBackClick = onBackClick + onBackClick = onBackClick, + dailyWordleRepository = dailyWordleRepository ) } } @@ -95,14 +100,17 @@ fun MainSettingsScreen( SettingsBaseItemData( key = SettingsScreenPageData.General.key, icon = Icons.Rounded.Settings, - name = stringResource(id = SettingsScreenPageData.General.stringRes), - colorRoles = MaterialTheme.extendedColors.getColorRolesByKey(key = CustomColor.Keys.Blue) + name = stringResource(id = SettingsScreenPageData.General.stringRes) ), SettingsBaseItemData( key = SettingsScreenPageData.Quiz.key, icon = Icons.Rounded.Quiz, - name = stringResource(id = SettingsScreenPageData.Quiz.stringRes), - colorRoles = MaterialTheme.extendedColors.getColorRolesByKey(key = CustomColor.Keys.Blue) + name = stringResource(id = SettingsScreenPageData.Quiz.stringRes) + ), + SettingsBaseItemData( + key = SettingsScreenPageData.Wordle.key, + icon = Icons.Rounded.Password, + name = stringResource(id = SettingsScreenPageData.Wordle.stringRes) ), ) @@ -112,7 +120,7 @@ fun MainSettingsScreen( topBar = { LargeTopAppBar( title = { - Text(text = stringResource(id = R.string.settings)) + Text(text = stringResource(id = CoreR.string.settings)) }, scrollBehavior = scrollBehavior, navigationIcon = { BackIconButton(onClick = onBackClick) } @@ -150,6 +158,7 @@ fun MainSettingsScreen( } @Composable +@OptIn(ExperimentalMaterial3Api::class) private fun SettingsBaseItem( modifier: Modifier = Modifier, data: SettingsBaseItemData, @@ -169,13 +178,13 @@ private fun SettingsBaseItem( modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) { Surface( - color = data.colorRoles.accent, + color = MaterialTheme.colorScheme.secondary, shape = CircleShape ) { Icon( imageVector = data.icon, contentDescription = data.name, - tint = data.colorRoles.onAccent, + tint = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.padding(12.dp) ) } @@ -191,8 +200,7 @@ private fun SettingsBaseItem( private data class SettingsBaseItemData( val key: ScreenKey, val icon: ImageVector, - val name: String, - val colorRoles: CustomColor.ColorRoles + val name: String ) @Composable diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/SettingsViewModel.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/SettingsViewModel.kt index 0921f90c..3afc3f6f 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/SettingsViewModel.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/SettingsViewModel.kt @@ -3,34 +3,30 @@ package com.infinitepower.newquiz.settings_presentation import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.infinitepower.newquiz.domain.repository.wordle.daily.DailyWordleRepository import com.infinitepower.newquiz.settings_presentation.data.SettingsScreenPageData import com.infinitepower.newquiz.settings_presentation.model.ScreenKey import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.* import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle + savedStateHandle: SavedStateHandle, + val dailyWordleRepository: DailyWordleRepository ) : ViewModel() { private val _uiState = MutableStateFlow(SettingsUiState()) val uiState = _uiState.asStateFlow() init { - viewModelScope.launch(Dispatchers.IO) { - savedStateHandle - .getStateFlow( - key = SettingsScreenNavArgs::screenKey.name, - initialValue = SettingsScreenPageData.MainPage.key.value - ).collect { key -> - _uiState.update { currentState -> - currentState.copy(screenKey = ScreenKey(key)) - } + savedStateHandle + .getStateFlow( + key = SettingsScreenNavArgs::screenKey.name, + initialValue = SettingsScreenPageData.MainPage.key.value + ).onEach { key -> + _uiState.update { currentState -> + currentState.copy(screenKey = ScreenKey(key)) } - } + }.launchIn(viewModelScope) } } \ No newline at end of file diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceGroupHeader.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceGroupHeader.kt index 28cebfac..45bcfa84 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceGroupHeader.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceGroupHeader.kt @@ -28,9 +28,9 @@ internal fun LazyItemScope.PreferenceGroupHeader(title: String) { ) { Text( text = title, - style = MaterialTheme.typography.labelMedium, + style = MaterialTheme.typography.labelLarge, modifier = Modifier.padding(start = spaceMedium), - color = MaterialTheme.colorScheme.secondary + color = MaterialTheme.colorScheme.primary ) } } diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceItem.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceItem.kt index 429dccf3..e1866e5f 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceItem.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceItem.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable +@ExperimentalMaterial3Api internal fun PreferenceItem( item: Preference.PreferenceItem<*>, prefs: Preferences?, diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceScreen.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceScreen.kt index 1d8b5a7c..a8298e4d 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceScreen.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferenceScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -19,15 +20,14 @@ import com.infinitepower.newquiz.settings_presentation.model.Preference * @param items [Preference] items which should be displayed on the preference screen. An item can be a single [PreferenceItem] or a group ([PreferenceGroup]) * @param dataStore a [DataStore] where the preferences will be saved * @param modifier [Modifier] to be applied to the preferenceScreen layout - * @param statusBarPadding whether statusBar padding is needed. Set to true if your app is laid out edgeToEdge */ @Composable +@ExperimentalMaterial3Api fun PreferenceScreen( items: List, dataStore: DataStore, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), - statusBarPadding: Boolean = false, + contentPadding: PaddingValues = PaddingValues(0.dp) ) { val dataStoreManager = remember { DataStoreManagerImpl(dataStore) @@ -37,8 +37,7 @@ fun PreferenceScreen( items = items, modifier = modifier, dataStoreManager = dataStoreManager, - contentPadding = contentPadding, - statusBarPadding = statusBarPadding + contentPadding = contentPadding ) } @@ -47,15 +46,14 @@ fun PreferenceScreen( * @param items [Preference] items which should be displayed on the preference screen. An item can be a single [PreferenceItem] or a group ([PreferenceGroup]) * @param dataStoreManager a [DataStoreManagerImpl] responsible for the dataStore backing the preference screen * @param modifier [Modifier] to be applied to the preferenceScreen layout - * @param statusBarPadding whether statusBar padding is needed. Set to true if your app is laid out edgeToEdge */ @Composable +@ExperimentalMaterial3Api fun PreferenceScreen( items: List, dataStoreManager: DataStoreManagerImpl, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), - statusBarPadding: Boolean = false, + contentPadding: PaddingValues = PaddingValues(0.dp) ) { val prefs by dataStoreManager.preferenceFlow.collectAsState(initial = null) diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferencesScreen.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferencesScreen.kt index 091819f0..02929848 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferencesScreen.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/PreferencesScreen.kt @@ -15,12 +15,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.infinitepower.newquiz.core.common.dataStore.settingsDataStore import com.infinitepower.newquiz.core.dataStore.manager.DataStoreManagerImpl +import com.infinitepower.newquiz.domain.repository.wordle.daily.DailyWordleRepository import com.infinitepower.newquiz.settings_presentation.data.SettingsScreenPageData @Composable @OptIn(ExperimentalMaterial3Api::class) fun PreferencesScreen( page: SettingsScreenPageData, + dailyWordleRepository: DailyWordleRepository, onBackClick: () -> Unit ) { val decayAnimationSpec = rememberSplineBasedDecay() @@ -61,6 +63,11 @@ fun PreferencesScreen( is SettingsScreenPageData.MainPage -> emptyList() is SettingsScreenPageData.General -> page.items(scope, dataStoreManager) is SettingsScreenPageData.Quiz -> page.items() + is SettingsScreenPageData.Wordle -> page.items( + scope, + dataStoreManager, + dailyWordleRepository + ) }, dataStore = dataStore, modifier = Modifier diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/DropDownPreferenceWidget.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/DropDownPreferenceWidget.kt index 163d2f26..a6686bb5 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/DropDownPreferenceWidget.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/DropDownPreferenceWidget.kt @@ -2,10 +2,7 @@ package com.infinitepower.newquiz.settings_presentation.components.widgets import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -15,6 +12,7 @@ import androidx.compose.ui.unit.dp import com.infinitepower.newquiz.settings_presentation.model.Preference @Composable +@ExperimentalMaterial3Api internal fun DropDownPreferenceWidget( preference: Preference.PreferenceItem.DropDownMenuPreference, value: String, diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/ListPreferenceWidget.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/ListPreferenceWidget.kt index 523eed15..018247d9 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/ListPreferenceWidget.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/ListPreferenceWidget.kt @@ -1,8 +1,5 @@ package com.infinitepower.newquiz.settings_presentation.components.widgets -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.selectable @@ -11,81 +8,102 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties +import com.infinitepower.newquiz.core.common.annotation.compose.PreviewNightLight +import com.infinitepower.newquiz.core.theme.NewQuizTheme import com.infinitepower.newquiz.settings_presentation.model.Preference @Composable -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) +@ExperimentalMaterial3Api internal fun ListPreferenceWidget( preference: Preference.PreferenceItem.ListPreference, value: String, onValueChange: (String) -> Unit ) { val (isDialogShown, showDialog) = remember { mutableStateOf(false) } + val dismissDialog = { showDialog(false) } + + val (newValue, setNewValue) = remember(value) { + mutableStateOf(value) + } TextPreferenceWidget( preference = preference, - summary = value, + summary = preference.entries[value], onClick = { showDialog(!isDialogShown) }, ) if (isDialogShown) { AlertDialog( - onDismissRequest = { showDialog(!isDialogShown) }, + onDismissRequest = dismissDialog, title = { Text(text = preference.title) }, text = { LazyColumn(modifier = Modifier.selectableGroup()) { items(preference.entries.keys.toList()) { key -> - val isSelected = value == key - val onSelected = { - onValueChange(key) - } - Row( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = isSelected, - onClick = { if (!isSelected) onSelected() } - ).padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = isSelected, - onClick = { if (!isSelected) onSelected() }, - ) - preference.entries[key]?.let { value -> - Text( - text = value, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(start = 16.dp) - ) - } - } + val isSelected = newValue == key + val onSelected = { setNewValue(key) } + + SelectableListItem( + text = preference.entries[key].orEmpty(), + isSelected = isSelected, + onClick = onSelected + ) } } }, - properties = DialogProperties( - usePlatformDefaultWidth = true - ), confirmButton = { TextButton( - onClick = { showDialog(false) } + onClick = { + dismissDialog() + onValueChange(newValue) + } ) { Text(text = "Confirm") } }, dismissButton = { - TextButton( - onClick = { showDialog(false) } - ) { + TextButton(onClick = dismissDialog) { Text(text = "Dismiss") } } ) } +} + +@Composable +@ExperimentalMaterial3Api +private fun SelectableListItem( + modifier: Modifier = Modifier, + text: String, + isSelected: Boolean, + onClick: () -> Unit +) { + ListItem( + headlineText = { Text(text = text) }, + leadingContent = { + RadioButton( + selected = isSelected, + onClick = onClick, + ) + }, + modifier = modifier.selectable( + selected = isSelected, + onClick = onClick + ) + ) +} + +@Composable +@PreviewNightLight +@OptIn(ExperimentalMaterial3Api::class) +private fun SelectableListItemPreview() { + NewQuizTheme { + Surface { + SelectableListItem( + text = "NewQuiz", + isSelected = true, + onClick = {} + ) + } + } } \ No newline at end of file diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/MultiSelectListPreferenceWidget.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/MultiSelectListPreferenceWidget.kt index 2f273d99..af311443 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/MultiSelectListPreferenceWidget.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/MultiSelectListPreferenceWidget.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.unit.dp import com.infinitepower.newquiz.settings_presentation.model.Preference @Composable -@OptIn(ExperimentalMaterial3Api::class) +@ExperimentalMaterial3Api internal fun MultiSelectListPreferenceWidget( preference: Preference.PreferenceItem.MultiSelectListPreference, values: Set, diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/SeekBarPreferenceWidget.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/SeekBarPreferenceWidget.kt index 202a5000..3403cb46 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/SeekBarPreferenceWidget.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/SeekBarPreferenceWidget.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,6 +19,7 @@ import com.infinitepower.newquiz.settings_presentation.model.Preference import kotlin.math.roundToInt @Composable +@ExperimentalMaterial3Api internal fun SeekBarPreferenceWidget( preference: Preference.PreferenceItem.SeekBarPreference, value: Int, diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/SwitchPreferenceWidget.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/SwitchPreferenceWidget.kt index 1cd90142..ccf60d96 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/SwitchPreferenceWidget.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/SwitchPreferenceWidget.kt @@ -1,5 +1,6 @@ package com.infinitepower.newquiz.settings_presentation.components.widgets +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -7,6 +8,7 @@ import com.infinitepower.newquiz.core.compose.preferences.LocalPreferenceEnabled import com.infinitepower.newquiz.settings_presentation.model.Preference @Composable +@ExperimentalMaterial3Api internal fun SwitchPreferenceWidget( preference: Preference.PreferenceItem.SwitchPreference, value: Boolean, diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/TextPreferenceWidget.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/TextPreferenceWidget.kt index fe04f9cf..c99a769c 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/TextPreferenceWidget.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/components/widgets/TextPreferenceWidget.kt @@ -1,6 +1,5 @@ package com.infinitepower.newquiz.settings_presentation.components.widgets -import androidx.annotation.StringRes import androidx.compose.foundation.clickable import androidx.compose.material3.* import androidx.compose.runtime.Composable @@ -10,11 +9,11 @@ import com.infinitepower.newquiz.core.ui.StatusWrapper import com.infinitepower.newquiz.settings_presentation.model.Preference @Composable -@OptIn(ExperimentalMaterial3Api::class) +@ExperimentalMaterial3Api internal fun TextPreferenceWidget( preference: Preference.PreferenceItem<*>, summary: String? = null, - onClick: () -> Unit = { }, + onClick: () -> Unit = {}, trailing: @Composable (() -> Unit)? = null ) { val isEnabled = LocalPreferenceEnabledStatus.current && preference.enabled @@ -60,7 +59,7 @@ internal fun TextPreferenceWidget( } @Composable -@OptIn(ExperimentalMaterial3Api::class) +@ExperimentalMaterial3Api internal fun TextPreferenceWidgetRes( preference: Preference.PreferenceItem<*>, summary: String? = null, @@ -92,7 +91,7 @@ internal fun TextPreferenceWidgetRes( } @Composable -@OptIn(ExperimentalMaterial3Api::class) +@ExperimentalMaterial3Api internal fun TextPreferenceWidget( preference: Preference.PreferenceItem<*>, summary: @Composable () -> Unit, diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/data/SettingsScreenPageData.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/data/SettingsScreenPageData.kt index f764997a..93ee1752 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/data/SettingsScreenPageData.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/data/SettingsScreenPageData.kt @@ -1,38 +1,44 @@ package com.infinitepower.newquiz.settings_presentation.data import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Language import androidx.compose.material.icons.rounded.QuestionMark import androidx.compose.material.icons.rounded.Visibility import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import com.infinitepower.newquiz.core.common.dataStore.SettingsCommon import com.infinitepower.newquiz.core.dataStore.manager.DataStoreManager -import com.infinitepower.newquiz.core.dataStore.manager.DataStoreManagerImpl -import com.infinitepower.newquiz.settings_presentation.R +import com.infinitepower.newquiz.domain.repository.wordle.daily.DailyWordleRepository +import com.infinitepower.newquiz.core.R as CoreR import com.infinitepower.newquiz.settings_presentation.model.Preference import com.infinitepower.newquiz.settings_presentation.model.ScreenKey import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch sealed class SettingsScreenPageData(val key: ScreenKey) { abstract val stringRes: Int companion object { - fun getPage(key: ScreenKey) = when(key) { + fun getPage(key: ScreenKey) = when (key) { MainPage.key -> MainPage General.key -> General Quiz.key -> Quiz + Wordle.key -> Wordle else -> MainPage } } object MainPage : SettingsScreenPageData(key = ScreenKey("main")) { override val stringRes: Int - get() = R.string.settings + get() = CoreR.string.settings } object General : SettingsScreenPageData(key = ScreenKey("general")) { override val stringRes: Int - get() = R.string.general + get() = CoreR.string.general @Composable fun items( @@ -50,13 +56,27 @@ sealed class SettingsScreenPageData(val key: ScreenKey) { contentDescription = "Show login card", ) } + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(id = CoreR.string.clear_settings), + summary = stringResource(id = CoreR.string.remove_all_saved_settings), + icon = { + Icon( + imageVector = Icons.Rounded.Delete, + contentDescription = stringResource(id = CoreR.string.clear_settings) + ) + }, + enabled = true, + onClick = { + scope.launch(Dispatchers.IO) { dataStoreManager.clearPreferences() } + } ) ) } object Quiz : SettingsScreenPageData(key = ScreenKey("quiz")) { override val stringRes: Int - get() = R.string.quiz + get() = CoreR.string.quiz @Composable fun items() = listOf( @@ -70,8 +90,80 @@ sealed class SettingsScreenPageData(val key: ScreenKey) { contentDescription = "Quick quiz question size", ) }, - valueRepresentation = { "$it" }, - valueRange = (5..20), + valueRange = (5..20) + ) + ) + } + + object Wordle : SettingsScreenPageData(key = ScreenKey("wordle")) { + override val stringRes: Int + get() = CoreR.string.wordle + + @Composable + fun items( + scope: CoroutineScope, + dataStoreManager: DataStoreManager, + dailyWordleRepository: DailyWordleRepository + ) = listOf( + Preference.PreferenceItem.SwitchPreference( + request = SettingsCommon.WordleHardMode, + title = stringResource(id = CoreR.string.hard_mode), + summary = stringResource(id = CoreR.string.any_revealed_hints_must_be_used_in_subsequest_guesses) + ), + Preference.PreferenceItem.SwitchPreference( + request = SettingsCommon.WordleColorBlindMode, + title = stringResource(id = CoreR.string.color_blind_mode), + summary = stringResource(id = CoreR.string.high_contrast_colors) + ), + Preference.PreferenceItem.SwitchPreference( + request = SettingsCommon.WordleLetterHints, + title = stringResource(id = CoreR.string.letter_hints), + summary = stringResource(id = CoreR.string.hint_above_the_letter_that_it_appears_twice_or_more_in_the_hidden_word) + ), + Preference.PreferenceGroup( + title = stringResource(id = CoreR.string.wordle_infinite), + preferenceItems = listOf( + Preference.PreferenceItem.ListPreference( + request = SettingsCommon.InfiniteWordleQuizLanguage, + title = stringResource(id = CoreR.string.quiz_language), + icon = { + Icon( + imageVector = Icons.Rounded.Language, + contentDescription = stringResource(id = CoreR.string.quiz_language), + ) + }, + entries = mapOf( + "en" to stringResource(id = CoreR.string.english), + "pt" to stringResource(id = CoreR.string.portuguese) + ) + ), + Preference.PreferenceItem.SwitchPreference( + request = SettingsCommon.WordleInfiniteRowsLimited, + title = stringResource(id = CoreR.string.rows_limited), + summary = stringResource(id = CoreR.string.wordle_infinite_row_limited) + ), + Preference.PreferenceItem.SeekBarPreference( + request = SettingsCommon.WordleInfiniteRowsLimit, + title = stringResource(id = CoreR.string.rows_limited), + summary = stringResource(id = CoreR.string.wordle_infinite_row_limit_value), + valueRange = 2..30, + dependency = listOf(SettingsCommon.WordleInfiniteRowsLimited) + ) + ) + ), + Preference.PreferenceGroup( + title = stringResource(id = CoreR.string.wordle_daily), + preferenceItems = listOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(id = CoreR.string.clean_calendar_data), + summary = stringResource(id = CoreR.string.clean_saved_calendar_wins_losses), + onClick = { + scope.launch(Dispatchers.IO) { + dailyWordleRepository.clearAllCalendarItems() + } + } + ) + ) ) ) } diff --git a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/model/Preference.kt b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/model/Preference.kt index 09742277..4e8b7f01 100644 --- a/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/model/Preference.kt +++ b/settings-presentation/src/main/java/com/infinitepower/newquiz/settings_presentation/model/Preference.kt @@ -31,7 +31,7 @@ sealed class Preference { data class TextPreference( override val title: String, override val summary: String? = null, - override val singleLineTitle: Boolean, + override val singleLineTitle: Boolean = true, override val dependency: List> = emptyList(), override val icon: @Composable (() -> Unit)? = null, override val enabled: Boolean = true, @@ -46,7 +46,7 @@ sealed class Preference { val request: PreferenceRequest, override val title: String, override val summary: String? = null, - override val singleLineTitle: Boolean, + override val singleLineTitle: Boolean = true, override val dependency: List> = emptyList(), override val icon: @Composable (() -> Unit)? = null, override val enabled: Boolean = true, @@ -60,12 +60,12 @@ sealed class Preference { val request: PreferenceRequest, override val title: String, override val summary: String? = null, - override val singleLineTitle: Boolean, + override val singleLineTitle: Boolean = true, override val dependency: List> = emptyList(), override val icon: @Composable (() -> Unit)? = null, override val enabled: Boolean = true, - val entries: Map, + val entries: Map ) : PreferenceItem() /** @@ -76,7 +76,7 @@ sealed class Preference { val request: PreferenceRequest>, override val title: String, override val summary: String? = null, - override val singleLineTitle: Boolean, + override val singleLineTitle: Boolean = true, override val dependency: List> = emptyList(), override val icon: @Composable (() -> Unit)? = null, override val enabled: Boolean = true, @@ -91,14 +91,14 @@ sealed class Preference { val request: PreferenceRequest, override val title: String, override val summary: String? = null, - override val singleLineTitle: Boolean, + override val singleLineTitle: Boolean = true, override val dependency: List> = emptyList(), override val icon: @Composable (() -> Unit)? = null, override val enabled: Boolean = true, val valueRange: ClosedRange = 0..10, val steps: Int = 0, - val valueRepresentation: (Int) -> String + val valueRepresentation: (Int) -> String = { it.toString() } ) : PreferenceItem() /** @@ -109,7 +109,7 @@ sealed class Preference { val request: PreferenceRequest, override val title: String, override val summary: String? = null, - override val singleLineTitle: Boolean, + override val singleLineTitle: Boolean = true, override val dependency: List> = emptyList(), override val icon: @Composable (() -> Unit)? = null, override val enabled: Boolean = true, diff --git a/settings-presentation/src/main/res/values/strings.xml b/settings-presentation/src/main/res/values/strings.xml index 3ec0f9d8..a6b3daec 100644 --- a/settings-presentation/src/main/res/values/strings.xml +++ b/settings-presentation/src/main/res/values/strings.xml @@ -1,6 +1,2 @@ - - Settings - General - Quiz - \ No newline at end of file + \ No newline at end of file diff --git a/versions.properties b/versions.properties index 52462394..935f3d3c 100644 --- a/versions.properties +++ b/versions.properties @@ -31,14 +31,14 @@ version.firebase-analytics-ktx=21.1.0 version.firebase-config-ktx=21.1.1 -version.google.accompanist=0.26.0-alpha +version.google.accompanist=0.26.1-alpha ## unused version.org.jacoco..org.jacoco.ant=0.8.8 version.leakcanary=2.9.1 - version.ktor=2.0.3 + version.ktor=2.1.0 version.kotlinx.serialization=1.4.0-RC @@ -63,12 +63,11 @@ version.google.dagger=2.43.2 ## unused version.google.android.play-services-ads=21.0.0 -version.google.android.material=1.7.0-alpha03 +version.google.android.material=1.7.0-beta01 version.firebase-crashlytics-gradle=2.9.1 version.firebase-bom=30.3.2 -## # available=30.3.2 version.com.google.truth..truth=1.1.3 @@ -87,7 +86,6 @@ version.com.airbnb.android..lottie-compose=5.2.0 version.coil-kt=2.1.0 version.androidx.work=2.8.0-alpha03 -## # available=2.8.0-alpha03 version.androidx.test.ext.junit=1.1.3 ## # available=1.1.4-alpha01 @@ -113,7 +111,6 @@ version.androidx.room=2.5.0-alpha02 version.androidx.paging-compose=1.0.0-alpha15 version.androidx.paging=3.2.0-alpha02 -## # available=3.2.0-alpha02 version.androidx.navigation-compose=2.5.1 @@ -128,30 +125,23 @@ version.androidx.hilt=1.0.0 version.androidx.core-splashscreen=1.0.0 version.androidx.core=1.9.0-beta01 -## # available=1.9.0-beta01 version.androidx.constraintlayout-compose=1.1.0-alpha03 version.androidx.compose.ui=1.3.0-alpha03 -## # available=1.3.0-alpha03 version.androidx.compose.material3=1.0.0-alpha16 -## # available=1.0.0-alpha16 version.androidx.compose.material=1.3.0-alpha03 -## # available=1.3.0-alpha03 ## unused version.androidx.compose.compiler=1.2.0-rc01 version.androidx.appcompat=1.6.0-beta01 -## # available=1.6.0-beta01 version.androidx.annotation=1.5.0-alpha02 -## # available=1.5.0-alpha02 version.androidx.activity=1.6.0-beta01 -## # available=1.6.0-beta01 plugin.com.google.gms.google-services=4.3.13 diff --git a/wordle/src/androidTest/java/com/infinitepower/newquiz/wordle/data/repository/wordle/daily/FakeDailyWordleRepository.kt b/wordle/src/androidTest/java/com/infinitepower/newquiz/wordle/data/repository/wordle/daily/FakeDailyWordleRepository.kt index df98e9f1..b24dd3e5 100644 --- a/wordle/src/androidTest/java/com/infinitepower/newquiz/wordle/data/repository/wordle/daily/FakeDailyWordleRepository.kt +++ b/wordle/src/androidTest/java/com/infinitepower/newquiz/wordle/data/repository/wordle/daily/FakeDailyWordleRepository.kt @@ -27,7 +27,7 @@ class FakeDailyWordleRepository @Inject constructor() : DailyWordleRepository { TODO("Not yet implemented") } - override suspend fun clearAll() { + override suspend fun clearAllCalendarItems() { TODO("Not yet implemented") } diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreen.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreen.kt index 5c6c0e77..797b55be 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreen.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreen.kt @@ -8,12 +8,19 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Info import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.infinitepower.newquiz.core.common.annotation.compose.PreviewNightLight import com.infinitepower.newquiz.core.theme.NewQuizTheme @@ -28,9 +35,15 @@ import com.infinitepower.newquiz.model.wordle.WordleItem import com.infinitepower.newquiz.model.wordle.WordleRowItem import com.infinitepower.newquiz.wordle.components.WordleKeyBoard import com.infinitepower.newquiz.wordle.components.WordleRowComponent +import com.infinitepower.newquiz.wordle.components.getItemRowBackgroundColor +import com.infinitepower.newquiz.wordle.components.getItemRowTextColor import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch +import com.infinitepower.newquiz.core.R as CoreR + +private const val WORDLE_BANNER_AD_ID = "ca-app-pub-1923025671607389/6274577111" +private const val WORDLE_REWARDED_AD_ID = "ca-app-pub-1923025671607389/9652850233" @Keep data class WordleScreenNavArgs( @@ -80,18 +93,30 @@ private fun WordleScreenImpl( val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() + val (infoDialogVisible, setInfoDialogVisibility) = remember { + mutableStateOf(false) + } + Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { SmallTopAppBar( title = { - Text(text = "Wordle") + Text(text = stringResource(id = CoreR.string.wordle)) }, navigationIcon = { IconButton(onClick = onBackClick) { Icon( imageVector = Icons.Rounded.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(id = CoreR.string.back) + ) + } + }, + actions = { + IconButton(onClick = { setInfoDialogVisibility(true) }) { + Icon( + imageVector = Icons.Rounded.Info, + contentDescription = stringResource(id = CoreR.string.info) ) } } @@ -101,12 +126,12 @@ private fun WordleScreenImpl( if (uiState.currentRowCompleted && !uiState.isGamedEnded) { ExtendedFloatingActionButton( text = { - Text(text = "Verify") + Text(text = stringResource(id = CoreR.string.verify)) }, icon = { Icon( imageVector = Icons.Rounded.Check, - contentDescription = "Verify" + contentDescription = stringResource(id = CoreR.string.verify) ) }, onClick = { @@ -118,7 +143,7 @@ private fun WordleScreenImpl( }, bottomBar = { BannerAd( - adId = "ca-app-pub-1923025671607389/6274577111", + adId = WORDLE_BANNER_AD_ID, adSize = getAdaptiveAdSize() ) } @@ -136,22 +161,36 @@ private fun WordleScreenImpl( horizontalAlignment = Alignment.CenterHorizontally, contentPadding = PaddingValues(bottom = spaceMedium) ) { - item { - if (uiState.loading) { + if (uiState.loading) { + item { CircularProgressIndicator( modifier = Modifier.testTag( WordleScreenTestTags.LOADING_PROGRESS_INDICATOR ) ) + + } + } + + if (uiState.errorMessage != null) { + item { + Text( + text = uiState.errorMessage, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.error + ) } } items(items = uiState.rows) { rowItem -> WordleRowComponent( wordleRowItem = rowItem, + word = uiState.word.orEmpty(), onItemClick = { index -> onEvent(WordleScreenUiEvent.OnRemoveKeyClick(index)) }, + isColorBlindEnabled = uiState.isColorBlindEnabled, + isLetterHintsEnabled = uiState.isLetterHintEnabled, modifier = Modifier.testTag(WordleScreenTestTags.WORDLE_ROW) ) } @@ -183,7 +222,7 @@ private fun WordleScreenImpl( onClick = { onEvent(WordleScreenUiEvent.OnPlayAgainClick) }, modifier = Modifier.weight(1f) ) { - Text(text = "Play Again") + Text(text = stringResource(id = CoreR.string.play_again)) } } @@ -191,7 +230,7 @@ private fun WordleScreenImpl( onClick = onBackClick, modifier = Modifier.weight(1f) ) { - Text(text = "Back") + Text(text = stringResource(id = CoreR.string.back)) } } } @@ -202,34 +241,177 @@ private fun WordleScreenImpl( AlertDialog( onDismissRequest = { setGameOverPopupVisibility(false) }, title = { - Text(text = "Game Over") + Text(text = stringResource(id = CoreR.string.game_over)) }, text = { - Text(text = "You lost the game.\nDo you want to watch one ad to try one more row?") + Text(text = stringResource(id = CoreR.string.you_lost_the_game_watch_ad_q)) }, confirmButton = { + val snackBarMessage = stringResource(id = CoreR.string.loading_rewarded_ad) TextButton( onClick = { setGameOverPopupVisibility(false) scope.launch { - snackbarHostState.showSnackbar("Loading rewarded ad!") + snackbarHostState.showSnackbar(snackBarMessage) } rewardedAdUtil?.loadAndShow( - adId = "ca-app-pub-1923025671607389/9652850233", + adId = WORDLE_REWARDED_AD_ID, onUserEarnedReward = { onEvent(WordleScreenUiEvent.AddOneRow) } ) } ) { - Text(text = "Watch ad") + Text(text = stringResource(id = CoreR.string.watch_ad)) } }, dismissButton = { TextButton(onClick = { setGameOverPopupVisibility(false) }) { - Text(text = "Close") + Text(text = stringResource(id = CoreR.string.close)) } } ) } + + if (infoDialogVisible) { + InfoDialog( + isColorBlindEnabled = uiState.isColorBlindEnabled, + onDismissRequest = { setInfoDialogVisibility(false) } + ) + } +} + +@Composable +private fun InfoDialog( + isColorBlindEnabled: Boolean, + onDismissRequest: () -> Unit +) { + // Word: QUIZ + val rowItem = WordleRowItem( + items = listOf( + WordleItem.fromChar('Q'), // None + WordleItem.Present(WordleChar('U')), + WordleItem.Correct(WordleChar('I')), + WordleItem.Present(WordleChar('Z')), + ) + ) + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(id = CoreR.string.info)) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + WordleRowComponent( + wordleRowItem = rowItem, + word = "QUIZ", + isPreview = true, + isColorBlindEnabled = isColorBlindEnabled, + onItemClick = {} + ) + Spacer(modifier = Modifier.padding(MaterialTheme.spacing.medium)) + InfoDialogCard(isColorBlindEnabled = isColorBlindEnabled) + } + }, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = CoreR.string.close)) + } + } + ) +} + +@Composable +private fun InfoDialogCard( + isColorBlindEnabled: Boolean +) { + val spaceMedium = MaterialTheme.spacing.medium + + val presentBackgroundColor = getItemRowBackgroundColor( + item = WordleItem.Present(WordleChar('C')), + isColorBlindEnabled = isColorBlindEnabled + ) + val presentTextColor = getItemRowTextColor( + item = WordleItem.Present(WordleChar('C')), + isColorBlindEnabled = isColorBlindEnabled + ) + + val correctBackgroundColor = getItemRowBackgroundColor( + item = WordleItem.Correct(WordleChar('C')), + isColorBlindEnabled = isColorBlindEnabled + ) + val correctTextColor = getItemRowTextColor( + item = WordleItem.Correct(WordleChar('C')), + isColorBlindEnabled = isColorBlindEnabled + ) + + Card { + Column( + modifier = Modifier.padding(spaceMedium), + verticalArrangement = Arrangement.spacedBy(spaceMedium) + ) { + // Char none: Q + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) + ) { + append('Q') + } + append(" is not in the target word.") + } + ) + + // Chars present: U, Z + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + fontWeight = FontWeight.Bold, + background = presentBackgroundColor, + color = presentTextColor, + fontSize = 18.sp + ) + ) { + append('U') + } + append(',') + withStyle( + style = SpanStyle( + fontWeight = FontWeight.Bold, + background = presentBackgroundColor, + color = presentTextColor, + fontSize = 18.sp + ) + ) { + append('Z') + } + append(" is in the word but in the wrong spot.") + } + ) + + // Char correct: I + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + fontWeight = FontWeight.Bold, + background = correctBackgroundColor, + color = correctTextColor, + fontSize = 18.sp + ) + ) { + append('I') + } + append(" is in the word and in the correct spot.") + } + ) + } + } } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreenUiState.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreenUiState.kt index c92cd7ea..81a2dacc 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreenUiState.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreenUiState.kt @@ -11,7 +11,11 @@ data class WordleScreenUiState( val rows: List = emptyList(), val currentRowPosition: Int = -1, val keysDisabled: List = emptyList(), - val day: String? = null + val day: String? = null, + val isColorBlindEnabled: Boolean = false, + val isLetterHintEnabled: Boolean = false, + val isHardModeEnabled: Boolean = false, + val errorMessage: String? = null ) { companion object { const val ALL_LETTERS = "QWERTYUIOPASDFGHJKLZXCVBNM" diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreenViewModel.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreenViewModel.kt index ded31640..bca6012a 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreenViewModel.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/WordleScreenViewModel.kt @@ -15,6 +15,7 @@ import com.infinitepower.newquiz.domain.repository.wordle.WordleRepository import com.infinitepower.newquiz.model.wordle.WordleItem import com.infinitepower.newquiz.model.wordle.WordleRowItem import com.infinitepower.newquiz.model.wordle.emptyRowItem +import com.infinitepower.newquiz.wordle.util.word.containsAllLastRevealedHints import com.infinitepower.newquiz.wordle.util.word.getKeysDisabled import com.infinitepower.newquiz.wordle.util.word.verifyFromWord import dagger.hilt.android.lifecycle.HiltViewModel @@ -34,6 +35,30 @@ class WordleScreenViewModel @Inject constructor( val uiState = _uiState.asStateFlow() init { + wordleRepository + .isColorBlindEnabled() + .onEach { res -> + _uiState.update { currentState -> + currentState.copy(isColorBlindEnabled = res.data == true) + } + }.launchIn(viewModelScope) + + wordleRepository + .isLetterHintEnabled() + .onEach { res -> + _uiState.update { currentState -> + currentState.copy(isLetterHintEnabled = res.data == true) + } + }.launchIn(viewModelScope) + + wordleRepository + .isHardModeEnabled() + .onEach { res -> + _uiState.update { currentState -> + currentState.copy(isHardModeEnabled = res.data == true) + } + }.launchIn(viewModelScope) + generateGame() } @@ -74,18 +99,20 @@ class WordleScreenViewModel @Inject constructor( if (res is Resource.Success) { res.data?.let { word -> - generateRows(word) + generateRows(word) } } } } - private fun generateRows(word: String, day: String? = null) { + private suspend fun generateRows(word: String, day: String? = null) { val rows = List(1) { emptyRowItem(size = word.length) } - val rowLimit = savedStateHandle.get(WordleScreenNavArgs::rowLimit.name) ?: Int.MAX_VALUE + val rowLimit = wordleRepository.getWordleMaxRows( + savedStateHandle.get(WordleScreenNavArgs::rowLimit.name) ?: Int.MAX_VALUE + ) wordleLoggingAnalytics.logGameStart(word.length, rowLimit, day) @@ -97,7 +124,8 @@ class WordleScreenViewModel @Inject constructor( rows = rows, currentRowPosition = 0, day = day, - keysDisabled = emptyList() + keysDisabled = emptyList(), + errorMessage = null ) } } @@ -112,7 +140,8 @@ class WordleScreenViewModel @Inject constructor( val currentRowItem = this[currentState.currentRowPosition] val newRow = currentRowItem.items.toMutableList().apply { - val emptyIndex = indexOfFirstOrNull { item -> item is WordleItem.Empty } ?: return + val emptyIndex = + indexOfFirstOrNull { item -> item is WordleItem.Empty } ?: return set(emptyIndex, WordleItem.fromChar(key)) } @@ -145,13 +174,35 @@ class WordleScreenViewModel @Inject constructor( private fun verifyRow() { _uiState.update { currentState -> + // Stops the verification if the word is null or current row is not completed. if (currentState.word == null) return if (!currentState.currentRowCompleted) return - val currentRow = currentState.rows.lastOrNull() ?: return - val currentItems = currentRow.items + // Get current row items, if the current row is null stop the verification. + // Current row is the last row of the list. + val currentItems = currentState.rows.lastOrNull()?.items ?: return + // Verifies items with the word and verifies if the word is correct val verifiedItems = currentItems verifyFromWord currentState.word + + if (currentState.isHardModeEnabled) { + val last2RowItems = currentState + .rows + .getOrNull(currentState.rows.lastIndex - 1) + ?.items + .orEmpty() + .filter { it is WordleItem.Correct || it is WordleItem.Present } + + val containsAllLastRevealedHints = + verifiedItems containsAllLastRevealedHints last2RowItems + + if (!containsAllLastRevealedHints) { + return@update currentState.copy( + errorMessage = "You need to use all hints from last row!" + ) + } + } + val isRowCorrect = verifiedItems.all { it is WordleItem.Correct } val newRowPosition = currentState.currentRowPosition + 1 @@ -160,8 +211,12 @@ class WordleScreenViewModel @Inject constructor( .rows .toMutableList() .apply { + // Updates the current row with the verified items set(currentState.currentRowPosition, WordleRowItem(verifiedItems)) + // Checks if is game end. + // Is game end if new row position >= row limit and current row is correct. + // If is not game end add new empty row. val gameEnd = newRowPosition >= currentState.rowLimit || isRowCorrect if (!gameEnd) add(emptyRowItem(currentState.word.length)) } @@ -171,7 +226,8 @@ class WordleScreenViewModel @Inject constructor( currentState.copy( currentRowPosition = newRowPosition, rows = newRows, - keysDisabled = currentState.keysDisabled + keysDisabled + keysDisabled = currentState.keysDisabled + keysDisabled, + errorMessage = null ) } } diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/components/WordleRowComponent.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/components/WordleRowComponent.kt index 6dd89934..51f0ae0f 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/components/WordleRowComponent.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/components/WordleRowComponent.kt @@ -5,19 +5,20 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.* -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.infinitepower.newquiz.core.R as CoreR import com.infinitepower.newquiz.core.common.annotation.compose.PreviewNightLight import com.infinitepower.newquiz.core.theme.CustomColor import com.infinitepower.newquiz.core.theme.NewQuizTheme @@ -30,17 +31,37 @@ import com.infinitepower.newquiz.model.wordle.WordleRowItem @Composable internal fun WordleRowComponent( modifier: Modifier = Modifier, + word: String, wordleRowItem: WordleRowItem, + isPreview: Boolean = false, + isColorBlindEnabled: Boolean = false, + isLetterHintsEnabled: Boolean = false, onItemClick: (index: Int) -> Unit ) { + val presentItems = wordleRowItem + .items + .filterIsInstance() + Row( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small), modifier = modifier ) { wordleRowItem.items.forEachIndexed { index, item -> + val wordCharCount = word.count { wordChar -> + wordChar == item.char.value && item is WordleItem.Present + } + + val itemCharCount = presentItems.count { presentItem -> + presentItem.char == item.char && item is WordleItem.Present + } + WordleComponent( item = item, - onClick = { onItemClick(index) } + enabled = !isPreview, + isColorBlindEnabled = isColorBlindEnabled, + onClick = { onItemClick(index) }, + charCount = wordCharCount, + isLetterHintsEnabled = isLetterHintsEnabled && wordCharCount != itemCharCount ) } } @@ -51,20 +72,64 @@ internal fun WordleRowComponent( internal fun WordleComponent( modifier: Modifier = Modifier, item: WordleItem, + enabled: Boolean = true, + isColorBlindEnabled: Boolean = false, + isLetterHintsEnabled: Boolean = false, + charCount: Int = 0, onClick: () -> Unit ) { - val backgroundColor = when (item) { - is WordleItem.Correct -> MaterialTheme.extendedColors.getColorAccentByKey(key = CustomColor.Keys.Green) - is WordleItem.Present -> MaterialTheme.extendedColors.getColorAccentByKey(key = CustomColor.Keys.Yellow) - else -> MaterialTheme.colorScheme.surface + if (charCount > 1 && isLetterHintsEnabled) { + BadgedBox( + badge = { + Badge { + val badgeNumber = charCount.toString() + Text( + badgeNumber, + modifier = Modifier.semantics { + contentDescription = "$badgeNumber new notifications" + } + ) + } + } + ) { + WordleComponentImpl( + modifier = modifier, + item = item, + enabled = enabled, + isColorBlindEnabled = isColorBlindEnabled, + onClick = onClick + ) + } + } else { + WordleComponentImpl( + modifier = modifier, + item = item, + enabled = enabled, + isColorBlindEnabled = isColorBlindEnabled, + onClick = onClick + ) } +} + +@Composable +@ExperimentalMaterial3Api +fun WordleComponentImpl( + modifier: Modifier = Modifier, + item: WordleItem, + enabled: Boolean = true, + isColorBlindEnabled: Boolean = false, + onClick: () -> Unit +) { + val backgroundColor = getItemRowBackgroundColor( + item = item, + isColorBlindEnabled = isColorBlindEnabled + ) val backgroundColorAnimated by animateColorAsState(targetValue = backgroundColor) - val textColor = when (item) { - is WordleItem.Correct -> MaterialTheme.extendedColors.getColorOnAccentByKey(key = CustomColor.Keys.Green) - is WordleItem.Present -> MaterialTheme.extendedColors.getColorOnAccentByKey(key = CustomColor.Keys.Yellow) - else -> MaterialTheme.colorScheme.onSurface - } + val textColor = getItemRowTextColor( + item = item, + isColorBlindEnabled = isColorBlindEnabled + ) val textColorAnimated by animateColorAsState(targetValue = textColor) val elevation = if (item.char.isEmpty()) 0.dp else 8.dp @@ -74,24 +139,21 @@ internal fun WordleComponent( BorderStroke(1.dp, MaterialTheme.colorScheme.outline) } else null + val itemDescription = item.getItemDescription() + Surface( shape = MaterialTheme.shapes.medium, modifier = modifier .size(50.dp) .testTag(WordleRowComponentTestingTags.WORDLE_COMPONENT_SURFACE) .semantics { - contentDescription = when (item) { - is WordleItem.Empty -> "Item empty" - is WordleItem.None -> "Item ${item.char} none" - is WordleItem.Present -> "Item ${item.char} present" - is WordleItem.Correct -> "Item ${item.char} correct" - } + contentDescription = itemDescription }, color = backgroundColorAnimated, tonalElevation = elevationAnimated, border = stroke, onClick = onClick, - enabled = item is WordleItem.None + enabled = item is WordleItem.None && enabled ) { Box( modifier = Modifier.fillMaxSize(), @@ -107,6 +169,65 @@ internal fun WordleComponent( } } +@Composable +@ReadOnlyComposable +internal fun getItemRowBackgroundColor( + item: WordleItem, + isColorBlindEnabled: Boolean +): Color { + val backgroundCorrectColor = MaterialTheme.extendedColors.getColorAccentByKey( + key = if (isColorBlindEnabled) { + CustomColor.Keys.Blue + } else CustomColor.Keys.Green + ) + + val backgroundPresentColor = MaterialTheme.extendedColors.getColorAccentByKey( + key = if (isColorBlindEnabled) { + CustomColor.Keys.Red + } else CustomColor.Keys.Yellow + ) + + return when (item) { + is WordleItem.Correct -> backgroundCorrectColor + is WordleItem.Present -> backgroundPresentColor + else -> MaterialTheme.colorScheme.surface + } +} + +@Composable +@ReadOnlyComposable +internal fun getItemRowTextColor( + item: WordleItem, + isColorBlindEnabled: Boolean +): Color { + val backgroundCorrectColor = MaterialTheme.extendedColors.getColorOnAccentByKey( + key = if (isColorBlindEnabled) { + CustomColor.Keys.Blue + } else CustomColor.Keys.Green + ) + + val backgroundPresentColor = MaterialTheme.extendedColors.getColorOnAccentByKey( + key = if (isColorBlindEnabled) { + CustomColor.Keys.Red + } else CustomColor.Keys.Yellow + ) + + return when (item) { + is WordleItem.Correct -> backgroundCorrectColor + is WordleItem.Present -> backgroundPresentColor + else -> MaterialTheme.colorScheme.onSurface + } +} + +@Composable +@ReadOnlyComposable +private fun WordleItem.getItemDescription() = when (this) { + is WordleItem.Empty -> stringResource(id = CoreR.string.item_empty) + is WordleItem.None -> stringResource(id = CoreR.string.item_i_none, this.char) + is WordleItem.Present -> stringResource(id = CoreR.string.item_i_present, this.char) + is WordleItem.Correct -> stringResource(id = CoreR.string.item_i_correct, this.char) +} + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal object WordleRowComponentTestingTags { const val WORDLE_COMPONENT_SURFACE = "WORDLE_COMPONENT_SURFACE" @@ -142,6 +263,7 @@ private fun WordleRowComponentPreview() { WordleItem.None(char = WordleChar('A')), WordleItem.Present(char = WordleChar('B')), WordleItem.Correct(char = WordleChar('C')), + WordleItem.Present(char = WordleChar('B')), ) ) @@ -149,8 +271,10 @@ private fun WordleRowComponentPreview() { Surface { WordleRowComponent( modifier = Modifier.padding(16.dp), + word = "QUIZ", wordleRowItem = item, - onItemClick = {} + onItemClick = {}, + isLetterHintsEnabled = true ) } } diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/DailyWordSelectorScreen.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/DailyWordSelectorScreen.kt index 869bd335..81bbdc85 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/DailyWordSelectorScreen.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/DailyWordSelectorScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.infinitepower.newquiz.core.common.viewmodel.NavEvent import com.infinitepower.newquiz.core.theme.spacing @@ -17,6 +18,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.spec.Direction import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch +import com.infinitepower.newquiz.core.R as CoreR @Composable @Destination @@ -48,8 +50,12 @@ private fun DailyWordSelectorScreenImpl( rememberTopAppBarState() ) + val fourLettersStr = stringResource(id = CoreR.string.four_letters) + val fiveLettersStr = stringResource(id = CoreR.string.five_letters) + val sixLettersStr = stringResource(id = CoreR.string.six_letters) + val tabs = remember { - listOf("4 Letters", "5 Letters", "6 Letters") + listOf(fourLettersStr, fiveLettersStr, sixLettersStr) } val snackbarHostState = remember { SnackbarHostState() } @@ -77,13 +83,13 @@ private fun DailyWordSelectorScreenImpl( Column { SmallTopAppBar( title = { - Text(text = "Wordle") + Text(text = stringResource(id = CoreR.string.wordle)) }, navigationIcon = { IconButton(onClick = onBackClick) { Icon( imageVector = Icons.Rounded.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(id = CoreR.string.back) ) } }, diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/DailyWordleSelectorViewModel.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/DailyWordleSelectorViewModel.kt index 7b17ec3f..1976662d 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/DailyWordleSelectorViewModel.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/DailyWordleSelectorViewModel.kt @@ -72,13 +72,13 @@ class DailyWordleSelectorViewModel @Inject constructor( .collect { res -> if (res is Resource.Success && res.data != null) { val wordleQuiz = WordleScreenDestination(rowLimit = 6, word = res.data, date = date.toString()) - sendUiEvent(NavEvent.Navigate(wordleQuiz)) + sendNavEventAsync(NavEvent.Navigate(wordleQuiz)) return@collect } if (res is Resource.Error) { Log.e("DailyWordleSelector", "Error while loading item", Throwable(res.message)) - sendUiEvent(NavEvent.ShowSnackBar(res.message ?: "Error while loading item")) + sendNavEventAsync(NavEvent.ShowSnackBar(res.message ?: "Error while loading item")) return@collect } } diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/components/CalendarView.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/components/CalendarView.kt index f03e3c6d..dbcf7fff 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/components/CalendarView.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/daily_word/components/CalendarView.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -31,6 +32,7 @@ import com.infinitepower.newquiz.model.wordle.daily.CalendarItemState import com.infinitepower.newquiz.model.wordle.daily.WordleDailyCalendarItem import kotlinx.datetime.* import kotlinx.datetime.TimeZone +import com.infinitepower.newquiz.core.R as CoreR @Composable @OptIn(ExperimentalMaterial3Api::class) @@ -139,7 +141,7 @@ private fun CalendarHeader( ) { HeaderNavigationButton( icon = Icons.Rounded.ArrowBack, - contentDescription = "Back month", + contentDescription = stringResource(id = CoreR.string.back_month), onClick = backMonth ) @@ -150,7 +152,7 @@ private fun CalendarHeader( HeaderNavigationButton( icon = Icons.Rounded.ArrowForward, - contentDescription = "Next month", + contentDescription = stringResource(id = CoreR.string.next_month), onClick = nextMonth ) } diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/list/WordleListScreen.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/list/WordleListScreen.kt index 2e1a4322..3f0bd51b 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/list/WordleListScreen.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/list/WordleListScreen.kt @@ -10,11 +10,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.stringResource import com.infinitepower.newquiz.core.theme.spacing import com.infinitepower.newquiz.core.ui.home_card.components.HomeCardItemContent import com.infinitepower.newquiz.core.ui.home_card.model.HomeCardItem -import com.infinitepower.newquiz.wordle.list.data.WordListCardItemData +import com.infinitepower.newquiz.wordle.list.data.getWordListCardItemData +import com.infinitepower.newquiz.core.R as CoreR import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator @@ -23,11 +24,9 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator fun WordleListScreen( navigator: DestinationsNavigator ) { - val cardItemData = remember { - WordListCardItemData(navigator) - } + val cardItemData = remember { getWordListCardItemData(navigator) } - WordleListScreenImpl(cardItemData = cardItemData.items) + WordleListScreenImpl(cardItemData = cardItemData) } @Composable @@ -46,7 +45,7 @@ private fun WordleListScreenImpl( topBar = { CenterAlignedTopAppBar( title = { - Text(text = "Wordle") + Text(text = stringResource(id = CoreR.string.wordle)) }, scrollBehavior = scrollBehavior ) diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/list/data/WordListCardItemData.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/list/data/WordListCardItemData.kt index 59fa7e06..30250ad0 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/list/data/WordListCardItemData.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/list/data/WordListCardItemData.kt @@ -4,36 +4,33 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Quiz import androidx.compose.material.icons.rounded.Today import com.infinitepower.newquiz.core.R -import com.infinitepower.newquiz.core.ui.home_card.data.CardItemDataCore import com.infinitepower.newquiz.core.ui.home_card.model.CardIcon import com.infinitepower.newquiz.core.ui.home_card.model.HomeCardItem import com.infinitepower.newquiz.wordle.destinations.DailyWordSelectorScreenDestination import com.infinitepower.newquiz.wordle.destinations.WordleScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -internal class WordListCardItemData( +fun getWordListCardItemData( navigator: DestinationsNavigator -) : CardItemDataCore { - override val items = listOf( - HomeCardItem.GroupTitle( - title = R.string.wordle_infinite, - ), - HomeCardItem.LargeCard( - title = R.string.wordle_infinite, - icon = CardIcon.Icon(Icons.Rounded.Quiz), - onClick = { - navigator.navigate(WordleScreenDestination()) - } - ), - HomeCardItem.GroupTitle( - title = R.string.wordle_daily, - ), - HomeCardItem.LargeCard( - title = R.string.wordle_daily, - icon = CardIcon.Icon(Icons.Rounded.Today), - onClick = { - navigator.navigate(DailyWordSelectorScreenDestination) - } - ), - ) -} \ No newline at end of file +) = listOf( + HomeCardItem.GroupTitle( + title = R.string.wordle_infinite, + ), + HomeCardItem.LargeCard( + title = R.string.wordle_infinite, + icon = CardIcon.Icon(Icons.Rounded.Quiz), + onClick = { + navigator.navigate(WordleScreenDestination()) + } + ), + HomeCardItem.GroupTitle( + title = R.string.wordle_daily, + ), + HomeCardItem.LargeCard( + title = R.string.wordle_daily, + icon = CardIcon.Icon(Icons.Rounded.Today), + onClick = { + navigator.navigate(DailyWordSelectorScreenDestination) + } + ), +) \ No newline at end of file diff --git a/wordle/src/main/java/com/infinitepower/newquiz/wordle/util/word/WordUtil.kt b/wordle/src/main/java/com/infinitepower/newquiz/wordle/util/word/WordUtil.kt index 09cae725..4dcf5831 100644 --- a/wordle/src/main/java/com/infinitepower/newquiz/wordle/util/word/WordUtil.kt +++ b/wordle/src/main/java/com/infinitepower/newquiz/wordle/util/word/WordUtil.kt @@ -2,13 +2,29 @@ package com.infinitepower.newquiz.wordle.util.word import com.infinitepower.newquiz.model.wordle.WordleItem +/** + * Verifies the input [WordleItem] list with [originalWord] + * + * This function will map all [WordleItem] list and return the corresponded verified item + * that can be [WordleItem.None], [WordleItem.Present] and [WordleItem.Correct]. + * + * ### Mapping List + * If the item is in the word and in the correct spot returns [WordleItem.Correct]. + * If the item is in the word and not in the correct spot returns [WordleItem.Present]. + * If the item is not in the word returns [WordleItem.None]. + * + * + * @param originalWord word to verify items + * @return verified [WordleItem] items + * @see [WordleItem] + * @author João Manaia + * @since 1.0.0 + */ internal infix fun List.verifyFromWord(originalWord: String): List { val removeChars = originalWord.filterIndexed { index, char -> getOrNull(index)?.char?.value != char }.toMutableList() - println(removeChars.toString()) - val newList = mapIndexed { index, wordleItem -> val char = wordleItem.char @@ -24,9 +40,35 @@ internal infix fun List.verifyFromWord(originalWord: String): List.containsAllLastRevealedHints( + lastRevealedHints: List +): Boolean { + val lastRevealedHintsChar = lastRevealedHints.map { item -> + item.char + } + + return map { item -> + item.char + }.containsAll(lastRevealedHintsChar) +} + +/** + * Gets keys to disable with list of [WordleItem]. + * + * If the item is [WordleItem.None] will disable the item key. + * If the list contains one item [WordleItem.Correct] or [WordleItem.Present] and one item [WordleItem.None] will + * not disable the key. + * + * @return keys disabled + * @see [WordleItem] + * @author João Manaia + * @since 1.0.0 + */ internal fun List.getKeysDisabled(): Set { return filter { item -> - val sameItemCount = count { (it is WordleItem.Correct || it is WordleItem.Present) && it.char == item.char } + val sameItemCount = count { char -> + (char is WordleItem.Correct || char is WordleItem.Present) && char.char == item.char + } item is WordleItem.None && sameItemCount == 0 }.mapNotNull { item -> val char = item.char.value diff --git a/wordle/src/test/java/com/infinitepower/newquiz/wordle/util/word/WordUtilTest.kt b/wordle/src/test/java/com/infinitepower/newquiz/wordle/util/word/WordUtilTest.kt index 0426df05..4603d0b5 100644 --- a/wordle/src/test/java/com/infinitepower/newquiz/wordle/util/word/WordUtilTest.kt +++ b/wordle/src/test/java/com/infinitepower/newquiz/wordle/util/word/WordUtilTest.kt @@ -103,4 +103,71 @@ internal class WordUtilTest { val expectedItems2 = listOf('F') assertThat(keysDisabled2).containsExactlyElementsIn(expectedItems2) } + + @Test + fun `wordle list item contains all last revealed hints, returns true`() { + val items = listOf( + WordleItem.Present(WordleChar('A')), + WordleItem.None(WordleChar('B')), + WordleItem.None(WordleChar('B')), + WordleItem.Correct(WordleChar('A')), + ) + + val lastRevealedHints = listOf( + WordleItem.Present(WordleChar('A')), + ) + + val containsAllLastRevealedHints = items containsAllLastRevealedHints lastRevealedHints + assertThat(containsAllLastRevealedHints).isTrue() + + val items2 = listOf( + WordleItem.Correct(WordleChar('E')), + WordleItem.None(WordleChar('F')), + WordleItem.None(WordleChar('N')), + WordleItem.None(WordleChar('F')), + WordleItem.Present(WordleChar('I')), + ) + + val lastRevealedHints2 = listOf( + WordleItem.Present(WordleChar('E')), + WordleItem.Present(WordleChar('I')), + ) + + val containsAllLastRevealedHints2 = items2 containsAllLastRevealedHints lastRevealedHints2 + assertThat(containsAllLastRevealedHints2).isTrue() + } + + @Test + fun `wordle list item not contains all last revealed hints, returns false`() { + val items = listOf( + WordleItem.Present(WordleChar('A')), + WordleItem.None(WordleChar('B')), + WordleItem.None(WordleChar('B')), + WordleItem.Correct(WordleChar('A')), + ) + + val lastRevealedHints = listOf( + WordleItem.Present(WordleChar('A')), + WordleItem.Correct(WordleChar('K')), + WordleItem.Present(WordleChar('Z')), + ) + + val containsAllLastRevealedHints = items containsAllLastRevealedHints lastRevealedHints + assertThat(containsAllLastRevealedHints).isFalse() + } + + @Test + fun `wordle list item contains all empty list last revealed hints, returns true`() { + val items = listOf( + WordleItem.Present(WordleChar('A')), + WordleItem.None(WordleChar('B')), + WordleItem.None(WordleChar('B')), + WordleItem.Correct(WordleChar('A')), + ) + + val lastRevealedHints = emptyList() + + val containsAllLastRevealedHints = items containsAllLastRevealedHints lastRevealedHints + assertThat(containsAllLastRevealedHints).isTrue() + } } \ No newline at end of file