diff --git a/app/build.gradle b/app/build.gradle index f4ad5d8b..d067a5d1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,4 +119,10 @@ dependencies { String ktor_version = project.ktor_version implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-cio:$ktor_version") + + // Logging + implementation 'com.jakewharton.timber:timber:5.0.1' + + // Lottie animations + implementation "com.airbnb.android:lottie:6.0.1" } diff --git a/app/src/androidTest/java/cz/movapp/app/ExampleInstrumentedTest.kt b/app/src/androidTest/java/cz/movapp/app/ExampleInstrumentedTest.kt index 3f1c9cfc..3f59bac5 100644 --- a/app/src/androidTest/java/cz/movapp/app/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/cz/movapp/app/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package cz.movapp.app -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/java/cz/movapp/android/Utils.kt b/app/src/main/java/cz/movapp/android/Utils.kt index b1cf721a..2e0861e5 100644 --- a/app/src/main/java/cz/movapp/android/Utils.kt +++ b/app/src/main/java/cz/movapp/android/Utils.kt @@ -3,6 +3,7 @@ package cz.movapp.android import android.content.Context import android.content.res.AssetFileDescriptor import android.media.MediaPlayer +import android.os.Build import android.view.View import android.view.inputmethod.InputMethodManager import androidx.fragment.app.FragmentActivity @@ -15,7 +16,8 @@ import java.text.Normalizer */ fun playSound( context: Context, - assetFileName: String + assetFileName: String, + playbackSpeed: Float = 1.0f ): MediaPlayer? { val afd: AssetFileDescriptor = context.assets.openFd(assetFileName) var player: MediaPlayer? = MediaPlayer() @@ -28,6 +30,13 @@ fun playSound( player = null } player?.prepare() + // FIXME will not work on lower Android versions + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + player?.playbackParams?.apply { + speed = playbackSpeed + player?.playbackParams = this + } + } player?.start() return player diff --git a/app/src/main/java/cz/movapp/android/UtilsAndroid.kt b/app/src/main/java/cz/movapp/android/UtilsAndroid.kt index 236c32d8..383649a4 100644 --- a/app/src/main/java/cz/movapp/android/UtilsAndroid.kt +++ b/app/src/main/java/cz/movapp/android/UtilsAndroid.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.onStart -import java.util.* +import java.util.Locale fun RecyclerView.getSavableScrollState(): Int { return when (this.layoutManager) { diff --git a/app/src/main/java/cz/movapp/app/App.kt b/app/src/main/java/cz/movapp/app/App.kt index fd3d9953..116637fd 100644 --- a/app/src/main/java/cz/movapp/app/App.kt +++ b/app/src/main/java/cz/movapp/app/App.kt @@ -5,6 +5,7 @@ import android.content.Context import android.os.StrictMode import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel +import timber.log.Timber const val TAG = "MOVAPP" @@ -23,6 +24,7 @@ class App : Application() { super.onCreate() if (BuildConfig.DEBUG) { StrictMode.enableDefaults() + Timber.plant(Timber.DebugTree()) } instance = this ctx = applicationContext diff --git a/app/src/main/java/cz/movapp/app/MainActivity.kt b/app/src/main/java/cz/movapp/app/MainActivity.kt index 3a9bd7d2..991ec004 100644 --- a/app/src/main/java/cz/movapp/app/MainActivity.kt +++ b/app/src/main/java/cz/movapp/app/MainActivity.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber class MainActivity : AppCompatActivity() { @@ -48,7 +49,12 @@ class MainActivity : AppCompatActivity() { navController = findNavController(R.id.nav_host_fragment_activity_main) - binding.bottomNavigation.setupWithNavController(navController) + binding.bottomNavigation.apply { + setupWithNavController(navController) + setOnItemReselectedListener { + Timber.d("menuItem $it") + } + } } diff --git a/app/src/main/java/cz/movapp/app/MediaPlayerForegroundService.kt b/app/src/main/java/cz/movapp/app/MediaPlayerForegroundService.kt index 424246ac..cc66f30a 100644 --- a/app/src/main/java/cz/movapp/app/MediaPlayerForegroundService.kt +++ b/app/src/main/java/cz/movapp/app/MediaPlayerForegroundService.kt @@ -1,6 +1,10 @@ package cz.movapp.app -import android.app.* +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -12,7 +16,11 @@ import android.media.MediaMetadata import android.media.MediaPlayer import android.media.session.MediaSession import android.media.session.PlaybackState -import android.os.* +import android.os.Build +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.PowerManager import androidx.localbroadcastmanager.content.LocalBroadcastManager import java.io.IOException diff --git a/app/src/main/java/cz/movapp/app/OnBoardingActivity.kt b/app/src/main/java/cz/movapp/app/OnBoardingActivity.kt index 7db2c9fb..a337e0e8 100644 --- a/app/src/main/java/cz/movapp/app/OnBoardingActivity.kt +++ b/app/src/main/java/cz/movapp/app/OnBoardingActivity.kt @@ -8,8 +8,8 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import com.google.android.material.tabs.TabLayoutMediator -import cz.movapp.app.ui.onboarding.OnBoardingFragmentAdapter import cz.movapp.app.databinding.ActivityOnBoardingBinding +import cz.movapp.app.ui.onboarding.OnBoardingFragmentAdapter val ONBOARDING_PASSED_RESULT_CODE = 1 val ONBOARDING_LEFT_RESULT_CODE = 2 diff --git a/app/src/main/java/cz/movapp/app/data/DictionaryDatasource.kt b/app/src/main/java/cz/movapp/app/data/DictionaryDatasource.kt index 6592a249..6537a6c7 100644 --- a/app/src/main/java/cz/movapp/app/data/DictionaryDatasource.kt +++ b/app/src/main/java/cz/movapp/app/data/DictionaryDatasource.kt @@ -3,16 +3,18 @@ package cz.movapp.app.data import android.content.Context import cz.movapp.android.createLangAssetsString import cz.movapp.android.stripDiacritics +import cz.movapp.app.ui.dictionary.DictionaryMetaCategoryData import cz.movapp.app.ui.dictionary.DictionarySectionsData import cz.movapp.app.ui.dictionary.DictionaryTranslationsData import org.json.JSONObject import java.io.IOException -import java.util.* +import java.util.Locale -class DictionaryDatasource { +object DictionaryDatasource { private val sectionsCache = mutableMapOf>() private val translationsCache = mutableMapOf>() + private val metaCategoriesCache = mutableMapOf>() private fun loadSectionsFromAssets(context: Context, langStorageString: String): List { var jsonString: String = "" @@ -101,14 +103,55 @@ class DictionaryDatasource { return translations } + private fun loadMetaCategoriesFromAssets(context: Context, langStorageString: String): List { + var jsonString = "" + val dict = mutableListOf() + + try { + jsonString = context.assets.open("${langStorageString}-dictionary.json").bufferedReader().use { it.readText() } + } catch (ioException: IOException) { + ioException.printStackTrace() + } + + val jsonArr = JSONObject(jsonString).getJSONArray("categories") + + for (i in 0 until jsonArr.length()) { + val jsonObj = jsonArr.getJSONObject(i) + + if (!jsonObj.optBoolean("metaOnly", false)) { + continue + } + + val id = jsonObj.getString("id") + val jsonNameObj = jsonObj.getJSONObject("name") + + val metaCategories = mutableListOf() + val jsonMetaCatsArr = jsonObj.getJSONArray("metacategories") + for (j in 0 until jsonMetaCatsArr.length()) { + metaCategories.add(jsonMetaCatsArr.getString(j)) + } + + dict.add(DictionaryMetaCategoryData( + id, + jsonNameObj.getString("main"), + jsonNameObj.getString("source"), + metaCategories) + ) + } + + return dict + } + fun loadSections(context: Context, langPair: LanguagePair): List { - var langStorageString = createLangAssetsString(langPair) - return lazySectionsCacheLoad(context, langStorageString) + return lazySectionsCacheLoad(context, createLangAssetsString(langPair)) } fun loadTranslations(context: Context, langPair: LanguagePair): List { - var langStorageString = createLangAssetsString(langPair) - return lazyTranslationsCacheLoad(context, langStorageString) + return lazyTranslationsCacheLoad(context, createLangAssetsString(langPair)) + } + + fun loadMetaCategories(context: Context, langPair: LanguagePair): List { + return lazyMetaCategoriesCacheLoad(context, createLangAssetsString(langPair)) } private fun lazySectionsCacheLoad(context: Context, langStorageString: String): List { @@ -132,4 +175,15 @@ class DictionaryDatasource { selected } } + + private fun lazyMetaCategoriesCacheLoad(context: Context, langStorageString: String): List { + var selected = metaCategoriesCache[langStorageString] + return if (selected == null) { + selected = loadMetaCategoriesFromAssets(context, langStorageString) + metaCategoriesCache[langStorageString] = selected + selected + } else { + selected + } + } } \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/data/Language.kt b/app/src/main/java/cz/movapp/app/data/Language.kt index bba99d40..90d860d1 100644 --- a/app/src/main/java/cz/movapp/app/data/Language.kt +++ b/app/src/main/java/cz/movapp/app/data/Language.kt @@ -3,7 +3,7 @@ package cz.movapp.app.data import androidx.annotation.DrawableRes import androidx.annotation.StringRes import cz.movapp.app.R -import java.util.* +import java.util.Locale /** * @param isReversed whether translation is reverse to data of json see cs-uk-dictionary.json diff --git a/app/src/main/java/cz/movapp/app/ui/alphabet/AlphabetFragment.kt b/app/src/main/java/cz/movapp/app/ui/alphabet/AlphabetFragment.kt index 1b5442ee..2f5772c1 100644 --- a/app/src/main/java/cz/movapp/app/ui/alphabet/AlphabetFragment.kt +++ b/app/src/main/java/cz/movapp/app/ui/alphabet/AlphabetFragment.kt @@ -11,7 +11,6 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import cz.movapp.android.getSavableScrollState -import cz.movapp.android.restoreSavableScrollState import cz.movapp.app.App import cz.movapp.app.MainViewModel import cz.movapp.app.databinding.FragmentAlphabetBinding diff --git a/app/src/main/java/cz/movapp/app/ui/alphabet/AlphabetFragmentAdapter.kt b/app/src/main/java/cz/movapp/app/ui/alphabet/AlphabetFragmentAdapter.kt index 52a533c8..b177f258 100644 --- a/app/src/main/java/cz/movapp/app/ui/alphabet/AlphabetFragmentAdapter.kt +++ b/app/src/main/java/cz/movapp/app/ui/alphabet/AlphabetFragmentAdapter.kt @@ -2,8 +2,6 @@ package cz.movapp.app.ui.alphabet import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter -import cz.movapp.app.ui.alphabet.AlphabetDirection -import cz.movapp.app.ui.alphabet.AlphabetFragment class AlphabetFragmentAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { override fun getItemCount(): Int = 2 diff --git a/app/src/main/java/cz/movapp/app/ui/children/ChildrenFairyTalesFragment.kt b/app/src/main/java/cz/movapp/app/ui/children/ChildrenFairyTalesFragment.kt index af6016ce..810c217f 100644 --- a/app/src/main/java/cz/movapp/app/ui/children/ChildrenFairyTalesFragment.kt +++ b/app/src/main/java/cz/movapp/app/ui/children/ChildrenFairyTalesFragment.kt @@ -5,8 +5,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController import cz.movapp.app.MainViewModel import cz.movapp.app.databinding.FragmentChildrenFairyTalesSelectionBinding diff --git a/app/src/main/java/cz/movapp/app/ui/children/ChildrenMemoryGameFragment.kt b/app/src/main/java/cz/movapp/app/ui/children/ChildrenMemoryGameFragment.kt index c5f1ed25..751706c6 100644 --- a/app/src/main/java/cz/movapp/app/ui/children/ChildrenMemoryGameFragment.kt +++ b/app/src/main/java/cz/movapp/app/ui/children/ChildrenMemoryGameFragment.kt @@ -2,19 +2,15 @@ package cz.movapp.app.ui.children import android.content.Context import android.content.pm.ActivityInfo -import android.content.res.ColorStateList -import android.content.res.Configuration import android.media.MediaPlayer import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.content.ContextCompat import androidx.core.view.setPadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer import com.google.android.material.internal.ViewUtils.dpToPx import cz.movapp.android.playSound import cz.movapp.app.MainViewModel diff --git a/app/src/main/java/cz/movapp/app/ui/children/ChildrenMemoryGameViewModel.kt b/app/src/main/java/cz/movapp/app/ui/children/ChildrenMemoryGameViewModel.kt index d5afb941..c463a9a0 100644 --- a/app/src/main/java/cz/movapp/app/ui/children/ChildrenMemoryGameViewModel.kt +++ b/app/src/main/java/cz/movapp/app/ui/children/ChildrenMemoryGameViewModel.kt @@ -2,7 +2,9 @@ package cz.movapp.app.ui.children import android.app.Application import android.graphics.drawable.Drawable -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import cz.movapp.app.App import cz.movapp.app.appModule import cz.movapp.app.data.ChildrenDatasource diff --git a/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryModel.kt b/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryModel.kt index 3fffd3d2..c3245ec1 100644 --- a/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryModel.kt +++ b/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryModel.kt @@ -1,6 +1,11 @@ package cz.movapp.app.ui.dictionary -data class DictionarySectionsData(val id: String, val main: String, val source: String, val phrases_ids: List) +data class DictionarySectionsData( + val id: String, + val main: String, + val source: String, + val phrases_ids: List +) data class DictionaryTranslationsData( val id: String, @@ -17,3 +22,10 @@ data class DictionaryTranslationsData( val source_sound_url: String, val source_sound_local: String ) + +data class DictionaryMetaCategoryData( + val id: String, + val nameSource: String, + val nameMain: String, + val metaCategories: MutableList +) \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryPhrasesSearchAllAdapter.kt b/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryPhrasesSearchAllAdapter.kt index 8320a1ed..be18b241 100644 --- a/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryPhrasesSearchAllAdapter.kt +++ b/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryPhrasesSearchAllAdapter.kt @@ -4,7 +4,7 @@ import android.content.Context import cz.movapp.android.stripDiacritics import cz.movapp.app.FavoritesViewModel import cz.movapp.app.data.LanguagePair -import java.util.* +import java.util.Locale class DictionaryPhrasesSearchAllAdapter( private val context: Context, diff --git a/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryViewModel.kt b/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryViewModel.kt index 6a97cf4c..3a1bf658 100644 --- a/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryViewModel.kt +++ b/app/src/main/java/cz/movapp/app/ui/dictionary/DictionaryViewModel.kt @@ -1,7 +1,10 @@ package cz.movapp.app.ui.dictionary import android.app.Application -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import cz.movapp.app.FavoritesViewModel import cz.movapp.app.data.DictionaryDatasource import cz.movapp.app.data.LanguagePair @@ -9,17 +12,22 @@ import cz.movapp.app.data.LanguagePair class DictionaryViewModel(private val app: Application, private val favoritesViewModel: FavoritesViewModel, private var langPair: LanguagePair) : AndroidViewModel(app) { - val sections: MutableLiveData = MutableLiveData().apply { value = - DictionaryPhraseSectionsAdapter(DictionaryDatasource().loadSections(app.applicationContext, langPair), langPair) + DictionaryPhraseSectionsAdapter(DictionaryDatasource.loadSections(app.applicationContext, langPair), langPair) } + val metaCategories: MutableLiveData> by lazy { + MutableLiveData>( + DictionaryDatasource.loadMetaCategories(app.applicationContext, langPair) + ) + } + val translations: MutableLiveData = MutableLiveData().apply { value = DictionaryPhraseSectionDetailAdapter( - DictionaryDatasource().loadTranslations(app.applicationContext, langPair), + DictionaryDatasource.loadTranslations(app.applicationContext, langPair), favoritesViewModel, langPair ) @@ -28,18 +36,17 @@ class DictionaryViewModel(private val app: Application, private val favoritesVie val favorites: MutableLiveData = MutableLiveData().apply { value = DictionaryFavoritesAdapter( - DictionaryDatasource().loadTranslations(app.applicationContext, langPair), + DictionaryDatasource.loadTranslations(app.applicationContext, langPair), favoritesViewModel, langPair ) } - val translationsSearches: MutableLiveData = MutableLiveData().apply { value = DictionaryPhrasesSearchAllAdapter( app, - DictionaryDatasource().loadTranslations(app.applicationContext, langPair), + DictionaryDatasource.loadTranslations(app.applicationContext, langPair), favoritesViewModel, langPair ) @@ -49,7 +56,7 @@ class DictionaryViewModel(private val app: Application, private val favoritesVie MutableLiveData().apply { value = DictionaryPhrasesSearchAllAdapter( app, - DictionaryDatasource().loadTranslations(app.applicationContext, langPair), + DictionaryDatasource.loadTranslations(app.applicationContext, langPair), favoritesViewModel, langPair ) @@ -60,20 +67,20 @@ class DictionaryViewModel(private val app: Application, private val favoritesVie sections.value = DictionaryPhraseSectionsAdapter( - DictionaryDatasource().loadSections(app.applicationContext, langPair), + DictionaryDatasource.loadSections(app.applicationContext, langPair), langPair ) translations.value = DictionaryPhraseSectionDetailAdapter( - DictionaryDatasource().loadTranslations(app.applicationContext, langPair), + DictionaryDatasource.loadTranslations(app.applicationContext, langPair), favoritesViewModel, langPair ) favorites.value = DictionaryFavoritesAdapter( - DictionaryDatasource().loadTranslations(app.applicationContext, langPair), + DictionaryDatasource.loadTranslations(app.applicationContext, langPair), favoritesViewModel, langPair ) @@ -81,7 +88,7 @@ class DictionaryViewModel(private val app: Application, private val favoritesVie translationsSearches.value = DictionaryPhrasesSearchAllAdapter( app, - DictionaryDatasource().loadTranslations(app.applicationContext, langPair), + DictionaryDatasource.loadTranslations(app.applicationContext, langPair), favoritesViewModel, langPair ) @@ -89,7 +96,7 @@ class DictionaryViewModel(private val app: Application, private val favoritesVie favoritesSearches.value = DictionaryPhrasesSearchAllAdapter( app, - DictionaryDatasource().loadTranslations(app.applicationContext, langPair), + DictionaryDatasource.loadTranslations(app.applicationContext, langPair), favoritesViewModel, langPair ) diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseConfig.kt b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseConfig.kt new file mode 100644 index 00000000..9abd7841 --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseConfig.kt @@ -0,0 +1,23 @@ +package cz.movapp.app.ui.exercise + +object ExerciseConfig { + val sizeDefault = 10 + val sizeList = arrayOf(10, 20, 30) + val levelDefault = 0 + val levelMin = 0 + val levelMax = 1 + val levelDownTresholdScore = 50 + val levelUpTresholdScore = 100 +} + +data class Level ( + val wordLimitMin: Int, + val wordLimitMax: Int, + val choiceLimit: Int +) + +val levels = arrayOf( + Level(1, 2, 4), + Level(2, 3, 5), + Level(2, 3, 8), +) \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseData.kt b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseData.kt new file mode 100644 index 00000000..6c3f4b78 --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseData.kt @@ -0,0 +1,16 @@ +package cz.movapp.app.ui.exercise + +import cz.movapp.app.ui.dictionary.DictionaryTranslationsData + +class ExerciseData ( + val numExercises: Int, + val questions: List +) + +class Question ( + /** + * index of correct answer in phrases list + */ + val phraseCorrectIndex: Int, + val phrases: List +) \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseState.kt b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseState.kt new file mode 100644 index 00000000..ec329af1 --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseState.kt @@ -0,0 +1,22 @@ +package cz.movapp.app.ui.exercise + +sealed class ExerciseState { + + /** + * current exercise is active + */ + class Exercise( + val exerciseType: ExerciseType, + val question: Question + ) : ExerciseState() + + /** + * current exercise solved + */ + object Complete : ExerciseState() + + /** + * all exercises in game done, game over + */ + object End : ExerciseState() +} \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseType.kt b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseType.kt new file mode 100644 index 00000000..1e4a7310 --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseType.kt @@ -0,0 +1,10 @@ +package cz.movapp.app.ui.exercise + +enum class ExerciseType { + TEXT_IDENTIFICATION, + AUDIO_IDENTIFICATION; + + companion object { + fun random(): ExerciseType = values().random() + } +} \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseViewModel.kt b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseViewModel.kt new file mode 100644 index 00000000..9fbddd2a --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/ExerciseViewModel.kt @@ -0,0 +1,161 @@ +package cz.movapp.app.ui.exercise + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import cz.movapp.android.playSound +import cz.movapp.app.App +import cz.movapp.app.BuildConfig +import cz.movapp.app.data.DictionaryDatasource +import cz.movapp.app.data.LanguagePair +import cz.movapp.app.ui.dictionary.DictionaryTranslationsData +import cz.movapp.app.ui.exercise.play.AnswerState +import cz.movapp.app.ui.exercise.play.ExercisePlayItem +import timber.log.Timber +import kotlin.random.Random + +class ExerciseViewModel : ViewModel() { + + private val _exerciseState = MutableLiveData() + val exerciseState: LiveData = _exerciseState + + private val level: Level = levels[0] + + private lateinit var exercise: ExerciseData + + private var currentExerciseIndex: Int = 0 + private var currentExerciseType: ExerciseType = ExerciseType.random() + + // startup params + private lateinit var selectedCategories: MutableList + private var numQuestions: Int = 0 + + + fun prepareExercise( + selectedCategories: MutableList, + numQuestions: Int + ) { + // save for exercise restart + this.selectedCategories = selectedCategories + this.numQuestions = numQuestions + + val category = selectedCategories.random() + + val sections = DictionaryDatasource.loadSections(App.ctx, LanguagePair.getDefault()) + + val section = sections.find { category.contentEquals(it.id) } + + val translations = DictionaryDatasource.loadTranslations(App.ctx, LanguagePair.getDefault()) + + val phrases = mutableListOf() + fillPhrases(phrases, section?.phrases_ids, translations) + + Timber.d("numPhrases matching word limit ${phrases.size}") + + val numRequiredPhrases = level.choiceLimit * numQuestions + if (phrases.size < numRequiredPhrases) { + Timber.i("out of required phrases count ${phrases.size} < $numRequiredPhrases") + + val numberOfPhrasesToFill = numRequiredPhrases - phrases.size + + val fillPhrasesIDs = sections + .filter { selectedCategories.contains(it.id) && it.id != category } + .map { it.phrases_ids } + .flatten() + .shuffled() + + fillPhrases(phrases, fillPhrasesIDs, translations, numberOfPhrasesToFill) + } + + val questions = mutableListOf() + while (questions.size < numQuestions) { + + val selectedPhrases = mutableListOf() + while (selectedPhrases.size < level.choiceLimit) { + val phrase = phrases.random() + selectedPhrases.add(phrase) + phrases.remove(phrase) + Timber.d("Q${questions.size} loop ${selectedPhrases.size}/${level.choiceLimit} (remaining ${phrases.size})") + } + + questions.add( + Question( + Random.nextInt(0, selectedPhrases.size), + selectedPhrases + ) + ) + } + + exercise = ExerciseData( + numQuestions, + questions + ) + } + + private fun fillPhrases(phrases: MutableList, phrasesIds: List?, translations: List, limit: Int? = null) { + var addedPhrases = 0 + run loop@{ + phrasesIds?.forEach { phraseId -> + translations.find { + phraseId.contentEquals(it.id) + }?.let { + val numWords = it.main_translation.trim().count { it == ' ' } + 1 + if (numWords >= level.wordLimitMin && numWords <= level.wordLimitMax) { + phrases.add(it) + addedPhrases++ + } + } + + limit?.takeIf { addedPhrases >= it }?.let { + return@loop + } + } + } + } + + fun startNewExercise() { + currentExerciseIndex = 0 + currentExerciseType = ExerciseType.random() + _exerciseState.value = ExerciseState.Exercise( + currentExerciseType, + exercise.questions[currentExerciseIndex] + ) + } + + fun restartExercise() { + prepareExercise(selectedCategories, numQuestions) + startNewExercise() + } + + fun onAnswerSelected(answer: ExercisePlayItem): AnswerState { + val question = exercise.questions[currentExerciseIndex] + val correctAnswer = question.phrases[question.phraseCorrectIndex] + if (answer.id == correctAnswer.id) { + _exerciseState.value = ExerciseState.Complete + return AnswerState.CORRECT + } + return AnswerState.WRONG + } + + fun onNextButtonClick() { + currentExerciseIndex++ + if (currentExerciseIndex >= exercise.numExercises) { + // end of game + _exerciseState.value = ExerciseState.End + } else { + _exerciseState.value = ExerciseState.Exercise( + currentExerciseType, + exercise.questions[currentExerciseIndex] + ) + } + } + + fun playLocalSound(context: Context, localId: String, playbackSpeed: Float = 1.0f) { + val question = exercise.questions[currentExerciseIndex] + val phrase = question.phrases.firstOrNull { it.id == localId } ?: return + val toPlay = phrase.source_sound_local + Timber.d("toPlay $toPlay") + playSound(context, toPlay, playbackSpeed) + } +} \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/finish/ExerciseFinishFragment.kt b/app/src/main/java/cz/movapp/app/ui/exercise/finish/ExerciseFinishFragment.kt new file mode 100644 index 00000000..10d261ea --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/finish/ExerciseFinishFragment.kt @@ -0,0 +1,41 @@ +package cz.movapp.app.ui.exercise.finish + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import cz.movapp.app.R +import cz.movapp.app.databinding.FragmentExerciseFinishBinding +import cz.movapp.app.ui.exercise.ExerciseViewModel + +class ExerciseFinishFragment : Fragment() { + + private lateinit var binding: FragmentExerciseFinishBinding + + private val exerciseViewModel: ExerciseViewModel by activityViewModels() + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentExerciseFinishBinding.inflate(inflater, container, false) + + binding.apply { + restart.setOnClickListener { + exerciseViewModel.restartExercise() + findNavController().popBackStack() + } + + setup.setOnClickListener { + findNavController().popBackStack(R.id.nav_exercise_setup, false) + } + } + + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/play/AnswerState.kt b/app/src/main/java/cz/movapp/app/ui/exercise/play/AnswerState.kt new file mode 100644 index 00000000..02f613a8 --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/play/AnswerState.kt @@ -0,0 +1,7 @@ +package cz.movapp.app.ui.exercise.play + +enum class AnswerState { + UNANSWERED, + CORRECT, + WRONG +} \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayAdapter.kt b/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayAdapter.kt new file mode 100644 index 00000000..1e7694f6 --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayAdapter.kt @@ -0,0 +1,66 @@ +package cz.movapp.app.ui.exercise.play + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import cz.movapp.app.databinding.ExerciseAudioItemBinding +import cz.movapp.app.databinding.ExercisePlayItemBinding + + +class ExercisePlayAdapter( + private val onAnswerSelected: (ExercisePlayItem, Int) -> AnswerState?, + private val onPlayNormal: (ExercisePlayItem) -> Unit, + private val onPlaySlow: (ExercisePlayItem) -> Unit, +) : RecyclerView.Adapter>() { + + companion object { + private const val VIEW_TYPE_TEXT = 1 + private const val VIEW_TYPE_AUDIO = 2 + } + + private val dataSet: MutableList = mutableListOf() + + fun setData(data: List) { + dataSet.clear() + dataSet.addAll(data) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { + return when (viewType) { + VIEW_TYPE_TEXT -> TextViewHolder( + onAnswerSelected, + ExercisePlayItemBinding.inflate( + LayoutInflater.from(viewGroup.context), + viewGroup, + false + ) + ) + + VIEW_TYPE_AUDIO -> AudioViewHolder( + onPlayNormal, + onPlaySlow, + onAnswerSelected, + ExerciseAudioItemBinding.inflate( + LayoutInflater.from(viewGroup.context), + viewGroup, + false + ) + ) + + else -> throw IllegalStateException("unknown viewType $viewType") + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = dataSet[position] + holder.bind(item, position) + } + + override fun getItemCount() = dataSet.size + + override fun getItemViewType(position: Int) = when (dataSet[position]) { + is TextExercisePlayItem -> VIEW_TYPE_TEXT + is AudioExercisePlayItem -> VIEW_TYPE_AUDIO + } +} \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayFragment.kt b/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayFragment.kt new file mode 100644 index 00000000..aa6bc197 --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayFragment.kt @@ -0,0 +1,100 @@ +package cz.movapp.app.ui.exercise.play + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import cz.movapp.app.R +import cz.movapp.app.databinding.FragmentExercisePlayBinding +import cz.movapp.app.ui.exercise.ExerciseState +import cz.movapp.app.ui.exercise.ExerciseType +import cz.movapp.app.ui.exercise.ExerciseViewModel + +class ExercisePlayFragment : Fragment() { + + private lateinit var binding: FragmentExercisePlayBinding + private lateinit var adapter: ExercisePlayAdapter + + private val exerciseViewModel: ExerciseViewModel by activityViewModels() + + companion object { + private const val PLAYBACK_SPEED_75_PERCENT = 0.75f + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + exerciseViewModel.exerciseState.observe(this) { exerciseState -> + when (exerciseState) { + is ExerciseState.Exercise -> { + binding.next.isVisible = false + val question = exerciseState.question + binding.word.text = question.phrases[question.phraseCorrectIndex].source_translation + + val phrases = question.phrases.map { + when (exerciseState.exerciseType) { + ExerciseType.TEXT_IDENTIFICATION -> TextExercisePlayItem( + it.id, + it.main_translation + ) + ExerciseType.AUDIO_IDENTIFICATION -> AudioExercisePlayItem( + it.id, + it.main_translation + ) + } + } + adapter.setData(phrases) + } + is ExerciseState.Complete -> { + binding.next.isVisible = true + } + is ExerciseState.End -> { + findNavController().navigate(R.id.nav_exercise_finish) + } + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentExercisePlayBinding.inflate(inflater, container, false) + + binding.apply { + + adapter = ExercisePlayAdapter( + onAnswerSelected = { answer, position -> + exerciseViewModel.playLocalSound(requireContext(), answer.id) + val ret = exerciseViewModel.onAnswerSelected(answer) + adapter.notifyItemChanged(position) + ret + }, + onPlayNormal = { answer -> + exerciseViewModel.playLocalSound(requireContext(), answer.id) + + }, + onPlaySlow = { answer -> + exerciseViewModel.playLocalSound(requireContext(), answer.id, PLAYBACK_SPEED_75_PERCENT) + } + ) + recyclerViewExercise.adapter = adapter + + home.setOnClickListener { + findNavController().popBackStack() + } + + next.setOnClickListener { + exerciseViewModel.onNextButtonClick() + } + } + + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayItem.kt b/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayItem.kt new file mode 100644 index 00000000..94121bae --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayItem.kt @@ -0,0 +1,19 @@ +package cz.movapp.app.ui.exercise.play + +sealed class ExercisePlayItem( + val id: String, + val name: String, + var answerState: AnswerState +) + +class TextExercisePlayItem( + id: String, + name: String, + answerState: AnswerState = AnswerState.UNANSWERED +) : ExercisePlayItem(id, name, answerState) + +class AudioExercisePlayItem( + id: String, + name: String, + answerState: AnswerState = AnswerState.UNANSWERED +) : ExercisePlayItem(id, name, answerState) \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayViewHolder.kt b/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayViewHolder.kt new file mode 100644 index 00000000..7754b5ae --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/play/ExercisePlayViewHolder.kt @@ -0,0 +1,86 @@ +package cz.movapp.app.ui.exercise.play + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import cz.movapp.app.databinding.ExerciseAudioItemBinding +import cz.movapp.app.databinding.ExercisePlayItemBinding + +sealed class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + abstract fun bind(item: @UnsafeVariance ITEM, position: Int) +} + +// TODO duplicate code for answerState + +class TextViewHolder( + val onItemClick: (ExercisePlayItem, Int) -> AnswerState?, + val binding: ExercisePlayItemBinding +) : ViewHolder(binding.root) { + + override fun bind(item: TextExercisePlayItem, position: Int) { + binding.apply { + textExerciseName.text = item.name + when (item.answerState) { + AnswerState.UNANSWERED -> { + root.isSelected = false + root.isActivated = false + } + + AnswerState.WRONG -> { + root.isSelected = true + root.isActivated = false + } + + AnswerState.CORRECT -> { + root.isActivated = true + } + } + root.setOnClickListener { + onItemClick(item, position)?.let { + item.answerState = it + } + } + } + } +} + +class AudioViewHolder( + val onPlayNormalClick: (ExercisePlayItem) -> Unit, + val onPlaySlowClick: (ExercisePlayItem) -> Unit, + val onCheckClick: (ExercisePlayItem, Int) -> AnswerState?, + val binding: ExerciseAudioItemBinding +) : ViewHolder(binding.root) { + + override fun bind(item: AudioExercisePlayItem, position: Int) { + binding.apply { + playNormal.setOnClickListener { + onPlayNormalClick(item) + } + + playSlow.setOnClickListener { + onPlaySlowClick(item) + } + + check.setOnClickListener { + onCheckClick(item, position)?.let { + item.answerState = it + } + } + + when (item.answerState) { + AnswerState.UNANSWERED -> { + check.isSelected = false + check.isActivated = false + } + + AnswerState.WRONG -> { + check.isSelected = true + check.isActivated = false + } + + AnswerState.CORRECT -> { + check.isActivated = true + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/setup/ExerciseSetupAdapter.kt b/app/src/main/java/cz/movapp/app/ui/exercise/setup/ExerciseSetupAdapter.kt new file mode 100644 index 00000000..97afcbd0 --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/setup/ExerciseSetupAdapter.kt @@ -0,0 +1,51 @@ +package cz.movapp.app.ui.exercise.setup + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import cz.movapp.app.databinding.ExerciseSetupItemBinding + + +class ExerciseSetupAdapter( + private val dataSet: List +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { + val binding = ExerciseSetupItemBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + val item = dataSet[position] + viewHolder.binding.apply { + textExerciseName.text = item.name + root.isSelected = item.selected + root.setOnClickListener { + item.selected = !item.selected + notifyItemChanged(position) + } + } + } + + override fun getItemCount() = dataSet.size + + fun selectedCategories(): MutableList { + val selectedCats = mutableListOf() + dataSet.forEach { + if (it.selected) { + selectedCats.addAll(it.metaCategories) + } + } + return selectedCats + } + + class ViewHolder( + val binding: ExerciseSetupItemBinding + ) : RecyclerView.ViewHolder(binding.root) + + data class Item( + val name: String, + val metaCategories: List, + var selected: Boolean = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/cz/movapp/app/ui/exercise/setup/ExerciseSetupFragment.kt b/app/src/main/java/cz/movapp/app/ui/exercise/setup/ExerciseSetupFragment.kt new file mode 100644 index 00000000..63c0e407 --- /dev/null +++ b/app/src/main/java/cz/movapp/app/ui/exercise/setup/ExerciseSetupFragment.kt @@ -0,0 +1,89 @@ +package cz.movapp.app.ui.exercise.setup + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import cz.movapp.app.BuildConfig +import cz.movapp.app.R +import cz.movapp.app.databinding.FragmentExerciseSetupBinding +import cz.movapp.app.ui.dictionary.DictionaryMetaCategoryData +import cz.movapp.app.ui.dictionary.DictionaryViewModel +import cz.movapp.app.ui.exercise.ExerciseViewModel +import kotlinx.coroutines.launch +import timber.log.Timber + +class ExerciseSetupFragment : Fragment() { + + private lateinit var binding: FragmentExerciseSetupBinding + + private val dictionarySharedViewModel: DictionaryViewModel by activityViewModels() + private val exerciseViewModel: ExerciseViewModel by activityViewModels() + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentExerciseSetupBinding.inflate(inflater, container, false) + + binding.apply { + start.setOnClickListener { + lifecycleScope.launch { + val selectedCategories = (binding.recyclerViewExercise.adapter as ExerciseSetupAdapter).selectedCategories() + val numExercises = binding.groupLength.checkedRadioButtonId.numExercises() + val error = when { + selectedCategories.isEmpty() -> getString(R.string.exercise_setup_error_select_categories) + numExercises == 0 -> getString(R.string.exercise_setup_error_select_length) + else -> null + } + if (error == null) { + binding.error.visibility = View.GONE + exerciseViewModel.prepareExercise(selectedCategories, numExercises) + exerciseViewModel.startNewExercise() + findNavController().navigate(R.id.to_play) + } + else { + binding.error.visibility = View.VISIBLE + binding.error.text = error + } + } + } + } + + dictionarySharedViewModel.metaCategories.observe(viewLifecycleOwner) { + if (BuildConfig.DEBUG) { + Timber.d("metaCats:\n${it.joinToString("\n")}") + } + setupButtons(it) + } + + return binding.root + } + + private fun setupButtons(metaCategories: List) { + + val items = metaCategories.map { + ExerciseSetupAdapter.Item( + it.nameSource, + it.metaCategories + ) + } + + binding.recyclerViewExercise.adapter = ExerciseSetupAdapter(items) + } + + private fun Int.numExercises(): Int { + return when (this) { + R.id.length_1 -> 1 + R.id.length_5 -> 5 + R.id.length_10 -> 10 + else -> 0 + } + } +} diff --git a/app/src/main/res/color/exercise_play_selector.xml b/app/src/main/res/color/exercise_play_selector.xml new file mode 100644 index 00000000..3078b577 --- /dev/null +++ b/app/src/main/res/color/exercise_play_selector.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/exercise_setup_selector.xml b/app/src/main/res/color/exercise_setup_selector.xml new file mode 100644 index 00000000..81323cf3 --- /dev/null +++ b/app/src/main/res/color/exercise_setup_selector.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/exercise_play_item_correct.xml b/app/src/main/res/drawable/exercise_play_item_correct.xml new file mode 100644 index 00000000..1505bb33 --- /dev/null +++ b/app/src/main/res/drawable/exercise_play_item_correct.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/exercise_play_item_selector.xml b/app/src/main/res/drawable/exercise_play_item_selector.xml new file mode 100644 index 00000000..b15c6f27 --- /dev/null +++ b/app/src/main/res/drawable/exercise_play_item_selector.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/exercise_play_item_unanswered.xml b/app/src/main/res/drawable/exercise_play_item_unanswered.xml new file mode 100644 index 00000000..3f82de69 --- /dev/null +++ b/app/src/main/res/drawable/exercise_play_item_unanswered.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/exercise_play_item_wrong.xml b/app/src/main/res/drawable/exercise_play_item_wrong.xml new file mode 100644 index 00000000..16dba2b8 --- /dev/null +++ b/app/src/main/res/drawable/exercise_play_item_wrong.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/exercise_setup_item_default.xml b/app/src/main/res/drawable/exercise_setup_item_default.xml new file mode 100644 index 00000000..3f82de69 --- /dev/null +++ b/app/src/main/res/drawable/exercise_setup_item_default.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/exercise_setup_item_selected.xml b/app/src/main/res/drawable/exercise_setup_item_selected.xml new file mode 100644 index 00000000..accf2672 --- /dev/null +++ b/app/src/main/res/drawable/exercise_setup_item_selected.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/exercise_setup_item_selector.xml b/app/src/main/res/drawable/exercise_setup_item_selector.xml new file mode 100644 index 00000000..47789166 --- /dev/null +++ b/app/src/main/res/drawable/exercise_setup_item_selector.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_beta.xml b/app/src/main/res/drawable/ic_beta.xml new file mode 100644 index 00000000..c3524a22 --- /dev/null +++ b/app/src/main/res/drawable/ic_beta.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_open_book.xml b/app/src/main/res/drawable/ic_open_book.xml new file mode 100644 index 00000000..2401fba3 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_book.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_playicon.xml b/app/src/main/res/drawable/ic_playicon.xml new file mode 100644 index 00000000..6b4566ef --- /dev/null +++ b/app/src/main/res/drawable/ic_playicon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_slowplay.xml b/app/src/main/res/drawable/ic_slowplay.xml new file mode 100644 index 00000000..176b323b --- /dev/null +++ b/app/src/main/res/drawable/ic_slowplay.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_speaker.xml b/app/src/main/res/drawable/ic_speaker.xml new file mode 100644 index 00000000..4484e088 --- /dev/null +++ b/app/src/main/res/drawable/ic_speaker.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_on_boarding.xml b/app/src/main/res/layout/activity_on_boarding.xml index 28034c10..e3ef25b1 100644 --- a/app/src/main/res/layout/activity_on_boarding.xml +++ b/app/src/main/res/layout/activity_on_boarding.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/layout/exercise_audio_item.xml b/app/src/main/res/layout/exercise_audio_item.xml new file mode 100644 index 00000000..7878c06f --- /dev/null +++ b/app/src/main/res/layout/exercise_audio_item.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/exercise_play_item.xml b/app/src/main/res/layout/exercise_play_item.xml new file mode 100644 index 00000000..2499c25d --- /dev/null +++ b/app/src/main/res/layout/exercise_play_item.xml @@ -0,0 +1,31 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/exercise_setup_item.xml b/app/src/main/res/layout/exercise_setup_item.xml new file mode 100644 index 00000000..f7d53a32 --- /dev/null +++ b/app/src/main/res/layout/exercise_setup_item.xml @@ -0,0 +1,31 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_children.xml b/app/src/main/res/layout/fragment_children.xml index 54dee39a..a8852247 100644 --- a/app/src/main/res/layout/fragment_children.xml +++ b/app/src/main/res/layout/fragment_children.xml @@ -1,11 +1,10 @@ + android:paddingEnd="@dimen/general_padding"> diff --git a/app/src/main/res/layout/fragment_exercise_finish.xml b/app/src/main/res/layout/fragment_exercise_finish.xml new file mode 100644 index 00000000..ca55e235 --- /dev/null +++ b/app/src/main/res/layout/fragment_exercise_finish.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + +