Skip to content

Commit

Permalink
Implement app language change on settings (#252)
Browse files Browse the repository at this point in the history
* Implement multiplatform strings on FolderInsert

* Implement multiplatform strings on Settings

* Implement multiplatform strings on FolderList

* Implement multiplatform strings on FolderPreview

* Implement multiplatform strings on Android App

* Implement multiplatform strings on Search

* Implement multiplatform strings on Monster Content

* Implement multiplatform strings on Monster Detail

* Fix ioS monster detail state changes

* iOS: Remove localizable strings

* Improve ios flow

* Make iosFlow internal

* Implement multiplatform strings on Monster Registration
  • Loading branch information
alexandregpereira authored Feb 13, 2024
1 parent 74d142c commit d5bd39d
Show file tree
Hide file tree
Showing 194 changed files with 3,378 additions and 2,967 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class HunterApplication : Application() {
internal companion object {
private val appModule = module {
factory { Dispatchers.Default }
viewModel { MainViewModel(get(), get(), get(), get(), get(), get(), get()) }
viewModel { MainViewModel(get(), get(), get(), get(), get(), get(), get(), get()) }
}

fun KoinApplication.initKoinModules() {
Expand Down
38 changes: 38 additions & 0 deletions app/src/main/kotlin/br/alexandregpereira/hunter/app/MainStrings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package br.alexandregpereira.hunter.app

import br.alexandregpereira.hunter.localization.Language

interface MainStrings {
val compendium: String
val search: String
val folders: String
val menu: String
}

internal data class MainEnStrings(
override val compendium: String = "Compendium",
override val search: String = "Search",
override val folders: String = "Folders",
override val menu: String = "Menu",
) : MainStrings

internal data class MainPtStrings(
override val compendium: String = "Compêndio",
override val search: String = "Buscar",
override val folders: String = "Pastas",
override val menu: String = "Menu",
) : MainStrings

internal data class MainEmptyStrings(
override val compendium: String = "",
override val search: String = "",
override val folders: String = "",
override val menu: String = "",
) : MainStrings

internal fun Language.getStrings(): MainStrings {
return when (this) {
Language.ENGLISH -> MainEnStrings()
Language.PORTUGUESE -> MainPtStrings()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ package br.alexandregpereira.hunter.app
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import br.alexandregpereira.hunter.app.BottomBarItemIcon.COMPENDIUM
import br.alexandregpereira.hunter.app.BottomBarItemIcon.FOLDERS
import br.alexandregpereira.hunter.app.BottomBarItemIcon.SEARCH
import br.alexandregpereira.hunter.app.BottomBarItemIcon.SETTINGS
import br.alexandregpereira.hunter.app.MainViewEvent.BottomNavigationItemClick
import br.alexandregpereira.hunter.event.folder.detail.FolderDetailResultListener
import br.alexandregpereira.hunter.event.folder.detail.collectOnVisibilityChanges
Expand All @@ -31,10 +35,11 @@ import br.alexandregpereira.hunter.event.monster.detail.collectOnVisibilityChang
import br.alexandregpereira.hunter.event.systembar.BottomBarEvent.AddTopContent
import br.alexandregpereira.hunter.event.systembar.BottomBarEvent.RemoveTopContent
import br.alexandregpereira.hunter.event.systembar.BottomBarEventManager
import br.alexandregpereira.hunter.event.systembar.dispatchRemoveTopContentEvent
import br.alexandregpereira.hunter.event.systembar.dispatchAddTopContentEvent
import br.alexandregpereira.hunter.event.systembar.dispatchRemoveTopContentEvent
import br.alexandregpereira.hunter.folder.preview.event.FolderPreviewEvent
import br.alexandregpereira.hunter.folder.preview.event.FolderPreviewEventDispatcher
import br.alexandregpereira.hunter.localization.AppReactiveLocalization
import br.alexandregpereira.hunter.monster.content.event.MonsterContentManagerEventListener
import br.alexandregpereira.hunter.monster.content.event.collectOnVisibilityChanges
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -50,6 +55,7 @@ class MainViewModel(
private val monsterContentManagerEventListener: MonsterContentManagerEventListener,
private val folderPreviewEventDispatcher: FolderPreviewEventDispatcher,
private val bottomBarEventManager: BottomBarEventManager,
private val appLocalization: AppReactiveLocalization,
) : ViewModel() {

private val _state = MutableStateFlow(savedStateHandle.getState())
Expand All @@ -61,6 +67,26 @@ class MainViewModel(
observeFolderDetailResults()
observeFolderListResults()
observeMonsterContentManagerEvents()
observeLanguageChanges()
}

private fun observeLanguageChanges() {
appLocalization.languageFlow.onEach { language ->
setState {
val strings = language.getStrings()
copy(
bottomBarItems = BottomBarItemIcon.entries.map {
when (it) {
COMPENDIUM -> BottomBarItem(icon = it, text = strings.compendium)
SEARCH -> BottomBarItem(icon = it, text = strings.search)
FOLDERS -> BottomBarItem(icon = it, text = strings.folders)
SETTINGS -> BottomBarItem(icon = it, text = strings.menu)
}
},
showBottomBar = topContentStack.isEmpty()
)
}
}.launchIn(viewModelScope)
}

private fun observeBottomBarEvents() {
Expand Down Expand Up @@ -109,8 +135,8 @@ class MainViewModel(
fun onEvent(event: MainViewEvent) {
when (event) {
is BottomNavigationItemClick -> {
setState { copy(bottomBarItemSelected = event.item) }
dispatchFolderPreviewEvent(show = event.item != BottomBarItem.SETTINGS)
setState { copy(bottomBarItemSelectedIndex = bottomBarItems.indexOf(event.item)) }
dispatchFolderPreviewEvent(show = event.item.icon != SETTINGS)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,48 +19,58 @@ package br.alexandregpereira.hunter.app
import androidx.lifecycle.SavedStateHandle

data class MainViewState(
val bottomBarItemSelected: BottomBarItem = BottomBarItem.COMPENDIUM,
internal val topContentStack: List<String> = listOf(),
val bottomBarItemSelectedIndex: Int = 0,
val bottomBarItems: List<BottomBarItem> = emptyList(),
internal val topContentStack: Set<String> = setOf(),
val showBottomBar: Boolean = false,
) {

val showBottomBar: Boolean = topContentStack.isEmpty()
val bottomBarItemSelected: BottomBarItem? = bottomBarItems.getOrNull(bottomBarItemSelectedIndex)
}

internal fun MainViewState.addTopContentStack(
topContent: String,
): MainViewState {
return copy(topContentStack = topContentStack + topContent)
val topContentStack = topContentStack + topContent
return copy(topContentStack = topContentStack, showBottomBar = topContentStack.isEmpty())
}

internal fun MainViewState.removeTopContentStack(
topContent: String,
): MainViewState {
val topContentStack = topContentStack.toMutableSet().apply {
remove(topContent)
}.toSet()
return copy(
topContentStack = topContentStack.toMutableList().apply {
remove(topContent)
}.toList()
topContentStack = topContentStack,
showBottomBar = topContentStack.isEmpty(),
)
}

enum class BottomBarItem(val iconRes: Int, val stringRes: Int) {
COMPENDIUM(iconRes = R.drawable.ic_book, stringRes = R.string.compendium),
SEARCH(iconRes = R.drawable.ic_search, stringRes = R.string.search),
FOLDERS(iconRes = R.drawable.ic_folder, stringRes = R.string.folders),
SETTINGS(iconRes = R.drawable.ic_menu, stringRes = R.string.menu)
enum class BottomBarItemIcon(val iconRes: Int) {
COMPENDIUM(iconRes = R.drawable.ic_book),
SEARCH(iconRes = R.drawable.ic_search),
FOLDERS(iconRes = R.drawable.ic_folder),
SETTINGS(iconRes = R.drawable.ic_menu)
}

data class BottomBarItem(
val icon: BottomBarItemIcon = BottomBarItemIcon.COMPENDIUM,
val text: String = "",
)

internal fun SavedStateHandle.getState(): MainViewState {
return MainViewState(
bottomBarItemSelected = BottomBarItem.entries[this["bottomBarItemSelected"] ?: 0],
topContentStack = this.get<Array<String>>("topContentStack")?.toMutableList()
?: mutableListOf(),
bottomBarItemSelectedIndex = this["bottomBarItemSelectedIndex"] ?: 0,
topContentStack = this.get<Array<String>>("topContentStack")?.toSet()
?: setOf(),
)
}

internal fun MainViewState.saveState(
savedStateHandle: SavedStateHandle
): MainViewState {
savedStateHandle["bottomBarItemSelected"] = this.bottomBarItemSelected.ordinal
savedStateHandle["bottomBarItemSelectedIndex"] = this.bottomBarItemSelectedIndex
savedStateHandle["topContentStack"] = this.topContentStack.toTypedArray()
return this
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationDefaults
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.LocalContentAlpha
Expand All @@ -56,7 +55,6 @@ import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Constraints
Expand All @@ -70,7 +68,8 @@ import kotlin.math.roundToInt
@Composable
fun BoxScope.AppBottomNavigation(
showBottomBar: Boolean,
bottomBarItemSelected: BottomBarItem,
bottomBarItemSelectedIndex: Int,
bottomBarItems: List<BottomBarItem>,
contentPadding: PaddingValues = PaddingValues(),
onClick: (BottomBarItem) -> Unit = {}
) = HunterTheme {
Expand All @@ -92,13 +91,13 @@ fun BoxScope.AppBottomNavigation(
modifier = Modifier.height(BottomNavigationHeight + paddingBottom)
.padding(bottom = paddingBottom)
) {
BottomBarItem.entries.forEach { bottomBarItem ->
bottomBarItems.forEachIndexed { i, bottomBarItem ->
AppBottomNavigationItem(
totalItems = BottomBarItem.entries.size,
indexSelected = bottomBarItemSelected.ordinal,
currentIndex = bottomBarItem.ordinal,
iconRes = bottomBarItem.iconRes,
nameRes = bottomBarItem.stringRes,
totalItems = bottomBarItems.size,
indexSelected = bottomBarItemSelectedIndex,
currentIndex = i,
iconRes = bottomBarItem.icon.iconRes,
name = bottomBarItem.text,
onClick = { onClick(bottomBarItem) }
)
}
Expand All @@ -113,7 +112,7 @@ private fun RowScope.AppBottomNavigationItem(
indexSelected: Int,
currentIndex: Int,
iconRes: Int,
nameRes: Int,
name: String,
onClick: () -> Unit
) {
AppBottomNavigationItem(
Expand All @@ -123,11 +122,11 @@ private fun RowScope.AppBottomNavigationItem(
icon = {
Icon(
painter = painterResource(iconRes),
contentDescription = stringResource(nameRes)
contentDescription = name
)
},
label = {
Text(text = stringResource(nameRes), maxLines = 1)
Text(text = name, maxLines = 1)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import br.alexandregpereira.hunter.app.BottomBarItem
import br.alexandregpereira.hunter.app.BottomBarItemIcon
import br.alexandregpereira.hunter.app.BuildConfig
import br.alexandregpereira.hunter.folder.list.FolderListFeature
import br.alexandregpereira.hunter.monster.compendium.MonsterCompendiumFeature
Expand All @@ -28,21 +29,22 @@ import br.alexandregpereira.hunter.settings.SettingsFeature

@Composable
fun BottomNavigationTransition(
bottomBarItemSelected: BottomBarItem,
bottomBarItemSelected: BottomBarItem?,
contentPadding: PaddingValues = PaddingValues()
) {
Crossfade(targetState = bottomBarItemSelected, label = "BottomNavigationTransition") { index ->
when (index) {
BottomBarItem.COMPENDIUM -> MonsterCompendiumFeature(
if (bottomBarItemSelected == null) return
Crossfade(targetState = bottomBarItemSelected, label = "BottomNavigationTransition") { item ->
when (item.icon) {
BottomBarItemIcon.COMPENDIUM -> MonsterCompendiumFeature(
contentPadding = contentPadding,
)
BottomBarItem.FOLDERS -> FolderListFeature(
BottomBarItemIcon.FOLDERS -> FolderListFeature(
contentPadding = contentPadding,
)
BottomBarItem.SEARCH -> SearchScreenFeature(
BottomBarItemIcon.SEARCH -> SearchScreenFeature(
contentPadding = contentPadding,
)
BottomBarItem.SETTINGS -> SettingsFeature(
BottomBarItemIcon.SETTINGS -> SettingsFeature(
versionName = BuildConfig.VERSION_NAME,
contentPadding = contentPadding,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ fun MainScreen(

AppBottomNavigation(
showBottomBar = state.showBottomBar,
bottomBarItemSelected = state.bottomBarItemSelected,
bottomBarItemSelectedIndex = state.bottomBarItemSelectedIndex,
bottomBarItems = state.bottomBarItems,
contentPadding = contentPadding,
onClick = { onEvent(BottomNavigationItemClick(item = it)) }
)
Expand Down
4 changes: 0 additions & 4 deletions app/src/main/res/values-pt-rBR/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,4 @@

<resources>
<string name="app_name">Compêndio</string>
<string name="compendium">Compêndio</string>
<string name="search">Buscar</string>
<string name="folders">Pastas</string>
<string name="menu">Menu</string>
</resources>
4 changes: 0 additions & 4 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,4 @@

<resources>
<string name="app_name">Compendium</string>
<string name="compendium">Compendium</string>
<string name="search">Search</string>
<string name="folders">Folders</string>
<string name="menu">Menu</string>
</resources>
Original file line number Diff line number Diff line change
@@ -1,28 +1,52 @@
package br.alexandregpereira.hunter.localization

import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow

interface AppLocalization {

fun getLanguage(): Language
}

interface AppReactiveLocalization : AppLocalization {

val languageFlow: Flow<Language>
}

interface MutableAppLocalization : AppLocalization {

fun setLanguage(language: String)
}

internal class AppLocalizationImpl : MutableAppLocalization {
internal class AppLocalizationImpl : MutableAppLocalization, AppReactiveLocalization {

private var language: Language = Language.ENGLISH
private var language: Language = getDefaultLanguage()
private val _languageFlow: MutableSharedFlow<Language> = MutableSharedFlow(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val languageFlow: Flow<Language> = _languageFlow.asSharedFlow()

override fun getLanguage(): Language {
return language
}

override fun setLanguage(language: String) {
this.language = Language.entries.firstOrNull { it.code == language } ?: Language.ENGLISH
this.language = Language.entries.firstOrNull { it.code == language } ?: getDefaultLanguage()
_languageFlow.tryEmit(this.language)
}

private fun getDefaultLanguage(): Language {
return Language.entries.firstOrNull {
it.code == getDeviceLangCode()
} ?: Language.ENGLISH
}
}

internal expect fun getDeviceLangCode(): String

enum class Language(val code: String) {
ENGLISH("en-us"),
PORTUGUESE("pt-br")
Expand Down
Loading

0 comments on commit d5bd39d

Please sign in to comment.